use super::*;
#[test]
fn test_header_double_slash() {
let h = header(CommentStyle::DoubleSlash);
assert!(h.contains("// This file is auto-generated by alef"));
assert!(!h.contains("Issues & docs:"));
assert!(!h.contains("github.com/xberg-io/alef"));
assert!(!h.contains("sample_crate"));
}
#[test]
fn test_header_for_config_omits_issues_url_when_unconfigured() {
let cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
"#,
)
.unwrap();
let resolved = cfg.resolve().unwrap().remove(0);
let h = header_for_config(CommentStyle::DoubleSlash, &resolved);
assert!(!h.contains("Issues & docs:"));
assert!(!h.contains("github.com/xberg-io/alef"));
}
#[test]
fn test_header_for_config_uses_configured_metadata() {
let cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[workspace.generated_header]
issues_url = "https://docs.example.invalid/alef"
regenerate_command = "task generate"
verify_command = "task verify"
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
"#,
)
.unwrap();
let resolved = cfg.resolve().unwrap().remove(0);
let h = header_for_config(CommentStyle::DoubleSlash, &resolved);
assert!(h.contains("// To regenerate: task generate"));
assert!(h.contains("// To verify freshness: task verify"));
assert!(h.contains("// Issues & docs: https://docs.example.invalid/alef"));
}
#[test]
fn test_header_for_config_uses_package_metadata_url() {
let cfg: crate::core::config::NewAlefConfig = toml::from_str(
r#"
[workspace]
languages = ["python"]
[[crates]]
name = "demo"
sources = ["src/lib.rs"]
[crates.package_metadata]
issues = "https://issues.example.invalid/demo"
"#,
)
.unwrap();
let resolved = cfg.resolve().unwrap().remove(0);
let h = header_for_config(CommentStyle::DoubleSlash, &resolved);
assert!(h.contains("// Issues & docs: https://issues.example.invalid/demo"));
}
#[test]
fn test_header_hash() {
let h = header(CommentStyle::Hash);
assert!(h.contains("# This file is auto-generated by alef"));
}
#[test]
fn test_header_block() {
let h = header(CommentStyle::Block);
assert!(h.starts_with("/*\n"));
assert!(h.contains(" * This file is auto-generated by alef"));
assert!(h.ends_with(" */\n"));
}
#[test]
fn test_inject_and_extract_rust() {
let h = header(CommentStyle::DoubleSlash);
let content = format!("{h}use foo;\n");
let hash = hash_content(&content);
let injected = inject_hash_line(&content, &hash);
assert!(injected.contains(HASH_PREFIX));
assert_eq!(extract_hash(&injected), Some(hash));
}
#[test]
fn test_inject_and_extract_python() {
let h = header(CommentStyle::Hash);
let content = format!("{h}import foo\n");
let hash = hash_content(&content);
let injected = inject_hash_line(&content, &hash);
assert!(injected.contains(&format!("# {HASH_PREFIX}")));
assert_eq!(extract_hash(&injected), Some(hash));
}
#[test]
fn test_inject_and_extract_c_block() {
let h = header(CommentStyle::Block);
let content = format!("{h}#include <stdio.h>\n");
let hash = hash_content(&content);
let injected = inject_hash_line(&content, &hash);
assert!(injected.contains(HASH_PREFIX));
assert!(
injected.contains(&format!(" * {HASH_PREFIX}")),
"expected ' * {HASH_PREFIX}' in block-comment header, got:\n{injected}"
);
assert!(
!injected.contains(&format!("// {HASH_PREFIX}")),
"block-comment header must not use '//' for the hash line, got:\n{injected}"
);
assert_eq!(extract_hash(&injected), Some(hash));
}
#[test]
fn test_inject_php_line2() {
let h = header(CommentStyle::DoubleSlash);
let content = format!("<?php\n{h}namespace Foo;\n");
let hash = hash_content(&content);
let injected = inject_hash_line(&content, &hash);
let lines: Vec<&str> = injected.lines().collect();
assert_eq!(lines[0], "<?php");
assert!(content_has_alef_marker(lines[1]));
assert!(lines.iter().any(|l| l.contains(HASH_PREFIX)));
assert_eq!(extract_hash(&injected), Some(hash));
}
#[test]
fn test_no_header_returns_unchanged() {
let content = "fn main() {}\n";
let injected = inject_hash_line(content, "abc123");
assert_eq!(injected, content);
assert_eq!(extract_hash(&injected), None);
}
#[test]
fn test_inject_and_extract_stamp_double_slash() {
let h = header(CommentStyle::DoubleSlash);
let content = format!("{h}use foo;\n");
let stamped = inject_stamp_line(&content, "handle-abi", "1");
assert!(stamped.contains("// alef:handle-abi:1"));
assert_eq!(extract_stamp(&stamped, "handle-abi").as_deref(), Some("1"));
}
#[test]
fn stamp_value_need_not_be_hex() {
let h = header(CommentStyle::Hash);
let content = format!("{h}import foo\n");
let stamped = inject_stamp_line(&content, "handle-abi", "not-a-hash-value");
assert_eq!(
extract_stamp(&stamped, "handle-abi").as_deref(),
Some("not-a-hash-value")
);
}
#[test]
fn stamp_and_hash_lines_coexist_and_extract_independently() {
let h = header(CommentStyle::DoubleSlash);
let content = format!("{h}use foo;\n");
let stamped = inject_stamp_line(&content, "handle-abi", "2");
let hash = hash_content(&stamped);
let finalized = inject_hash_line(&stamped, &hash);
assert_eq!(extract_stamp(&finalized, "handle-abi").as_deref(), Some("2"));
assert_eq!(extract_hash(&finalized), Some(hash));
}
#[test]
fn extract_stamp_does_not_match_a_different_key() {
let content = "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:abc123\nuse foo;\n";
assert_eq!(extract_stamp(content, "handle-abi"), None);
}
#[test]
fn extract_stamp_returns_none_without_a_header_marker() {
let content = "fn main() {}\n";
assert_eq!(inject_stamp_line(content, "handle-abi", "1"), content);
assert_eq!(extract_stamp(content, "handle-abi"), None);
}
#[test]
fn stamp_shapes_round_trip_across_comment_styles() {
let fixtures = [
(
"double slash",
"// auto-generated by alef\n// alef:handle-abi:1\nvalue\n",
),
("hash", "# auto-generated by alef\n# alef:handle-abi:1\nvalue\n"),
("block", "/* auto-generated by alef\n * alef:handle-abi:1\n */\nvalue\n"),
(
"html",
"<!-- auto-generated by alef -->\n<!-- alef:handle-abi:1 -->\nvalue\n",
),
];
for (name, content) in fixtures {
assert_eq!(extract_stamp(content, "handle-abi").as_deref(), Some("1"), "{name}");
}
}
#[test]
fn test_strip_hash_line() {
let content = "// auto-generated by alef\n// alef:hash:abc123\nuse foo;\n";
let stripped = strip_hash_line(content);
assert_eq!(stripped, "// auto-generated by alef\nuse foo;\n");
}
#[test]
fn generated_hash_stamp_shapes_round_trip() {
let fixtures = [
(
"double slash",
"// auto-generated by alef\n// alef:hash:abc123\nvalue\n",
),
("hash", "# auto-generated by alef\n# alef:hash:abc123\nvalue\n"),
("block", "/* auto-generated by alef\n * alef:hash:abc123\n */\nvalue\n"),
(
"html",
"<!-- auto-generated by alef -->\n<!-- alef:hash:abc123 -->\nvalue\n",
),
];
for (name, content) in fixtures {
assert_eq!(extract_hash(content).as_deref(), Some("abc123"), "{name}");
assert!(!strip_hash_line(content).contains(HASH_PREFIX), "{name}");
}
}
#[test]
fn markdown_frontmatter_stamp_at_raw_line_ten_round_trips() {
let content =
"---\ntitle: Fixture\nlayout: reference\n---\n\nintro\n\nmetadata\n\n<!-- auto-generated by alef -->\nbody\n";
let injected = inject_hash_line(content, "abc123");
assert_eq!(injected.lines().nth(10), Some("<!-- alef:hash:abc123 -->"));
assert_eq!(extract_hash(&injected).as_deref(), Some("abc123"));
assert_eq!(strip_hash_line(&injected), content);
}
#[test]
fn hash_like_body_content_is_never_extracted_or_stripped() {
let fixtures = [
(
"prose",
"// auto-generated by alef\nbody mentions alef:hash:abc123 in prose\n",
),
(
"wrong shape",
"// auto-generated by alef\n// alef:hash:abc123 trailing text\n",
),
(
"stamp after marker at raw line ten",
"prefix\nprefix\nprefix\nprefix\nprefix\nprefix\nprefix\nprefix\nprefix\nprefix\n// auto-generated by alef\n// alef:hash:abc123\n",
),
(
"structured body stamp",
"// auto-generated by alef\nbody\n// alef:hash:abc123\n",
),
];
for (name, content) in fixtures {
assert_eq!(extract_hash(content), None, "{name}");
assert_eq!(strip_hash_line(content), content, "{name}");
}
}
#[test]
fn test_roundtrip() {
let h = header(CommentStyle::Hash);
let original = format!("{h}import sys\n");
let hash = hash_content(&original);
let injected = inject_hash_line(&original, &hash);
let stripped = strip_hash_line(&injected);
assert_eq!(stripped, original);
assert_eq!(hash_content(&stripped), hash);
}
#[test]
fn test_content_has_alef_marker_true_without_hash_line() {
let content = "// This file is auto-generated by alef — DO NOT EDIT.\nfn hello() {}\n";
assert!(extract_hash(content).is_none(), "fixture must not carry a hash line");
assert!(
content_has_alef_marker(content),
"a headered file missing its hash must still be recognized as alef-owned"
);
}
#[test]
fn test_content_has_alef_marker_true_with_hash_line() {
let content = "// This file is auto-generated by alef — DO NOT EDIT.\n// alef:hash:abc123\nfn hello() {}\n";
assert!(content_has_alef_marker(content));
}
#[test]
fn test_content_has_alef_marker_true_for_alt_marker() {
let content = "// Generated by alef. DO NOT EDIT.\nfunc hello() {}\n";
assert!(content_has_alef_marker(content));
}
#[test]
fn test_content_has_alef_marker_false_for_user_owned_file() {
let content = "// hand-written helper\nfn hello() {}\n";
assert!(!content_has_alef_marker(content));
}
#[test]
fn test_content_has_alef_marker_false_when_marker_outside_scan_window() {
let padding = "// padding\n".repeat(MARKER_SCAN_LINES);
let content = format!("{padding}// This file is auto-generated by alef — DO NOT EDIT.\nfn hello() {{}}\n");
assert!(!content_has_alef_marker(&content));
}
#[test]
fn a_marker_at_the_deepest_legal_line_stamps_inside_polys_scan_window() {
let padding = "# padding\n".repeat(MARKER_SCAN_LINES - 1);
let content = format!("{padding}# This file is auto-generated by alef — DO NOT EDIT.\nkey = \"value\"\n");
assert!(
content_has_alef_marker(&content),
"control: a marker on line {MARKER_SCAN_LINES} must still be claimed, else this test \
measures nothing"
);
let stamped = inject_hash_line(&content, "abc123");
let stamp_line = stamped
.lines()
.position(|line| line.contains("alef:hash:"))
.map(|index| index + 1)
.expect("a claimed file must be stamped");
assert_eq!(
stamp_line,
deepest_hash_line(),
"the stamp must land on the line immediately after the marker"
);
assert!(
stamp_line <= POLY_GENERATED_SCAN_LINES,
"stamp landed on line {stamp_line}, past poly's {POLY_GENERATED_SCAN_LINES}-line window: \
alef would claim a file poly still reformats"
);
}
#[test]
fn test_compute_file_hash_distinct_content_yields_distinct_hashes() {
let inputs_hash = compute_inputs_hash("sources", b"[workspace]\nlanguages = [\"rust\"]\n");
let a = compute_file_hash(&inputs_hash, "fn one() {}\n");
let b = compute_file_hash(&inputs_hash, "fn two() {}\n");
assert_ne!(a, b, "distinct file bodies must yield distinct embedded hashes");
}
#[test]
fn test_compute_file_hash_same_content_yields_same_hash() {
let inputs_hash = compute_inputs_hash("sources", b"[workspace]\nlanguages = [\"rust\"]\n");
let a = compute_file_hash(&inputs_hash, "fn one() {}\n");
let b = compute_file_hash(&inputs_hash, "fn one() {}\n");
assert_eq!(a, b, "identical content under identical inputs must hash identically");
}
use std::path::{Path, PathBuf};
use tempfile::tempdir;
fn write_file(dir: &Path, name: &str, content: &str) -> PathBuf {
let path = dir.join(name);
std::fs::write(&path, content).unwrap();
path
}
#[test]
fn sources_hash_changes_when_path_changes_even_if_content_same() {
let dir = tempdir().unwrap();
let s_a = write_file(dir.path(), "a.rs", "fn a() {}");
std::fs::create_dir_all(dir.path().join("moved")).unwrap();
let s_b = write_file(dir.path(), "moved/a.rs", "fn a() {}");
let h_a = compute_sources_hash(&[s_a]).unwrap();
let h_b = compute_sources_hash(&[s_b]).unwrap();
assert_ne!(
h_a, h_b,
"same content at a different path can produce different IR (rust_path differs)"
);
}
#[test]
fn sources_hash_errors_on_missing_source() {
let dir = tempdir().unwrap();
let bogus = dir.path().join("does-not-exist.rs");
assert!(compute_sources_hash(&[bogus]).is_err());
}
#[test]
fn sources_hash_stable_across_runs() {
let dir = tempdir().unwrap();
let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
let sources = vec![s1, s2];
let h1 = compute_sources_hash(&sources).unwrap();
let h2 = compute_sources_hash(&sources).unwrap();
assert_eq!(h1, h2);
}
#[test]
fn sources_hash_path_order_independent() {
let dir = tempdir().unwrap();
let s1 = write_file(dir.path(), "a.rs", "fn a() {}");
let s2 = write_file(dir.path(), "b.rs", "fn b() {}");
let h_forward = compute_sources_hash(&[s1.clone(), s2.clone()]).unwrap();
let h_reverse = compute_sources_hash(&[s2, s1]).unwrap();
assert_eq!(h_forward, h_reverse);
}
#[test]
fn sources_hash_changes_with_content() {
let dir = tempdir().unwrap();
let s = write_file(dir.path(), "a.rs", "fn a() {}");
let h_before = compute_sources_hash(std::slice::from_ref(&s)).unwrap();
std::fs::write(&s, "fn a() { let _ = 1; }").unwrap();
let h_after = compute_sources_hash(&[s]).unwrap();
assert_ne!(h_before, h_after);
}
#[test]
fn file_hash_idempotent_under_strip_hash_line() {
let sources_hash = "abc123";
let bare = "// auto-generated by alef\nfn body() {}\n";
let with_line = "// auto-generated by alef\n// alef:hash:deadbeef\nfn body() {}\n";
let h1 = compute_file_hash(sources_hash, bare);
let h2 = compute_file_hash(sources_hash, with_line);
assert_eq!(h1, h2, "hash must ignore an existing alef:hash: line");
}
#[test]
fn file_hash_changes_when_sources_change() {
let content = "// auto-generated by alef\nfn body() {}\n";
let h_a = compute_file_hash("sources_a", content);
let h_b = compute_file_hash("sources_b", content);
assert_ne!(h_a, h_b);
}
#[test]
fn file_hash_changes_when_content_changes() {
let sources_hash = "abc123";
let h_a = compute_file_hash(sources_hash, "fn a() {}\n");
let h_b = compute_file_hash(sources_hash, "fn b() {}\n");
assert_ne!(h_a, h_b);
}
#[test]
fn file_hash_is_domain_separated_from_a_bare_blake3_digest() {
let content = "fn a() {}\n";
let h = compute_file_hash("sources_hash", content);
assert_eq!(h.len(), 64, "blake3 hex output is 64 chars");
assert_ne!(
h,
blake3::hash(content.as_bytes()).to_hex().to_string(),
"file hash must fold in the inputs hash, not digest the content alone"
);
}
#[test]
fn inputs_hash_is_stable() {
let h1 = compute_inputs_hash("abc", b"toml");
let h2 = compute_inputs_hash("abc", b"toml");
assert_eq!(h1, h2, "compute_inputs_hash must be deterministic");
assert_eq!(h1.len(), 64, "blake3 hex output is 64 chars");
}
#[test]
fn inputs_hash_changes_when_sources_hash_changes() {
let h1 = compute_inputs_hash("sources_a", b"toml");
let h2 = compute_inputs_hash("sources_b", b"toml");
assert_ne!(h1, h2);
}
#[test]
fn inputs_hash_changes_when_alef_toml_changes() {
let h1 = compute_inputs_hash("sources", b"[workspace]\nlanguages=[\"python\"]\n");
let h2 = compute_inputs_hash("sources", b"[workspace]\nlanguages=[\"ruby\"]\n");
assert_ne!(h1, h2);
}
#[test]
fn inputs_hash_uses_domain_separator() {
let h = compute_inputs_hash("", b"");
assert_eq!(h.len(), 64);
let plain_empty = blake3::hash(b"").to_hex().to_string();
assert_ne!(
h, plain_empty,
"inputs hash must include the alef:inputs domain separator and CODEGEN_FORMAT_VERSION"
);
}
#[test]
fn inputs_hash_invariant_to_toml_comment_change() {
let without_comment = b"[workspace]\nlanguages = [\"python\"]\n" as &[u8];
let with_comment = b"# repo-level config\n[workspace]\nlanguages = [\"python\"]\n" as &[u8];
let h1 = compute_inputs_hash("sources_abc", without_comment);
let h2 = compute_inputs_hash("sources_abc", with_comment);
assert_eq!(
h1, h2,
"comment-only change to alef.toml must not invalidate generated files"
);
}
#[test]
fn inputs_hash_invariant_to_toml_whitespace_and_key_reorder() {
let a = b"[workspace]\nbar = 2\nfoo = 1\n" as &[u8];
let b_bytes = b"[workspace]\nfoo = 1\nbar = 2\n" as &[u8];
let h1 = compute_inputs_hash("sources_xyz", a);
let h2 = compute_inputs_hash("sources_xyz", b_bytes);
assert_eq!(
h1, h2,
"key-reordering in alef.toml must not invalidate generated files"
);
}
#[test]
fn inputs_hash_invariant_to_toml_crlf_vs_lf() {
let lf = b"[workspace]\nlanguages = [\"ruby\"]\n" as &[u8];
let crlf = b"[workspace]\r\nlanguages = [\"ruby\"]\r\n" as &[u8];
let h1 = compute_inputs_hash("sources_def", lf);
let h2 = compute_inputs_hash("sources_def", crlf);
assert_eq!(
h1, h2,
"CRLF vs LF difference in alef.toml must not invalidate generated files"
);
}
#[test]
fn inputs_hash_stable_independent_of_alef_rev() {
let h1 = compute_inputs_hash("stable_sources", b"[workspace]\nlanguages = [\"python\"]\n");
let h2 = compute_inputs_hash("stable_sources", b"[workspace]\nlanguages = [\"python\"]\n");
assert_eq!(
h1, h2,
"hash must be stable; ALEF_REV is not an input to compute_inputs_hash"
);
assert_eq!(h1.len(), 64);
}
#[test]
fn inputs_hash_tolerates_empty_alef_toml() {
let h = compute_inputs_hash("some_sources_hash", b"");
assert_eq!(h.len(), 64);
}
#[test]
fn inputs_hash_alef_version_pin_table() {
struct Case {
name: &'static str,
toml_a: &'static [u8],
toml_b: &'static [u8],
expect_equal: bool,
}
let cases = [
Case {
name: "alef_version bump alone does not change the hash",
toml_a: b"[workspace]\nalef_version = \"0.61.0\"\nlanguages = [\"python\"]\n",
toml_b: b"[workspace]\nalef_version = \"0.61.1\"\nlanguages = [\"python\"]\n",
expect_equal: true,
},
Case {
name: "adding an alef_version pin where none existed does not change the hash",
toml_a: b"[workspace]\nlanguages = [\"python\"]\n",
toml_b: b"[workspace]\nalef_version = \"0.61.1\"\nlanguages = [\"python\"]\n",
expect_equal: true,
},
Case {
name: "a real workspace key change still changes the hash (control)",
toml_a: b"[workspace]\nalef_version = \"0.61.0\"\nlanguages = [\"python\"]\n",
toml_b: b"[workspace]\nalef_version = \"0.61.0\"\nlanguages = [\"ruby\"]\n",
expect_equal: false,
},
Case {
name: "changing alef_version together with a real key still changes the hash",
toml_a: b"[workspace]\nalef_version = \"0.61.0\"\nlanguages = [\"python\"]\n",
toml_b: b"[workspace]\nalef_version = \"0.61.1\"\nlanguages = [\"ruby\"]\n",
expect_equal: false,
},
];
for case in cases {
let h1 = compute_inputs_hash("sources_pin_table", case.toml_a);
let h2 = compute_inputs_hash("sources_pin_table", case.toml_b);
if case.expect_equal {
assert_eq!(h1, h2, "case `{}` expected equal hashes", case.name);
} else {
assert_ne!(h1, h2, "case `{}` expected different hashes", case.name);
}
}
}
#[test]
fn inputs_hash_differs_from_file_hash() {
let sources = "abc";
let content = "fn a() {}\n";
let ih = compute_inputs_hash(sources, content.as_bytes());
let fh = compute_file_hash(sources, content);
assert_ne!(ih, fh, "inputs hash and file hash must not collide");
}
#[test]
fn crate_sources_hash_differs_across_crates_with_disjoint_sources() {
use crate::core::config::resolved::ResolvedCrateConfig;
let dir = tempdir().unwrap();
let a = write_file(dir.path(), "a.rs", "fn a() {}");
let b = write_file(dir.path(), "b.rs", "fn b() {}");
let make_cfg = |name: &str, sources: Vec<std::path::PathBuf>| ResolvedCrateConfig {
name: name.to_string(),
sources,
source_crates: vec![],
version_from: "Cargo.toml".to_string(),
core_import: None,
workspace_root: None,
skip_core_import: false,
error_type: None,
error_constructor: None,
features: vec![],
path_mappings: Default::default(),
extra_dependencies: Default::default(),
auto_path_mappings: true,
languages: vec![],
targets: Default::default(),
python: None,
node: None,
ruby: None,
php: None,
elixir: None,
wasm: None,
ffi: None,
go: None,
java: None,
dart: None,
kotlin: None,
kotlin_android: None,
jni: None,
swift: None,
gleam: None,
csharp: None,
r: None,
zig: None,
exclude: Default::default(),
include: Default::default(),
output_paths: Default::default(),
explicit_output: Default::default(),
lint: Default::default(),
test: Default::default(),
setup: Default::default(),
update: Default::default(),
clean: Default::default(),
build_commands: Default::default(),
generate: Default::default(),
generate_overrides: Default::default(),
dto: Default::default(),
tools: Default::default(),
opaque_types: Default::default(),
client_constructors: Default::default(),
sync: None,
citation: None,
publish: None,
e2e: None,
adapters: vec![],
trait_bridges: vec![],
services: vec![],
handler_contracts: vec![],
scaffold: None,
package_metadata: None,
readme: None,
docs: None,
custom_files: Default::default(),
custom_modules: Default::default(),
custom_registrations: Default::default(),
suppress_validation_codes: Vec::new(),
untagged_union_text_types: vec![],
poly: Default::default(),
extra_clippy_allows: vec![],
crate_attributes: vec![],
cargo_lints: Default::default(),
};
let cfg_a = make_cfg("alpha", vec![a]);
let cfg_b = make_cfg("beta", vec![b]);
let hash_a = compute_crate_sources_hash(&cfg_a).unwrap();
let hash_b = compute_crate_sources_hash(&cfg_b).unwrap();
assert_ne!(
hash_a, hash_b,
"crates with disjoint sources must produce different hashes"
);
}
#[test]
fn crate_sources_hash_includes_source_crates() {
use crate::core::config::{SourceCrate, resolved::ResolvedCrateConfig};
let dir = tempdir().unwrap();
let a = write_file(dir.path(), "a.rs", "fn a() {}");
let b = write_file(dir.path(), "b.rs", "fn b() {}");
let make_cfg =
|sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
let source_crates = if source_crate_sources.is_empty() {
vec![]
} else {
vec![SourceCrate {
name: "extra-crate".to_string(),
sources: source_crate_sources,
roots: vec![],
from_registry: false,
}]
};
ResolvedCrateConfig {
name: "test".to_string(),
sources,
source_crates,
version_from: "Cargo.toml".to_string(),
core_import: None,
workspace_root: None,
skip_core_import: false,
error_type: None,
error_constructor: None,
features: vec![],
path_mappings: Default::default(),
extra_dependencies: Default::default(),
auto_path_mappings: true,
languages: vec![],
targets: Default::default(),
python: None,
node: None,
ruby: None,
php: None,
elixir: None,
wasm: None,
ffi: None,
go: None,
java: None,
dart: None,
kotlin: None,
kotlin_android: None,
jni: None,
swift: None,
gleam: None,
csharp: None,
r: None,
zig: None,
exclude: Default::default(),
include: Default::default(),
output_paths: Default::default(),
explicit_output: Default::default(),
lint: Default::default(),
test: Default::default(),
setup: Default::default(),
update: Default::default(),
clean: Default::default(),
build_commands: Default::default(),
generate: Default::default(),
generate_overrides: Default::default(),
dto: Default::default(),
tools: Default::default(),
opaque_types: Default::default(),
client_constructors: Default::default(),
sync: None,
citation: None,
publish: None,
e2e: None,
adapters: vec![],
trait_bridges: vec![],
services: vec![],
handler_contracts: vec![],
scaffold: None,
package_metadata: None,
readme: None,
docs: None,
custom_files: Default::default(),
custom_modules: Default::default(),
custom_registrations: Default::default(),
suppress_validation_codes: Vec::new(),
untagged_union_text_types: vec![],
poly: Default::default(),
extra_clippy_allows: vec![],
crate_attributes: vec![],
cargo_lints: Default::default(),
}
};
let cfg_without_extra = make_cfg(vec![a.clone()], vec![]);
let cfg_with_extra = make_cfg(vec![a.clone()], vec![b.clone()]);
let hash_without = compute_crate_sources_hash(&cfg_without_extra).unwrap();
let hash_with = compute_crate_sources_hash(&cfg_with_extra).unwrap();
assert_ne!(
hash_without, hash_with,
"adding a source_crate source file must change the hash"
);
}
#[test]
fn compute_crate_sources_hash_dedupes_overlapping_paths() {
use crate::core::config::{SourceCrate, resolved::ResolvedCrateConfig};
let dir = tempdir().unwrap();
let a = write_file(dir.path(), "a.rs", "fn a() {}");
let b = write_file(dir.path(), "b.rs", "fn b() {}");
let make_cfg =
|sources: Vec<std::path::PathBuf>, source_crate_sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
let source_crates = if source_crate_sources.is_empty() {
vec![]
} else {
vec![SourceCrate {
name: "extra-crate".to_string(),
sources: source_crate_sources,
roots: vec![],
from_registry: false,
}]
};
ResolvedCrateConfig {
name: "test".to_string(),
sources,
source_crates,
version_from: "Cargo.toml".to_string(),
core_import: None,
workspace_root: None,
skip_core_import: false,
error_type: None,
error_constructor: None,
features: vec![],
path_mappings: Default::default(),
extra_dependencies: Default::default(),
auto_path_mappings: true,
languages: vec![],
targets: Default::default(),
python: None,
node: None,
ruby: None,
php: None,
elixir: None,
wasm: None,
ffi: None,
go: None,
java: None,
dart: None,
kotlin: None,
kotlin_android: None,
jni: None,
swift: None,
gleam: None,
csharp: None,
r: None,
zig: None,
exclude: Default::default(),
include: Default::default(),
output_paths: Default::default(),
explicit_output: Default::default(),
lint: Default::default(),
test: Default::default(),
setup: Default::default(),
update: Default::default(),
clean: Default::default(),
build_commands: Default::default(),
generate: Default::default(),
generate_overrides: Default::default(),
dto: Default::default(),
tools: Default::default(),
opaque_types: Default::default(),
client_constructors: Default::default(),
sync: None,
citation: None,
publish: None,
e2e: None,
adapters: vec![],
trait_bridges: vec![],
services: vec![],
handler_contracts: vec![],
scaffold: None,
package_metadata: None,
readme: None,
docs: None,
custom_files: Default::default(),
custom_modules: Default::default(),
custom_registrations: Default::default(),
suppress_validation_codes: Vec::new(),
untagged_union_text_types: vec![],
poly: Default::default(),
extra_clippy_allows: vec![],
crate_attributes: vec![],
cargo_lints: Default::default(),
}
};
let cfg_with_dupes = make_cfg(vec![a.clone(), a.clone(), b.clone()], vec![a.clone()]);
let cfg_unique = make_cfg(vec![a.clone(), b.clone()], vec![]);
let hash_dup = compute_crate_sources_hash(&cfg_with_dupes).unwrap();
let hash_unique = compute_crate_sources_hash(&cfg_unique).unwrap();
assert_eq!(
hash_dup, hash_unique,
"duplicate source paths must not affect the per-crate sources hash"
);
}
#[test]
fn compute_crate_sources_hash_is_order_independent() {
use crate::core::config::resolved::ResolvedCrateConfig;
let dir = tempdir().unwrap();
let a = write_file(dir.path(), "a.rs", "fn a() {}");
let b = write_file(dir.path(), "b.rs", "fn b() {}");
let c = write_file(dir.path(), "c.rs", "fn c() {}");
let make_cfg = |sources: Vec<std::path::PathBuf>| -> ResolvedCrateConfig {
ResolvedCrateConfig {
name: "test".to_string(),
sources,
source_crates: vec![],
version_from: "Cargo.toml".to_string(),
core_import: None,
workspace_root: None,
skip_core_import: false,
error_type: None,
error_constructor: None,
features: vec![],
path_mappings: Default::default(),
extra_dependencies: Default::default(),
auto_path_mappings: true,
languages: vec![],
targets: Default::default(),
python: None,
node: None,
ruby: None,
php: None,
elixir: None,
wasm: None,
ffi: None,
go: None,
java: None,
dart: None,
kotlin: None,
kotlin_android: None,
jni: None,
swift: None,
gleam: None,
csharp: None,
r: None,
zig: None,
exclude: Default::default(),
include: Default::default(),
output_paths: Default::default(),
explicit_output: Default::default(),
lint: Default::default(),
test: Default::default(),
setup: Default::default(),
update: Default::default(),
clean: Default::default(),
build_commands: Default::default(),
generate: Default::default(),
generate_overrides: Default::default(),
dto: Default::default(),
tools: Default::default(),
opaque_types: Default::default(),
client_constructors: Default::default(),
sync: None,
citation: None,
publish: None,
e2e: None,
adapters: vec![],
trait_bridges: vec![],
services: vec![],
handler_contracts: vec![],
scaffold: None,
package_metadata: None,
readme: None,
docs: None,
custom_files: Default::default(),
custom_modules: Default::default(),
custom_registrations: Default::default(),
suppress_validation_codes: Vec::new(),
untagged_union_text_types: vec![],
poly: Default::default(),
extra_clippy_allows: vec![],
crate_attributes: vec![],
cargo_lints: Default::default(),
}
};
let cfg1 = make_cfg(vec![a.clone(), b.clone(), c.clone()]);
let cfg2 = make_cfg(vec![c.clone(), a.clone(), b.clone()]);
let cfg3 = make_cfg(vec![b.clone(), c.clone(), a.clone()]);
let h1 = compute_crate_sources_hash(&cfg1).unwrap();
let h2 = compute_crate_sources_hash(&cfg2).unwrap();
let h3 = compute_crate_sources_hash(&cfg3).unwrap();
assert_eq!(h1, h2, "reordering sources must not change the hash");
assert_eq!(h2, h3, "reordering sources must not change the hash");
}
#[test]
fn file_hash_round_trip_via_inject_extract() {
let sources_hash = "abc123";
let raw = "// auto-generated by alef\nfn body() {}\n";
let file_hash = compute_file_hash(sources_hash, raw);
let on_disk = inject_hash_line(raw, &file_hash);
let extracted = extract_hash(&on_disk).expect("hash line should be present");
let recomputed = compute_file_hash(sources_hash, &on_disk);
assert_eq!(extracted, file_hash);
assert_eq!(recomputed, file_hash);
assert_eq!(extracted, recomputed, "verify must reproduce the embedded hash");
}
#[test]
fn content_marker_detected_for_both_header_spellings() {
assert!(content_has_alef_marker(
"// This file is auto-generated by alef. DO NOT EDIT.\n"
));
assert!(content_has_alef_marker("# Generated by alef. Do not edit by hand.\n"));
assert!(content_has_alef_marker("#ifndef X\n\n/* auto-generated by alef */\n"));
}
#[test]
fn content_marker_absent_for_unmarked_and_late_markers() {
assert!(!content_has_alef_marker("fn handwritten() {}\n"));
let late = "x\n".repeat(20) + "// auto-generated by alef\n";
assert!(
!content_has_alef_marker(&late),
"a marker past the scanned prefix must not count, matching what verify reads"
);
}
#[test]
fn content_marker_accepts_swift_capital_a_spelling() {
assert!(content_has_alef_marker(
"// Auto-generated by alef — do not edit by hand.\n"
));
}
#[test]
fn content_marker_accepts_lowercase_generated_with_no_auto_prefix() {
assert!(content_has_alef_marker(
"# This file is generated by alef sync-versions; do not edit by hand.\n"
));
assert!(content_has_alef_marker("// DO NOT EDIT — generated by alef\n"));
}
#[test]
fn content_marker_accepts_gos_code_generated_convention() {
assert!(content_has_alef_marker("// Code generated by alef — DO NOT EDIT.\n"));
}
#[test]
fn content_marker_accepts_standard_spelling_as_positive_control() {
assert!(content_has_alef_marker(STANDARD_HEADER_LINE));
}
#[test]
fn content_marker_rejects_prose_that_only_shares_words_with_a_marker() {
assert!(!content_has_alef_marker("# Test apps are driven by alef\n"));
assert!(!content_has_alef_marker(
"// This module was generated for the alef project by a human.\n"
));
}
#[test]
fn every_known_marker_spelling_is_recognized() {
for spelling in marker_spellings::all_known_marker_texts() {
assert!(
content_has_alef_marker(spelling),
"content_has_alef_marker must accept a marker alef itself emits:\n{spelling}"
);
}
}
#[test]
fn near_miss_marker_detects_a_word_order_variant() {
let content = "// alef generated this file, do not edit\nfn main() {}\n";
assert_eq!(
near_miss_marker(content),
Some("// alef generated this file, do not edit")
);
}
#[test]
fn near_miss_marker_detects_a_hyphenated_variant() {
let content = "// This alef-generated file should not be edited.\nvalue\n";
assert_eq!(
near_miss_marker(content),
Some("// This alef-generated file should not be edited.")
);
}
#[test]
fn near_miss_marker_is_none_when_a_real_marker_already_matches() {
assert_eq!(near_miss_marker(STANDARD_HEADER_LINE), None);
assert_eq!(
near_miss_marker("// Auto-generated by alef — do not edit by hand.\n"),
None
);
}
#[test]
fn near_miss_marker_is_none_for_unrelated_content() {
assert_eq!(near_miss_marker("fn handwritten() {}\n"), None);
assert_eq!(near_miss_marker("# Test apps are driven by alef\n"), None);
assert_eq!(
near_miss_marker("// This file was generated by a different tool.\n"),
None
);
}
#[test]
fn near_miss_marker_only_scans_the_leading_window() {
let late = "x\n".repeat(20) + "// alef generated this\n";
assert_eq!(
near_miss_marker(&late),
None,
"a near miss past the scanned prefix must not count, matching content_has_alef_marker's own window"
);
}
mod marker_spellings;