use super::super::version_core::*;
use super::super::version_registry::{render_registry_version, update_zig_package_hash};
use super::super::version_swift::compute_sha256_hex;
use super::super::version_text::*;
use super::*;
use crate::cli::pipeline::generate;
use crate::core::config::{CitationAuthor, CitationConfig};
#[test]
fn to_pep440_no_prerelease_passthrough() {
assert_eq!(to_pep440("1.2.3"), "1.2.3");
assert_eq!(to_pep440("0.1.0"), "0.1.0");
}
#[test]
fn to_pep440_rc_prerelease() {
assert_eq!(to_pep440("0.1.0-rc.1"), "0.1.0rc1");
assert_eq!(to_pep440("4.10.0-rc.9"), "4.10.0rc9");
}
#[test]
fn to_pep440_alpha_beta_prerelease() {
assert_eq!(to_pep440("1.0.0-alpha.2"), "1.0.0a2");
assert_eq!(to_pep440("1.0.0-beta.3"), "1.0.0b3");
}
#[test]
fn to_pep440_strips_internal_dots() {
assert_eq!(to_pep440("0.1.0-rc.1.2"), "0.1.0rc12");
}
#[test]
fn zon_version_regex_anchors_to_dot_version_only() {
let re = regex::Regex::new(r#"(?m)^\s*\.version\s*=\s*"([^"]*)""#).expect("valid regex");
let zon = r#".{
.name = .my_pkg,
.version = "1.9.0-rc.1",
.fingerprint = 0x6f52c41163f42c8c,
.minimum_zig_version = "0.16.0",
}
"#;
let captures: Vec<_> = re.captures_iter(zon).collect();
assert_eq!(
captures.len(),
1,
"regex must match exactly one line, not .minimum_zig_version"
);
assert_eq!(&captures[0][1], "1.9.0-rc.1");
}
#[test]
fn matched_version_equals_treats_quote_style_uniformly() {
assert!(matched_version_equals("VERSION = '1.0.0'", "1.0.0"));
assert!(matched_version_equals("VERSION = \"1.0.0\"", "1.0.0"));
assert!(!matched_version_equals("VERSION = '1.0.0'", "2.0.0"));
assert!(matched_version_equals("<version>1.0.0</version>", "1.0.0"));
assert!(matched_version_equals("Version: 1.0.0", "1.0.0"));
}
fn citation_author_person() -> CitationAuthor {
CitationAuthor {
family_names: Some("Hirschfeld".to_string()),
given_names: Some("Na'aman".to_string()),
name: None,
email: Some("naaman@sample_crate.dev".to_string()),
orcid: Some("https://orcid.org/0009-0000-2247-5072".to_string()),
}
}
fn citation_author_entity() -> CitationAuthor {
CitationAuthor {
family_names: None,
given_names: None,
name: Some("SampleCrate, Inc.".to_string()),
email: None,
orcid: None,
}
}
fn citation_config_mit() -> CitationConfig {
CitationConfig {
title: "sample-markdown".to_string(),
abstract_: "Fast markup conversion converter.".to_string(),
authors: vec![citation_author_person()],
message: "If you use this software, please cite it using the metadata below.".to_string(),
repository_code: "https://github.com/sample_crate-dev/sample-markdown".to_string(),
url: Some("https://sample_crate.dev".to_string()),
license: Some("MIT".to_string()),
date_released: Some("2026-05-17".to_string()),
doi: None,
}
}
#[test]
fn render_citation_cff_mit_full_round_trip() {
let rendered = render_citation_cff(&citation_config_mit(), "3.5.0", None);
let expected = r#"# This file is generated by alef sync-versions; do not edit by hand.
# Source: [workspace.citation] in alef.toml + workspace version in Cargo.toml.
cff-version: 1.2.0
message: "If you use this software, please cite it using the metadata below."
title: sample-markdown
abstract: "Fast markup conversion converter."
authors:
- family-names: Hirschfeld
given-names: "Na'aman"
email: "naaman@sample_crate.dev"
orcid: "https://orcid.org/0009-0000-2247-5072"
repository-code: "https://github.com/sample_crate-dev/sample-markdown"
url: "https://sample_crate.dev"
license: MIT
version: 3.5.0
date-released: 2026-05-17
"#;
assert_eq!(rendered, expected);
}
#[test]
fn render_citation_cff_elv2_with_entity_author() {
let mut config = citation_config_mit();
config.title = "sample_crate".to_string();
config.repository_code = "https://github.com/sample_crate-dev/sample_crate".to_string();
config.license = Some("Elastic-2.0".to_string());
config.authors = vec![citation_author_person(), citation_author_entity()];
let rendered = render_citation_cff(&config, "5.0.0-rc.1", None);
assert!(rendered.contains(" - family-names: Hirschfeld\n given-names: \"Na'aman\""));
assert!(rendered.contains(" - name: \"SampleCrate, Inc.\"\n"));
assert!(rendered.contains("license: Elastic-2.0\n"));
assert!(rendered.contains("version: 5.0.0-rc.1\n"));
}
#[test]
fn render_citation_cff_falls_back_to_cargo_license() {
let mut config = citation_config_mit();
config.license = None;
let rendered = render_citation_cff(&config, "1.0.0", Some("Apache-2.0"));
assert!(rendered.contains("license: Apache-2.0\n"));
}
#[test]
fn render_citation_cff_omits_optional_fields_when_unset() {
let config = CitationConfig {
title: "tiny".to_string(),
abstract_: "Tiny library.".to_string(),
authors: vec![citation_author_person()],
message: "Cite me.".to_string(),
repository_code: "https://example.com/tiny".to_string(),
url: None,
license: None,
date_released: None,
doi: None,
};
let rendered = render_citation_cff(&config, "0.1.0", None);
assert!(!rendered.contains("url:"));
assert!(!rendered.contains("license:"));
assert!(!rendered.contains("date-released:"));
assert!(!rendered.contains("doi:"));
}
#[test]
fn render_citation_cff_idempotent_for_unchanged_version() {
let config = citation_config_mit();
let first = render_citation_cff(&config, "3.5.0", None);
let second = render_citation_cff(&config, "3.5.0", None);
assert_eq!(first, second);
}
#[test]
fn replace_citation_version_unquoted_scalar() {
let content = "cff-version: 1.2.0\ntitle: example\nversion: 1.0.0\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert!(new.contains("version: 2.0.0\n"));
assert!(new.contains("title: example\n"));
assert!(!new.contains("1.0.0"));
}
#[test]
fn replace_citation_version_double_quoted_preserves_quotes() {
let content = "version: \"1.0.0\"\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert_eq!(new, "version: \"2.0.0\"\n");
}
#[test]
fn replace_citation_version_single_quoted_preserves_quotes() {
let content = "version: '1.0.0'\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert_eq!(new, "version: '2.0.0'\n");
}
#[test]
fn replace_citation_version_rc_suffix_passes_through() {
let content = "version: 5.0.0-rc.1\n";
let new = replace_citation_version(content, "5.0.0-rc.2").expect("regex matched");
assert_eq!(new, "version: 5.0.0-rc.2\n");
}
#[test]
fn replace_citation_version_no_op_when_already_current() {
let content = "version: 1.0.0\n";
assert!(replace_citation_version(content, "1.0.0").is_none());
}
#[test]
fn replace_citation_version_ignores_nested_version_keys() {
let content = "version: 1.0.0\nreferences:\n - type: software\n version: 9.9.9\n";
let new = replace_citation_version(content, "2.0.0").expect("regex matched");
assert!(new.starts_with("version: 2.0.0\n"));
assert!(new.contains(" version: 9.9.9\n"));
}
#[test]
fn test_replace_version_pattern_ruby_version() {
let content = r#"# This file is auto-generated by alef
module SampleCrate
VERSION = "1.0.0"
end
"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(
new_content,
r#"# This file is auto-generated by alef
module SampleCrate
VERSION = "2.0.0"
end
"#
);
}
#[test]
fn test_replace_version_pattern_ruby_version_single_quotes() {
let content = "VERSION = '1.5.2'";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(new_content, "VERSION = \"2.0.0\"");
}
#[test]
fn test_replace_version_pattern_ruby_version_double_quotes() {
let content = "VERSION = \"1.5.2\"";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "3.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert_eq!(new_content, "VERSION = \"3.0.0\"");
}
#[test]
fn test_replace_version_pattern_ruby_in_module() {
let content = r#"module MyGem
VERSION = "0.5.0"
end"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "1.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert!(new_content.contains("VERSION = \"1.0.0\""));
assert!(!new_content.contains("0.5.0"));
}
#[test]
fn test_replace_version_pattern_no_match() {
let content = "NOTHING = \"1.0.0\"";
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_none());
}
#[test]
fn test_replace_version_pattern_preserves_other_content() {
let content = r#"# frozen_string_literal: true
module SampleCrate
VERSION = "1.0.0"
# Other stuff
CONST = "something"
end"#;
let result = replace_version_pattern(content, r#"VERSION\s*=\s*['"][^'"]*['"]"#, "2.0.0");
assert!(result.is_some());
let new_content = result.unwrap();
assert!(new_content.contains("# frozen_string_literal: true"));
assert!(new_content.contains("CONST = \"something\""));
assert!(new_content.contains("VERSION = \"2.0.0\""));
}
#[test]
fn test_finalize_hashes_updates_alef_hash_line() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("version.rb");
let content = "# This file is auto-generated by alef — do not edit manually.\n# frozen_string_literal: true\n\nmodule MyGem\n VERSION = '2.0.0'\nend\n";
std::fs::write(&path, content).expect("write");
let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";
let n = generate::finalize_hashes(&paths, "test-sources-hash", alef_toml_bytes).expect("finalize ok");
assert_eq!(n, 1, "finalize_hashes must update the file with the alef:hash line");
let updated = std::fs::read_to_string(&path).expect("read");
assert!(
updated.contains("alef:hash:"),
"file must contain alef:hash: after finalize_hashes, got:\n{updated}"
);
}
#[test]
fn test_finalize_hashes_is_idempotent() {
let dir = tempfile::tempdir().expect("tempdir");
let path = dir.path().join("version.rb");
let content =
"# This file is auto-generated by alef — do not edit manually.\n\nmodule MyGem\n VERSION = '2.0.0'\nend\n";
std::fs::write(&path, content).expect("write");
let paths: std::collections::HashSet<std::path::PathBuf> = std::iter::once(path.clone()).collect();
let alef_toml_bytes = b"[workspace]\nlanguages = [\"ruby\"]\n";
let _ = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("first finalize");
let after_first = std::fs::read_to_string(&path).expect("read after first");
let n2 = generate::finalize_hashes(&paths, "sources", alef_toml_bytes).expect("second finalize");
assert_eq!(n2, 0, "second finalize_hashes must be a no-op (same inputs hash)");
let after_second = std::fs::read_to_string(&path).expect("read after second");
assert_eq!(after_first, after_second, "content must not change on second finalize");
}
const GEMFILE_LOCK_SAMPLE: &str = "\
PATH
remote: .
specs:
sample_crate (4.10.0.pre.rc.13)
rb_sys (~> 0.9)
GEM
remote: https://rubygems.org/
specs:
rake (13.4.2)
PLATFORMS
ruby
DEPENDENCIES
sample_crate!
CHECKSUMS
sample_crate (4.10.0.pre.rc.13)
rake (13.4.2) sha256=abcdef
BUNDLED WITH
4.0.7
";
#[test]
fn sync_gemfile_lock_updates_both_occurrences() {
let result = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains(" sample_crate (4.10.0.pre.rc.14)"),
"PATH specs entry not updated:\n{new}"
);
assert!(
new.contains(" sample_crate (4.10.0.pre.rc.14)"),
"CHECKSUMS entry not updated:\n{new}"
);
assert!(
new.contains("rake (13.4.2)"),
"non-path gem version must not change:\n{new}"
);
assert!(!new.contains("4.10.0.pre.rc.13"), "old version must be removed:\n{new}");
}
#[test]
fn sync_gemfile_lock_is_idempotent() {
let first = sync_gemfile_lock(GEMFILE_LOCK_SAMPLE, "4.10.0.pre.rc.14").unwrap();
let second = sync_gemfile_lock(&first, "4.10.0.pre.rc.14");
assert!(
second.is_none(),
"second call with same version must return None (already in sync)"
);
}
#[test]
fn sync_gemfile_lock_preserves_trailing_newline() {
let with_newline = format!("{GEMFILE_LOCK_SAMPLE}\n");
let result = sync_gemfile_lock(&with_newline, "4.10.0.pre.rc.99").unwrap();
assert!(result.ends_with('\n'), "trailing newline must be preserved");
}
#[test]
fn sync_gemfile_lock_no_path_gem_returns_none() {
let content = "GEM\n remote: https://rubygems.org/\n specs:\n rake (13.4.2)\n";
let result = sync_gemfile_lock(content, "1.0.0");
assert!(result.is_none(), "no PATH gem means nothing to update");
}
#[test]
fn restore_gleam_dep_ranges_repairs_corrupted_workspace_version_ranges() {
let corrupted = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\ntarget = \"erlang\"\n\n[dependencies]\ngleam_stdlib = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n\n[dev-dependencies]\ngleeunit = \">= 5.0.0-rc.1 and < 5.0.0-rc.1\"\n";
let healed = restore_gleam_dep_ranges(corrupted);
assert!(
healed.contains("gleam_stdlib = \">= 0.34.0 and < 2.0.0\""),
"gleam_stdlib should be restored to canonical range, got:\n{healed}"
);
assert!(
healed.contains("gleeunit = \">= 1.0.0 and < 2.0.0\""),
"gleeunit should be restored to canonical range, got:\n{healed}"
);
assert!(
healed.contains("version = \"5.0.0-rc.1\""),
"package version must not be rewritten, got:\n{healed}"
);
}
#[test]
fn restore_gleam_dep_ranges_is_idempotent_on_healthy_input() {
let healthy = "name = \"sample_crate\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\ngleam_stdlib = \">= 0.34.0 and < 2.0.0\"\n\n[dev-dependencies]\ngleeunit = \">= 1.0.0 and < 2.0.0\"\n";
let healed = restore_gleam_dep_ranges(healthy);
assert_eq!(healed, healthy, "healthy gleam.toml must not be rewritten");
}
#[test]
fn test_replace_version_pattern_root_package_json_only_top_level() {
let content = r#"{
"name": "sample_crate-root",
"version": "4.9.5",
"private": true,
"devDependencies": {
"@vitest/coverage-v8": "^4.1.5",
"tsx": "^4.21.0",
"typescript": "^6.0.3"
},
"pnpm": {
"overrides": {
"glob": "10.5.0"
}
}
}
"#;
let new_content = replace_version_pattern(content, r#""version":\s*"[^"]*""#, "5.0.0-rc.1")
.expect("root package.json version must update");
assert!(
new_content.contains(r#""version": "5.0.0-rc.1""#),
"top-level version must be rewritten, got:\n{new_content}"
);
assert!(
!new_content.contains(r#""version": "4.9.5""#),
"old version must be removed, got:\n{new_content}"
);
assert!(
new_content.contains("\"@vitest/coverage-v8\": \"^4.1.5\""),
"devDependency version specs must not be touched, got:\n{new_content}"
);
assert!(
new_content.contains("\"glob\": \"10.5.0\""),
"pnpm overrides must not be touched, got:\n{new_content}"
);
}
static CWD_LOCK: std::sync::Mutex<()> = std::sync::Mutex::new(());
#[test]
fn sync_versions_writes_root_and_node_crate_package_json() {
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.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::write(
root.join("package.json"),
"{\n \"name\": \"mylib-root\",\n \"version\": \"0.9.0\",\n \"private\": true\n}\n",
)
.expect("write root package.json");
std::fs::create_dir_all(root.join("crates/mylib-node")).expect("mkdir crates/mylib-node");
std::fs::write(
root.join("crates/mylib-node/package.json"),
"{\n \"name\": \"mylib\",\n \"version\": \"0.9.0\"\n}\n",
)
.expect("write crates/mylib-node/package.json");
let alef_toml = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"mylib\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_pkg = std::fs::read_to_string(root.join("package.json")).expect("read root package.json");
assert!(
root_pkg.contains(r#""version": "1.0.0""#),
"root package.json must be bumped to canonical version, got:\n{root_pkg}"
);
assert!(
!root_pkg.contains("0.9.0"),
"old version must be gone from root package.json, got:\n{root_pkg}"
);
let node_pkg = std::fs::read_to_string(root.join("crates/mylib-node/package.json"))
.expect("read crates/mylib-node/package.json");
assert!(
node_pkg.contains(r#""version": "1.0.0""#),
"crates/*-node/package.json must be bumped to canonical version, got:\n{node_pkg}"
);
}
#[test]
fn sync_versions_bumps_napi_platform_pins_and_manifests() {
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.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("crates/mylib-node")).expect("mkdir crates/mylib-node");
std::fs::write(
root.join("crates/mylib-node/package.json"),
"{\n \"name\": \"@scope/mylib\",\n \"version\": \"0.9.0\",\n \"optionalDependencies\": {\n \"@scope/mylib-linux-x64-gnu\": \"0.9.0\",\n \"@scope/mylib-darwin-arm64\": \"0.9.0\",\n \"@scope/mylib-win32-x64-msvc\": \"0.9.0\"\n }\n}\n",
)
.expect("write crates/mylib-node/package.json");
for platform in &["linux-x64-gnu", "darwin-arm64", "win32-x64-msvc"] {
let dir = root.join(format!("crates/mylib-node/npm/{platform}"));
std::fs::create_dir_all(&dir).expect("mkdir platform dir");
std::fs::write(
dir.join("package.json"),
format!("{{\n \"name\": \"@scope/mylib-{platform}\",\n \"version\": \"0.9.0\"\n}}\n"),
)
.expect("write platform package.json");
}
let alef_toml = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"mylib\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let crate_pkg = std::fs::read_to_string(root.join("crates/mylib-node/package.json"))
.expect("read crates/mylib-node/package.json");
assert!(
!crate_pkg.contains("0.9.0"),
"old version must be gone from crates/mylib-node/package.json (including optionalDependencies), got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-linux-x64-gnu": "1.0.0""#),
"optionalDependencies pin to linux-x64-gnu must be bumped, got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-darwin-arm64": "1.0.0""#),
"optionalDependencies pin to darwin-arm64 must be bumped, got:\n{crate_pkg}"
);
assert!(
crate_pkg.contains(r#""@scope/mylib-win32-x64-msvc": "1.0.0""#),
"optionalDependencies pin to win32-x64-msvc must be bumped, got:\n{crate_pkg}"
);
for platform in &["linux-x64-gnu", "darwin-arm64", "win32-x64-msvc"] {
let manifest = std::fs::read_to_string(root.join(format!("crates/mylib-node/npm/{platform}/package.json")))
.expect("read platform package.json");
assert!(
manifest.contains(r#""version": "1.0.0""#),
"platform manifest {platform} must be bumped, got:\n{manifest}"
);
assert!(
!manifest.contains("0.9.0"),
"old version must be gone from platform manifest {platform}, got:\n{manifest}"
);
}
}
#[test]
fn sync_versions_bumps_both_python_pyprojects_to_pep440_prerelease() {
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.2\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/python")).expect("mkdir packages/python");
std::fs::write(
root.join("packages/python/pyproject.toml"),
"[project]\nname = \"mylib\"\nversion = \"0.15.5\"\n",
)
.expect("write packages/python/pyproject.toml");
std::fs::create_dir_all(root.join("crates/mylib-py/src")).expect("mkdir crates/mylib-py/src");
std::fs::write(
root.join("crates/mylib-py/src/pyproject.toml"),
"[project]\nname = \"mylib\"\nversion = \"0.15.5\"\n",
)
.expect("write crates/mylib-py/src/pyproject.toml");
let alef_toml = format!(
"[workspace]\nlanguages = [\"python\"]\n[[crates]]\nname = \"mylib\"\nsources = []\nversion_from = \"{}\"\n[crates.output]\npython = \"crates/mylib-py/src/\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let consumer =
std::fs::read_to_string(root.join("packages/python/pyproject.toml")).expect("read consumer pyproject");
assert!(
consumer.contains(r#"version = "0.15.6rc2""#),
"consumer pyproject must be PEP 440 normalised, got:\n{consumer}"
);
let source = std::fs::read_to_string(root.join("crates/mylib-py/src/pyproject.toml"))
.expect("read source-template pyproject");
assert!(
source.contains(r#"version = "0.15.6rc2""#),
"source-template pyproject must be PEP 440 normalised, got:\n{source}"
);
assert!(
!source.contains("0.15.5") && !source.contains("0.15.6-rc.2"),
"source-template must hold only the normalised version, got:\n{source}"
);
}
#[test]
fn patch_workspace_dep_versions_all_dep_table_shapes() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml = r#"[package]
name = "crate-a"
version = "5.0.0-rc.1"
[dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1", optional = true }
serde = "1.0"
[dev-dependencies]
crate-c = { path = "../crate-c", version = "5.0.0-rc.1" }
tempfile = "3"
[build-dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1" }
[target.'cfg(unix)'.dependencies]
crate-b = { path = "../crate-b", version = "5.0.0-rc.1", optional = true }
libc = "0.2"
[workspace.dependencies]
crate-c = { path = "../crate-c", version = "5.0.0-rc.1", default-features = false }
tokio = { version = "1.0", features = ["full"] }
"#;
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = ["crate-b", "crate-c"].iter().map(|s| s.to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "5.0.0-rc.2", &members).expect("patch ok");
assert!(changed, "at least one version pin must have been updated");
let result = std::fs::read_to_string(&path).expect("read");
let crate_b_lines: Vec<&str> = result
.lines()
.filter(|l| l.contains("crate-b") && l.contains("version"))
.collect();
assert!(
!crate_b_lines.is_empty(),
"expected crate-b dep lines with version=:\n{result}"
);
for line in &crate_b_lines {
assert!(
line.contains("5.0.0-rc.2"),
"crate-b pin not bumped:\n {line}\nfull:\n{result}"
);
}
let crate_c_lines: Vec<&str> = result
.lines()
.filter(|l| l.contains("crate-c") && l.contains("version"))
.collect();
assert!(
!crate_c_lines.is_empty(),
"expected crate-c dep lines with version=:\n{result}"
);
for line in &crate_c_lines {
assert!(
line.contains("5.0.0-rc.2"),
"crate-c pin not bumped:\n {line}\nfull:\n{result}"
);
}
assert!(
result.contains(r#"serde = "1.0""#),
"serde must not be touched:\n{result}"
);
assert!(
result.contains(r#"tempfile = "3""#),
"tempfile must not be touched:\n{result}"
);
assert!(
result.contains(r#"libc = "0.2""#),
"libc must not be touched:\n{result}"
);
assert!(
result.contains(r#"tokio = { version = "1.0", features = ["full"] }"#),
"tokio must not be touched:\n{result}"
);
}
#[test]
fn patch_workspace_dep_versions_is_idempotent() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml = "[package]\nname = \"crate-a\"\nversion = \"5.0.0-rc.2\"\n\n[dependencies]\ncrate-b = { path = \"../crate-b\", version = \"5.0.0-rc.2\" }\n";
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = std::iter::once("crate-b".to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "5.0.0-rc.2", &members).expect("patch ok");
assert!(!changed, "no change expected when already at target version");
}
#[test]
fn patch_workspace_dep_versions_skips_path_only_deps() {
use std::collections::HashSet;
let dir = tempfile::tempdir().expect("tempdir");
let cargo_toml =
"[package]\nname = \"crate-a\"\nversion = \"1.0.0\"\n\n[dependencies]\ncrate-b = { path = \"../crate-b\" }\n";
let path = dir.path().join("Cargo.toml");
std::fs::write(&path, cargo_toml).expect("write");
let members: HashSet<String> = std::iter::once("crate-b".to_string()).collect();
let changed = patch_workspace_dep_versions(path.to_str().unwrap(), "2.0.0", &members).expect("patch ok");
assert!(!changed, "path-only deps without version= must not be touched");
}
#[test]
fn sync_versions_patches_dep_tables_on_version_change() {
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();
fn write_file(dir: &std::path::Path, rel: &str, content: &str) {
let path = dir.join(rel);
if let Some(parent) = path.parent() {
std::fs::create_dir_all(parent).expect("mkdir");
}
std::fs::write(path, content).expect("write");
}
write_file(
root,
"Cargo.toml",
"[workspace.package]\nversion = \"5.0.0-rc.2\"\n\n[workspace]\nresolver = \"2\"\nmembers = [\"crates/alpha\", \"crates/beta\"]\n\n[workspace.dependencies]\nalpha = { path = \"crates/alpha\", version = \"5.0.0-rc.1\", default-features = false }\nserde = \"1.0\"\n",
);
write_file(
root,
"crates/alpha/Cargo.toml",
"[package]\nname = \"alpha\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\nserde = \"1.0\"\n",
);
write_file(
root,
"crates/beta/Cargo.toml",
"[package]\nname = \"beta\"\nversion = \"5.0.0-rc.1\"\n\n[dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\", optional = true }\nserde = \"1.0\"\n\n[dev-dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\" }\ntempfile = \"3\"\n\n[build-dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\" }\n\n[target.'cfg(unix)'.dependencies]\nalpha = { path = \"../alpha\", version = \"5.0.0-rc.1\", features = [\"unix\"] }\nlibc = \"0.2\"\n",
);
let alef_toml_content = format!(
"[workspace]\nlanguages = [\"node\"]\n[[crates]]\nname = \"alpha\"\nsources = []\nversion_from = \"{}\"\n",
root.join("Cargo.toml").display().to_string().replace('\\', "/")
);
write_file(root, "alef.toml", &alef_toml_content);
let alef_toml_path = root.join("alef.toml");
let cfg: NewAlefConfig = toml::from_str(&alef_toml_content).expect("parse alef.toml");
let mut resolved = cfg.resolve().expect("resolve");
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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_cargo = std::fs::read_to_string(root.join("Cargo.toml")).expect("read root");
assert!(
root_cargo.contains(r#"alpha = { path = "crates/alpha", version = "5.0.0-rc.2""#),
"root [workspace.dependencies] alpha must be bumped to rc.2:\n{root_cargo}"
);
assert!(
root_cargo.contains(r#"serde = "1.0""#),
"root serde must be untouched:\n{root_cargo}"
);
let alpha_cargo = std::fs::read_to_string(root.join("crates/alpha/Cargo.toml")).expect("read alpha");
assert!(
alpha_cargo.contains("version = \"5.0.0-rc.2\""),
"alpha [package] must be bumped:\n{alpha_cargo}"
);
let beta_cargo = std::fs::read_to_string(root.join("crates/beta/Cargo.toml")).expect("read beta");
let alpha_version_lines: Vec<&str> = beta_cargo
.lines()
.filter(|l| l.contains("alpha") && l.contains("version"))
.collect();
assert!(
!alpha_version_lines.is_empty(),
"expected alpha dep lines with version= in beta:\n{beta_cargo}"
);
for line in &alpha_version_lines {
assert!(
line.contains("5.0.0-rc.2"),
"alpha pin not bumped to rc.2 in beta:\n {line}\nfull:\n{beta_cargo}"
);
}
assert!(
!beta_cargo.contains("5.0.0-rc.1"),
"old rc.1 must be gone from beta:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"serde = "1.0""#),
"serde must not be touched:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"tempfile = "3""#),
"tempfile must not be touched:\n{beta_cargo}"
);
assert!(
beta_cargo.contains(r#"libc = "0.2""#),
"libc must not be touched:\n{beta_cargo}"
);
}
#[test]
fn run_optional_logs_but_does_not_fail_on_missing_binary() {
super::super::helpers::run_optional("nonexistent_binary_12345", &["arg1", "arg2"]);
}
#[test]
fn run_optional_succeeds_for_simple_command() {
super::super::helpers::run_optional("echo", &["test"]);
}
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);
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}"
);
}
const JAVA_E2E_POM: &str = r#"<?xml version="1.0" encoding="UTF-8"?>
<project>
<groupId>dev.sample_crate.sample_crawler</groupId>
<artifactId>sample_crawler-e2e-java</artifactId>
<version>0.1.0</version>
<dependencies>
<dependency>
<groupId>dev.sample_crate.sample_crawler</groupId>
<artifactId>sample_crawler</artifactId>
<version>0.3.0-rc.27</version>
<scope>system</scope>
<systemPath>${project.basedir}/../../packages/java/target/sample_crawler-0.3.0-rc.27.jar</systemPath>
</dependency>
<dependency>
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter</artifactId>
<version>${junit.version}</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
"#;
#[test]
fn sync_e2e_java_pom_updates_dependency_version_and_system_path() {
let result = sync_e2e_java_pom(JAVA_E2E_POM, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("<version>0.3.0-rc.28</version>"),
"dependency version must be updated:\n{new}"
);
assert!(
new.contains("sample_crawler-0.3.0-rc.28.jar"),
"systemPath must be updated:\n{new}"
);
assert!(
new.contains("<version>0.1.0</version>"),
"project version must be unchanged:\n{new}"
);
assert!(
new.contains("<version>${junit.version}</version>"),
"junit version placeholder must be unchanged:\n{new}"
);
assert!(!new.contains("0.3.0-rc.27"), "old version must be removed:\n{new}");
}
#[test]
fn sync_e2e_java_pom_is_idempotent() {
let first = sync_e2e_java_pom(JAVA_E2E_POM, "0.3.0-rc.28").unwrap();
let second = sync_e2e_java_pom(&first, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
#[test]
fn sync_e2e_java_pom_no_system_scope_returns_none() {
let content = "<?xml version=\"1.0\"?>\n<project><version>0.1.0</version></project>\n";
assert!(
sync_e2e_java_pom(content, "1.0.0").is_none(),
"no system-scope dep means nothing to update"
);
}
const GO_MOD_E2E: &str = "\
module e2e_go
go 1.26
require (
\tgithub.com/sample_crate-dev/sample_crawler/packages/go v0.3.0-rc.27
\tgithub.com/stretchr/testify v1.11.1
)
replace github.com/sample_crate-dev/sample_crawler/packages/go => ../../packages/go
";
#[test]
fn sync_e2e_go_mod_updates_library_require_line() {
let fragment = "github.com/sample_crate-dev/sample_crawler/packages/go";
let result = sync_e2e_go_mod(GO_MOD_E2E, fragment, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("github.com/sample_crate-dev/sample_crawler/packages/go v0.3.0-rc.28"),
"library require line must be updated:\n{new}"
);
assert!(
new.contains("github.com/stretchr/testify v1.11.1"),
"testify version must be unchanged:\n{new}"
);
assert!(!new.contains("v0.3.0-rc.27"), "old version must be gone:\n{new}");
}
#[test]
fn sync_e2e_go_mod_is_idempotent() {
let fragment = "github.com/sample_crate-dev/sample_crawler/packages/go";
let first = sync_e2e_go_mod(GO_MOD_E2E, fragment, "0.3.0-rc.28").unwrap();
let second = sync_e2e_go_mod(&first, fragment, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
const DART_PUBSPEC_LOCK: &str = "\
# Generated by pub
packages:
async:
dependency: transitive
description:
name: async
sha256: abc123
url: \"https://pub.dev\"
source: hosted
version: \"1.19.1\"
sample_crawler:
dependency: \"direct main\"
description:
path: \"../../packages/dart\"
relative: true
source: path
version: \"0.3.0-rc.23\"
logging:
dependency: transitive
description:
name: logging
sha256: def456
url: \"https://pub.dev\"
source: hosted
version: \"1.2.0\"
";
#[test]
fn sync_e2e_dart_pubspec_lock_updates_path_source_version() {
let result = sync_e2e_dart_pubspec_lock(DART_PUBSPEC_LOCK, "0.3.0-rc.28");
assert!(result.is_some(), "expected Some when version changes");
let new = result.unwrap();
assert!(
new.contains("version: \"0.3.0-rc.28\""),
"path-source version must be updated:\n{new}"
);
assert!(
new.contains("version: \"1.19.1\""),
"hosted async version must be unchanged:\n{new}"
);
assert!(
new.contains("version: \"1.2.0\""),
"hosted logging version must be unchanged:\n{new}"
);
assert!(!new.contains("0.3.0-rc.23"), "old version must be gone:\n{new}");
}
#[test]
fn sync_e2e_dart_pubspec_lock_is_idempotent() {
let first = sync_e2e_dart_pubspec_lock(DART_PUBSPEC_LOCK, "0.3.0-rc.28").unwrap();
let second = sync_e2e_dart_pubspec_lock(&first, "0.3.0-rc.28");
assert!(second.is_none(), "second call with same version must be a no-op");
}
#[test]
fn sync_e2e_dart_pubspec_lock_no_path_source_returns_none() {
let content = "packages:\n async:\n dependency: transitive\n description:\n name: async\n url: \"https://pub.dev\"\n source: hosted\n version: \"1.19.1\"\n";
assert!(
sync_e2e_dart_pubspec_lock(content, "0.3.0-rc.28").is_none(),
"no path-source means nothing to update"
);
}
#[test]
fn sync_versions_regenerates_test_apps_pins() {
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.2.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("fixtures")).expect("mkdir fixtures");
let alef_toml = format!(
concat!(
"[workspace]\n",
"languages = [\"python\"]\n\n",
"[[crates]]\n",
"name = \"mylib\"\n",
"sources = []\n",
"version_from = \"{cargo_toml}\"\n\n",
"[crates.e2e]\n",
"fixtures = \"fixtures\"\n",
"languages = [\"python\"]\n\n",
"[crates.e2e.call]\n",
"module = \"mylib\"\n",
"function = \"parse\"\n\n",
"[crates.e2e.registry.packages.python]\n",
"name = \"mylib\"\n",
"version = \"0.0.0\"\n",
),
cargo_toml = 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, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let updated_toml = std::fs::read_to_string(&alef_toml_path).expect("read alef.toml");
assert!(
updated_toml.contains("version = \"1.2.3\""),
"alef.toml registry package version must be updated to 1.2.3:\n{updated_toml}"
);
assert!(
!updated_toml.contains("version = \"0.0.0\""),
"stale 0.0.0 must be gone from alef.toml:\n{updated_toml}"
);
let pyproject_path = root.join("test_apps/python/pyproject.toml");
assert!(
pyproject_path.exists(),
"test_apps/python/pyproject.toml must be generated by auto-regen"
);
let pyproject = std::fs::read_to_string(&pyproject_path).expect("read pyproject.toml");
assert!(
pyproject.contains("mylib==1.2.3"),
"test_apps/python/pyproject.toml must pin the new registry version 1.2.3:\n{pyproject}"
);
assert!(
!pyproject.contains("mylib==0.0.0"),
"stale registry pin mylib==0.0.0 must be gone from test_apps/python/pyproject.toml:\n{pyproject}"
);
}
#[test]
fn sync_versions_updates_go_module_version_in_download_ffi() {
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.9.0-rc.14\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let download_ffi_dir = root.join("packages/go/cmd/download_ffi");
std::fs::create_dir_all(&download_ffi_dir).expect("mkdir download_ffi");
let stale_main_go = concat!(
"// Tool to download platform-specific FFI libraries from GitHub releases.\n",
"package main\n\nconst (\n",
"\tmoduleVersion = \"1.9.0-rc.13\"\n",
"\trepoURL = \"https://github.com/example/mylib\"\n",
")\n",
);
std::fs::write(download_ffi_dir.join("main.go"), stale_main_go).expect("write main.go");
let alef_toml = format!(
concat!(
"[workspace]\nlanguages = [\"go\"]\n\n",
"[[crates]]\nname = \"mylib\"\nsources = []\n",
"version_from = \"{cargo_toml}\"\n",
),
cargo_toml = 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: crate::core::config::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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let updated_main = std::fs::read_to_string(download_ffi_dir.join("main.go")).expect("read main.go");
assert!(
updated_main.contains("moduleVersion = \"1.9.0-rc.14\""),
"moduleVersion must be updated to 1.9.0-rc.14:\n{updated_main}"
);
assert!(
!updated_main.contains("1.9.0-rc.13"),
"stale rc.13 moduleVersion must be gone from main.go:\n{updated_main}"
);
assert!(
updated_main.contains("repoURL"),
"other constants must be preserved:\n{updated_main}"
);
}
#[test]
fn sync_versions_regenerates_scaffold_version_fields() {
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.2.3\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
std::fs::create_dir_all(root.join("packages/r")).expect("mkdir packages/r");
let stale_description = concat!(
"Package: mylib\nTitle: My Library\nVersion: 0.0.0\nDescription: A library.\n",
"License: MIT\nEncoding: UTF-8\nRoxygenNote: 7.3.1\n",
);
std::fs::write(root.join("packages/r/DESCRIPTION"), stale_description).expect("write DESCRIPTION");
let alef_toml = format!(
concat!(
"[workspace]\n",
"languages = [\"r\"]\n\n",
"[[crates]]\n",
"name = \"mylib\"\n",
"sources = []\n",
"version_from = \"{cargo_toml}\"\n",
),
cargo_toml = 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, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let description_path = root.join("packages/r/DESCRIPTION");
assert!(
description_path.exists(),
"packages/r/DESCRIPTION must exist after scaffold regen"
);
let description = std::fs::read_to_string(&description_path).expect("read DESCRIPTION");
assert!(
description.contains("Version: 1.2"),
"DESCRIPTION must contain the new version 1.2.x after scaffold regen:\n{description}"
);
assert!(
!description.contains("Version: 0.0.0"),
"stale Version: 0.0.0 must be gone from DESCRIPTION:\n{description}"
);
}
#[test]
fn sync_versions_bumps_kotlin_android_gradle_coordinates_version() {
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.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let gradle_content = concat!(
"plugins {\n",
" id(\"com.android.library\") version \"8.13.0\"\n",
" kotlin(\"android\") version \"2.3.21\"\n",
"}\n",
"\n",
"mavenPublishing {\n",
" coordinates(\n",
" groupId = \"dev.example\",\n",
" artifactId = \"mylib-android\",\n",
" version = \"1.9.0-rc.16\",\n",
" )\n",
"}\n",
);
std::fs::create_dir_all(root.join("packages/kotlin-android")).expect("mkdir");
std::fs::write(root.join("packages/kotlin-android/build.gradle.kts"), gradle_content)
.expect("write build.gradle.kts");
let alef_toml = format!(
"[workspace]\nlanguages = [\"kotlin_android\"]\n[[crates]]\nname = \"mylib\"\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);
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-android/build.gradle.kts")).expect("read build.gradle.kts");
assert!(
gradle.contains("version = \"1.9.0-rc.17\""),
"kotlin-android coordinates version must be bumped:\n{gradle}"
);
assert!(
gradle.contains(r#"kotlin("android") version "2.3.21""#),
"kotlin plugin version must not change:\n{gradle}"
);
assert!(
gradle.contains(r#"id("com.android.library") version "8.13.0""#),
"android plugin version must not change:\n{gradle}"
);
assert!(
!gradle.contains("1.9.0-rc.16"),
"stale rc.16 version must be gone:\n{gradle}"
);
}
#[test]
fn sync_versions_bumps_nested_python_init_version() {
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.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let py_module_dir = root.join("packages/python/mylib");
std::fs::create_dir_all(&py_module_dir).expect("mkdir");
std::fs::write(
py_module_dir.join("__init__.py"),
"\"\"\"mylib public API.\"\"\"\n\n__version__ = \"1.9.0-rc.16\"\n",
)
.expect("write __init__.py");
let alef_toml = format!(
"[workspace]\nlanguages = [\"python\"]\n[[crates]]\nname = \"mylib\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let content = std::fs::read_to_string(py_module_dir.join("__init__.py")).expect("read __init__.py");
assert!(
content.contains("__version__ = \"1.9.0-rc.17\""),
"nested __version__ must be bumped:\n{content}"
);
assert!(
!content.contains("1.9.0-rc.16"),
"stale rc.16 __version__ must be gone:\n{content}"
);
}
#[test]
fn sync_versions_bumps_swift_package_from_version() {
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.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let swift_pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"\n",
"let package = Package(\n",
" name: \"TestApp\",\n",
" dependencies: [\n",
" .package(url: \"https://example.com/alef-sample/mylib.git\", from: \"1.9.0-rc.16\"),\n",
" ],\n",
" targets: []\n",
")\n",
);
let swift_dir = root.join("test_apps/swift");
std::fs::create_dir_all(&swift_dir).expect("mkdir");
std::fs::write(swift_dir.join("Package.swift"), swift_pkg_content).expect("write Package.swift");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"mylib\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let swift_pkg = std::fs::read_to_string(swift_dir.join("Package.swift")).expect("read Package.swift");
assert!(
swift_pkg.contains("from: \"1.9.0-rc.17\""),
"swift from: version must be bumped:\n{swift_pkg}"
);
assert!(
!swift_pkg.contains("from: \"1.9.0-rc.16\""),
"stale rc.16 from: version must be gone:\n{swift_pkg}"
);
assert!(
swift_pkg.contains("https://example.com/alef-sample/mylib.git"),
"repo URL must be preserved:\n{swift_pkg}"
);
}
#[test]
fn sync_versions_root_package_swift_placeholder_survives_scaffold_regen() {
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.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let root_pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"let package = Package(name: \"MyLib\", targets: [\n",
" .binaryTarget(\n",
" name: \"RustBridge\",\n",
" url: \"https://example.com/alef-sample/mylib/releases/download/v__ALEF_SWIFT_VERSION__/MyLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), root_pkg_content).expect("write root Package.swift");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"mylib\"\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, false, true);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
let root_pkg = std::fs::read_to_string(root.join("Package.swift")).expect("read root Package.swift");
assert!(
!root_pkg.contains("v__ALEF_SWIFT_VERSION__"),
"root Package.swift must not retain the version placeholder after sync_versions, got:\n{root_pkg}"
);
assert!(
root_pkg.contains("/releases/download/v1.9.0-rc.17/"),
"root Package.swift URL must point at substituted version v1.9.0-rc.17, got:\n{root_pkg}"
);
assert!(
root_pkg.contains("__ALEF_SWIFT_CHECKSUM__"),
"root Package.swift must retain the checksum placeholder when skip_swift_checksum=true, got:\n{root_pkg}"
);
}
#[test]
fn sync_versions_bumps_c_download_ffi_sh_version() {
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.9.0-rc.17\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let sh_content = concat!(
"#!/usr/bin/env bash\n",
"set -euo pipefail\n",
"\n",
"REPO_URL=\"https://example.com/alef-sample/mylib\"\n",
"VERSION=\"1.9.0-rc.16\"\n",
"FFI_PKG_NAME=\"mylib-ffi\"\n",
);
let e2e_c_dir = root.join("e2e/c");
std::fs::create_dir_all(&e2e_c_dir).expect("mkdir e2e/c");
std::fs::write(e2e_c_dir.join("download_ffi.sh"), sh_content).expect("write e2e download_ffi.sh");
let test_apps_c_dir = root.join("test_apps/c");
std::fs::create_dir_all(&test_apps_c_dir).expect("mkdir test_apps/c");
std::fs::write(test_apps_c_dir.join("download_ffi.sh"), sh_content).expect("write test_apps download_ffi.sh");
let alef_toml = format!(
"[workspace]\nlanguages = [\"c\"]\n[[crates]]\nname = \"mylib\"\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);
let _ = std::env::set_current_dir(&original_cwd);
sync_result.expect("sync_versions ok");
for (label, dir) in [("e2e", &e2e_c_dir), ("test_apps", &test_apps_c_dir)] {
let content = std::fs::read_to_string(dir.join("download_ffi.sh"))
.unwrap_or_else(|_| panic!("read {label}/c/download_ffi.sh"));
assert!(
content.contains("VERSION=\"1.9.0-rc.17\""),
"{label}/c/download_ffi.sh VERSION must be bumped:\n{content}"
);
assert!(
!content.contains("VERSION=\"1.9.0-rc.16\""),
"{label}/c/download_ffi.sh stale rc.16 must be gone:\n{content}"
);
assert!(
content.contains("REPO_URL="),
"{label}/c/download_ffi.sh REPO_URL must be preserved:\n{content}"
);
}
}
#[test]
fn compute_sha256_hex_empty_input() {
let hex = compute_sha256_hex(b"");
assert_eq!(
hex, "e3b0c44298fc1c149afbf4c8996fb92427ae41e4649b934ca495991b7852b855",
"SHA-256 of empty input must match reference"
);
}
#[test]
fn compute_sha256_hex_abc() {
let hex = compute_sha256_hex(b"abc");
assert_eq!(
hex, "ba7816bf8f01cfea414140de5dae2223b00361a396177a9cb410ff61f20015ad",
"SHA-256 of 'abc' must match reference"
);
}
#[test]
fn precompute_swift_checksum_substitutes_when_zip_present() {
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 = \"2.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"import PackageDescription\n",
"let package = Package(name: \"TestLib\", targets: [\n",
" .binaryTarget(\n",
" name: \"RustBridge\",\n",
" url: \"https://example.com/testlib/releases/download/v2.0.0/TestLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), pkg_content).expect("write Package.swift");
let swift_crate_dir = root.join("crates/testlib-swift");
std::fs::create_dir_all(&swift_crate_dir).expect("mkdir swift crate");
std::fs::write(
swift_crate_dir.join("Cargo.toml"),
"[package]\nname = \"testlib-swift\"\nversion = \"2.0.0\"\n",
)
.expect("write swift Cargo.toml");
let bundle_dir = root.join("dist/swift-artifactbundle");
std::fs::create_dir_all(&bundle_dir).expect("mkdir bundle dir");
let zip_content = b"fake-artifactbundle-zip-content-for-testing";
std::fs::write(bundle_dir.join("TestLib-rs.artifactbundle.zip"), zip_content).expect("write fake zip");
let expected_checksum = compute_sha256_hex(zip_content);
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"testlib\"\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");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("chdir");
let result = precompute_swift_checksum(&resolved_cfg);
let _ = std::env::set_current_dir(&original_cwd);
let checksum = result
.expect("precompute_swift_checksum must succeed")
.expect("must return Some(checksum) when zip is present");
assert_eq!(
checksum, expected_checksum,
"returned checksum must equal in-process SHA-256 of the fake zip"
);
let pkg_result = std::fs::read_to_string(root.join("Package.swift")).expect("read");
assert!(
!pkg_result.contains("__ALEF_SWIFT_CHECKSUM__"),
"Package.swift must not retain the placeholder after precompute, got:\n{pkg_result}"
);
assert!(
pkg_result.contains(&expected_checksum),
"Package.swift must contain the computed checksum, got:\n{pkg_result}"
);
let sidecar =
std::fs::read_to_string(root.join("target/alef-swift-checksum.txt")).expect("sidecar file must exist");
assert_eq!(
sidecar.trim(),
expected_checksum,
"sidecar must contain the computed checksum"
);
}
#[test]
fn precompute_swift_checksum_skips_when_no_zip_and_build_fails() {
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 = \"2.0.0\"\n\n[workspace]\nresolver = \"2\"\nmembers = []\n",
)
.expect("write Cargo.toml");
let pkg_content = concat!(
"// swift-tools-version: 6.0\n",
"let package = Package(name: \"TestLib\", targets: [\n",
" .binaryTarget(name: \"RustBridge\",\n",
" url: \"https://example.com/v2.0.0/TestLib-rs.artifactbundle.zip\",\n",
" checksum: \"__ALEF_SWIFT_CHECKSUM__\"\n",
" ),\n",
"])\n",
);
std::fs::write(root.join("Package.swift"), pkg_content).expect("write Package.swift");
let swift_crate_dir = root.join("crates/testlib-swift");
std::fs::create_dir_all(&swift_crate_dir).expect("mkdir swift crate");
std::fs::write(
swift_crate_dir.join("Cargo.toml"),
"[package]\nname = \"testlib-swift\"\nversion = \"2.0.0\"\n[lib]\nname = \"nonexistent_guaranteed_fail\"\n",
)
.expect("write swift Cargo.toml");
let alef_toml = format!(
"[workspace]\nlanguages = [\"swift\"]\n[[crates]]\nname = \"testlib\"\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");
let resolved_cfg = resolved.remove(0);
std::env::set_current_dir(root).expect("chdir");
let result = precompute_swift_checksum(&resolved_cfg);
let _ = std::env::set_current_dir(&original_cwd);
assert!(
result.is_ok(),
"precompute_swift_checksum must not propagate build errors, got: {:?}",
result
);
assert!(result.unwrap().is_none(), "must return None when build fails");
let pkg_result = std::fs::read_to_string(root.join("Package.swift")).expect("read");
assert!(
pkg_result.contains("__ALEF_SWIFT_CHECKSUM__"),
"Package.swift must retain placeholder when build fails, got:\n{pkg_result}"
);
}