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}
272
273impl InspectSnapshot {
274 pub fn new(
275 project_root: PathBuf,
276 inspect_dir: PathBuf,
277 config: Arc<Config>,
278 symbol_cache: SharedSymbolCache,
279 ) -> Self {
280 Self {
281 project_root,
282 inspect_dir,
283 config,
284 symbol_cache,
285 }
286 }
287}
288
289#[derive(Clone)]
290pub struct WorkerCtx {
291 pub project_root: PathBuf,
292 pub inspect_dir: PathBuf,
293 pub config: Arc<Config>,
294 pub symbol_cache: SharedSymbolCache,
295}
296
297impl From<&InspectSnapshot> for WorkerCtx {
298 fn from(snapshot: &InspectSnapshot) -> Self {
299 Self {
300 project_root: snapshot.project_root.clone(),
301 inspect_dir: snapshot.inspect_dir.clone(),
302 config: Arc::clone(&snapshot.config),
303 symbol_cache: Arc::clone(&snapshot.symbol_cache),
304 }
305 }
306}
307
308#[derive(Clone)]
309pub struct InspectJob {
310 pub job_id: u64,
311 pub key: JobKey,
312 pub category: InspectCategory,
313 pub scope_files: Vec<PathBuf>,
314 pub project_root: PathBuf,
315 pub inspect_dir: PathBuf,
316 pub config: Arc<Config>,
317 pub symbol_cache: SharedSymbolCache,
318 pub callgraph_snapshot: Option<Arc<CallgraphSnapshot>>,
319}
320
321impl InspectJob {
322 pub fn worker_ctx(&self) -> WorkerCtx {
323 WorkerCtx {
324 project_root: self.project_root.clone(),
325 inspect_dir: self.inspect_dir.clone(),
326 config: Arc::clone(&self.config),
327 symbol_cache: Arc::clone(&self.symbol_cache),
328 }
329 }
330}
331
332pub(crate) fn is_js_ts_language(language: LangId) -> bool {
333 matches!(
334 language,
335 LangId::TypeScript | LangId::Tsx | LangId::JavaScript
336 )
337}
338
339pub(crate) fn dead_code_supports_language(language: LangId) -> bool {
340 is_js_ts_language(language) || callgraph_store_dead_code_supports_language(language)
341}
342
343pub(crate) fn dead_code_skipped_language(file: &Path) -> Option<&'static str> {
344 let language = crate::parser::detect_language(file)?;
345 (!dead_code_supports_language(language)).then(|| language_name(language))
346}
347
348fn callgraph_store_dead_code_supports_language(language: LangId) -> bool {
349 let supported_by_store_liveness = matches!(
355 language,
356 LangId::Rust | LangId::Go | LangId::C | LangId::Cpp | LangId::Zig | LangId::CSharp
357 );
358 supported_by_store_liveness && !crate::calls::call_node_kinds(language).is_empty()
359}
360
361pub(crate) fn language_name(language: LangId) -> &'static str {
362 match language {
363 LangId::TypeScript => "typescript",
364 LangId::Tsx => "tsx",
365 LangId::JavaScript => "javascript",
366 LangId::Python => "python",
367 LangId::Rust => "rust",
368 LangId::Go => "go",
369 LangId::C => "c",
370 LangId::Cpp => "cpp",
371 LangId::Zig => "zig",
372 LangId::CSharp => "csharp",
373 LangId::Bash => "bash",
374 LangId::Html => "html",
375 LangId::Markdown => "markdown",
376 LangId::Yaml => "yaml",
377 LangId::Solidity => "solidity",
378 LangId::Scss => "scss",
379 LangId::Vue => "vue",
380 LangId::Json => "json",
381 LangId::Scala => "scala",
382 LangId::Java => "java",
383 LangId::Ruby => "ruby",
384 LangId::Kotlin => "kotlin",
385 LangId::Swift => "swift",
386 LangId::Php => "php",
387 LangId::Lua => "lua",
388 LangId::Perl => "perl",
389 LangId::Pascal => "pascal",
390 LangId::R => "r",
391 LangId::ObjC => "objc",
392 }
393}
394
395#[derive(Debug, Clone, Default)]
396pub struct CallgraphSnapshot {
397 pub generated_at: Option<SystemTime>,
398 pub files: Vec<PathBuf>,
399 pub exported_symbols: Vec<CallgraphExport>,
400 pub outbound_calls: Vec<CallgraphOutboundCall>,
401 pub entry_points: BTreeSet<PathBuf>,
402 pub entry_point_symbols: BTreeMap<PathBuf, BTreeSet<String>>,
403}
404
405#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
406pub struct CallgraphExport {
407 pub file: PathBuf,
408 pub symbol: String,
409 pub kind: String,
410 pub line: u32,
411}
412
413pub(crate) const DISPATCHED_CALLEE_SEPARATOR: char = '\u{1f}';
414pub(crate) const CALLGRAPH_PROVENANCE_TREESITTER: &str = "treesitter";
415pub(crate) const CALLGRAPH_PROVENANCE_REEXPORT: &str = "reexport";
416
417fn default_callgraph_outbound_provenance() -> String {
418 CALLGRAPH_PROVENANCE_TREESITTER.to_string()
419}
420
421#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
422pub struct CallgraphOutboundCall {
423 pub caller_file: PathBuf,
424 pub caller_symbol: String,
425 pub target: String,
426 pub line: u32,
427 #[serde(default = "default_callgraph_outbound_provenance")]
428 pub provenance: String,
429}
430
431#[derive(Debug, Clone)]
432pub struct FileContribution {
433 pub category: InspectCategory,
434 pub file_path: PathBuf,
435 pub freshness: FileFreshness,
436 pub contribution: serde_json::Value,
437 pub type_ref_names: BTreeSet<String>,
438}
439
440impl FileContribution {
441 pub fn new(
442 category: InspectCategory,
443 file_path: impl Into<PathBuf>,
444 freshness: FileFreshness,
445 contribution: serde_json::Value,
446 ) -> Self {
447 let type_ref_names = type_ref_names_from_contribution(&contribution);
448 Self {
449 category,
450 file_path: file_path.into(),
451 freshness,
452 contribution,
453 type_ref_names,
454 }
455 }
456
457 pub fn with_type_ref_names<I>(mut self, type_ref_names: I) -> Self
458 where
459 I: IntoIterator<Item = String>,
460 {
461 self.type_ref_names = type_ref_names.into_iter().collect();
462 self.contribution =
463 contribution_with_type_ref_names(self.contribution, &self.type_ref_names);
464 self
465 }
466}
467
468pub(crate) fn type_ref_names_from_contribution(
469 contribution: &serde_json::Value,
470) -> BTreeSet<String> {
471 contribution
472 .get("type_ref_names")
473 .and_then(serde_json::Value::as_array)
474 .into_iter()
475 .flatten()
476 .filter_map(serde_json::Value::as_str)
477 .map(str::trim)
478 .filter(|name| !name.is_empty())
479 .map(str::to_string)
480 .collect()
481}
482
483pub(crate) fn contribution_with_type_ref_names(
484 mut contribution: serde_json::Value,
485 type_ref_names: &BTreeSet<String>,
486) -> serde_json::Value {
487 if let serde_json::Value::Object(object) = &mut contribution {
488 if type_ref_names.is_empty() {
489 object.remove("type_ref_names");
490 } else {
491 object.insert(
492 "type_ref_names".to_string(),
493 serde_json::Value::Array(
494 type_ref_names
495 .iter()
496 .map(|name| serde_json::Value::String(name.clone()))
497 .collect(),
498 ),
499 );
500 }
501 }
502 contribution
503}
504
505#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
506#[serde(rename_all = "snake_case")]
507pub enum JobStatus {
508 Queued,
509 Running,
510 Completed,
511 Failed,
512}
513
514#[derive(Debug, Clone)]
515pub struct InspectScanSuccess {
516 pub scanned_files: Vec<PathBuf>,
517 pub contributions: Vec<FileContribution>,
518 pub aggregate: serde_json::Value,
519}
520
521#[derive(Debug, Clone)]
522pub struct InspectResult {
523 pub job_id: u64,
524 pub key: JobKey,
525 pub category: InspectCategory,
526 pub project_root: PathBuf,
527 pub inspect_dir: PathBuf,
528 pub config: Arc<Config>,
529 pub outcome: Result<InspectScanSuccess, String>,
530 pub duration: Duration,
531}
532
533impl InspectResult {
534 pub fn success(job: &InspectJob, success: InspectScanSuccess, duration: Duration) -> Self {
535 Self {
536 job_id: job.job_id,
537 key: job.key.clone(),
538 category: job.category,
539 project_root: job.project_root.clone(),
540 inspect_dir: job.inspect_dir.clone(),
541 config: Arc::clone(&job.config),
542 outcome: Ok(success),
543 duration,
544 }
545 }
546
547 pub fn failed(job: &InspectJob, message: impl Into<String>, duration: Duration) -> Self {
548 Self {
549 job_id: job.job_id,
550 key: job.key.clone(),
551 category: job.category,
552 project_root: job.project_root.clone(),
553 inspect_dir: job.inspect_dir.clone(),
554 config: Arc::clone(&job.config),
555 outcome: Err(message.into()),
556 duration,
557 }
558 }
559}
560
561#[derive(Debug, Clone, Serialize)]
562#[serde(tag = "status", rename_all = "snake_case")]
563pub enum JobOutcome {
564 Fresh {
565 payload: serde_json::Value,
566 },
567 Stale {
568 cached: Option<serde_json::Value>,
569 in_flight: bool,
570 },
571 Pending {
572 in_flight: bool,
573 },
574 Failed {
575 message: String,
576 },
577}
578
579impl JobOutcome {
580 pub fn payload(&self) -> Option<&serde_json::Value> {
581 match self {
582 JobOutcome::Fresh { payload } => Some(payload),
583 JobOutcome::Stale { cached, .. } => cached.as_ref(),
584 JobOutcome::Pending { .. } | JobOutcome::Failed { .. } => None,
585 }
586 }
587
588 pub fn is_stale(&self) -> bool {
589 matches!(self, JobOutcome::Stale { .. })
590 }
591
592 pub fn is_pending(&self) -> bool {
593 matches!(self, JobOutcome::Pending { .. })
594 }
595
596 pub fn summary_status(&self) -> Option<&'static str> {
597 match self {
598 JobOutcome::Fresh { .. } => None,
599 JobOutcome::Stale { .. } => Some("stale"),
600 JobOutcome::Pending { .. } => Some("pending"),
601 JobOutcome::Failed { .. } => Some("failed"),
602 }
603 }
604}
605
606pub(crate) fn is_test_support_file(relative_path: &str) -> bool {
617 let normalized = relative_path.replace('\\', "/");
618 normalized.split('/').any(|segment| {
619 matches!(
620 segment,
621 "fixtures"
622 | "__fixtures__"
623 | "testdata"
624 | "test-data"
625 | "__mocks__"
626 | "__snapshots__"
627 | "corpora"
628 )
629 })
630}
631
632pub(crate) fn is_test_file(relative_path: &str) -> bool {
642 let normalized = relative_path.replace('\\', "/");
643
644 if normalized
648 .split('/')
649 .any(|segment| matches!(segment, "__tests__" | "__test__" | "tests"))
650 {
651 return true;
652 }
653
654 let file = normalized.rsplit('/').next().unwrap_or(&normalized);
655 let lower = file.to_ascii_lowercase();
656
657 if lower.contains(".test.") || lower.contains(".spec.") {
659 return true;
660 }
661
662 if lower.ends_with("_test.rs")
664 || lower.ends_with("_test.go")
665 || lower.ends_with("_test.py")
666 || lower.ends_with("_test.rb")
667 || lower.ends_with("_test.exs")
668 || lower.ends_with("_spec.rb")
669 || (lower.starts_with("test_") && lower.ends_with(".py"))
670 {
671 return true;
672 }
673
674 const CAMEL_SUFFIXES: &[&str] = &[
677 "Test.java",
678 "Tests.java",
679 "Test.kt",
680 "Tests.kt",
681 "Test.cs",
682 "Tests.cs",
683 "Test.swift",
684 "Tests.swift",
685 "Test.scala",
686 "Spec.scala",
687 ];
688 CAMEL_SUFFIXES.iter().any(|suffix| file.ends_with(suffix))
689}
690
691pub(crate) fn normalize_path(path: &Path) -> PathBuf {
692 #[cfg(windows)]
698 let path = &windows_non_verbatim_path(path);
699
700 let mut result = PathBuf::new();
701 for component in path.components() {
702 match component {
703 std::path::Component::CurDir => {}
704 std::path::Component::ParentDir => {
705 if !result.pop() {
706 result.push(component);
707 }
708 }
709 other => result.push(other.as_os_str()),
710 }
711 }
712 result
713}
714
715pub(crate) fn canonicalize_normalized(path: &Path) -> PathBuf {
722 match std::fs::canonicalize(path) {
723 Ok(canonical) => normalize_path(&canonical),
724 Err(_) => normalize_path(path),
725 }
726}
727
728#[cfg(windows)]
729fn windows_non_verbatim_path(path: &Path) -> PathBuf {
730 let mut raw = path.to_string_lossy().replace('/', "\\");
731 if let Some(stripped) = raw.strip_prefix("\\\\?\\UNC\\") {
732 raw = format!("\\\\{stripped}");
733 } else if let Some(stripped) = raw.strip_prefix("\\\\?\\") {
734 raw = stripped.to_string();
735 } else if let Some(stripped) = raw.strip_prefix("\\\\??\\") {
736 raw = stripped.to_string();
737 }
738
739 if raw.as_bytes().get(1) == Some(&b':') {
740 let drive = raw.as_bytes()[0];
741 if drive.is_ascii_lowercase() {
742 raw.replace_range(0..1, &(drive as char).to_ascii_uppercase().to_string());
743 }
744 }
745
746 PathBuf::from(raw)
747}
748
749#[cfg(test)]
750mod test_support_tests {
751 use super::{is_test_file, is_test_support_file};
752
753 #[test]
754 fn is_test_file_matches_real_test_files() {
755 for p in [
757 "src/foo.test.ts",
758 "src/foo.test.tsx",
759 "src/bar.spec.js",
760 "packages/x/component.test.jsx",
761 "app/foo.test.mjs",
762 "src/comp.spec.vue",
763 ] {
764 assert!(is_test_file(p), "{p} should be a test file");
765 }
766 assert!(is_test_file("packages/x/__tests__/reading.ts"));
768 assert!(is_test_file("crates/aft/tests/integration/main.rs"));
769 assert!(is_test_file("crates/aft/src/foo_test.rs"));
771 assert!(is_test_file("pkg/handler_test.go"));
772 assert!(is_test_file("app/test_models.py"));
773 assert!(is_test_file("app/models_test.py"));
774 assert!(is_test_file("spec/user_spec.rb"));
775 assert!(is_test_file("src/main/UserServiceTest.java"));
777 assert!(is_test_file("src/FooTests.cs"));
778 assert!(is_test_file("Sources/AppTests.swift"));
779 assert!(is_test_file("packages\\x\\__tests__\\a.ts"));
781 }
782
783 #[test]
784 fn is_test_file_rejects_product_files() {
785 for p in [
786 "crates/aft/src/inspect/job.rs",
787 "packages/x/src/index.ts",
788 "src/contestant.ts", "src/greatest.ts", "src/latest.java", "src/my_attestation.py", "src/test/helper.ts", ] {
794 assert!(!is_test_file(p), "{p} must NOT be a test file");
795 }
796 }
797
798 #[test]
799 fn matches_conventional_support_dirs() {
800 assert!(is_test_support_file("crates/aft/tests/fixtures/sample.ts"));
801 assert!(is_test_support_file(
802 "packages/x/__tests__/e2e/fixtures/a.ts"
803 ));
804 assert!(is_test_support_file(
805 "benchmarks/codegraph/corpora/repo/lib.go"
806 ));
807 assert!(is_test_support_file("src/__mocks__/fs.ts"));
808 assert!(is_test_support_file("src/__snapshots__/render.snap"));
809 assert!(is_test_support_file("internal/testdata/golden.json"));
810 assert!(is_test_support_file("crates\\aft\\tests\\fixtures\\x.rs"));
812 }
813
814 #[test]
815 fn does_not_match_product_or_test_files() {
816 assert!(!is_test_support_file("crates/aft/src/inspect/job.rs"));
818 assert!(!is_test_support_file(
820 "packages/x/__tests__/reading.test.ts"
821 ));
822 assert!(!is_test_support_file(
823 "crates/aft/tests/integration/main.rs"
824 ));
825 assert!(!is_test_support_file("src/fixturesHelper.ts"));
827 assert!(!is_test_support_file("src/my_corpora_loader.rs"));
828 }
829}