alef 0.107.12

Opinionated polyglot binding generator for Rust libraries
Documentation
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_e2e_header_names_the_e2e_regenerate_command() {
    // alef generate's own summary line states it "never generates docs or e2e/test-app
    // output" -- an e2e-generated file whose header tells the reader to run plain
    // `alef generate` is pointing at a command that structurally cannot regenerate it.
    let h = e2e_header(CommentStyle::DoubleSlash);
    assert!(h.contains("// To regenerate: alef e2e generate"));
    assert!(!h.contains("// To regenerate: alef generate\n"));
}

#[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");
    // Stamp first, matching the documented ordering: `inject_hash_line` must run
    // last so the embedded `alef:hash:` reflects the stamped content. ~keep
    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 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);
}

/// `content_has_alef_marker` must recognize a headered file even when it has no
/// `alef:hash:` line yet -- this is the exact case `extract_hash(..).is_some()`
/// gets wrong (see #524): a stripped-but-headered file must still be
/// detectable as alef-owned so it can be re-stamped, not silently ignored.
#[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"
    );
}

/// `content_has_alef_marker` must also recognize a fully-stamped file (marker +
/// hash) -- the hash line's presence is irrelevant to marker detection.
#[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));
}

/// `content_has_alef_marker` must recognize the alternate marker used by custom
/// headers (Swift, Kotlin, Dart, Gleam, Zig, JNI).
#[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));
}

/// A file with no marker at all (user-owned code) must not be recognized.
#[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));
}

/// A marker line beyond the first `MARKER_SCAN_LINES` lines must not count --
/// matches the same bound `extract_hash` and `inject_hash_line` already use.
#[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));
}

/// The boundary that decides whether alef and poly agree about a file: a marker at alef's
/// deepest legal position (line `MARKER_SCAN_LINES`) stamps the very next line, and that line
/// must still be inside the first `POLY_GENERATED_SCAN_LINES` lines poly reads. Asserts the
/// marker really is claimed first, so the line-number assertion cannot pass over a file alef
/// never claimed at all.
#[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"
    );
}

/// Two files with different bodies must embed different hashes -- a same-per-repo hash
/// (the observed #524 symptom: two distinct values across an entire consumer repo) means
/// the hash isn't actually covering file content.
#[test]
fn test_compute_file_hash_distinct_content_yields_distinct_hashes() {
    let a = compute_file_hash("fn one() {}\n");
    let b = compute_file_hash("fn two() {}\n");
    assert_ne!(a, b, "distinct file bodies must yield distinct embedded hashes");
}

/// The same content hashed twice must be stable.
#[test]
fn test_compute_file_hash_same_content_yields_same_hash() {
    let a = compute_file_hash("fn one() {}\n");
    let b = compute_file_hash("fn one() {}\n");
    assert_eq!(a, b, "identical content must hash identically");
}

use std::path::{Path, PathBuf};
use tempfile::tempdir;

pub(super) 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);
}

mod marker_detection;
mod marker_negation;
mod marker_spellings;