use super::*;
const GRADLE_BUILD_SAMPLE: &str = r#"import org.jetbrains.kotlin.gradle.dsl.JvmTarget
plugins {
`java-library`
kotlin("jvm") version "2.3.21"
`maven-publish`
id("org.jlleitschuh.gradle.ktlint") version "12.1.0"
}
group = "dev.example"
version = "0.15.6-rc.2"
repositories {
mavenCentral()
}
dependencies {
api("net.java.dev.jna:jna:5.14.0")
}
ktlint {
version.set("1.0.1")
}
"#;
#[test]
fn replace_gradle_project_version_bumps_only_project_version() {
let out = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").expect("project version bumped");
assert!(
out.contains("version = \"0.15.6-rc.3\""),
"project version must be bumped:\n{out}"
);
assert!(
out.contains(r#"kotlin("jvm") version "2.3.21""#),
"kotlin plugin version must not change:\n{out}"
);
assert!(
out.contains(r#"id("org.jlleitschuh.gradle.ktlint") version "12.1.0""#),
"ktlint plugin version must not change:\n{out}"
);
assert!(
out.contains(r#"version.set("1.0.1")"#),
"ktlint extension version must not change:\n{out}"
);
assert!(
out.contains(r#"api("net.java.dev.jna:jna:5.14.0")"#),
"jna coordinate must not change:\n{out}"
);
assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
}
#[test]
fn replace_gradle_project_version_is_idempotent() {
let first = replace_gradle_project_version(GRADLE_BUILD_SAMPLE, "0.15.6-rc.3").unwrap();
assert!(
replace_gradle_project_version(&first, "0.15.6-rc.3").is_none(),
"second call with same version must return None"
);
}
#[test]
fn replace_gradle_project_version_no_project_version_returns_none() {
let content = "plugins {\n kotlin(\"jvm\") version \"2.3.21\"\n}\n";
assert!(replace_gradle_project_version(content, "1.0.0").is_none());
}
const NIF_CARGO_LOCK_SAMPLE: &str = r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 4
[[package]]
name = "example_core"
version = "0.15.6-rc.2"
dependencies = [
"serde",
]
[[package]]
name = "example_nif"
version = "0.15.6-rc.2"
dependencies = [
"example_core",
"rustler",
]
[[package]]
name = "serde"
version = "1.0.219"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "5f0e2c6ed6606019b4e29e69dbaba95b11854410e5347d525002456dbbb786b6"
"#;
#[test]
fn sync_cargo_lock_path_versions_bumps_only_sourceless_entries() {
let out = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").expect("lock updated");
assert!(
out.contains("version = 4"),
"lock format version line must be preserved:\n{out}"
);
assert_eq!(
out.matches("version = \"0.15.6-rc.3\"").count(),
2,
"both local crates must be bumped:\n{out}"
);
assert!(!out.contains("0.15.6-rc.2"), "old version must be gone:\n{out}");
assert!(
out.contains("version = \"1.0.219\""),
"registry dep version must not change:\n{out}"
);
assert!(
out.contains("source = \"registry+https://github.com/rust-lang/crates.io-index\""),
"registry source line must be preserved:\n{out}"
);
}
#[test]
fn sync_cargo_lock_path_versions_is_idempotent() {
let first = sync_cargo_lock_path_versions(NIF_CARGO_LOCK_SAMPLE, "0.15.6-rc.3").unwrap();
assert!(
sync_cargo_lock_path_versions(&first, "0.15.6-rc.3").is_none(),
"second call with same version must return None"
);
}
#[test]
fn sync_cargo_lock_path_versions_preserves_no_trailing_newline() {
let no_newline = NIF_CARGO_LOCK_SAMPLE.trim_end_matches('\n');
let out = sync_cargo_lock_path_versions(no_newline, "0.15.6-rc.3").unwrap();
assert!(
!out.ends_with('\n'),
"absence of trailing newline must be preserved:\n{out:?}"
);
}
#[test]
fn sync_cargo_lock_path_versions_all_registry_returns_none() {
let content = "version = 4\n\n[[package]]\nname = \"serde\"\nversion = \"1.0.219\"\nsource = \"registry+x\"\n";
assert!(sync_cargo_lock_path_versions(content, "9.9.9").is_none());
}
#[test]
fn sync_docs_version_badges_updates_api_files_only() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
std::fs::write(
dir.join("api-rust.md"),
"## Rust API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n\nbody\n",
)
.expect("write api-rust.md");
std::fs::write(
dir.join("api-python.md"),
"## Python API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write api-python.md");
std::fs::write(
dir.join("configuration.md"),
"## Configuration <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write configuration.md");
let updated = sync_docs_version_badges(dir, "0.15.6-rc.3");
assert_eq!(
updated.len(),
2,
"only the two api-*.md files must be updated: {updated:?}"
);
let rust = std::fs::read_to_string(dir.join("api-rust.md")).unwrap();
assert!(
rust.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
"rust badge must be bumped:\n{rust}"
);
let config = std::fs::read_to_string(dir.join("configuration.md")).unwrap();
assert!(
config.contains("v0.15.6-rc.2"),
"non-api doc must not be touched:\n{config}"
);
}
#[test]
fn sync_docs_version_badges_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let dir = tmp.path();
std::fs::write(
dir.join("api-go.md"),
"## Go API Reference <span class=\"version-badge\">v1.0.0</span>\n",
)
.expect("write api-go.md");
let _ = sync_docs_version_badges(dir, "1.0.0");
let second = sync_docs_version_badges(dir, "1.0.0");
assert!(second.is_empty(), "second call with same version must be a no-op");
}
#[test]
fn sync_versions_bumps_kotlin_gradle_nif_lock_and_docs_badges() {
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 = \"0.15.6-rc.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/kotlin")).expect("mkdir packages/kotlin");
std::fs::write(root.join("packages/kotlin/build.gradle.kts"), GRADLE_BUILD_SAMPLE).expect("write build.gradle.kts");
std::fs::create_dir_all(root.join("packages/elixir/native/example_nif")).expect("mkdir native");
std::fs::write(
root.join("packages/elixir/native/example_nif/Cargo.lock"),
NIF_CARGO_LOCK_SAMPLE,
)
.expect("write Cargo.lock");
std::fs::create_dir_all(root.join("docs/reference")).expect("mkdir docs/reference");
std::fs::write(
root.join("docs/reference/api-elixir.md"),
"## Elixir API Reference <span class=\"version-badge\">v0.15.6-rc.2</span>\n",
)
.expect("write api-elixir.md");
let alef_toml = format!(
"[workspace]\nlanguages = [\"kotlin\", \"elixir\"]\n[[crates]]\nname = \"example\"\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 gradle = std::fs::read_to_string(root.join("packages/kotlin/build.gradle.kts")).expect("read gradle");
assert!(
gradle.contains("version = \"0.15.6-rc.3\""),
"kotlin gradle project version must be bumped:\n{gradle}"
);
assert!(
gradle.contains(r#"kotlin("jvm") version "2.3.21""#),
"kotlin plugin version must not change:\n{gradle}"
);
let lock = std::fs::read_to_string(root.join("packages/elixir/native/example_nif/Cargo.lock")).expect("read lock");
assert_eq!(
lock.matches("version = \"0.15.6-rc.3\"").count(),
2,
"both local NIF lock entries must be bumped:\n{lock}"
);
assert!(
lock.contains("version = \"1.0.219\""),
"registry dep in lock must not change:\n{lock}"
);
let badge = std::fs::read_to_string(root.join("docs/reference/api-elixir.md")).expect("read api-elixir.md");
assert!(
badge.contains("<span class=\"version-badge\">v0.15.6-rc.3</span>"),
"docs version badge must be bumped:\n{badge}"
);
}
#[test]
fn update_zig_package_hash_rc_prerelease() {
let existing = "sample_pkg-1.4.0-rc.50-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9";
let result = update_zig_package_hash(existing, "1.4.0-rc.50", "1.4.0-rc.53");
assert_eq!(
result,
Some("sample_pkg-1.4.0-rc.53-Jfgk_HsxAQAl3_LX7NCs1l27EHcYVF9dieEDCVAwUxK9".to_string()),
"rc prerelease version must be substituted in hash"
);
}
#[test]
fn update_zig_package_hash_release_version() {
let existing = "sample_pkg-1.4.0-rc.53-AbCd_XyZ123456789";
let result = update_zig_package_hash(existing, "1.4.0-rc.53", "1.4.0");
assert_eq!(
result,
Some("sample_pkg-1.4.0-AbCd_XyZ123456789".to_string()),
"release version must substitute prerelease"
);
}
#[test]
fn update_zig_package_hash_same_version_is_none() {
let existing = "mylib-0.1.0-rc.1-SomeBase64Hash";
let result = update_zig_package_hash(existing, "0.1.0-rc.1", "0.1.0-rc.1");
assert_eq!(result, None, "same version must return None");
}
#[test]
fn update_zig_package_hash_malformed_hash_is_none() {
let existing = "notenoughparts";
let result = update_zig_package_hash(existing, "0.1.0", "0.2.0");
assert_eq!(result, None, "malformed hash must return None");
}
#[test]
fn render_registry_version_python_pep440_rc_prerelease() {
let result = render_registry_version("python", "0.3.0-rc.28", ">=0.1.0rc9");
assert_eq!(result, Some(">=0.3.0rc28".to_string()));
}
#[test]
fn render_registry_version_python_pep440_release() {
let result = render_registry_version("python", "1.0.0", ">=0.9.0");
assert_eq!(result, Some(">=1.0.0".to_string()));
}
#[test]
fn render_registry_version_python_already_current_is_none() {
let result = render_registry_version("python", "0.3.0-rc.28", ">=0.3.0rc28");
assert_eq!(result, None);
}
#[test]
fn render_registry_version_node_semver_rc() {
let result = render_registry_version("node", "0.3.0-rc.28", "^0.1.0-rc.9");
assert_eq!(result, Some("^0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_elixir_hex_constraint() {
let result = render_registry_version("elixir", "0.3.0-rc.28", "~> 0.1.0-rc.9");
assert_eq!(result, Some("~> 0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_ruby_rubygems_prerelease() {
let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.1.0.pre.rc.9");
assert_eq!(result, Some(">= 0.3.0.pre.rc.28".to_string()));
}
#[test]
fn render_registry_version_ruby_already_current_is_none() {
let result = render_registry_version("ruby", "0.3.0-rc.28", ">= 0.3.0.pre.rc.28");
assert_eq!(result, None);
}
#[test]
fn render_registry_version_go_module_version() {
let result = render_registry_version("go", "0.3.0-rc.28", "v0.1.0-rc.9");
assert_eq!(result, Some("v0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_rust_bare_semver() {
let result = render_registry_version("rust", "0.3.0-rc.28", "0.1.0-rc.9");
assert_eq!(result, Some("0.3.0-rc.28".to_string()));
}
#[test]
fn render_registry_version_php_composer_range() {
let result = render_registry_version("php", "0.3.0-rc.28", ">=0.1.0-rc.9");
assert_eq!(result, Some(">=0.3.0-rc.28".to_string()));
}
#[test]
fn sync_registry_package_versions_rewrites_all_language_entries() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nalef_version = \"0.19.0\"\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry]\noutput = \"test_apps\"\n\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \">=0.1.0rc9\"\n\n",
"[crates.e2e.registry.packages.node]\n",
"name = \"@myorg/mylib\"\n",
"version = \"^0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.elixir]\n",
"name = \"mylib\"\n",
"version = \"~> 0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.ruby]\n",
"name = \"mylib\"\n",
"version = \">= 0.1.0.pre.rc.9\"\n",
),
)
.expect("write alef.toml");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(changed, "must report at least one change");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated.contains("version = \">=0.3.0rc28\""),
"python version must be PEP 440 formatted: {updated}"
);
assert!(
updated.contains("version = \"^0.3.0-rc.28\""),
"node version must preserve ^ prefix: {updated}"
);
assert!(
updated.contains("version = \"~> 0.3.0-rc.28\""),
"elixir version must preserve ~> prefix: {updated}"
);
assert!(
updated.contains("version = \">= 0.3.0.pre.rc.28\""),
"ruby version must be RubyGems formatted: {updated}"
);
assert!(updated.contains("name = \"mylib\""), "package names must be preserved");
assert!(
updated.contains("name = \"@myorg/mylib\""),
"node name must be preserved"
);
}
#[test]
fn sync_registry_package_versions_skips_entries_without_version_field() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
let original = concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.go]\n",
"module = \"github.com/myorg/mylib\"\n",
);
std::fs::write(&alef_toml_path, original).expect("write");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(!changed, "no version field → no change");
let content = std::fs::read_to_string(&alef_toml_path).expect("read");
assert!(!content.contains("version"), "version must not be inserted: {content}");
}
#[test]
fn sync_registry_package_versions_is_idempotent() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.python]\n",
"version = \">=0.3.0rc28\"\n",
),
)
.expect("write");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(!changed, "already-current version must be a no-op");
}
#[test]
fn sync_registry_package_versions_preserves_toml_comments_and_order() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
let original = concat!(
"# Top-level comment\n",
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"# Registry section comment\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \">=0.1.0rc9\"\n",
);
std::fs::write(&alef_toml_path, original).expect("write");
sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read");
assert!(
updated.contains("# Top-level comment"),
"top-level comment must be preserved: {updated}"
);
assert!(
updated.contains("# Registry section comment"),
"registry section comment must be preserved: {updated}"
);
let name_pos = updated.find("name = ").expect("name field present");
let ver_pos = updated.find("version = ").expect("version field present");
assert!(name_pos < ver_pos, "name must appear before version in output");
}
#[test]
fn sync_registry_package_versions_handles_go_and_bare_semver_langs() {
let tmp = tempfile::tempdir().expect("tempdir");
let alef_toml_path = tmp.path().join("alef.toml");
std::fs::write(
&alef_toml_path,
concat!(
"[workspace]\nlanguages = []\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n\n",
"[crates.e2e.registry.packages.go]\n",
"module = \"github.com/myorg/mylib\"\n",
"version = \"v0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.rust]\n",
"name = \"mylib\"\n",
"version = \"0.1.0-rc.9\"\n\n",
"[crates.e2e.registry.packages.php]\n",
"name = \"myorg/mylib\"\n",
"version = \">=0.1.0-rc.9\"\n",
),
)
.expect("write alef.toml");
let changed = sync_registry_package_versions(&alef_toml_path, "0.3.0-rc.28").expect("sync ok");
assert!(changed, "must report at least one change");
let updated = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated.contains("version = \"v0.3.0-rc.28\""),
"go version must have v prefix: {updated}"
);
assert!(
updated.contains("version = \"0.3.0-rc.28\""),
"rust bare semver must be updated: {updated}"
);
assert!(
updated.contains("version = \">=0.3.0-rc.28\""),
"php composer constraint must be updated: {updated}"
);
}