alef 0.77.0

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

/// Regression coverage for alef #152: `sync_versions` must bump the self-referential
/// dependency pin in the Rust e2e harness manifest (`<e2e.output>/rust/Cargo.toml` and
/// `<e2e.registry.output>/rust/Cargo.toml`) alongside `[package] version`, for BOTH the
/// renamed dependency-key spelling (`dep_name = { package = "crate-name", version = "..." }`,
/// used when the crate name contains a hyphen) and the plain spelling (`dep_name = "..."`,
/// used when the Cargo package name has no hyphen so the dependency key already equals the
/// crate name and no `package = "..."` rename is required).
///
/// Real-world symptom (a consumer at 1.3.2): a plain `alef sync-versions --bump patch` (no
/// `--regen` — the default; regenerating code is opt-in, see `SyncVersions::regen`'s doc)
/// bumped `[package] version` in `test_apps/rust/Cargo.toml` to 1.3.2 and re-stamped
/// `alef:hash:`, but left `samplewidget = { version = "1.3.1", ... }` untouched. This was
/// initially misdiagnosed as a `package = "..."` rename discriminator (the hyphenated
/// consumers' renamed-form pins looked correctly bumped in their committed trees) — but
/// reproducing against that consumer's actual alef.toml and source tree showed the rename
/// spelling was never the deciding factor:
/// `sync_rust_test_app_version`/`sync_rust_harness_cargo_toml`
/// (`src/cli/pipeline/version_workspace.rs`) called `write_version_to_cargo_toml` (patches
/// `[package] version` only) but never called `patch_workspace_dep_versions` (patches the
/// dependency pin) at all, in either spelling — unlike the sibling
/// `packages/ruby/ext/*/native/Cargo.toml` block in `version.rs`, which already pairs both
/// calls for exactly this reason. The hyphenated consumers' committed pins were correct only
/// because a later, separate full regen (`alef generate`/`alef all`, which does run
/// `render_cargo_toml`) happened to land before the plain-form one's did — not because
/// `sync-versions` itself distinguished the two spellings. Both spellings are exercised here
/// so a regression narrowed to only one of `patch_workspace_dep_versions`' two match arms
/// (bare key vs. `package = "..."` key) cannot silently return. ~keep
fn run_registry_dep_pin_case(package_name: &str, expect_package_rename: bool) {
    use crate::core::config::NewAlefConfig;

    let _guard = CWD_LOCK.lock().unwrap_or_else(|e| e.into_inner());
    let original_cwd = std::env::current_dir().expect("cwd");

    let tmp = tempfile::tempdir().expect("tempdir");
    let root = tmp.path();

    std::fs::write(
        root.join("Cargo.toml"),
        format!("[package]\nname = \"{package_name}\"\nversion = \"1.3.1\"\nedition = \"2024\"\n"),
    )
    .expect("write Cargo.toml");

    // The dependency key is always the underscored form of the crate name — this is what
    // `resolve_crate_name`/`dep_name` derive in `src/e2e/codegen/rust/mod.rs`.
    let dep_name = package_name.replace('-', "_");
    let dep_line = if expect_package_rename {
        format!("{dep_name} = {{ package = \"{package_name}\", version = \"1.3.1\" }}")
    } else {
        format!("{dep_name} = {{ version = \"1.3.1\" }}")
    };

    // Registry-mode harness: `<e2e.registry.output>/rust/Cargo.toml` (default "test_apps").
    let test_apps_rust_dir = root.join("test_apps/rust");
    std::fs::create_dir_all(&test_apps_rust_dir).expect("mkdir test_apps/rust");
    std::fs::write(
        test_apps_rust_dir.join("Cargo.toml"),
        format!(
            concat!(
                "# This file is auto-generated by alef — DO NOT EDIT.\n",
                "# alef:hash:deadbeef\n",
                "# To regenerate: alef generate\n",
                "# To verify freshness: alef verify\n",
                "\n",
                "[workspace]\n",
                "\n",
                "[package]\n",
                "name = \"{package_name}-e2e-rust\"\n",
                "version = \"1.3.1\"\n",
                "edition = \"2024\"\n",
                "license = \"MIT\"\n",
                "publish = false\n",
                "\n",
                "[dependencies]\n",
                "{dep_line}\n",
            ),
            package_name = package_name,
            dep_line = dep_line,
        ),
    )
    .expect("write test_apps/rust/Cargo.toml");

    let alef_toml = format!(
        concat!(
            "[workspace]\n",
            "languages = [\"rust\"]\n\n",
            "[[crates]]\n",
            "name = \"{package_name}\"\n",
            "sources = []\n",
            "version_from = \"Cargo.toml\"\n\n",
            "[crates.e2e]\n",
            "languages = []\n\n",
            "[crates.e2e.call]\n",
            "module = \"{package_name}\"\n",
            "function = \"hello\"\n",
        ),
        package_name = package_name,
    );
    let alef_toml_path = root.join("alef.toml");
    std::fs::write(&alef_toml_path, &alef_toml).expect("write alef.toml");

    let cfg: NewAlefConfig = toml::from_str(&alef_toml).expect("parse alef.toml");
    let mut resolved = cfg.resolve().expect("resolve config");
    let resolved_cfg = resolved.remove(0);

    std::env::set_current_dir(root).expect("set_current_dir");
    // no_regen = true: matches the real, default `alef sync-versions` CLI invocation (no
    // `--regen`), which is what actually reproduced alef #152 — the direct-patch step alone
    // must keep the dependency pin in sync, without ever running the full e2e regenerator.
    let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, Some("patch"), true, true, None);
    let _ = std::env::set_current_dir(&original_cwd);
    sync_result.expect("sync_versions ok");

    let out = std::fs::read_to_string(test_apps_rust_dir.join("Cargo.toml")).expect("read test_apps/rust/Cargo.toml");
    assert!(
        out.contains("version = \"1.3.2\""),
        "test_apps/rust/Cargo.toml [package] version must be bumped to 1.3.2, got:\n{out}"
    );
    assert!(
        !out.contains("1.3.1"),
        "stale version 1.3.1 must be gone from test_apps/rust/Cargo.toml (checked both [package] \
         and the {dep_name} dependency pin), got:\n{out}"
    );
}

#[test]
fn sync_versions_bumps_registry_dep_pin_with_package_rename() {
    // Hyphenated crate name: dependency key differs from the crate name, so the pin uses the
    // `package = "..."` rename form (hyphenated multi-segment crate shape).
    run_registry_dep_pin_case("sample-widget-lib", true);
}

#[test]
fn sync_versions_bumps_registry_dep_pin_without_package_rename() {
    // Crate name has no hyphen: the dependency key already equals the crate name, so the pin
    // uses the plain form with no `package = "..."` key (unhyphenated crate shape, alef #152).
    run_registry_dep_pin_case("samplewidget", false);
}