#![cfg(target_os = "linux")]
#![allow(clippy::unwrap_used, clippy::expect_used)]
use skif_core::HashSet;
use std::io;
use std::num::NonZeroUsize;
use std::path::Path;
use std::sync::{Arc, Mutex};
use std::thread::{self, ThreadId};
use skif::{ConfigOverrides, DirEntry, FileSystem, Linter, MemoryFs, RunOptions, load_config};
fn thread_count() -> usize {
std::fs::read_dir("/proc/self/task").unwrap().count()
}
#[derive(Default)]
struct Seen {
threads: HashSet<ThreadId>,
max_thread_count: usize,
calls: usize,
}
struct Recording {
inner: MemoryFs,
seen: Mutex<Seen>,
}
impl Recording {
fn note(&self) {
let mut seen = self.seen.lock().unwrap();
seen.threads.insert(thread::current().id());
seen.max_thread_count = seen.max_thread_count.max(thread_count());
seen.calls += 1;
}
fn reset(&self) {
*self.seen.lock().unwrap() = Seen::default();
}
}
impl FileSystem for Recording {
fn read_to_string(&self, path: &Path) -> io::Result<Arc<str>> {
self.note();
self.inner.read_to_string(path)
}
fn read_dir(&self, path: &Path) -> io::Result<Vec<DirEntry>> {
self.note();
self.inner.read_dir(path)
}
fn is_file(&self, path: &Path) -> bool {
self.note();
self.inner.is_file(path)
}
fn is_dir(&self, path: &Path) -> bool {
self.note();
self.inner.is_dir(path)
}
}
fn fixture() -> MemoryFs {
let mut fs = MemoryFs::new().with_file(
"/ws/package.json",
r#"{"name": "root", "private": true, "packageManager": "pnpm@10.0.0", "workspaces": ["packages/*"]}"#,
);
for name in ["a", "b", "c", "d"] {
fs = fs
.with_file(
format!("/ws/packages/{name}/package.json"),
format!(r#"{{"name": "{name}", "version": "1.0.0", "private": true, "main": "index.js"}}"#),
)
.with_file(
format!("/ws/packages/{name}/index.js"),
"import './one.js';\nimport './two.js';\nimport './three.js';\n",
)
.with_file(format!("/ws/packages/{name}/one.js"), "export const one = 1;\n")
.with_file(format!("/ws/packages/{name}/two.js"), "export const two = 2;\n")
.with_file(format!("/ws/packages/{name}/three.js"), "export const three = 3;\n");
}
fs
}
fn observe(fs: &Arc<Recording>, threads: NonZeroUsize) -> (usize, HashSet<ThreadId>, usize) {
let config = load_config(Path::new("/ws"), fs.as_ref(), &ConfigOverrides::default()).unwrap();
let linter = Linter::with_fs(&config, Arc::clone(fs) as Arc<dyn FileSystem>).unwrap();
let options = RunOptions::default().with_threads(threads);
fs.reset();
let workspace = linter
.discover_with_fs(Path::new("/ws"), fs.as_ref(), &options)
.unwrap();
assert!(workspace.all_packages().count() >= 4);
let report = linter.run_with(&workspace, &options).unwrap();
let seen = fs.seen.lock().unwrap();
(report.diagnostics.len(), seen.threads.clone(), seen.max_thread_count)
}
#[test]
fn one_thread_runs_discovery_and_graph_building_on_the_calling_thread() {
let fs = Arc::new(Recording {
inner: fixture(),
seen: Mutex::new(Seen::default()),
});
let caller = thread::current().id();
let before = thread_count();
let (sequential_diagnostics, threads, max_count) = observe(&fs, NonZeroUsize::MIN);
assert!(fs.seen.lock().unwrap().calls > 20, "the run must reach the file system");
assert_eq!(
threads,
[caller].into_iter().collect::<HashSet<_>>(),
"work ran off the calling thread"
);
assert!(
max_count <= before,
"{max_count} threads during the run, {before} before"
);
let (pooled_diagnostics, threads, _) = observe(&fs, NonZeroUsize::new(2).unwrap());
assert!(
!threads.contains(&caller),
"a 2-thread pool should run the work on its own threads"
);
assert_eq!(pooled_diagnostics, sequential_diagnostics);
}