use serde::{Deserialize, Serialize};
use std::path::Path;
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)]
#[serde(rename_all = "lowercase")]
pub enum ArtifactClass {
Product,
Vendored,
Fixture,
Generated,
Template,
}
impl ArtifactClass {
pub fn is_artifact(self) -> bool {
self != ArtifactClass::Product
}
}
#[derive(Debug, Clone, Copy, Default, PartialEq, Eq, Serialize, Deserialize)]
pub struct ArtifactFenceStats {
#[serde(default)]
pub vendored: usize,
#[serde(default)]
pub fixtures: usize,
#[serde(default)]
pub generated: usize,
#[serde(default)]
pub templates: usize,
}
impl ArtifactFenceStats {
pub fn record(&mut self, class: ArtifactClass) {
match class {
ArtifactClass::Product => {}
ArtifactClass::Vendored => self.vendored += 1,
ArtifactClass::Fixture => self.fixtures += 1,
ArtifactClass::Generated => self.generated += 1,
ArtifactClass::Template => self.templates += 1,
}
}
pub fn total(&self) -> usize {
self.vendored + self.fixtures + self.generated + self.templates
}
pub fn is_empty(&self) -> bool {
self.total() == 0
}
pub fn summary_line(&self) -> String {
if self.is_empty() {
return String::new();
}
let mut parts = Vec::new();
if self.vendored > 0 {
parts.push(format!("vendored({})", self.vendored));
}
if self.fixtures > 0 {
parts.push(format!("fixtures({})", self.fixtures));
}
if self.generated > 0 {
parts.push(format!("generated({})", self.generated));
}
if self.templates > 0 {
parts.push(format!("templates({})", self.templates));
}
format!("excluded: {}", parts.join(", "))
}
}
const MINIFIED_LINE_LEN: usize = 5000;
fn content_head_looks_minified(content_head: &str) -> bool {
content_head
.lines()
.take(10)
.any(|line| line.len() > MINIFIED_LINE_LEN)
}
fn path_segment(lower: &str, segment: &str) -> bool {
lower.starts_with(&format!("{}/", segment)) || lower.contains(&format!("/{}/", segment))
}
pub fn artifact_class(path: &str, content_head: Option<&str>) -> ArtifactClass {
let lower = path
.trim_start_matches("./")
.replace('\\', "/")
.to_ascii_lowercase();
let filename = lower.rsplit('/').next().unwrap_or(lower.as_str());
if path_segment(&lower, "fixtures")
|| path_segment(&lower, "fixture")
|| lower.contains("__fixtures__")
|| path_segment(&lower, "testdata")
{
return ArtifactClass::Fixture;
}
if path_segment(&lower, "vendor")
|| path_segment(&lower, "vendored")
|| path_segment(&lower, "node_modules")
|| path_segment(&lower, "third_party")
|| path_segment(&lower, "thirdparty")
|| filename.ends_with(".min.js")
|| filename.ends_with(".min.mjs")
|| filename.ends_with(".min.cjs")
|| filename.ends_with(".min.css")
|| filename.ends_with(".bundle.js")
{
return ArtifactClass::Vendored;
}
if path_segment(&lower, "dist")
|| path_segment(&lower, "public_dist")
|| path_segment(&lower, "__generated__")
|| filename.ends_with(".lock")
|| filename.contains("-lock.")
|| filename.ends_with(".map")
|| is_generated_path(&lower)
{
return ArtifactClass::Generated;
}
if filename.contains(".example")
|| filename.contains(".sample")
|| filename.contains(".template")
|| filename.starts_with("example.")
|| filename.starts_with("sample.")
|| filename.starts_with("template.")
{
return ArtifactClass::Template;
}
if let Some(head) = content_head
&& content_head_looks_minified(head)
{
return ArtifactClass::Vendored;
}
ArtifactClass::Product
}
pub fn is_test_file(path: &str) -> bool {
let p = path.replace('\\', "/").to_lowercase();
let filename = p.rsplit('/').next().unwrap_or(p.as_str());
if p.contains("/tests/")
|| p.starts_with("tests/")
|| p.contains("/test/")
|| p.starts_with("test/")
|| p.contains("__tests__")
|| p.contains("/spec/")
|| p.starts_with("spec/")
|| p.contains("/fixtures/")
|| p.starts_with("fixtures/")
|| p.contains("/mocks/")
|| p.contains("__mocks__")
|| p.contains("test-utils")
|| p.contains("/testing/")
{
return true;
}
filename.contains("_test.")
|| filename.contains(".test.")
|| filename.contains("_spec.")
|| filename.contains(".spec.")
|| filename.contains("_tests.")
|| filename.starts_with("test_")
|| filename.starts_with("spec_")
|| filename.starts_with("tests.")
|| filename == "conftest.py"
|| p.ends_with("setup.ts")
|| p.ends_with("setup.tsx")
}
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
pub enum TestClassification {
Production,
UnitTest,
IntegrationTest,
TestFixture,
TestHelper,
}
pub fn is_dev_file(path: &str) -> bool {
path.contains("__tests__")
|| path.contains("stories")
|| path.contains(".stories.")
|| path.contains("story.")
|| path.contains("fixture")
|| path.contains("fixtures")
}
pub fn detect_language(ext: &str) -> String {
match ext {
"ts" | "tsx" => "ts".to_string(),
"js" | "jsx" | "mjs" | "cjs" => "js".to_string(),
"astro" => "astro".to_string(),
"rs" => "rs".to_string(),
"py" => "py".to_string(),
"rb" => "ruby".to_string(),
"dart" => "dart".to_string(),
"kt" | "kts" => "kotlin".to_string(),
"css" => "css".to_string(),
"sh" | "bash" | "zsh" | "fish" => "shell".to_string(),
"mk" | "make" => "make".to_string(),
"zig" | "zon" => "zig".to_string(),
"swift" => "swift".to_string(),
"m" | "mm" => "objc".to_string(),
"c" | "h" => "c".to_string(),
"cc" | "cpp" | "cxx" | "hpp" => "cpp".to_string(),
other => other.to_string(),
}
}
fn path_extension(lower: &str) -> &str {
lower.rsplit_once('.').map(|(_, ext)| ext).unwrap_or("")
}
pub fn resource_kind(path: &str) -> Option<&'static str> {
let lower = path
.trim_start_matches("./")
.replace('\\', "/")
.to_ascii_lowercase();
let filename = lower.rsplit('/').next().unwrap_or(lower.as_str());
let ext = path_extension(&lower);
if lower.starts_with(".github/workflows/") && matches!(ext, "yml" | "yaml") {
return Some("workflow");
}
if lower.contains("/locales/")
|| lower.starts_with("locales/")
|| lower.contains("/locale/")
|| lower.starts_with("locale/")
|| lower.contains("/i18n/")
|| lower.starts_with("i18n/")
|| lower.contains("/lang/")
|| lower.starts_with("lang/")
{
return Some("locale");
}
if matches!(ext, "md" | "markdown" | "mdx" | "rst") {
return Some("doc");
}
let config_filename = matches!(
filename,
".loctignore"
| ".loctreeignore"
| "cargo.toml"
| "package.json"
| "pyproject.toml"
| "tsconfig.json"
| "deno.json"
| "deno.jsonc"
| "pnpm-workspace.yaml"
| "docker-compose.yml"
| "docker-compose.yaml"
| "config.toml"
| "suppressions.toml"
);
if config_filename
|| lower.contains("/config/")
|| lower.starts_with("config/")
|| lower.contains("/.loctree/")
|| filename.ends_with(".config.js")
|| filename.ends_with(".config.ts")
|| filename.ends_with(".config.json")
|| filename.ends_with(".config.toml")
|| filename.ends_with(".config.yaml")
|| filename.ends_with(".config.yml")
{
return Some("config");
}
if matches!(
ext,
"json"
| "jsonc"
| "toml"
| "yaml"
| "yml"
| "storyboard"
| "xib"
| "properties"
| "xml"
| "svg"
) {
return Some("resource");
}
if ext == "txt" {
return Some("doc");
}
None
}
pub fn detect_language_from_filename(filename: &str) -> String {
match filename {
"Makefile" | "makefile" | "GNUmakefile" | "BSDmakefile" => "make".to_string(),
".loctignore" | ".loctreeignore" => "config".to_string(),
_ => String::new(),
}
}
pub fn is_semantic_code_language(lang: &str) -> bool {
matches!(
lang,
"rs" | "ts"
| "tsx"
| "js"
| "jsx"
| "mjs"
| "cjs"
| "py"
| "go"
| "dart"
| "swift"
| "m"
| "mm"
| "c"
| "cc"
| "cpp"
| "cxx"
| "h"
| "hpp"
| "zig"
| "shell"
| "make"
| "astro"
| "vue"
| "svelte"
)
}
pub fn is_test_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("__tests__")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| lower.starts_with("test_")
|| lower.contains("/tests/")
|| lower.starts_with("tests/")
|| lower.contains("/test_")
}
pub fn should_exclude_from_reports(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("/tests/")
|| lower.starts_with("tests/")
|| lower.contains("__tests__")
|| lower.contains("__mocks__")
|| lower.contains("/fixtures/")
|| lower.contains("/fixture/")
|| lower.contains("__fixtures__")
|| lower.contains("/mocks/")
|| lower.contains("/mock/")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.ts")
|| lower.ends_with("_test.tsx")
|| lower.ends_with("_test.js")
|| lower.ends_with("_test.jsx")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| lower.ends_with("_test.py")
|| lower.contains("/test_utils/")
|| lower.contains("/test_helpers/")
|| lower.contains("/testing/")
}
fn is_story_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("stories") || lower.contains(".story.") || lower.contains(".stories.")
}
fn is_generated_path(path: &str) -> bool {
let lower = path.to_ascii_lowercase();
lower.contains("generated")
|| lower.contains("codegen")
|| lower.contains("/gen/")
|| lower.ends_with(".gen.ts")
|| lower.ends_with(".gen.tsx")
|| lower.ends_with(".gen.rs")
|| lower.ends_with(".g.rs")
|| lower.ends_with(".g.dart")
|| lower.ends_with(".freezed.dart")
|| lower.ends_with(".gr.dart")
|| lower.ends_with(".pb.dart")
|| lower.ends_with(".pbjson.dart")
|| lower.ends_with(".pbenum.dart")
|| lower.ends_with(".pbserver.dart")
|| lower.ends_with(".config.dart")
}
pub fn classify_test_path(path: &Path) -> TestClassification {
let path_str = path.to_str().unwrap_or("");
let lower = path_str.to_ascii_lowercase();
let filename = path.file_name().and_then(|n| n.to_str()).unwrap_or("");
let filename_lower = filename.to_ascii_lowercase();
if (lower.contains("fixture") || lower.contains("fixtures") || lower.contains("mock"))
&& !filename_lower.starts_with("test_")
{
return TestClassification::TestFixture;
}
let is_python = filename_lower.ends_with(".py");
let is_python_test_file = is_python && filename_lower.starts_with("test_");
if !is_python_test_file
&& (filename_lower.contains("test_helper")
|| filename_lower.contains("test_utils")
|| filename_lower == "setup.py"
|| lower.contains("testing/"))
{
return TestClassification::TestHelper;
}
if (lower.starts_with("tests/") || lower.contains("/tests/")) && !lower.contains("__tests__") {
return TestClassification::IntegrationTest;
}
if lower.contains("__tests__")
|| lower.contains(".test.")
|| lower.contains(".spec.")
|| lower.ends_with("_test.rs")
|| lower.ends_with("_tests.rs")
|| lower.ends_with("_test.go")
|| lower.ends_with("_test.dart")
|| filename_lower.starts_with("test_") || lower.contains("/test_")
{
return TestClassification::UnitTest;
}
TestClassification::Production
}
pub fn has_test_code(content: &str, lang: &str) -> bool {
match lang {
"rs" | "rust" => content.contains("#[cfg(test)]") || content.contains("#[test]"),
"ts" | "js" | "tsx" | "jsx" => {
content.contains("describe(") || content.contains("it(") || content.contains("test(")
}
"py" | "python" => {
content.contains("def test_")
|| content.contains("import unittest")
|| content.contains("import pytest")
}
"go" => content.contains("func Test") || content.contains("testing.T"),
_ => false,
}
}
pub fn test_patterns(lang: &str) -> Vec<&'static str> {
match lang {
"rs" | "rust" => vec!["*_test.rs", "*_tests.rs", "tests/**/*.rs"],
"ts" | "tsx" | "js" | "jsx" => vec![
"*.test.ts",
"*.test.tsx",
"*.test.js",
"*.test.jsx",
"*.spec.ts",
"*.spec.tsx",
"*.spec.js",
"*.spec.jsx",
"__tests__/**/*",
],
"py" | "python" => vec!["test_*.py", "*_test.py", "tests/**/*.py"],
"go" => vec!["*_test.go"],
"dart" => vec!["*_test.dart", "test/**/*.dart"],
_ => vec![],
}
}
pub fn file_kind(path: &str) -> (String, bool, bool) {
let generated = is_generated_path(path);
let test = is_test_path(path);
let story = is_story_path(path);
let lower = path.to_ascii_lowercase();
let config = lower.contains("config/")
|| lower.contains("/config/")
|| lower == ".loctignore"
|| lower == ".loctreeignore"
|| lower.ends_with("config.ts")
|| lower.ends_with("config.tsx")
|| lower.ends_with("config.js")
|| lower.ends_with("config.rs")
|| lower.ends_with(".config.ts")
|| lower.ends_with(".config.js")
|| lower.ends_with(".config.json");
let kind = if generated {
"generated"
} else if test {
"test"
} else if story {
"story"
} else if let Some(resource) = resource_kind(path) {
resource
} else if config {
"config"
} else {
"code"
};
(kind.to_string(), test, generated)
}
pub fn language_from_path(path: &str) -> String {
let p = Path::new(path);
let ext = p
.extension()
.and_then(|e| e.to_str())
.unwrap_or_default()
.to_lowercase();
if !ext.is_empty() {
return detect_language(&ext);
}
let filename = p.file_name().and_then(|n| n.to_str()).unwrap_or_default();
detect_language_from_filename(filename)
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn detects_dev_and_non_dev_files() {
assert!(is_dev_file("features/__tests__/thing.ts"));
assert!(is_dev_file("components/Button.stories.tsx"));
assert!(is_dev_file("fixtures/foo.rs"));
assert!(!is_dev_file("src/app.tsx"));
}
#[test]
fn classifies_file_kinds_and_flags() {
let (kind, test, is_generated) = file_kind("src/generated/foo.gen.ts");
assert_eq!(kind, "generated");
assert!(!test);
assert!(is_generated);
let (kind, test, is_generated) = file_kind("src/components/Button.story.tsx");
assert_eq!(kind, "story");
assert!(!is_generated);
assert!(!test);
let (kind, test, _) = file_kind("src/__tests__/foo.test.ts");
assert_eq!(kind, "test");
assert!(test);
let (kind, _, _) = file_kind("config/vite.config.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("docs/README.md");
assert_eq!(kind, "doc");
let (kind, _, _) = file_kind(".github/workflows/ci.yml");
assert_eq!(kind, "workflow");
let (kind, _, _) = file_kind("locales/en.json");
assert_eq!(kind, "locale");
let (kind, _, _) = file_kind("src/features/app.tsx");
assert_eq!(kind, "code");
}
#[test]
fn classifies_resource_membership() {
assert_eq!(resource_kind("docs/guide.md"), Some("doc"));
assert_eq!(resource_kind("Cargo.toml"), Some("config"));
assert_eq!(resource_kind(".github/workflows/ci.yaml"), Some("workflow"));
assert_eq!(resource_kind("src/i18n/pl.json"), Some("locale"));
assert_eq!(resource_kind("fixtures/data.json"), Some("resource"));
assert_eq!(
resource_kind("legacy/MarkdownEditor/Base.lproj/Main.storyboard"),
Some("resource")
);
assert_eq!(resource_kind("Views/MainWindow.xib"), Some("resource"));
assert_eq!(
resource_kind("src/main/resources/messages/LoctreeBundle.properties"),
Some("resource")
);
assert_eq!(
resource_kind("src/main/resources/META-INF/loctree-lsp.xml"),
Some("resource")
);
assert_eq!(resource_kind("notes/todo.txt"), Some("doc"));
assert_eq!(resource_kind("src/main.rs"), None);
}
#[test]
fn detects_language_from_path() {
assert_eq!(language_from_path("foo/bar.tsx"), "ts");
assert_eq!(language_from_path("foo/bar.rs"), "rs");
assert_eq!(language_from_path("foo/bar.py"), "py");
assert_eq!(language_from_path("foo/bar.css"), "css");
assert_eq!(language_from_path("foo/bar.unknown"), "unknown");
}
#[test]
fn detect_language_all_extensions() {
assert_eq!(detect_language("ts"), "ts");
assert_eq!(detect_language("tsx"), "ts");
assert_eq!(detect_language("js"), "js");
assert_eq!(detect_language("jsx"), "js");
assert_eq!(detect_language("mjs"), "js");
assert_eq!(detect_language("cjs"), "js");
assert_eq!(detect_language("rs"), "rs");
assert_eq!(detect_language("py"), "py");
assert_eq!(detect_language("go"), "go");
assert_eq!(detect_language("kt"), "kotlin");
assert_eq!(detect_language("kts"), "kotlin");
assert_eq!(detect_language("css"), "css");
assert_eq!(detect_language("html"), "html");
assert_eq!(detect_language("astro"), "astro");
assert_eq!(detect_language("sh"), "shell");
assert_eq!(detect_language("bash"), "shell");
assert_eq!(detect_language("zsh"), "shell");
assert_eq!(detect_language("fish"), "shell");
assert_eq!(detect_language("mk"), "make");
assert_eq!(detect_language("zig"), "zig");
assert_eq!(detect_language("zon"), "zig");
assert_eq!(detect_language("storyboard"), "storyboard");
assert_eq!(detect_language("xib"), "xib");
}
#[test]
fn detects_language_from_filename_for_makefiles() {
assert_eq!(detect_language_from_filename("Makefile"), "make");
assert_eq!(detect_language_from_filename("makefile"), "make");
assert_eq!(detect_language_from_filename("GNUmakefile"), "make");
assert_eq!(detect_language_from_filename("BSDmakefile"), "make");
assert_eq!(detect_language_from_filename(".loctignore"), "config");
assert_eq!(detect_language_from_filename(".loctreeignore"), "config");
assert_eq!(detect_language_from_filename("random"), "");
assert_eq!(detect_language_from_filename("Dockerfile"), "");
}
#[test]
fn is_dev_file_variations() {
assert!(is_dev_file("src/__tests__/Button.test.ts"));
assert!(is_dev_file("__tests__/unit/helper.ts"));
assert!(is_dev_file("components/stories/Button.tsx"));
assert!(is_dev_file("Button.stories.tsx"));
assert!(is_dev_file("Button.story.tsx"));
assert!(is_dev_file("test/fixtures/data.json"));
assert!(is_dev_file("fixture/mock.ts"));
assert!(!is_dev_file("src/components/Button.tsx"));
assert!(!is_dev_file("lib/utils.ts"));
assert!(!is_dev_file("src/store/index.ts"));
}
#[test]
fn is_test_path_variations() {
assert!(is_test_path("src/__tests__/foo.ts"));
assert!(is_test_path("src/Button.test.tsx"));
assert!(is_test_path("utils.spec.ts"));
assert!(is_test_path("lib_test.rs"));
assert!(is_test_path("module_tests.rs"));
assert!(is_test_path("SRC/__TESTS__/FOO.TS")); assert!(is_test_path("test_parser.py"));
assert!(is_test_path("src/test_utils.py"));
assert!(is_test_path("tests/api/test.rs"));
assert!(is_test_path("src/tests/integration.py"));
assert!(!is_test_path("src/Button.tsx"));
assert!(!is_test_path("testing.ts")); }
#[test]
fn is_story_path_variations() {
assert!(is_story_path("Button.stories.tsx"));
assert!(is_story_path("Button.story.tsx"));
assert!(is_story_path("components/stories/Button.tsx"));
assert!(is_story_path("BUTTON.STORIES.TSX"));
assert!(!is_story_path("src/Button.tsx"));
assert!(!is_story_path("history.ts")); }
#[test]
fn is_generated_path_variations() {
assert!(is_generated_path("src/generated/types.ts"));
assert!(is_generated_path("lib/codegen/schema.ts"));
assert!(is_generated_path("out/gen/api.ts"));
assert!(is_generated_path("types.gen.ts"));
assert!(is_generated_path("api.gen.tsx"));
assert!(is_generated_path("schema.gen.rs"));
assert!(is_generated_path("proto.g.rs"));
assert!(is_generated_path("SRC/GENERATED/FOO.TS"));
assert!(!is_generated_path("src/utils.ts"));
assert!(!is_generated_path("generic.ts")); }
#[test]
fn file_kind_config_variations() {
let (kind, _, _) = file_kind("src/config/app.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("vite.config.ts");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("tailwind.config.js");
assert_eq!(kind, "config");
let (kind, _, _) = file_kind("app.config.json");
assert_eq!(kind, "config");
}
#[test]
fn file_kind_priority_generated_over_test() {
let (kind, test, generated) = file_kind("__tests__/generated/mock.gen.ts");
assert_eq!(kind, "generated");
assert!(test); assert!(generated);
}
#[test]
fn file_kind_priority_test_over_story() {
let (kind, test, _) = file_kind("Button.stories.test.ts");
assert_eq!(kind, "test");
assert!(test);
}
#[test]
fn language_from_path_edge_cases() {
assert_eq!(language_from_path("Makefile"), "make");
assert_eq!(language_from_path("src/GNUmakefile"), "make");
assert_eq!(language_from_path("src/noext"), "");
assert_eq!(language_from_path(".gitignore"), "");
assert_eq!(language_from_path(".env"), "");
assert_eq!(language_from_path("file.test.ts"), "ts");
assert_eq!(language_from_path("app.module.tsx"), "ts");
assert_eq!(language_from_path("deploy.sh"), "shell");
assert_eq!(language_from_path("common.mk"), "make");
assert_eq!(language_from_path("main.zig"), "zig");
assert_eq!(language_from_path("build.zon"), "zig");
assert_eq!(language_from_path("Main.kt"), "kotlin");
assert_eq!(language_from_path("build.gradle.kts"), "kotlin");
}
#[test]
fn classify_test_path_unit_tests() {
assert_eq!(
classify_test_path(Path::new("src/components/Button.test.tsx")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("utils.spec.ts")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("src/__tests__/helper.ts")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("src/parser_test.rs")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("lib/module_tests.rs")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("test_utils.py")),
TestClassification::UnitTest
);
assert_eq!(
classify_test_path(Path::new("handler_test.go")),
TestClassification::UnitTest
);
}
#[test]
fn classify_test_path_integration_tests() {
assert_eq!(
classify_test_path(Path::new("tests/api/endpoints.rs")),
TestClassification::IntegrationTest
);
assert_eq!(
classify_test_path(Path::new("tests/integration/database.rs")),
TestClassification::IntegrationTest
);
assert_eq!(
classify_test_path(Path::new("src/__tests__/integration.test.ts")),
TestClassification::UnitTest
);
}
#[test]
fn classify_test_path_fixtures() {
assert_eq!(
classify_test_path(Path::new("tests/fixtures/data.json")),
TestClassification::TestFixture
);
assert_eq!(
classify_test_path(Path::new("__tests__/fixture/mock.ts")),
TestClassification::TestFixture
);
assert_eq!(
classify_test_path(Path::new("test/mock/server.rs")),
TestClassification::TestFixture
);
}
#[test]
fn classify_test_path_helpers() {
assert_eq!(
classify_test_path(Path::new("tests/test_helper.rs")),
TestClassification::TestHelper
);
assert_eq!(
classify_test_path(Path::new("__tests__/test_utils.ts")),
TestClassification::TestHelper
);
assert_eq!(
classify_test_path(Path::new("testing/setup.py")),
TestClassification::TestHelper
);
}
#[test]
fn classify_test_path_production() {
assert_eq!(
classify_test_path(Path::new("src/components/Button.tsx")),
TestClassification::Production
);
assert_eq!(
classify_test_path(Path::new("lib/parser.rs")),
TestClassification::Production
);
assert_eq!(
classify_test_path(Path::new("utils/helpers.py")),
TestClassification::Production
);
}
#[test]
fn has_test_code_rust() {
let code_with_test_module = r#"
fn main() {}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(2 + 2, 4);
}
}
"#;
assert!(has_test_code(code_with_test_module, "rs"));
let code_with_test_fn = r#"
#[test]
fn test_something() {}
"#;
assert!(has_test_code(code_with_test_fn, "rust"));
let production_code = r#"
fn parse(input: &str) -> Result<(), Error> {
Ok(())
}
"#;
assert!(!has_test_code(production_code, "rs"));
}
#[test]
fn has_test_code_typescript() {
let jest_test = r#"
describe('Button', () => {
it('should render', () => {
expect(true).toBe(true);
});
});
"#;
assert!(has_test_code(jest_test, "ts"));
let vitest_test = r#"
test('adds 1 + 2 to equal 3', () => {
expect(1 + 2).toBe(3);
});
"#;
assert!(has_test_code(vitest_test, "tsx"));
let production = r#"
export function add(a: number, b: number): number {
return a + b;
}
"#;
assert!(!has_test_code(production, "ts"));
}
#[test]
fn has_test_code_python() {
let unittest_code = r#"
import unittest
class TestMath(unittest.TestCase):
def test_addition(self):
self.assertEqual(1 + 1, 2)
"#;
assert!(has_test_code(unittest_code, "py"));
let pytest_code = r#"
import pytest
def test_addition():
assert 1 + 1 == 2
"#;
assert!(has_test_code(pytest_code, "python"));
let test_function = r#"
def test_something():
pass
"#;
assert!(has_test_code(test_function, "py"));
let production = r#"
def add(a, b):
return a + b
"#;
assert!(!has_test_code(production, "py"));
}
#[test]
fn has_test_code_go() {
let go_test = r#"
package main
import "testing"
func TestAdd(t *testing.T) {
result := Add(1, 2)
if result != 3 {
t.Errorf("Expected 3, got %d", result)
}
}
"#;
assert!(has_test_code(go_test, "go"));
let production = r#"
package main
func Add(a, b int) int {
return a + b
}
"#;
assert!(!has_test_code(production, "go"));
}
#[test]
fn artifact_class_vendored_minified() {
assert_eq!(
artifact_class("loctree-rs/src/analyzer/assets/cytoscape.min.js", None),
ArtifactClass::Vendored
);
assert_eq!(
artifact_class("vendor/lodash/index.js", None),
ArtifactClass::Vendored
);
assert_eq!(
artifact_class("web/node_modules/react/index.js", None),
ArtifactClass::Vendored
);
let minified = "x".repeat(6000);
assert_eq!(
artifact_class("assets/lib.js", Some(&minified)),
ArtifactClass::Vendored
);
assert_eq!(
artifact_class("src/app.js", Some("import x from './y';\n")),
ArtifactClass::Product
);
}
#[test]
fn artifact_class_generated_lockfiles_and_dist() {
assert_eq!(
artifact_class("public_dist/index.html", None),
ArtifactClass::Generated
);
assert_eq!(
artifact_class("dist/bundle.js", None),
ArtifactClass::Generated
);
assert_eq!(
artifact_class("package-lock.json", None),
ArtifactClass::Generated
);
assert_eq!(artifact_class("Cargo.lock", None), ArtifactClass::Generated);
assert_eq!(
artifact_class("pnpm-lock.yaml", None),
ArtifactClass::Generated
);
assert_eq!(
artifact_class("public_dist/app-1234.js.map", None),
ArtifactClass::Generated
);
assert_eq!(
artifact_class("src/generated/types.gen.ts", None),
ArtifactClass::Generated
);
}
#[test]
fn artifact_class_fixture_and_template() {
assert_eq!(
artifact_class("tests/fixtures/diamond/a.ts", None),
ArtifactClass::Fixture
);
assert_eq!(
artifact_class("tools/fixtures/sample.py", None),
ArtifactClass::Fixture
);
assert_eq!(
artifact_class(".env.example", None),
ArtifactClass::Template
);
assert_eq!(
artifact_class("config/settings.sample.toml", None),
ArtifactClass::Template
);
assert_eq!(
artifact_class("deploy/nginx.conf.template", None),
ArtifactClass::Template
);
assert_eq!(
artifact_class("tests/fixtures/dist/bundle.js", None),
ArtifactClass::Fixture
);
}
#[test]
fn artifact_class_product_untouched() {
assert_eq!(artifact_class("src/main.rs", None), ArtifactClass::Product);
assert_eq!(
artifact_class("loctree-rs/src/analyzer/classify.rs", None),
ArtifactClass::Product
);
assert_eq!(
artifact_class("examples/demo/app.ts", None),
ArtifactClass::Product,
"library examples are NOT artifacts (library_mode handles them)"
);
}
#[test]
fn artifact_fence_stats_summary_line() {
let mut stats = ArtifactFenceStats::default();
assert!(stats.is_empty());
assert_eq!(stats.summary_line(), "");
stats.record(ArtifactClass::Vendored);
stats.record(ArtifactClass::Vendored);
stats.record(ArtifactClass::Generated);
stats.record(ArtifactClass::Product); assert_eq!(stats.total(), 3);
assert_eq!(stats.summary_line(), "excluded: vendored(2), generated(1)");
}
#[test]
fn canonical_is_test_file_union() {
assert!(is_test_file("src/Button.test.tsx"));
assert!(is_test_file("src/__tests__/foo.ts"));
assert!(is_test_file("vitest.setup.ts"));
assert!(is_test_file("src/test-utils/render.tsx"));
assert!(is_test_file("tests/e2e_cli.rs"));
assert!(is_test_file("conftest.py"));
assert!(is_test_file("src/fixtures/data.ts"));
assert!(is_test_file("module_tests.rs"));
assert!(is_test_file("test_parser.py"));
assert!(!is_test_file("src/main.rs"));
assert!(!is_test_file("src/components/Button.tsx"));
assert!(!is_test_file("attestation.rs"));
}
#[test]
fn test_patterns_all_languages() {
let rust_patterns = test_patterns("rs");
assert!(rust_patterns.contains(&"*_test.rs"));
assert!(rust_patterns.contains(&"*_tests.rs"));
assert!(rust_patterns.contains(&"tests/**/*.rs"));
let ts_patterns = test_patterns("ts");
assert!(ts_patterns.contains(&"*.test.ts"));
assert!(ts_patterns.contains(&"*.spec.tsx"));
assert!(ts_patterns.contains(&"__tests__/**/*"));
let py_patterns = test_patterns("py");
assert!(py_patterns.contains(&"test_*.py"));
assert!(py_patterns.contains(&"*_test.py"));
assert!(py_patterns.contains(&"tests/**/*.py"));
let go_patterns = test_patterns("go");
assert!(go_patterns.contains(&"*_test.go"));
let unknown_patterns = test_patterns("unknown");
assert!(unknown_patterns.is_empty());
}
}