use assert_cmd::Command;
use assert_cmd::cargo::cargo_bin_cmd;
use loctree::slicer::{HolographicSlice, SliceConfig};
use loctree::snapshot::Snapshot;
use std::path::{Path, PathBuf};
use tempfile::TempDir;
fn fixtures_path() -> PathBuf {
PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("tests/fixtures")
}
fn loct() -> Command {
cargo_bin_cmd!("loct")
}
fn copy_dir_all(src: &Path, dst: &Path) -> std::io::Result<()> {
std::fs::create_dir_all(dst)?;
for entry in std::fs::read_dir(src)? {
let entry = entry?;
let ty = entry.file_type()?;
let dest_path = dst.join(entry.file_name());
if ty.is_dir() {
copy_dir_all(&entry.path(), &dest_path)?;
} else {
std::fs::copy(entry.path(), dest_path)?;
}
}
Ok(())
}
fn scan_fixture_in_tempdir(fixture_name: &str) -> (TempDir, Snapshot) {
let temp = TempDir::new().expect("tempdir");
let fixture = fixtures_path().join(fixture_name);
copy_dir_all(&fixture, temp.path()).expect("copy fixture");
let output = loct().current_dir(temp.path()).output().expect("loct runs");
assert!(
output.status.success(),
"loct failed in {}: stderr={}",
temp.path().display(),
String::from_utf8_lossy(&output.stderr)
);
let loctree_root = Snapshot::find_loctree_root(temp.path()).expect("loctree root after scan");
let snapshot = Snapshot::load(&loctree_root).expect("load snapshot");
(temp, snapshot)
}
fn languages_in(snapshot: &Snapshot) -> Vec<String> {
let mut langs: Vec<String> = snapshot
.files
.iter()
.map(|f| f.language.clone())
.filter(|l| !l.is_empty())
.collect();
langs.sort();
langs.dedup();
langs
}
#[test]
fn shell_project_is_analyzed() {
let (_temp, snapshot) = scan_fixture_in_tempdir("shell_project");
assert!(
snapshot.files.len() >= 3,
"expected >= 3 shell files, got {}",
snapshot.files.len()
);
let langs = languages_in(&snapshot);
assert!(
langs.iter().any(|l| l == "shell"),
"expected shell language; got {:?}",
langs
);
}
#[test]
fn makefile_project_is_analyzed() {
let (_temp, snapshot) = scan_fixture_in_tempdir("makefile_project");
assert!(
snapshot.files.len() >= 2,
"expected >= 2 make files, got {}",
snapshot.files.len()
);
let langs = languages_in(&snapshot);
assert!(
langs.iter().any(|l| l == "make"),
"expected make language; got {:?}",
langs
);
let has_makefile = snapshot
.files
.iter()
.any(|f| f.path.ends_with("Makefile") && f.language == "make");
assert!(
has_makefile,
"Makefile not classified as make; files={:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.language.clone()))
.collect::<Vec<_>>()
);
}
#[test]
fn zig_project_is_analyzed() {
let (_temp, snapshot) = scan_fixture_in_tempdir("zig_project");
assert!(
snapshot.files.len() >= 2,
"expected >= 2 zig files, got {}",
snapshot.files.len()
);
let langs = languages_in(&snapshot);
assert!(
langs.iter().any(|l| l == "zig"),
"expected zig language; got {:?}",
langs
);
}
#[test]
fn kotlin_project_is_visible_scan_only() {
let (_temp, snapshot) = scan_fixture_in_tempdir("kotlin_project");
let kotlin_files: Vec<_> = snapshot
.files
.iter()
.filter(|f| f.language == "kotlin")
.collect();
assert!(
kotlin_files.len() >= 2,
"expected Kotlin .kt/.kts files in snapshot, got {:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.language.clone()))
.collect::<Vec<_>>()
);
assert!(
kotlin_files
.iter()
.all(|f| f.imports.is_empty() && f.exports.is_empty()),
"Kotlin is scan-only until a parser lands; got {:?}",
kotlin_files
);
}
#[test]
fn resource_files_have_first_class_membership() {
let (_temp, snapshot) = scan_fixture_in_tempdir("resource_membership");
let file = |path: &str| {
snapshot
.files
.iter()
.find(|f| f.path == path)
.unwrap_or_else(|| {
panic!(
"expected {path} in snapshot; got {:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.language.clone(), f.kind.clone()))
.collect::<Vec<_>>()
)
})
};
let readme = file("README.md");
assert_eq!(readme.language, "md");
assert_eq!(readme.kind, "doc");
assert_eq!(readme.resource_kind.as_deref(), Some("doc"));
let cargo = file("Cargo.toml");
assert_eq!(cargo.language, "toml");
assert_eq!(cargo.kind, "config");
assert_eq!(cargo.resource_kind.as_deref(), Some("config"));
let workflow = file(".github/workflows/ci.yml");
assert_eq!(workflow.language, "yml");
assert_eq!(workflow.kind, "workflow");
assert_eq!(workflow.resource_kind.as_deref(), Some("workflow"));
let locale = file("locales/en.json");
assert_eq!(locale.language, "json");
assert_eq!(locale.kind, "locale");
assert_eq!(locale.resource_kind.as_deref(), Some("locale"));
let resource = file("data/schema.json");
assert_eq!(resource.language, "json");
assert_eq!(resource.kind, "resource");
assert_eq!(resource.resource_kind.as_deref(), Some("resource"));
let storyboard = file("Base.lproj/Main.storyboard");
assert_eq!(storyboard.language, "storyboard");
assert_eq!(storyboard.kind, "resource");
assert_eq!(storyboard.resource_kind.as_deref(), Some("resource"));
let slice = HolographicSlice::from_path(
&snapshot,
".github/workflows/ci.yml",
&SliceConfig::default(),
)
.expect("workflow resource is sliceable");
let core = slice.core.first().expect("slice core");
assert_eq!(core.path, ".github/workflows/ci.yml");
assert_eq!(core.kind, "workflow");
assert_eq!(core.resource_kind.as_deref(), Some("workflow"));
let json = slice.to_json();
assert_eq!(json["core"][0]["kind"], "workflow");
assert_eq!(json["core"][0]["resource_kind"], "workflow");
let storyboard_slice = HolographicSlice::from_path(
&snapshot,
"Base.lproj/Main.storyboard",
&SliceConfig::default(),
)
.expect("storyboard resource is sliceable");
let storyboard_core = storyboard_slice
.core
.first()
.expect("storyboard slice core");
assert_eq!(storyboard_core.path, "Base.lproj/Main.storyboard");
assert_eq!(storyboard_core.kind, "resource");
assert_eq!(storyboard_core.resource_kind.as_deref(), Some("resource"));
}
#[test]
fn shell_source_edges_resolved() {
let (_temp, snapshot) = scan_fixture_in_tempdir("shell_project");
let has_common_import = snapshot.files.iter().any(|f| {
f.language == "shell"
&& f.imports.iter().any(|i| {
i.resolved_path
.as_ref()
.map(|p| p.to_ascii_lowercase().ends_with("common.sh"))
.unwrap_or(false)
})
});
assert!(
has_common_import,
"expected resolved import to common.sh; imports={:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.imports.clone()))
.collect::<Vec<_>>()
);
}
#[test]
fn makefile_include_edges_resolved() {
let (_temp, snapshot) = scan_fixture_in_tempdir("makefile_project");
let has_common_mk_import = snapshot.files.iter().any(|f| {
f.language == "make"
&& f.imports.iter().any(|i| {
i.resolved_path
.as_ref()
.map(|p| p.to_ascii_lowercase().ends_with("common.mk"))
.unwrap_or(false)
})
});
assert!(
has_common_mk_import,
"expected resolved include of common.mk; imports={:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.imports.clone()))
.collect::<Vec<_>>()
);
}
#[test]
fn zig_import_edges_resolved() {
let (_temp, snapshot) = scan_fixture_in_tempdir("zig_project");
let has_helpers_import = snapshot.files.iter().any(|f| {
f.language == "zig"
&& f.imports.iter().any(|i| {
i.resolved_path
.as_ref()
.map(|p| p.to_ascii_lowercase().ends_with("helpers.zig"))
.unwrap_or(false)
})
});
assert!(
has_helpers_import,
"expected resolved @import(helpers.zig); imports={:?}",
snapshot
.files
.iter()
.map(|f| (f.path.clone(), f.imports.clone()))
.collect::<Vec<_>>()
);
}