alef 0.115.12

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

/// `sync_versions` must bump the `[package] version` field itself in the Ruby
/// native (Magnus) crate manifest at `packages/ruby/ext/<gem>_rb/native/Cargo.toml`,
/// not just the core-crate dependency pin inside it (covered separately by
/// `sync_versions_bumps_ruby_native_core_dep_pin`).
///
/// Regression test: the only rewrite ever applied to this file was
/// `patch_workspace_dep_versions`, which walks `[dependencies]` /
/// `[dev-dependencies]` / `[build-dependencies]` tables and only touches entries
/// whose key names a workspace member — it never looks at the file's own
/// `[package].version` field. `write_version_to_cargo_toml` (which does update
/// `[package].version`) was never called for this path, so the crate's own
/// declared version silently drifted from the workspace version on every bump.
#[test]
fn sync_versions_bumps_ruby_native_package_version_field() {
    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"),
        "[workspace.package]\nversion = \"1.3.1\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
    )
    .expect("write Cargo.toml");

    let native_dir = root.join("packages/ruby/ext/demolib_rb/native");
    std::fs::create_dir_all(&native_dir).expect("mkdir ruby native");
    std::fs::write(
        native_dir.join("Cargo.toml"),
        concat!(
            "# Code generated by alef — DO NOT EDIT.\n",
            "# alef:hash:deadbeef\n",
            "[package]\n",
            "name = \"demolib-rb\"\n",
            "version = \"1.3.0\"\n",
            "edition = \"2024\"\n",
            "\n",
            "[dependencies]\n",
            "magnus = \"0.7\"\n",
            // Coincidentally carries the same digits as the pre-bump project version.
            // A naive string replace across the file would corrupt this; the fix must
            // only ever touch the `[package].version` key and named workspace-member
            // dependency pins.
            "serde = \"1.3.0\"\n",
            "demolib = { version = \"1.3.0\", path = \"../../../../../crates/demolib\", features = [\"full\"] }\n",
        ),
    )
    .expect("write ruby native Cargo.toml");

    let alef_toml = format!(
        "[workspace]\nlanguages = [\"ruby\"]\n[[crates]]\nname = \"demolib\"\nsources = []\nversion_from = \"{}\"\n",
        root.join("Cargo.toml").display().to_string().replace('\\', "/")
    );
    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");
    let sync_result = sync_versions(&resolved_cfg, &alef_toml_path, None, true, true, None);
    let _ = std::env::set_current_dir(&original_cwd);
    sync_result.expect("sync_versions ok");

    let out = std::fs::read_to_string(native_dir.join("Cargo.toml")).expect("read ruby native Cargo.toml");
    assert!(
        out.contains("name = \"demolib-rb\"\nversion = \"1.3.1\""),
        "ruby native crate's own [package] version must be bumped to 1.3.1, got:\n{out}"
    );
    assert!(
        !out.contains("name = \"demolib-rb\"\nversion = \"1.3.0\""),
        "stale [package] version 1.3.0 must be gone, got:\n{out}"
    );
    assert!(
        out.contains(r#"demolib = { version = "1.3.1""#),
        "core dep pin must still be bumped to 1.3.1, got:\n{out}"
    );
    assert!(
        out.contains(r#"magnus = "0.7""#),
        "external magnus dep must not be touched, got:\n{out}"
    );
    assert!(
        out.contains("serde = \"1.3.0\""),
        "external serde dep pin coincidentally matching the old project version must be left \
         byte-identical, got:\n{out}"
    );
}

#[test]
fn extra_cargo_manifest_syncs_core_requirement_without_touching_external_versions() {
    for old_package_version in ["2.0.0", "1.23.0"] {
        for dependency_key in ["demo", "renamed"] {
            check_extra_cargo_manifest(old_package_version, dependency_key);
        }
    }
}

fn check_extra_cargo_manifest(old_package_version: &str, dependency_key: &str) {
    let _guard = CWD_LOCK.lock().unwrap_or_else(|error| error.into_inner());
    let original_cwd = std::env::current_dir().expect("cwd");
    let temporary = tempfile::tempdir().expect("tempdir");
    let root = temporary.path();
    std::fs::write(root.join("Cargo.toml"), "[workspace.package]\nversion = \"1.23.0\"\n").expect("root manifest");
    let native = root.join("packages/elixir/native/demo_nif");
    std::fs::create_dir_all(&native).expect("native directory");
    let manifest = format!(
        "[package]\nname = \"demo_nif\"\nversion = \"{old_package_version}\"\n\n\
         [dependencies]\n{dependency_key} = {{ package = \"demo\", version = \"2.0.0\", path = \"../../../../crates/demo\" }}\n\
         external = {{ version = \"2.0.0\", path = \"../../../../vendor/external\" }}\n"
    );
    std::fs::write(native.join("Cargo.toml"), &manifest).expect("native manifest");
    let config_text = concat!(
        "[workspace]\nlanguages = [\"elixir\"]\n",
        "[workspace.sync]\nextra_paths = [\"packages/elixir/native/demo_nif/Cargo.toml\"]\n",
        "[[crates]]\nname = \"demo\"\nsources = []\nversion_from = \"Cargo.toml\"\n"
    );
    std::fs::write(root.join("alef.toml"), config_text).expect("write config");
    let config: crate::core::config::NewAlefConfig = toml::from_str(config_text).expect("config");
    let resolved = config.resolve().expect("resolve config").remove(0);
    std::env::set_current_dir(root).expect("fixture cwd");
    let result = sync_versions(&resolved, &root.join("alef.toml"), None, true, true, None);
    std::env::set_current_dir(original_cwd).expect("restore cwd");
    result.expect("sync versions");
    let output = std::fs::read_to_string(native.join("Cargo.toml")).expect("updated manifest");
    let document: toml::Value = toml::from_str(&output).expect("valid TOML");
    assert_eq!(document["package"]["version"].as_str(), Some("1.23.0"));
    assert_eq!(
        document["dependencies"][dependency_key]["version"].as_str(),
        Some("1.23.0")
    );
    assert_eq!(document["dependencies"]["external"]["version"].as_str(), Some("2.0.0"));
    assert_eq!(
        document["dependencies"][dependency_key]["path"].as_str(),
        Some("../../../../crates/demo")
    );
}

#[test]
fn extra_cargo_manifest_accepts_inherited_and_virtual_package_versions() {
    for (header, dependency_table) in [
        (
            "[package]\nname = \"demo_nif\"\nversion.workspace = true\n",
            "dependencies",
        ),
        ("[workspace]\n", "workspace.dependencies"),
    ] {
        let temporary = tempfile::tempdir().expect("tempdir");
        let path = temporary.path().join("Cargo.toml");
        let input = format!("{header}[{dependency_table}]\ndemo = {{ version = \"2.0.0\", path = \"../demo\" }}\n");
        std::fs::write(&path, input).expect("manifest");
        let mut updated = Vec::new();
        apply_extra_path_rewrite(&path, "1.23.0", "demo", &mut updated).expect("sync inherited manifest");
        let output = std::fs::read_to_string(&path).expect("output");
        assert!(output.starts_with(header));
        let document: toml::Value = toml::from_str(&output).expect("TOML");
        let dependency_owner = if dependency_table == "workspace.dependencies" {
            &document["workspace"]
        } else {
            &document
        };
        assert_eq!(
            dependency_owner["dependencies"]["demo"]["version"].as_str(),
            Some("1.23.0")
        );
        assert_eq!(updated, vec![path.to_string_lossy().to_string()]);
    }
}

#[test]
fn extra_cargo_manifest_preserves_external_package_alias_shadowing_core_name() {
    let temporary = tempfile::tempdir().expect("tempdir");
    let path = temporary.path().join("Cargo.toml");
    let input = concat!(
        "[package]\nname = \"demo_nif\"\nversion = \"1.23.0\"\n",
        "[dependencies]\ndemo = { package = \"external\", version = \"2.0.0\", path = \"../external\" }\n"
    );
    std::fs::write(&path, input).expect("manifest");
    let mut updated = Vec::new();
    apply_extra_path_rewrite(&path, "1.23.0", "demo", &mut updated).expect("sync external alias");
    assert_eq!(std::fs::read_to_string(path).expect("output"), input);
    assert!(updated.is_empty());
}