1use std::collections::{BTreeMap, BTreeSet};
2use std::fmt;
3use std::hash::{Hash, Hasher};
4use std::path::{Path, PathBuf};
5use std::str::FromStr;
6use std::sync::Arc;
7use std::time::{Duration, SystemTime};
8
9use serde::{Deserialize, Serialize};
10
11use crate::cache_freshness::FileFreshness;
12use crate::config::Config;
13use crate::parser::{LangId, SharedSymbolCache};
14
15#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
16#[serde(rename_all = "snake_case")]
17pub enum InspectCategory {
18 Diagnostics,
19 Metrics,
20 Todos,
21 DeadCode,
22 UnusedExports,
23 Duplicates,
24 Cycles,
25 Complexity,
26 CircularDeps,
27 OutdatedDeps,
28 Vulnerabilities,
29 TestCoverageGaps,
30 ApiSurface,
31}
32
33impl InspectCategory {
34 pub const ACTIVE: [InspectCategory; 7] = [
35 InspectCategory::Diagnostics,
36 InspectCategory::Metrics,
37 InspectCategory::Todos,
38 InspectCategory::DeadCode,
39 InspectCategory::UnusedExports,
40 InspectCategory::Duplicates,
41 InspectCategory::Cycles,
42 ];
43
44 pub const DISABLED: [InspectCategory; 6] = [
45 InspectCategory::Complexity,
46 InspectCategory::CircularDeps,
47 InspectCategory::OutdatedDeps,
48 InspectCategory::Vulnerabilities,
49 InspectCategory::TestCoverageGaps,
50 InspectCategory::ApiSurface,
51 ];
52
53 pub fn as_str(self) -> &'static str {
54 match self {
55 InspectCategory::Diagnostics => "diagnostics",
56 InspectCategory::Metrics => "metrics",
57 InspectCategory::Todos => "todos",
58 InspectCategory::DeadCode => "dead_code",
59 InspectCategory::UnusedExports => "unused_exports",
60 InspectCategory::Duplicates => "duplicates",
61 InspectCategory::Cycles => "cycles",
62 InspectCategory::Complexity => "complexity",
63 InspectCategory::CircularDeps => "circular_deps",
64 InspectCategory::OutdatedDeps => "outdated_deps",
65 InspectCategory::Vulnerabilities => "vulnerabilities",
66 InspectCategory::TestCoverageGaps => "test_coverage_gaps",
67 InspectCategory::ApiSurface => "api_surface",
68 }
69 }
70
71 pub fn tier(self) -> InspectTier {
72 match self {
73 InspectCategory::Diagnostics | InspectCategory::Metrics | InspectCategory::Todos => {
74 InspectTier::Tier1
75 }
76 InspectCategory::DeadCode
77 | InspectCategory::UnusedExports
78 | InspectCategory::Duplicates
79 | InspectCategory::Cycles
80 | InspectCategory::Complexity
81 | InspectCategory::CircularDeps
82 | InspectCategory::ApiSurface => InspectTier::Tier2,
83 InspectCategory::OutdatedDeps
84 | InspectCategory::Vulnerabilities
85 | InspectCategory::TestCoverageGaps => InspectTier::Tier3,
86 }
87 }
88
89 pub fn is_tier2(self) -> bool {
90 self.tier() == InspectTier::Tier2
91 }
92
93 pub fn is_active(self) -> bool {
94 Self::ACTIVE.contains(&self)
95 }
96
97 pub fn active() -> &'static [InspectCategory] {
98 &Self::ACTIVE
99 }
100
101 pub fn disabled() -> &'static [InspectCategory] {
102 &Self::DISABLED
103 }
104}
105
106impl fmt::Display for InspectCategory {
107 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
108 formatter.write_str(self.as_str())
109 }
110}
111
112impl FromStr for InspectCategory {
113 type Err = InspectCategoryParseError;
114
115 fn from_str(value: &str) -> Result<Self, Self::Err> {
116 match value {
117 "diagnostics" => Ok(Self::Diagnostics),
118 "metrics" => Ok(Self::Metrics),
119 "todos" => Ok(Self::Todos),
120 "dead_code" => Ok(Self::DeadCode),
121 "unused_exports" => Ok(Self::UnusedExports),
122 "duplicates" => Ok(Self::Duplicates),
123 "cycles" => Ok(Self::Cycles),
124 "complexity" => Ok(Self::Complexity),
125 "circular_deps" => Ok(Self::CircularDeps),
126 "outdated_deps" => Ok(Self::OutdatedDeps),
127 "vulnerabilities" => Ok(Self::Vulnerabilities),
128 "test_coverage_gaps" => Ok(Self::TestCoverageGaps),
129 "api_surface" => Ok(Self::ApiSurface),
130 other => Err(InspectCategoryParseError(other.to_string())),
131 }
132 }
133}
134
135#[derive(Debug, Clone, PartialEq, Eq)]
136pub struct InspectCategoryParseError(String);
137
138impl fmt::Display for InspectCategoryParseError {
139 fn fmt(&self, formatter: &mut fmt::Formatter<'_>) -> fmt::Result {
140 write!(formatter, "unknown inspect category '{}'", self.0)
141 }
142}
143
144impl std::error::Error for InspectCategoryParseError {}
145
146#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)]
147#[serde(rename_all = "snake_case")]
148pub enum InspectTier {
149 Tier1,
150 Tier2,
151 Tier3,
152}
153
154#[derive(Debug, Clone, PartialEq, Eq)]
155pub struct JobScope {
156 project_root: PathBuf,
157 roots: Vec<PathBuf>,
158 scope_hash: String,
159}
160
161impl JobScope {
162 pub fn for_project(project_root: impl Into<PathBuf>) -> Self {
163 let project_root = project_root.into();
164 Self {
165 roots: Vec::new(),
166 scope_hash: "project".to_string(),
167 project_root,
168 }
169 }
170
171 pub fn from_roots(project_root: impl Into<PathBuf>, roots: Vec<PathBuf>) -> Self {
172 let project_root = project_root.into();
173 let mut roots = roots
174 .into_iter()
175 .map(|root| normalize_path(&root))
176 .collect::<Vec<_>>();
177 roots.sort();
178 roots.dedup();
179
180 if roots.is_empty() || (roots.len() == 1 && normalize_path(&project_root) == roots[0]) {
181 return Self::for_project(project_root);
182 }
183
184 let mut hasher = std::collections::hash_map::DefaultHasher::new();
185 for root in &roots {
186 root.to_string_lossy().hash(&mut hasher);
187 "\0".hash(&mut hasher);
188 }
189
190 Self {
191 project_root,
192 roots,
193 scope_hash: format!("{:016x}", hasher.finish()),
194 }
195 }
196
197 pub fn project_root(&self) -> &Path {
198 &self.project_root
199 }
200
201 pub fn roots(&self) -> &[PathBuf] {
202 &self.roots
203 }
204
205 pub fn scope_hash(&self) -> &str {
206 &self.scope_hash
207 }
208
209 pub fn is_project_wide(&self) -> bool {
210 self.roots.is_empty()
211 }
212
213 pub fn contains(&self, path: &Path) -> bool {
214 if self.roots.is_empty() {
215 return true;
216 }
217 let normalized = normalize_path(path);
218 self.roots.iter().any(|root| normalized.starts_with(root))
219 }
220
221 pub fn contains_display_path(&self, value: &str) -> bool {
222 let path = PathBuf::from(value);
223 if path.is_absolute() {
224 self.contains(&path)
225 } else {
226 self.contains(&self.project_root.join(path))
227 }
228 }
229}
230
231#[derive(Debug, Clone, PartialEq, Eq, Hash, PartialOrd, Ord, Serialize, Deserialize)]
232pub struct JobKey {
233 pub category: InspectCategory,
234 #[serde(skip_serializing_if = "Option::is_none")]
235 pub scope_hash: Option<String>,
236}
237
238impl JobKey {
239 pub fn for_category_scope(category: InspectCategory, scope: &JobScope) -> Self {
240 if category.is_tier2() {
241 Self::for_project_category(category)
242 } else {
243 Self {
244 category,
245 scope_hash: Some(scope.scope_hash().to_string()),
246 }
247 }
248 }
249
250 pub fn for_project_category(category: InspectCategory) -> Self {
251 Self {
252 category,
253 scope_hash: None,
254 }
255 }
256
257 pub fn display_key(&self) -> String {
258 match &self.scope_hash {
259 Some(scope_hash) => format!("{}:{scope_hash}", self.category),
260 None => self.category.to_string(),
261 }
262 }
263}
264
265#[derive(Clone)]
266pub struct InspectSnapshot {
267 pub project_root: PathBuf,
268 pub inspect_dir: PathBuf,
269 pub config: Arc<Config>,
270 pub symbol_cache: SharedSymbolCache,
271 pub inspect_writer: bool,
272 pub callgraph_writer: bool,
273}
274
275impl InspectSnapshot {
276 pub fn new(
277 project_root: PathBuf,
278 inspect_dir: PathBuf,
279 config: Arc<Config>,
280 symbol_cache: SharedSymbolCache,
281 ) -> Self {
282 Self::new_with_capabilities(project_root, inspect_dir, config, symbol_cache, true, true)
283 }
284
285 pub fn new_with_capabilities(
286 project_root: PathBuf,
287 inspect_dir: PathBuf,
288 config: Arc<Config>,
289 symbol_cache: SharedSymbolCache,
290 inspect_writer: bool,
291 callgraph_writer: bool,
292 ) -> Self {
293 Self {
294 project_root,
295 inspect_dir,
296 config,
297 symbol_cache,
298 inspect_writer,
299 callgraph_writer,
300 }
301 }
302}
303
304#[derive(Clone)]
305pub struct WorkerCtx {
306 pub project_root: PathBuf,
307 pub inspect_dir: PathBuf,
308 pub config: Arc<Config>,
309 pub symbol_cache: SharedSymbolCache,
310}
311
312impl From<&InspectSnapshot> for WorkerCtx {
313 fn from(snapshot: &InspectSnapshot) -> Self {
314 Self {
315 project_root: snapshot.project_root.clone(),
316 inspect_dir: snapshot.inspect_dir.clone(),
317 config: Arc::clone(&snapshot.config),
318 symbol_cache: Arc::clone(&snapshot.symbol_cache),
319 }
320 }
321}
322
323#[derive(Clone)]
324pub struct InspectJob {
325 pub job_id: u64,
326 pub key: JobKey,
327 pub category: InspectCategory,
328 pub scope_files: Vec<PathBuf>,
329 pub project_root: PathBuf,
330 pub inspect_dir: PathBuf,
331 pub config: Arc<Config>,
332 pub symbol_cache: SharedSymbolCache,
333 pub inspect_writer: bool,
334 pub callgraph_writer: bool,
335 pub callgraph_snapshot: Option<Arc<CallgraphSnapshot>>,
336}
337
338impl InspectJob {
339 pub fn worker_ctx(&self) -> WorkerCtx {
340 WorkerCtx {
341 project_root: self.project_root.clone(),
342 inspect_dir: self.inspect_dir.clone(),
343 config: Arc::clone(&self.config),
344 symbol_cache: Arc::clone(&self.symbol_cache),
345 }
346 }
347}
348
349pub(crate) fn is_js_ts_language(language: LangId) -> bool {
350 matches!(
351 language,
352 LangId::TypeScript | LangId::Tsx | LangId::JavaScript
353 )
354}
355
356pub(crate) fn dead_code_supports_language(language: LangId) -> bool {
357 is_js_ts_language(language) || callgraph_store_dead_code_supports_language(language)
358}
359
360pub(crate) fn dead_code_skipped_language(file: &Path) -> Option<&'static str> {
361 let language = crate::parser::detect_language(file)?;
362 (!dead_code_supports_language(language)).then(|| language_name(language))
363}
364
365fn callgraph_store_dead_code_supports_language(language: LangId) -> bool {
366 let supported_by_store_liveness = matches!(
372 language,
373 LangId::Rust | LangId::Go | LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp
374 );
375 supported_by_store_liveness && !crate::calls::call_node_kinds(language).is_empty()
376}
377
378pub(crate) fn language_name(language: LangId) -> &'static str {
379 match language {
380 LangId::TypeScript => "typescript",
381 LangId::Tsx => "tsx",
382 LangId::JavaScript => "javascript",
383 LangId::Python => "python",
384 LangId::Rust => "rust",
385 LangId::Go => "go",
386 LangId::C => "c",
387 LangId::Cpp => "cpp",
388 LangId::Zig => "zig",
389 LangId::CSharp => "csharp",
390 LangId::Bash => "bash",
391 LangId::Html => "html",
392 LangId::Markdown => "markdown",
393 LangId::Yaml => "yaml",
394 LangId::Solidity => "solidity",
395 LangId::Scss => "scss",
396 LangId::Vue => "vue",
397 LangId::Json => "json",
398 LangId::Scala => "scala",
399 LangId::Java => "java",
400 LangId::Ruby => "ruby",
401 LangId::Kotlin => "kotlin",
402 LangId::Swift => "swift",
403 LangId::Php => "php",
404 LangId::Lua => "lua",
405 LangId::Perl => "perl",
406 LangId::Pascal => "pascal",
407 LangId::R => "r",
408 LangId::Groovy => "groovy",
409 LangId::ObjC => "objc",
410 }
411}
412
413#[derive(Debug, Clone, Default)]
414pub struct CallgraphSnapshot {
415 pub generated_at: Option<SystemTime>,
416 pub files: Vec<PathBuf>,
417 pub exported_symbols: Vec<CallgraphExport>,
418 pub outbound_calls: Vec<CallgraphOutboundCall>,
419 pub entry_points: BTreeSet<PathBuf>,
420 pub entry_point_symbols: BTreeMap<PathBuf, BTreeSet<String>>,
421}
422
423#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
424pub struct CallgraphExport {
425 pub file: PathBuf,
426 pub symbol: String,
427 pub kind: String,
428 pub line: u32,
429}
430
431pub(crate) const DISPATCHED_CALLEE_SEPARATOR: char = '\u{1f}';
432pub(crate) const CALLGRAPH_PROVENANCE_TREESITTER: &str = "treesitter";
433pub(crate) const CALLGRAPH_PROVENANCE_REEXPORT: &str = "reexport";
434
435fn default_callgraph_outbound_provenance() -> String {
436 CALLGRAPH_PROVENANCE_TREESITTER.to_string()
437}
438
439#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
440pub struct CallgraphOutboundCall {
441 pub caller_file: PathBuf,
442 pub caller_symbol: String,
443 pub target: String,
444 pub line: u32,
445 #[serde(default = "default_callgraph_outbound_provenance")]
446 pub provenance: String,
447}
448
449#[derive(Debug, Clone)]
450pub struct FileContribution {
451 pub category: InspectCategory,
452 pub file_path: PathBuf,
453 pub freshness: FileFreshness,
454 pub contribution: serde_json::Value,
455 pub type_ref_names: BTreeSet<String>,
456}
457
458impl FileContribution {
459 pub fn new(
460 category: InspectCategory,
461 file_path: impl Into<PathBuf>,
462 freshness: FileFreshness,
463 contribution: serde_json::Value,
464 ) -> Self {
465 let type_ref_names = type_ref_names_from_contribution(&contribution);
466 Self {
467 category,
468 file_path: file_path.into(),
469 freshness,
470 contribution,
471 type_ref_names,
472 }
473 }
474
475 pub fn with_type_ref_names<I>(mut self, type_ref_names: I) -> Self
476 where
477 I: IntoIterator<Item = String>,
478 {
479 self.type_ref_names = type_ref_names.into_iter().collect();
480 self.contribution =
481 contribution_with_type_ref_names(self.contribution, &self.type_ref_names);
482 self
483 }
484}
485
486pub(crate) fn type_ref_names_from_contribution(
487 contribution: &serde_json::Value,
488) -> BTreeSet<String> {
489 contribution
490 .get("type_ref_names")
491 .and_then(serde_json::Value::as_array)
492 .into_iter()
493 .flatten()
494 .filter_map(serde_json::Value::as_str)
495 .map(str::trim)
496 .filter(|name| !name.is_empty())
497 .map(str::to_string)
498 .collect()
499}
500
501pub(crate) fn contribution_with_type_ref_names(
502 mut contribution: serde_json::Value,
503 type_ref_names: &BTreeSet<String>,
504) -> serde_json::Value {
505 if let serde_json::Value::Object(object) = &mut contribution {
506 if type_ref_names.is_empty() {
507 object.remove("type_ref_names");
508 } else {
509 object.insert(
510 "type_ref_names".to_string(),
511 serde_json::Value::Array(
512 type_ref_names
513 .iter()
514 .map(|name| serde_json::Value::String(name.clone()))
515 .collect(),
516 ),
517 );
518 }
519 }
520 contribution
521}
522
523#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
524#[serde(rename_all = "snake_case")]
525pub enum JobStatus {
526 Queued,
527 Running,
528 Completed,
529 Failed,
530}
531
532#[derive(Debug, Clone)]
533pub struct InspectScanSuccess {
534 pub scanned_files: Vec<PathBuf>,
535 pub contributions: Vec<FileContribution>,
536 pub aggregate: serde_json::Value,
537}
538
539#[derive(Debug, Clone)]
540pub struct InspectResult {
541 pub job_id: u64,
542 pub key: JobKey,
543 pub category: InspectCategory,
544 pub project_root: PathBuf,
545 pub inspect_dir: PathBuf,
546 pub config: Arc<Config>,
547 pub outcome: Result<InspectScanSuccess, String>,
548 pub duration: Duration,
549}
550
551impl InspectResult {
552 pub fn success(job: &InspectJob, success: InspectScanSuccess, duration: Duration) -> Self {
553 Self {
554 job_id: job.job_id,
555 key: job.key.clone(),
556 category: job.category,
557 project_root: job.project_root.clone(),
558 inspect_dir: job.inspect_dir.clone(),
559 config: Arc::clone(&job.config),
560 outcome: Ok(success),
561 duration,
562 }
563 }
564
565 pub fn failed(job: &InspectJob, message: impl Into<String>, duration: Duration) -> Self {
566 Self {
567 job_id: job.job_id,
568 key: job.key.clone(),
569 category: job.category,
570 project_root: job.project_root.clone(),
571 inspect_dir: job.inspect_dir.clone(),
572 config: Arc::clone(&job.config),
573 outcome: Err(message.into()),
574 duration,
575 }
576 }
577}
578
579#[derive(Debug, Clone, Serialize)]
580#[serde(tag = "status", rename_all = "snake_case")]
581pub enum JobOutcome {
582 Fresh {
583 payload: serde_json::Value,
584 },
585 Stale {
586 cached: Option<serde_json::Value>,
587 in_flight: bool,
588 },
589 Pending {
590 in_flight: bool,
591 },
592 Failed {
593 message: String,
594 },
595}
596
597impl JobOutcome {
598 pub fn payload(&self) -> Option<&serde_json::Value> {
599 match self {
600 JobOutcome::Fresh { payload } => Some(payload),
601 JobOutcome::Stale { cached, .. } => cached.as_ref(),
602 JobOutcome::Pending { .. } | JobOutcome::Failed { .. } => None,
603 }
604 }
605
606 pub fn is_stale(&self) -> bool {
607 matches!(self, JobOutcome::Stale { .. })
608 }
609
610 pub fn is_pending(&self) -> bool {
611 matches!(self, JobOutcome::Pending { .. })
612 }
613
614 pub fn summary_status(&self) -> Option<&'static str> {
615 match self {
616 JobOutcome::Fresh { .. } => None,
617 JobOutcome::Stale { .. } => Some("stale"),
618 JobOutcome::Pending { .. } => Some("pending"),
619 JobOutcome::Failed { .. } => Some("failed"),
620 }
621 }
622}
623
624pub(crate) fn is_test_support_file(relative_path: &str) -> bool {
635 let normalized = relative_path.replace('\\', "/");
636 normalized.split('/').any(|segment| {
637 matches!(
638 segment,
639 "fixtures"
640 | "__fixtures__"
641 | "testdata"
642 | "test-data"
643 | "__mocks__"
644 | "__snapshots__"
645 | "corpora"
646 )
647 })
648}
649
650pub(crate) fn is_test_file(relative_path: &str) -> bool {
660 let normalized = relative_path.replace('\\', "/");
661
662 if normalized
666 .split('/')
667 .any(|segment| matches!(segment, "__tests__" | "__test__" | "tests"))
668 {
669 return true;
670 }
671
672 let file = normalized.rsplit('/').next().unwrap_or(&normalized);
673 let lower = file.to_ascii_lowercase();
674
675 if lower.contains(".test.") || lower.contains(".spec.") {
677 return true;
678 }
679
680 if lower.ends_with("_test.rs")
682 || lower.ends_with("_test.go")
683 || lower.ends_with("_test.py")
684 || lower.ends_with("_test.rb")
685 || lower.ends_with("_test.exs")
686 || lower.ends_with("_spec.rb")
687 || (lower.starts_with("test_") && lower.ends_with(".py"))
688 {
689 return true;
690 }
691
692 const CAMEL_SUFFIXES: &[&str] = &[
695 "Test.java",
696 "Tests.java",
697 "Test.kt",
698 "Tests.kt",
699 "Test.cs",
700 "Tests.cs",
701 "Test.swift",
702 "Tests.swift",
703 "Test.scala",
704 "Spec.scala",
705 ];
706 CAMEL_SUFFIXES.iter().any(|suffix| file.ends_with(suffix))
707}
708
709pub(crate) fn normalize_path(path: &Path) -> PathBuf {
710 #[cfg(windows)]
716 let path = &windows_non_verbatim_path(path);
717
718 let mut result = PathBuf::new();
719 for component in path.components() {
720 match component {
721 std::path::Component::CurDir => {}
722 std::path::Component::ParentDir => {
723 if !result.pop() {
724 result.push(component);
725 }
726 }
727 other => result.push(other.as_os_str()),
728 }
729 }
730 result
731}
732
733pub(crate) fn canonicalize_normalized(path: &Path) -> PathBuf {
740 match std::fs::canonicalize(path) {
741 Ok(canonical) => normalize_path(&canonical),
742 Err(_) => normalize_path(path),
743 }
744}
745
746#[cfg(windows)]
747fn windows_non_verbatim_path(path: &Path) -> PathBuf {
748 let mut raw = path.to_string_lossy().replace('/', "\\");
749 if let Some(stripped) = raw.strip_prefix("\\\\?\\UNC\\") {
750 raw = format!("\\\\{stripped}");
751 } else if let Some(stripped) = raw.strip_prefix("\\\\?\\") {
752 raw = stripped.to_string();
753 } else if let Some(stripped) = raw.strip_prefix("\\\\??\\") {
754 raw = stripped.to_string();
755 }
756
757 if raw.as_bytes().get(1) == Some(&b':') {
758 let drive = raw.as_bytes()[0];
759 if drive.is_ascii_lowercase() {
760 raw.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string());
761 }
762 }
763
764 PathBuf::from(raw)
765}
766
767#[cfg(test)]
768mod test_support_tests {
769 use super::{is_test_file, is_test_support_file};
770
771 #[test]
772 fn is_test_file_matches_real_test_files() {
773 for p in [
775 "src/foo.test.ts",
776 "src/foo.test.tsx",
777 "src/bar.spec.js",
778 "packages/x/component.test.jsx",
779 "app/foo.test.mjs",
780 "src/comp.spec.vue",
781 ] {
782 assert!(is_test_file(p), "{p} should be a test file");
783 }
784 assert!(is_test_file("packages/x/__tests__/reading.ts"));
786 assert!(is_test_file("crates/aft/tests/integration/main.rs"));
787 assert!(is_test_file("crates/aft/src/foo_test.rs"));
789 assert!(is_test_file("pkg/handler_test.go"));
790 assert!(is_test_file("app/test_models.py"));
791 assert!(is_test_file("app/models_test.py"));
792 assert!(is_test_file("spec/user_spec.rb"));
793 assert!(is_test_file("src/main/UserServiceTest.java"));
795 assert!(is_test_file("src/FooTests.cs"));
796 assert!(is_test_file("Sources/AppTests.swift"));
797 assert!(is_test_file("packages\\x\\__tests__\\a.ts"));
799 }
800
801 #[test]
802 fn is_test_file_rejects_product_files() {
803 for p in [
804 "crates/aft/src/inspect/job.rs",
805 "packages/x/src/index.ts",
806 "src/contestant.ts", "src/greatest.ts", "src/latest.java", "src/my_attestation.py", "src/test/helper.ts", ] {
812 assert!(!is_test_file(p), "{p} must NOT be a test file");
813 }
814 }
815
816 #[test]
817 fn matches_conventional_support_dirs() {
818 assert!(is_test_support_file("crates/aft/tests/fixtures/sample.ts"));
819 assert!(is_test_support_file(
820 "packages/x/__tests__/e2e/fixtures/a.ts"
821 ));
822 assert!(is_test_support_file(
823 "benchmarks/codegraph/corpora/repo/lib.go"
824 ));
825 assert!(is_test_support_file("src/__mocks__/fs.ts"));
826 assert!(is_test_support_file("src/__snapshots__/render.snap"));
827 assert!(is_test_support_file("internal/testdata/golden.json"));
828 assert!(is_test_support_file("crates\\aft\\tests\\fixtures\\x.rs"));
830 }
831
832 #[test]
833 fn does_not_match_product_or_test_files() {
834 assert!(!is_test_support_file("crates/aft/src/inspect/job.rs"));
836 assert!(!is_test_support_file(
838 "packages/x/__tests__/reading.test.ts"
839 ));
840 assert!(!is_test_support_file(
841 "crates/aft/tests/integration/main.rs"
842 ));
843 assert!(!is_test_support_file("src/fixturesHelper.ts"));
845 assert!(!is_test_support_file("src/my_corpora_loader.rs"));
846 }
847}