alef 0.25.9

Opinionated polyglot binding generator for Rust libraries
Documentation
use super::*;

fn cargo_only_config(cargo: ScaffoldCargo) -> ResolvedCrateConfig {
    let mut cfg = test_config();
    cfg.scaffold = Some(ScaffoldConfig {
        description: Some("Test library".to_string()),
        license: Some("MIT".to_string()),
        repository: Some("https://github.com/test/my-lib".to_string()),
        homepage: None,
        authors: vec!["Alice".to_string()],
        keywords: vec!["test".to_string()],
        generated_header: None,
        precommit: None,
        cargo: Some(cargo),
    });
    cfg
}

#[test]
fn cargo_config_default_renders_canonical_six_target_template() {
    let rendered = render_cargo_config(&ScaffoldCargo::default());

    // Header marker so finalize_hashes will stamp it.
    assert!(rendered.starts_with("# This file is auto-generated by alef. DO NOT EDIT.\n"));
    assert!(rendered.contains("# Re-generate with: alef scaffold\n"));

    // Canonical sections, in fixed order.
    assert!(rendered.contains("[build]\nincremental = true"));
    assert!(rendered.contains("[net]\ngit-fetch-with-cli = true"));
    assert!(rendered.contains("[registries.crates-io]\nprotocol = \"sparse\""));

    // All six target families present by default.
    assert!(rendered.contains("[target.'cfg(target_os = \"macos\")']"));
    assert!(rendered.contains("link-arg=-Wl,-undefined,dynamic_lookup"));
    assert!(rendered.contains("[target.x86_64-pc-windows-msvc]"));
    assert!(rendered.contains("[target.i686-pc-windows-msvc]"));
    assert!(rendered.contains("[target.aarch64-unknown-linux-gnu]"));
    assert!(rendered.contains("[target.x86_64-unknown-linux-musl]"));
    assert!(rendered.contains("[target.wasm32-unknown-unknown]"));
    assert!(rendered.contains("getrandom_backend=\\\"wasm_js\\\""));

    // No [env] block when none declared.
    assert!(!rendered.contains("\n[env]\n"));
}

#[test]
fn cargo_config_re_render_is_byte_identical() {
    let cargo = ScaffoldCargo::default();
    let first = render_cargo_config(&cargo);
    let second = render_cargo_config(&cargo);
    assert_eq!(first, second);
}

#[test]
fn cargo_config_disabling_individual_targets_omits_their_blocks() {
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets {
            i686_pc_windows_msvc: false,
            x86_64_unknown_linux_musl: false,
            ..ScaffoldCargoTargets::default()
        },
        build_jobs: 4,
        env: Default::default(),
    };
    let rendered = render_cargo_config(&cargo);
    assert!(!rendered.contains("[target.i686-pc-windows-msvc]"));
    assert!(!rendered.contains("[target.x86_64-unknown-linux-musl]"));
    // Other targets remain.
    assert!(rendered.contains("[target.x86_64-pc-windows-msvc]"));
    assert!(rendered.contains("[target.aarch64-unknown-linux-gnu]"));
    assert!(rendered.contains("[target.'cfg(target_os = \"macos\")']"));
}

#[test]
fn cargo_config_disabling_macos_omits_dynamic_lookup() {
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets {
            macos_dynamic_lookup: false,
            ..ScaffoldCargoTargets::default()
        },
        build_jobs: 4,
        env: Default::default(),
    };
    let rendered = render_cargo_config(&cargo);
    assert!(!rendered.contains("dynamic_lookup"));
    assert!(!rendered.contains("cfg(target_os = \"macos\")"));
}

#[test]
fn cargo_config_env_plain_string_renders_into_env_block() {
    let mut env = std::collections::HashMap::new();
    env.insert("MY_VAR".to_string(), ScaffoldCargoEnvValue::Plain("hello".to_string()));
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 4,
        env,
    };
    let rendered = render_cargo_config(&cargo);
    assert!(rendered.contains("\n[env]\n"));
    assert!(rendered.contains("MY_VAR = \"hello\"\n"));
}

#[test]
fn cargo_config_env_structured_value_renders_with_relative() {
    let mut env = std::collections::HashMap::new();
    env.insert(
        "RUBY".to_string(),
        ScaffoldCargoEnvValue::Structured {
            value: "scripts/preferred-ruby.sh".to_string(),
            relative: true,
        },
    );
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 4,
        env,
    };
    let rendered = render_cargo_config(&cargo);
    assert!(rendered.contains("[env]\n"));
    assert!(rendered.contains("RUBY = { value = \"scripts/preferred-ruby.sh\", relative = true }\n"));
}

