1use std::fs::File;
27use std::path::{Path, PathBuf};
28
29use crate::manifest::{TreeGate, TreeTool};
30
31const COMPLETE: &str = ".complete";
32const MAX_TYPED_SAMPLE: usize = 256;
37const TYPED: &str = ".typed";
38const UNTYPED: &str = ".untyped";
39
40const LOCKFILES: &[&str] = &[
42 "package-lock.json",
43 "npm-shrinkwrap.json",
44 "pnpm-lock.yaml",
45 "yarn.lock",
46 "bun.lockb",
47 "uv.lock",
48 "poetry.lock",
49 "Pipfile.lock",
50 "Cargo.lock",
51 "go.sum",
52];
53
54pub fn config_like(path: &str) -> bool {
58 let base = path.rsplit('/').next().unwrap_or(path);
59 LOCKFILES.contains(&base)
60 || [".json", ".toml", ".yaml", ".yml"]
61 .iter()
62 .any(|ext| base.ends_with(ext))
63 || base.contains("config")
64 || (base.starts_with('.') && (base.contains("rc") || base.ends_with("ignore")))
65}
66
67fn git_dir() -> Option<PathBuf> {
68 crate::git::stdout(&["rev-parse", "--absolute-git-dir"]).map(PathBuf::from)
69}
70
71pub fn gate_dir(gate: &TreeGate) -> Option<PathBuf> {
73 Some(git_dir()?.join("amont-cache").join(&gate.name))
74}
75
76pub fn tool_version(
82 cwd: &Path,
83 gate: &TreeGate,
84 deadline: std::time::Instant,
85 cancel: &std::sync::atomic::AtomicBool,
86) -> Option<String> {
87 let words: Vec<&str> = gate.command.split_whitespace().collect();
88 if words.first() == Some(&"uvx") && words.get(1).is_some_and(|w| w.contains('@')) {
90 return Some("pinned-in-command".into());
91 }
92 match gate.tool {
93 TreeTool::Eslint | TreeTool::Prettier => {
94 let pkg = cwd
95 .join("node_modules")
96 .join(gate.tool.as_str())
97 .join("package.json");
98 Some(
99 std::fs::read_to_string(pkg)
100 .ok()
101 .and_then(|t| json_string_field(&t, "version"))
102 .unwrap_or_else(|| "absent".into()),
103 )
104 }
105 _ => {
106 let probe: Vec<String> = if words.starts_with(&["uv", "run"]) {
107 [
108 "uv",
109 "run",
110 "--frozen",
111 "--no-sync",
112 gate.tool.as_str(),
113 "--version",
114 ]
115 .iter()
116 .map(|s| s.to_string())
117 .collect()
118 } else if gate.tool == TreeTool::Gofmt {
119 vec!["go".into(), "version".into()]
120 } else {
121 vec![gate.tool.as_str().into(), "--version".into()]
122 };
123 match crate::tree_run::run_output(&probe, cwd, deadline, cancel) {
127 Ok(out) => Some(
128 Some(out.lines().next().unwrap_or("").trim().to_string())
129 .filter(|v| !v.is_empty())
130 .unwrap_or_else(|| "absent".into()),
131 ),
132 Err(crate::tree_run::TreeRun::TimedOut | crate::tree_run::TreeRun::Cancelled) => {
133 None
134 }
135 Err(_) => Some("absent".into()),
136 }
137 }
138 }
139}
140
141fn json_string_field(text: &str, field: &str) -> Option<String> {
145 let key = format!("\"{field}\"");
146 let at = text.find(&key)? + key.len();
147 let rest = text[at..].trim_start().strip_prefix(':')?.trim_start();
148 let rest = rest.strip_prefix('"')?;
149 let end = rest.find('"')?;
150 Some(rest[..end].to_string())
151}
152
153pub fn namespace(gate: &TreeGate, version: &str) -> Option<String> {
156 let staged = crate::git::stdout(&["ls-files", "-s"])?;
157 let mut material = format!(
158 "amont-tree-ns-v1\ncommand {}\nversion {}\n",
159 gate.command, version
160 );
161 for line in staged.lines() {
162 let Some((meta, path)) = line.split_once('\t') else {
164 continue;
165 };
166 if config_like(path)
167 || gate
168 .inputs
169 .iter()
170 .any(|i| path == i || path.starts_with(&format!("{i}/")))
171 {
172 material.push_str(meta);
173 material.push(' ');
174 material.push_str(path);
175 material.push('\n');
176 }
177 }
178 hash(&material)
179}
180
181pub(crate) fn hash(material: &str) -> Option<String> {
186 crate::git::stdout_piped(&["hash-object", "--stdin"], material)
187 .map(|s| s.trim().to_string())
188 .filter(|s| !s.is_empty())
189}
190
191pub struct Lock {
193 _file: File,
194}
195
196#[cfg(unix)]
197extern "C" {
198 #[link_name = "flock"]
199 fn libc_flock(fd: i32, op: i32) -> i32;
200}
201
202#[cfg(unix)]
203const LOCK_EX: i32 = 2;
204#[cfg(unix)]
205const LOCK_NB: i32 = 4;
206
207pub fn try_lock(gate: &TreeGate) -> Option<Lock> {
211 lock_in(&gate_dir(gate)?)
212}
213
214fn lock_in(dir: &Path) -> Option<Lock> {
217 std::fs::create_dir_all(dir).ok()?;
218 let file = std::fs::OpenOptions::new()
219 .create(true)
220 .truncate(false)
221 .write(true)
222 .open(dir.join(".lock"))
223 .ok()?;
224 #[cfg(unix)]
225 {
226 use std::os::unix::io::AsRawFd;
227 if unsafe { libc_flock(file.as_raw_fd(), LOCK_EX | LOCK_NB) } != 0 {
229 return None;
230 }
231 }
232 Some(Lock { _file: file })
233}
234
235pub fn enter_namespace(gate: &TreeGate, ns: &str) -> Option<PathBuf> {
238 let dir = gate_dir(gate)?;
239 if let Ok(entries) = std::fs::read_dir(&dir) {
240 for e in entries.filter_map(Result::ok) {
241 let name = e.file_name();
242 let name = name.to_string_lossy();
243 if name != ns && !name.starts_with('.') && e.path().is_dir() {
244 let _ = std::fs::remove_dir_all(e.path());
245 }
246 }
247 }
248 let ns_dir = dir.join(ns);
249 std::fs::create_dir_all(&ns_dir).ok()?;
250 Some(ns_dir)
251}
252
253const DURATIONS: &str = ".durations";
254const LAST_MS: &str = ".last_ms";
255
256pub fn duration_of(id: &str) -> Option<u64> {
260 let path = git_dir()?.join("amont-cache").join(DURATIONS);
261 let text = std::fs::read_to_string(path).ok()?;
262 text.lines().find_map(|l| {
263 let (k, v) = l.split_once(' ')?;
264 (k == id).then(|| v.trim().parse().ok()).flatten()
265 })
266}
267
268pub fn record_durations(measured: &[(String, u64)]) {
270 if measured.is_empty() {
271 return;
272 }
273 let Some(dir) = git_dir().map(|d| d.join("amont-cache")) else {
274 return;
275 };
276 let _ = std::fs::create_dir_all(&dir);
277 let path = dir.join(DURATIONS);
278 let mut rows: Vec<(String, u64)> = std::fs::read_to_string(&path)
279 .unwrap_or_default()
280 .lines()
281 .filter_map(|l| {
282 let (k, v) = l.split_once(' ')?;
283 Some((k.to_string(), v.trim().parse().ok()?))
284 })
285 .filter(|(k, _)| !measured.iter().any(|(m, _)| m == k))
286 .collect();
287 rows.extend(measured.iter().cloned());
288 let body: String = rows.iter().map(|(k, v)| format!("{k} {v}\n")).collect();
289 let tmp = dir.join(format!("{DURATIONS}.tmp.{}", std::process::id()));
290 if std::fs::write(&tmp, body).is_ok() {
291 let _ = std::fs::rename(&tmp, path);
292 }
293}
294
295pub fn gate_last_ms(gate: &TreeGate) -> Option<u64> {
301 last_ms(&gate_dir(gate)?)
302}
303
304pub fn record_gate_last_ms(gate_name: &str, ms: u64) {
305 if let Some(dir) = git_dir().map(|d| d.join("amont-cache").join(gate_name)) {
306 let _ = std::fs::create_dir_all(&dir);
307 record_last_ms(&dir, ms);
308 }
309}
310
311fn last_ms(ns_dir: &Path) -> Option<u64> {
312 std::fs::read_to_string(ns_dir.join(LAST_MS))
313 .ok()?
314 .trim()
315 .parse()
316 .ok()
317}
318
319fn record_last_ms(ns_dir: &Path, ms: u64) {
320 let tmp = ns_dir.join(format!("{LAST_MS}.tmp.{}", std::process::id()));
321 if std::fs::write(&tmp, ms.to_string()).is_ok() {
322 let _ = std::fs::rename(&tmp, ns_dir.join(LAST_MS));
323 }
324}
325
326pub fn is_warm(gate: &TreeGate, ns: &str) -> bool {
328 gate_dir(gate)
329 .map(|d| d.join(ns).join(COMPLETE).is_file())
330 .unwrap_or(false)
331}
332
333pub fn mark_complete(ns_dir: &Path) {
336 let tmp = ns_dir.join(format!("{COMPLETE}.tmp.{}", std::process::id()));
337 if std::fs::write(&tmp, b"complete\n").is_ok() {
338 let _ = std::fs::rename(&tmp, ns_dir.join(COMPLETE));
339 }
340}
341
342pub fn config_matches_index(cwd: &Path) -> bool {
345 let Some(changed) = crate::git::stdout_in(cwd, &["diff", "--name-only", "--relative"]) else {
346 return false;
347 };
348 let Some(untracked) =
349 crate::git::stdout_in(cwd, &["ls-files", "--others", "--exclude-standard"])
350 else {
351 return false;
352 };
353 !changed.lines().chain(untracked.lines()).any(config_like)
354}
355
356pub fn config_text_is_typed(cwd: &Path) -> bool {
359 let files = crate::git::stdout_in(cwd, &["ls-files"]).unwrap_or_default();
360 files
361 .lines()
362 .filter(|p| {
363 let base = p.rsplit('/').next().unwrap_or(p);
364 base.starts_with("eslint.config.") || base.starts_with(".eslintrc")
365 })
366 .filter_map(|p| std::fs::read_to_string(cwd.join(p)).ok())
367 .any(|t| {
368 t.contains("projectService") || (t.contains("parserOptions") && t.contains("project"))
369 })
370}
371
372pub fn config_is_typed(config: &crate::json_read::Value) -> bool {
376 let Some(opts) = config
377 .get("languageOptions")
378 .and_then(|l| l.get("parserOptions"))
379 .or_else(|| config.get("parserOptions"))
380 else {
381 return false;
382 };
383 let project = opts.get("project").is_some_and(|p| {
384 !matches!(
385 p,
386 crate::json_read::Value::Null | crate::json_read::Value::Bool(false)
387 )
388 });
389 let service = opts.get("projectService").is_some_and(|v| {
390 !matches!(
391 v,
392 crate::json_read::Value::Null | crate::json_read::Value::Bool(false)
393 )
394 });
395 project || service
396}
397
398pub fn typed_eslint(
420 cwd: &Path,
421 ns_dir: &Path,
422 deadline: std::time::Instant,
423 cancel: &std::sync::atomic::AtomicBool,
424) -> bool {
425 if ns_dir.join(TYPED).is_file() {
426 return true;
427 }
428 if ns_dir.join(UNTYPED).is_file() {
429 return false;
430 }
431 if !config_matches_index(cwd) {
437 return true;
438 }
439 let bin = cwd.join("node_modules").join(".bin").join("eslint");
440 if !bin.is_file() {
441 return true;
442 }
443 if config_text_is_typed(cwd) {
444 let _ = std::fs::write(ns_dir.join(TYPED), b"");
445 return true;
446 }
447 let files = crate::git::stdout_in(
451 cwd,
452 &[
453 "ls-files", "*.ts", "*.tsx", "*.js", "*.jsx", "*.mjs", "*.cjs",
454 ],
455 )
456 .unwrap_or_default();
457 let mut seen: Vec<(String, String)> = Vec::new();
463 let mut sample: Vec<&str> = Vec::new();
464 for file in files.lines() {
465 let (dir, base) = file.rsplit_once('/').unwrap_or(("", file));
466 let ext = base.rsplit_once('.').map(|(_, e)| e).unwrap_or("");
467 let key = (dir.to_string(), ext.to_string());
468 if !seen.contains(&key) {
469 seen.push(key);
470 sample.push(file);
471 }
472 }
473 if sample.len() > MAX_TYPED_SAMPLE {
474 return true;
475 }
476 let mut answered = 0;
477 for file in sample {
478 let argv = vec![
479 bin.display().to_string(),
480 "--print-config".to_string(),
481 file.to_string(),
482 ];
483 match crate::tree_run::run_output(&argv, cwd, deadline, cancel) {
484 Ok(out) => match crate::json_read::parse(out.trim()) {
485 Some(config) if config_is_typed(&config) => {
486 let _ = std::fs::write(ns_dir.join(TYPED), b"");
487 return true;
488 }
489 Some(_) => answered += 1,
490 None => {}
492 },
493 Err(_) => return true,
494 }
495 }
496 if answered == 0 {
497 return true;
498 }
499 let _ = std::fs::write(ns_dir.join(UNTYPED), b"");
500 false
501}
502
503pub fn cache_flags(
506 cwd: &Path,
507 gate: &TreeGate,
508 ns_dir: &Path,
509 deadline: std::time::Instant,
510 cancel: &std::sync::atomic::AtomicBool,
511) -> String {
512 let file = match gate.tool {
513 TreeTool::Eslint if !typed_eslint(cwd, ns_dir, deadline, cancel) => ".eslintcache",
514 TreeTool::Prettier => ".prettiercache",
515 _ => return String::new(),
516 };
517 format!(
518 "--cache --cache-strategy content --cache-location {}",
519 ns_dir.join(file).display()
520 )
521}
522
523#[cfg(test)]
524mod tests {
525 use super::*;
526
527 #[test]
528 fn tree_cache_config_like_by_basename() {
529 for p in [
530 "package.json",
531 "web/tsconfig.base.json",
532 "eslint.config.mjs",
533 ".prettierrc",
534 ".eslintrc.cjs",
535 ".gitignore",
536 ".prettierignore",
537 "pyproject.toml",
538 "uv.lock",
539 "go.sum",
540 "pnpm-lock.yaml",
541 ] {
542 assert!(config_like(p), "{p}");
543 }
544 for p in ["src/config/app.ts", "src/main.ts", "README.md", "Makefile"] {
545 assert!(!config_like(p), "{p}");
546 }
547 }
548
549 #[cfg(unix)]
553 #[test]
554 fn tree_cache_a_held_lock_is_busy_not_awaited() {
555 let dir = std::env::temp_dir().join(format!("amont-lock-test-{}", std::process::id()));
556 let first = lock_in(&dir).expect("first lock");
557 assert!(lock_in(&dir).is_none(), "a second lock was granted");
558 drop(first);
559 let until = std::time::Instant::now() + std::time::Duration::from_secs(5);
563 let mut again = lock_in(&dir);
564 while again.is_none() && std::time::Instant::now() < until {
565 std::thread::sleep(std::time::Duration::from_millis(20));
566 again = lock_in(&dir);
567 }
568 assert!(again.is_some(), "the lock was never released");
569 let _ = std::fs::remove_dir_all(dir);
570 }
571
572 #[test]
573 fn tree_cache_typed_configs_are_recognised() {
574 use crate::json_read::parse;
575 let typed = [
576 r#"{"languageOptions":{"parserOptions":{"project":"./tsconfig.json"}}}"#,
577 r#"{"languageOptions":{"parserOptions":{"project":true}}}"#,
578 r#"{"languageOptions":{"parserOptions":{"projectService":true}}}"#,
579 r#"{"languageOptions":{"parserOptions":{"projectService":{"allowDefaultProject":["*.js"]}}}}"#,
580 r#"{"parserOptions":{"project":["a.json"]}}"#,
581 ];
582 for t in typed {
583 assert!(config_is_typed(&parse(t).unwrap()), "{t}");
584 }
585 let untyped = [
586 r#"{"languageOptions":{"parserOptions":{"project":null}}}"#,
587 r#"{"languageOptions":{"parserOptions":{"projectService":false}}}"#,
588 r#"{"languageOptions":{"parserOptions":{}}}"#,
589 r#"{"rules":{}}"#,
590 ];
591 for t in untyped {
592 assert!(!config_is_typed(&parse(t).unwrap()), "{t}");
593 }
594 }
595
596 #[test]
598 fn tree_cache_undetermined_eslint_reads_typed() {
599 let d = std::env::temp_dir().join(format!("amont-typed-{}", std::process::id()));
600 let ns = d.join("ns");
601 std::fs::create_dir_all(&ns).unwrap();
602 let typed = typed_eslint(
603 &d,
604 &ns,
605 std::time::Instant::now() + std::time::Duration::from_secs(5),
606 &std::sync::atomic::AtomicBool::new(false),
607 );
608 assert!(typed);
609 assert!(!ns.join(TYPED).exists() && !ns.join(UNTYPED).exists());
610 let _ = std::fs::remove_dir_all(d);
611 }
612
613 #[cfg(unix)]
616 #[test]
617 fn tree_cache_one_typed_file_makes_eslint_typed() {
618 use std::os::unix::fs::PermissionsExt;
619 let d = std::env::temp_dir().join(format!("amont-typed-mix-{}", std::process::id()));
620 let _ = std::fs::remove_dir_all(&d);
621 let bin = d.join("node_modules/.bin");
622 std::fs::create_dir_all(&bin).unwrap();
623 std::fs::create_dir_all(d.join("src")).unwrap();
624 std::fs::write(d.join("a.js"), "").unwrap();
625 std::fs::write(d.join("src/b.ts"), "").unwrap();
626 for args in [vec!["init", "-q"], vec!["add", "a.js", "src/b.ts"]] {
627 assert!(std::process::Command::new("git")
628 .args(&args)
629 .current_dir(&d)
630 .status()
631 .unwrap()
632 .success());
633 }
634 let eslint = bin.join("eslint");
635 std::fs::write(
636 &eslint,
637 "#!/bin/sh\ncase \"$2\" in\n src/*) echo '{\"languageOptions\":{\"parserOptions\":{\"projectService\":true}}}' ;;\n *) echo '{\"languageOptions\":{\"parserOptions\":{}}}' ;;\nesac\n",
638 )
639 .unwrap();
640 std::fs::set_permissions(&eslint, std::fs::Permissions::from_mode(0o755)).unwrap();
641 let ns = d.join("ns");
642 std::fs::create_dir_all(&ns).unwrap();
643 let typed = typed_eslint(
648 &d,
649 &ns,
650 std::time::Instant::now() + std::time::Duration::from_secs(300),
651 &std::sync::atomic::AtomicBool::new(false),
652 );
653 assert!(typed, "one typed file must make the namespace typed");
654 assert!(ns.join(TYPED).exists());
655 let _ = std::fs::remove_dir_all(d);
656 }
657
658 #[cfg(unix)]
659 fn git_repo(d: &Path, paths: &[String]) {
660 assert!(std::process::Command::new("git")
661 .args(["init", "-q"])
662 .current_dir(d)
663 .status()
664 .unwrap()
665 .success());
666 for chunk in paths.chunks(200) {
667 assert!(std::process::Command::new("git")
668 .arg("add")
669 .args(chunk)
670 .current_dir(d)
671 .status()
672 .unwrap()
673 .success());
674 }
675 }
676
677 #[cfg(unix)]
680 #[test]
681 fn tree_cache_a_filename_scoped_typed_config_reads_typed() {
682 use std::os::unix::fs::PermissionsExt;
683 let d = std::env::temp_dir().join(format!("amont-typed-text-{}", std::process::id()));
684 let _ = std::fs::remove_dir_all(&d);
685 std::fs::create_dir_all(d.join("node_modules/.bin")).unwrap();
686 std::fs::write(d.join("a.js"), "").unwrap();
687 std::fs::write(
688 d.join("eslint.config.mjs"),
689 "export default [{ files: ['**/*.test.ts'], languageOptions: { parserOptions: { projectService: true } } }];\n",
690 )
691 .unwrap();
692 git_repo(&d, &["a.js".into(), "eslint.config.mjs".into()]);
693 let eslint = d.join("node_modules/.bin/eslint");
694 std::fs::write(&eslint, "#!/bin/sh\necho '{\"rules\":{}}'\n").unwrap();
695 std::fs::set_permissions(&eslint, std::fs::Permissions::from_mode(0o755)).unwrap();
696 let ns = d.join("ns");
697 std::fs::create_dir_all(&ns).unwrap();
698 assert!(typed_eslint(
699 &d,
700 &ns,
701 std::time::Instant::now() + std::time::Duration::from_secs(20),
702 &std::sync::atomic::AtomicBool::new(false),
703 ));
704 assert!(ns.join(TYPED).exists(), "the text check remembers typed");
705 let _ = std::fs::remove_dir_all(d);
706 }
707
708 #[cfg(unix)]
712 #[test]
713 fn tree_cache_unstaged_config_is_never_remembered() {
714 use std::os::unix::fs::PermissionsExt;
715 let d = std::env::temp_dir().join(format!("amont-typed-dirty-{}", std::process::id()));
716 let _ = std::fs::remove_dir_all(&d);
717 std::fs::create_dir_all(d.join("node_modules/.bin")).unwrap();
718 std::fs::write(d.join("a.js"), "").unwrap();
719 std::fs::write(
720 d.join("eslint.config.mjs"),
721 "export default [{ languageOptions: { parserOptions: { projectService: true } } }];\n",
722 )
723 .unwrap();
724 git_repo(&d, &["a.js".into(), "eslint.config.mjs".into()]);
725 std::fs::write(d.join("eslint.config.mjs"), "export default [];\n").unwrap();
727 let eslint = d.join("node_modules/.bin/eslint");
728 std::fs::write(&eslint, "#!/bin/sh\necho '{\"rules\":{}}'\n").unwrap();
729 std::fs::set_permissions(&eslint, std::fs::Permissions::from_mode(0o755)).unwrap();
730 let ns = d.join("ns");
731 std::fs::create_dir_all(&ns).unwrap();
732 assert!(typed_eslint(
733 &d,
734 &ns,
735 std::time::Instant::now() + std::time::Duration::from_secs(20),
736 &std::sync::atomic::AtomicBool::new(false),
737 ));
738 assert!(!ns.join(TYPED).exists() && !ns.join(UNTYPED).exists());
739 let _ = std::fs::remove_dir_all(d);
740 }
741
742 #[test]
745 fn tree_cache_the_uv_probe_never_syncs() {
746 if std::process::Command::new("uv")
747 .arg("--version")
748 .output()
749 .is_err()
750 {
751 eprintln!("uv unavailable — skipping");
752 return;
753 }
754 let d = std::env::temp_dir().join(format!("amont-uv-probe-{}", std::process::id()));
755 let _ = std::fs::remove_dir_all(&d);
756 std::fs::create_dir_all(&d).unwrap();
757 std::fs::write(
758 d.join("pyproject.toml"),
759 "[project]\nname = \"probe\"\nversion = \"0.0.0\"\nrequires-python = \">=3.9\"\ndependencies = [\"six\"]\n",
760 )
761 .unwrap();
762 let made = std::process::Command::new("uv")
763 .args(["venv", "-q", ".venv"])
764 .current_dir(&d)
765 .status()
766 .map(|s| s.success())
767 .unwrap_or(false);
768 if !made {
769 eprintln!("uv venv failed — skipping");
770 let _ = std::fs::remove_dir_all(d);
771 return;
772 }
773 let mtime = |p: &Path| std::fs::metadata(p).and_then(|m| m.modified()).ok();
774 let venv = d.join(".venv");
775 let before = (mtime(&venv), mtime(&venv.join("pyvenv.cfg")));
776 std::thread::sleep(std::time::Duration::from_millis(1100));
777 let gate =
778 crate::manifest::tree_gates("tree pr pyright * attest uv run pyright .").remove(0);
779 let _ = tool_version(
780 &d,
781 &gate,
782 std::time::Instant::now() + std::time::Duration::from_secs(30),
783 &std::sync::atomic::AtomicBool::new(false),
784 );
785 assert_eq!(
786 before,
787 (mtime(&venv), mtime(&venv.join("pyvenv.cfg"))),
788 ".venv changed"
789 );
790 assert!(!d.join("uv.lock").exists(), "the probe wrote a lock");
791 fn holds(dir: &Path, name: &str) -> bool {
792 std::fs::read_dir(dir)
793 .into_iter()
794 .flatten()
795 .flatten()
796 .any(|e| {
797 e.file_name().to_string_lossy().starts_with(name)
798 || (e.path().is_dir() && holds(&e.path(), name))
799 })
800 }
801 assert!(!holds(&venv, "six"), "the probe installed a dependency");
802 let _ = std::fs::remove_dir_all(d);
803 }
804
805 #[cfg(unix)]
807 #[test]
808 fn tree_cache_more_pairs_than_the_cap_read_typed_unremembered() {
809 use std::os::unix::fs::PermissionsExt;
810 let d = std::env::temp_dir().join(format!("amont-typed-cap-{}", std::process::id()));
811 let _ = std::fs::remove_dir_all(&d);
812 std::fs::create_dir_all(d.join("node_modules/.bin")).unwrap();
813 let mut paths = Vec::new();
814 for i in 0..=MAX_TYPED_SAMPLE {
815 let p = format!("d{i:04}/f.js");
816 std::fs::create_dir_all(d.join(format!("d{i:04}"))).unwrap();
817 std::fs::write(d.join(&p), "").unwrap();
818 paths.push(p);
819 }
820 git_repo(&d, &paths);
821 let eslint = d.join("node_modules/.bin/eslint");
822 std::fs::write(&eslint, "#!/bin/sh\necho '{\"rules\":{}}'\n").unwrap();
823 std::fs::set_permissions(&eslint, std::fs::Permissions::from_mode(0o755)).unwrap();
824 let ns = d.join("ns");
825 std::fs::create_dir_all(&ns).unwrap();
826 assert!(typed_eslint(
827 &d,
828 &ns,
829 std::time::Instant::now() + std::time::Duration::from_secs(20),
830 &std::sync::atomic::AtomicBool::new(false),
831 ));
832 assert!(!ns.join(TYPED).exists() && !ns.join(UNTYPED).exists());
833 let _ = std::fs::remove_dir_all(d);
834 }
835
836 #[cfg(unix)]
839 #[test]
840 fn tree_cache_many_untyped_files_before_src_do_not_hide_it() {
841 use std::os::unix::fs::PermissionsExt;
842 let d = std::env::temp_dir().join(format!("amont-typed-many-{}", std::process::id()));
843 let _ = std::fs::remove_dir_all(&d);
844 let bin = d.join("node_modules/.bin");
845 std::fs::create_dir_all(&bin).unwrap();
846 std::fs::create_dir_all(d.join("scripts")).unwrap();
847 std::fs::create_dir_all(d.join("src")).unwrap();
848 let mut paths = Vec::new();
849 for i in 0..12 {
850 let p = format!("a{i:02}.config.js");
851 std::fs::write(d.join(&p), "").unwrap();
852 paths.push(p);
853 let p = format!("scripts/s{i:02}.js");
854 std::fs::write(d.join(&p), "").unwrap();
855 paths.push(p);
856 }
857 std::fs::write(d.join("src/z.ts"), "").unwrap();
858 paths.push("src/z.ts".into());
859 assert!(std::process::Command::new("git")
860 .args(["init", "-q"])
861 .current_dir(&d)
862 .status()
863 .unwrap()
864 .success());
865 assert!(std::process::Command::new("git")
866 .arg("add")
867 .args(&paths)
868 .current_dir(&d)
869 .status()
870 .unwrap()
871 .success());
872 let eslint = bin.join("eslint");
873 std::fs::write(
874 &eslint,
875 "#!/bin/sh\ncase \"$2\" in\n src/*) echo '{\"languageOptions\":{\"parserOptions\":{\"projectService\":true}}}' ;;\n *) echo '{\"languageOptions\":{\"parserOptions\":{}}}' ;;\nesac\n",
876 )
877 .unwrap();
878 std::fs::set_permissions(&eslint, std::fs::Permissions::from_mode(0o755)).unwrap();
879 let ns = d.join("ns");
880 std::fs::create_dir_all(&ns).unwrap();
881 assert!(typed_eslint(
882 &d,
883 &ns,
884 std::time::Instant::now() + std::time::Duration::from_secs(30),
885 &std::sync::atomic::AtomicBool::new(false),
886 ));
887 let _ = std::fs::remove_dir_all(d);
888 }
889
890 #[test]
891 fn tree_cache_reads_a_package_version() {
892 let pkg = "{\n \"name\": \"eslint\",\n \"version\": \"9.12.0\",\n \"x\": 1\n}";
893 assert_eq!(json_string_field(pkg, "version").as_deref(), Some("9.12.0"));
894 assert_eq!(json_string_field("{}", "version"), None);
895 }
896}