use super::*;
#[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-markup".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-markup".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-markup
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-markup"
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}"
);
}