#[test]
fn cargo_config_env_keys_are_sorted_for_determinism() {
    let mut env = std::collections::HashMap::new();
    env.insert("ZED".to_string(), ScaffoldCargoEnvValue::Plain("z".to_string()));
    env.insert("ALPHA".to_string(), ScaffoldCargoEnvValue::Plain("a".to_string()));
    env.insert("MID".to_string(), ScaffoldCargoEnvValue::Plain("m".to_string()));
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 4,
        env,
    };
    let rendered = render_cargo_config(&cargo);
    let env_section = rendered.split("[env]\n").nth(1).expect("env section present");
    let alpha_pos = env_section.find("ALPHA").expect("ALPHA present");
    let mid_pos = env_section.find("MID").expect("MID present");
    let zed_pos = env_section.find("ZED").expect("ZED present");
    assert!(alpha_pos < mid_pos);
    assert!(mid_pos < zed_pos);
}

#[test]
fn cargo_config_env_string_with_quotes_is_escaped() {
    let mut env = std::collections::HashMap::new();
    env.insert(
        "QUOTED".to_string(),
        ScaffoldCargoEnvValue::Plain(r#"a"b\c"#.to_string()),
    );
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 4,
        env,
    };
    let rendered = render_cargo_config(&cargo);
    // Backslashes doubled, quotes escaped.
    assert!(rendered.contains("QUOTED = \"a\\\"b\\\\c\"\n"));
}

#[test]
fn cargo_config_default_includes_build_jobs_limit() {
    let rendered = render_cargo_config(&ScaffoldCargo::default());
    // Default is 4 jobs to prevent OOM on 16 GB dev machines.
    assert!(
        rendered.contains("[build]\nincremental = true\njobs = 4\n"),
        "build_jobs default (4) must be in [build] section; got:\n{rendered}"
    );
}

#[test]
fn cargo_config_build_jobs_zero_disables_limit() {
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 0,
        env: Default::default(),
    };
    let rendered = render_cargo_config(&cargo);
    assert!(
        !rendered.contains("jobs = "),
        "build_jobs = 0 must not emit jobs limit; got:\n{rendered}"
    );
}

#[test]
fn cargo_config_build_jobs_custom_value_renders() {
    let cargo = ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 2,
        env: Default::default(),
    };
    let rendered = render_cargo_config(&cargo);
    assert!(
        rendered.contains("jobs = 2\n"),
        "custom build_jobs value must render; got:\n{rendered}"
    );
}

#[test]
fn scaffold_emits_cargo_config_when_scaffold_cargo_is_set() {
    let config = cargo_only_config(ScaffoldCargo::default());
    let api = test_api();
    let all_files = scaffold(&api, &config, &[Language::Wasm]).unwrap();
    let cargo_file = all_files
        .iter()
        .find(|f| f.path == std::path::Path::new(".cargo/config.toml"))
        .expect(".cargo/config.toml should be emitted when [scaffold.cargo] is set");
    assert!(
        cargo_file.generated_header,
        "generated_header must be true so verify walks it"
    );
    assert!(cargo_file.content.contains("auto-generated by alef"));
    assert!(cargo_file.content.contains("dynamic_lookup"));
    assert!(cargo_file.content.contains("[target.x86_64-pc-windows-msvc]"));
}

#[test]
fn scaffold_skips_cargo_config_in_legacy_mode_when_file_exists() {
    // No `[scaffold.cargo]` opt-in. Existing-file check is filesystem-bound, so
    // we only assert that scaffold() does not panic and produces no `.cargo/config.toml`
    // entry when the legacy create-if-missing branch detects the file already exists.
    // (The existing tests filter `.cargo/config.toml` out via `language_files()`,
    // implicitly relying on this branch never producing a hash-headered file.)
    let config = test_config(); // scaffold.cargo is None
    let api = test_api();
    let all_files = scaffold(&api, &config, &[Language::Python]).unwrap();
    let cargo_files: Vec<_> = all_files
        .iter()
        .filter(|f| f.path == std::path::Path::new(".cargo/config.toml"))
        .collect();
    // Legacy branch is gated on Wasm + !exists. We're not requesting Wasm here,
    // so no .cargo/config.toml should appear regardless of filesystem state.
    assert!(
        cargo_files.is_empty(),
        "legacy branch should not emit .cargo/config.toml without Wasm",
    );
}

#[test]
fn scaffold_emits_cargo_config_with_env_block_for_sample_markup_style_ruby_path() {
    let mut env = std::collections::HashMap::new();
    env.insert(
        "RUBY".to_string(),
        ScaffoldCargoEnvValue::Structured {
            value: "scripts/preferred-ruby.sh".to_string(),
            relative: true,
        },
    );
    let config = cargo_only_config(ScaffoldCargo {
        targets: ScaffoldCargoTargets::default(),
        build_jobs: 4,
        env,
    });
    let api = test_api();
    let all_files = scaffold(&api, &config, &[Language::Ruby]).unwrap();
    let cargo_file = all_files
        .iter()
        .find(|f| f.path == std::path::Path::new(".cargo/config.toml"))
        .expect(".cargo/config.toml should be emitted");
    assert!(cargo_file.content.contains("[env]\n"));
    assert!(
        cargo_file
            .content
            .contains("RUBY = { value = \"scripts/preferred-ruby.sh\", relative = true }")
    );
}