aboutsummaryrefslogtreecommitdiffhomepage
path: root/crates/mozart-core/src/version_bumper.rs
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 00:37:54 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 00:37:54 +0900
commit0a8e5935e6305819bb02d8c69e2f046ff397913a (patch)
treee5a288e679477b1603d7989e986ca22bbe590aa4 /crates/mozart-core/src/version_bumper.rs
parentb5af594fec7da72b15c9a202c641af0494db6355 (diff)
downloadphp-mozart-0a8e5935e6305819bb02d8c69e2f046ff397913a.tar.gz
php-mozart-0a8e5935e6305819bb02d8c69e2f046ff397913a.tar.zst
php-mozart-0a8e5935e6305819bb02d8c69e2f046ff397913a.zip
refactor(workspace): split monolithic crate into 6 workspace crates
Extract modules from the single `mozart` crate into 5 focused library crates to improve compilation parallelism and architectural clarity: - mozart-constraint: version constraint parser (independent) - mozart-core: base types, console, validation, platform utilities - mozart-archiver: archive creation (tar, zip, bzip2) - mozart-registry: Packagist API, cache, resolver, downloader, lockfile - mozart-autoload: autoloader generation and PHP scanner Refactor Console::from_cli and build_cache_config to accept primitive args instead of &Cli to break circular dependencies. Introduce [workspace.dependencies] for centralized version management. Remove 9 unused direct dependencies from the CLI crate. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
Diffstat (limited to 'crates/mozart-core/src/version_bumper.rs')
-rw-r--r--crates/mozart-core/src/version_bumper.rs667
1 files changed, 667 insertions, 0 deletions
diff --git a/crates/mozart-core/src/version_bumper.rs b/crates/mozart-core/src/version_bumper.rs
new file mode 100644
index 0000000..43c21d6
--- /dev/null
+++ b/crates/mozart-core/src/version_bumper.rs
@@ -0,0 +1,667 @@
+/// Version constraint bumper.
+///
+/// Given a constraint string (from composer.json) and the installed version
+/// (from composer.lock), computes a new constraint string that raises the
+/// lower bound to match the installed version.
+///
+/// Returns `None` if no change is needed, or `Some(new_constraint)` if the
+/// constraint should be updated.
+pub fn bump_requirement(
+ constraint_str: &str,
+ pretty_version: &str,
+ version_normalized: Option<&str>,
+) -> Option<String> {
+ let constraint = constraint_str.trim();
+
+ // Strip and preserve stability flag (@dev, @beta, etc.)
+ let (constraint_body, stability_flag) = strip_stability_flag(constraint);
+
+ // Dev constraints (dev-master, dev-main, etc.) are left unchanged
+ if constraint_body.trim().starts_with("dev-") {
+ return None;
+ }
+
+ // Skip dev installed versions that have no alias
+ // An alias looks like "dev-master as 1.0.0" — the version string in the lock
+ // would be "dev-master" without " as ".
+ if pretty_version.starts_with("dev-") && !pretty_version.contains(" as ") {
+ return None;
+ }
+ if let Some(norm) = version_normalized
+ && norm.starts_with("dev-")
+ && !pretty_version.contains(" as ")
+ {
+ return None;
+ }
+
+ // Resolve the actual version string to use for bumping.
+ // If the pretty_version contains an inline alias (e.g. "dev-master as 1.0.0"),
+ // take the alias target. Otherwise use pretty_version directly.
+ let installed_version = resolve_installed_version(pretty_version, version_normalized);
+
+ // Handle OR constraints (^1.0 || ^2.0)
+ if constraint_body.contains("||") {
+ return bump_or_constraint(constraint_body, &installed_version, stability_flag);
+ }
+
+ // Single constraint
+ bump_single(constraint_body.trim(), &installed_version, stability_flag)
+}
+
+// ─── OR constraint handling ───────────────────────────────────────────────────
+
+fn bump_or_constraint(
+ constraint_body: &str,
+ installed_version: &str,
+ stability_flag: Option<&str>,
+) -> Option<String> {
+ let parts: Vec<&str> = constraint_body.split("||").map(str::trim).collect();
+
+ // Determine which major the installed version belongs to
+ let installed_major = parse_major(installed_version);
+
+ let mut changed = false;
+ let mut new_parts: Vec<String> = Vec::new();
+
+ for part in &parts {
+ let part_trimmed = part.trim();
+ // Determine the major range this disjunct covers
+ let part_major = constraint_major(part_trimmed);
+
+ // Only bump the disjunct whose major matches the installed version's major
+ if part_major == installed_major {
+ if let Some(bumped) = bump_single(part_trimmed, installed_version, None) {
+ new_parts.push(bumped);
+ changed = true;
+ } else {
+ new_parts.push(part_trimmed.to_string());
+ }
+ } else {
+ new_parts.push(part_trimmed.to_string());
+ }
+ }
+
+ if !changed {
+ return None;
+ }
+
+ let joined = new_parts.join(" || ");
+ let result = append_stability_flag(&joined, stability_flag);
+ Some(result)
+}
+
+// ─── Single constraint handling ───────────────────────────────────────────────
+
+fn bump_single(
+ constraint: &str,
+ installed_version: &str,
+ stability_flag: Option<&str>,
+) -> Option<String> {
+ // AND constraints (space-separated multiple operators like ">=1.0 <2.0" or
+ // comma-separated like ">=1.0,<2.0") are not supported for bumping — leave unchanged.
+ // We detect them by checking for a space or comma after the version spec begins.
+ // Quick check: if the constraint contains a space (ignoring leading operators),
+ // it's likely a multi-part AND constraint.
+ let after_op = constraint
+ .trim_start_matches('^')
+ .trim_start_matches('~')
+ .trim_start_matches(">=")
+ .trim_start_matches("<=")
+ .trim_start_matches("!=")
+ .trim_start_matches('>')
+ .trim_start_matches('<')
+ .trim_start_matches('=');
+ if after_op.contains(' ') || after_op.contains(',') {
+ return None;
+ }
+
+ // Caret: ^X.Y.Z
+ if let Some(rest) = constraint.strip_prefix('^') {
+ return bump_caret(rest.trim(), installed_version, stability_flag);
+ }
+
+ // Tilde: ~X.Y.Z
+ if let Some(rest) = constraint.strip_prefix('~') {
+ return bump_tilde(rest.trim(), installed_version, stability_flag);
+ }
+
+ // Wildcard: * or X.*
+ if constraint == "*" || constraint.ends_with(".*") {
+ return bump_wildcard(constraint, installed_version, stability_flag);
+ }
+
+ // Greater-or-equal: >=X.Y
+ if let Some(rest) = constraint.strip_prefix(">=") {
+ return bump_gte(rest.trim(), installed_version, stability_flag);
+ }
+
+ // Other operators (exact, <, <=, >, !=, range) — leave unchanged
+ None
+}
+
+// ─── Caret bump ───────────────────────────────────────────────────────────────
+
+/// `^X.Y.Z` → bump to installed version if it is greater.
+///
+/// The caret prefix is preserved; segments from installed version replace
+/// those in the constraint (trimming trailing zeros appropriately).
+fn bump_caret(rest: &str, installed_version: &str, stability_flag: Option<&str>) -> Option<String> {
+ let constraint_segments = parse_version_segments(rest);
+ let installed_segments = parse_version_segments(installed_version);
+
+ // The constraint length determines how many segments to compare/output
+ let n_constraint = constraint_segments.len().max(1);
+
+ // Compare: if installed <= current lower bound, no change needed
+ // We compare as many segments as the installed version has
+ let current_lower: Vec<u64> = constraint_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+ let installed: Vec<u64> = installed_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+
+ if installed <= current_lower {
+ return None;
+ }
+
+ // Build new constraint segments: use installed version, but only up to
+ // the number of non-trivial segments needed.
+ // We output at least as many segments as the original constraint had,
+ // but trim trailing zeros.
+ let mut new_segs: Vec<u64> = installed_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(n_constraint.max(installed_segments.len()))
+ .collect();
+
+ // Trim trailing zeros (but keep at least n_constraint segments, minimum 1)
+ while new_segs.len() > n_constraint && new_segs.last() == Some(&0) {
+ new_segs.pop();
+ }
+ // Also trim trailing zeros beyond 1 segment
+ while new_segs.len() > 1 && new_segs.last() == Some(&0) {
+ new_segs.pop();
+ }
+
+ let version_str = new_segs
+ .iter()
+ .map(|n| n.to_string())
+ .collect::<Vec<_>>()
+ .join(".");
+
+ let new_constraint = format!("^{version_str}");
+ let result = append_stability_flag(&new_constraint, stability_flag);
+ Some(result)
+}
+
+// ─── Tilde bump ───────────────────────────────────────────────────────────────
+
+/// `~X.Y.Z` (3 segments) → bump patch: `~X.Y.new_patch`
+/// `~X.Y` (2 segments) → convert to caret: `^X.Y.new_patch`
+fn bump_tilde(rest: &str, installed_version: &str, stability_flag: Option<&str>) -> Option<String> {
+ let constraint_segments = parse_version_segments(rest);
+ let installed_segments = parse_version_segments(installed_version);
+
+ let current_lower: Vec<u64> = constraint_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+ let installed: Vec<u64> = installed_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+
+ if installed <= current_lower {
+ return None;
+ }
+
+ let major = installed_segments.first().copied().unwrap_or(0);
+ let minor = installed_segments.get(1).copied().unwrap_or(0);
+ let patch = installed_segments.get(2).copied().unwrap_or(0);
+
+ let new_constraint = if constraint_segments.len() >= 3 {
+ // ~X.Y.Z → keep tilde, bump patch
+ if patch == 0 {
+ format!("~{major}.{minor}.0")
+ } else {
+ format!("~{major}.{minor}.{patch}")
+ }
+ } else {
+ // ~X.Y → convert to caret
+ if patch == 0 {
+ format!("^{major}.{minor}")
+ } else {
+ format!("^{major}.{minor}.{patch}")
+ }
+ };
+
+ let result = append_stability_flag(&new_constraint, stability_flag);
+ Some(result)
+}
+
+// ─── Wildcard bump ────────────────────────────────────────────────────────────
+
+/// `*` → `>=installed`
+/// `X.*` → `>=installed` (trimming trailing zeros)
+fn bump_wildcard(
+ constraint: &str,
+ installed_version: &str,
+ stability_flag: Option<&str>,
+) -> Option<String> {
+ let installed_segments = parse_version_segments(installed_version);
+
+ // Trim trailing zeros
+ let mut segs = installed_segments.clone();
+ while segs.len() > 1 && segs.last() == Some(&0) {
+ segs.pop();
+ }
+
+ let version_str = segs
+ .iter()
+ .map(|n| n.to_string())
+ .collect::<Vec<_>>()
+ .join(".");
+
+ // For plain wildcard "*", always produce >=installed
+ if constraint == "*" {
+ let new_constraint = format!(">={version_str}");
+ return Some(append_stability_flag(&new_constraint, stability_flag));
+ }
+
+ // For "X.*", if installed is at that major, produce >=installed
+ let base = constraint.trim_end_matches(".*");
+ let base_segs = parse_version_segments(base);
+ let current_lower: Vec<u64> = base_segs
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+ let installed: Vec<u64> = installed_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+
+ if installed <= current_lower {
+ return None;
+ }
+
+ let new_constraint = format!(">={version_str}");
+ Some(append_stability_flag(&new_constraint, stability_flag))
+}
+
+// ─── GTE bump ─────────────────────────────────────────────────────────────────
+
+/// `>=X.Y` → raise to installed version (trimming trailing zeros)
+fn bump_gte(rest: &str, installed_version: &str, stability_flag: Option<&str>) -> Option<String> {
+ let constraint_segments = parse_version_segments(rest);
+ let installed_segments = parse_version_segments(installed_version);
+
+ let current_lower: Vec<u64> = constraint_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+ let installed: Vec<u64> = installed_segments
+ .iter()
+ .copied()
+ .chain(std::iter::repeat(0))
+ .take(4)
+ .collect();
+
+ if installed <= current_lower {
+ return None;
+ }
+
+ // Trim trailing zeros from installed version
+ let mut segs = installed_segments.clone();
+ while segs.len() > 1 && segs.last() == Some(&0) {
+ segs.pop();
+ }
+
+ let version_str = segs
+ .iter()
+ .map(|n| n.to_string())
+ .collect::<Vec<_>>()
+ .join(".");
+
+ let new_constraint = format!(">={version_str}");
+ let result = append_stability_flag(&new_constraint, stability_flag);
+ Some(result)
+}
+
+// ─── Helpers ──────────────────────────────────────────────────────────────────
+
+/// Strip a trailing `@stability` flag from a constraint string.
+/// Returns (body, flag) where flag is the `@...` suffix (without the `@`).
+fn strip_stability_flag(constraint: &str) -> (&str, Option<&str>) {
+ let known = ["@dev", "@alpha", "@beta", "@RC", "@rc", "@stable"];
+ for flag in &known {
+ if let Some(body) = constraint.strip_suffix(flag) {
+ let flag_str = &constraint[body.len()..];
+ return (body.trim_end(), Some(flag_str));
+ }
+ }
+ (constraint, None)
+}
+
+/// Append an optional stability flag to a constraint string.
+fn append_stability_flag(constraint: &str, flag: Option<&str>) -> String {
+ match flag {
+ Some(f) => format!("{constraint}{f}"),
+ None => constraint.to_string(),
+ }
+}
+
+/// Parse a version string into numeric segments.
+/// Handles "1.2.3", "1.2", "1", etc.
+/// Stops at any non-numeric/non-dot character.
+fn parse_version_segments(version: &str) -> Vec<u64> {
+ // Strip inline alias: "dev-master as 1.0.0" → "1.0.0"
+ let version = if let Some(pos) = version.find(" as ") {
+ &version[pos + 4..]
+ } else {
+ version
+ };
+
+ // Strip leading v/V
+ let version = version
+ .strip_prefix('v')
+ .or_else(|| version.strip_prefix('V'))
+ .unwrap_or(version);
+
+ // Take up to any pre-release suffix (first '-' or '+')
+ let version = version.split(['-', '+']).next().unwrap_or(version);
+
+ version
+ .split('.')
+ .filter_map(|s| s.parse::<u64>().ok())
+ .collect()
+}
+
+/// Parse the major version number from a version string.
+fn parse_major(version: &str) -> Option<u64> {
+ parse_version_segments(version).into_iter().next()
+}
+
+/// Determine the major version that a single disjunct constraint covers.
+/// For `^1.2`, returns `Some(1)`. For `^0.3`, returns `Some(0)`.
+fn constraint_major(constraint: &str) -> Option<u64> {
+ if let Some(rest) = constraint.strip_prefix('^') {
+ return parse_version_segments(rest).into_iter().next();
+ }
+ if let Some(rest) = constraint.strip_prefix('~') {
+ return parse_version_segments(rest).into_iter().next();
+ }
+ if let Some(rest) = constraint.strip_prefix(">=") {
+ return parse_version_segments(rest).into_iter().next();
+ }
+ // Try as plain version
+ parse_version_segments(constraint).into_iter().next()
+}
+
+/// Resolve the installed version string to use for comparison.
+/// Handles inline aliases (e.g., "dev-main as 2.1.0" → "2.1.0").
+fn resolve_installed_version<'a>(
+ pretty_version: &'a str,
+ _version_normalized: Option<&'a str>,
+) -> String {
+ // If pretty_version contains an inline alias, use the alias target
+ if let Some(pos) = pretty_version.find(" as ") {
+ return pretty_version[pos + 4..].trim().to_string();
+ }
+
+ // If version_normalized is available and not a dev branch, prefer it
+ // for more precise comparison, but use pretty_version for output
+ // Actually we use pretty_version for building constraint strings
+ // since normalized versions have extra .0 suffixes
+
+ // Use pretty_version as-is (strip leading 'v' for normalization)
+ pretty_version
+ .strip_prefix('v')
+ .unwrap_or(pretty_version)
+ .to_string()
+}
+
+// ─── Tests ────────────────────────────────────────────────────────────────────
+
+#[cfg(test)]
+mod tests {
+ use super::*;
+
+ // ── Caret bumps ───────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_caret_bump_basic() {
+ // ^1.0 + 1.2.1 → ^1.2.1
+ let result = bump_requirement("^1.0", "1.2.1", Some("1.2.1.0"));
+ assert_eq!(result, Some("^1.2.1".to_string()));
+ }
+
+ #[test]
+ fn test_caret_no_change_at_lower_bound() {
+ // ^1.2 + 1.2.0 → None (already at lower bound)
+ let result = bump_requirement("^1.2", "1.2.0", Some("1.2.0.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_caret_no_change_exact_match() {
+ // ^1.2.1 + 1.2.1 → None
+ let result = bump_requirement("^1.2.1", "1.2.1", Some("1.2.1.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_caret_bump_zero_major() {
+ // ^0.3 + 0.3.5 → ^0.3.5
+ let result = bump_requirement("^0.3", "0.3.5", Some("0.3.5.0"));
+ assert_eq!(result, Some("^0.3.5".to_string()));
+ }
+
+ #[test]
+ fn test_caret_bump_three_segments() {
+ // ^1.0.0 + 1.2.1 → ^1.2.1
+ let result = bump_requirement("^1.0.0", "1.2.1", Some("1.2.1.0"));
+ assert_eq!(result, Some("^1.2.1".to_string()));
+ }
+
+ #[test]
+ fn test_caret_bump_minor_only() {
+ // ^1.2 + 1.5.0 → ^1.5 (trailing zero trimmed)
+ let result = bump_requirement("^1.2", "1.5.0", Some("1.5.0.0"));
+ assert_eq!(result, Some("^1.5".to_string()));
+ }
+
+ // ── Tilde bumps ───────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_tilde_three_segment_bump() {
+ // ~2.0.0 + 2.0.3 → ~2.0.3
+ let result = bump_requirement("~2.0.0", "2.0.3", Some("2.0.3.0"));
+ assert_eq!(result, Some("~2.0.3".to_string()));
+ }
+
+ #[test]
+ fn test_tilde_two_segment_becomes_caret() {
+ // ~2.0 + 2.0.3 → ^2.0.3
+ let result = bump_requirement("~2.0", "2.0.3", Some("2.0.3.0"));
+ assert_eq!(result, Some("^2.0.3".to_string()));
+ }
+
+ #[test]
+ fn test_tilde_no_change() {
+ // ~2.0.3 + 2.0.3 → None
+ let result = bump_requirement("~2.0.3", "2.0.3", Some("2.0.3.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_tilde_two_segment_no_patch() {
+ // ~2.3 + 2.5.0 → ^2.5 (patch is 0, trimmed)
+ let result = bump_requirement("~2.3", "2.5.0", Some("2.5.0.0"));
+ assert_eq!(result, Some("^2.5".to_string()));
+ }
+
+ // ── Wildcard bumps ────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_wildcard_star() {
+ // * + 1.2.3 → >=1.2.3
+ let result = bump_requirement("*", "1.2.3", Some("1.2.3.0"));
+ assert_eq!(result, Some(">=1.2.3".to_string()));
+ }
+
+ #[test]
+ fn test_wildcard_major_star() {
+ // 2.* + 2.5.0 → >=2.5
+ let result = bump_requirement("2.*", "2.5.0", Some("2.5.0.0"));
+ assert_eq!(result, Some(">=2.5".to_string()));
+ }
+
+ #[test]
+ fn test_wildcard_no_change() {
+ // 2.* + 2.0.0 → None (installed is at lower bound)
+ let result = bump_requirement("2.*", "2.0.0", Some("2.0.0.0"));
+ assert_eq!(result, None);
+ }
+
+ // ── GTE bumps ─────────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_gte_bump() {
+ // >=1.2 + 1.5.0 → >=1.5
+ let result = bump_requirement(">=1.2", "1.5.0", Some("1.5.0.0"));
+ assert_eq!(result, Some(">=1.5".to_string()));
+ }
+
+ #[test]
+ fn test_gte_no_change() {
+ // >=1.5 + 1.5.0 → None
+ let result = bump_requirement(">=1.5", "1.5.0", Some("1.5.0.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_gte_with_patch() {
+ // >=1.2.0 + 1.5.3 → >=1.5.3
+ let result = bump_requirement(">=1.2.0", "1.5.3", Some("1.5.3.0"));
+ assert_eq!(result, Some(">=1.5.3".to_string()));
+ }
+
+ // ── OR constraints ────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_or_constraint_bumps_matching_major() {
+ // ^1.2 || ^2.3 + 1.3.0 → ^1.3 || ^2.3
+ let result = bump_requirement("^1.2 || ^2.3", "1.3.0", Some("1.3.0.0"));
+ assert_eq!(result, Some("^1.3 || ^2.3".to_string()));
+ }
+
+ #[test]
+ fn test_or_constraint_bumps_second_major() {
+ // ^1.2 || ^2.3 + 2.5.0 → ^1.2 || ^2.5
+ let result = bump_requirement("^1.2 || ^2.3", "2.5.0", Some("2.5.0.0"));
+ assert_eq!(result, Some("^1.2 || ^2.5".to_string()));
+ }
+
+ #[test]
+ fn test_or_constraint_no_change() {
+ // ^1.2 || ^2.3 + 1.2.0 → None
+ let result = bump_requirement("^1.2 || ^2.3", "1.2.0", Some("1.2.0.0"));
+ assert_eq!(result, None);
+ }
+
+ // ── Dev constraints ───────────────────────────────────────────────────────
+
+ #[test]
+ fn test_dev_constraint_unchanged() {
+ // dev-master → None
+ let result = bump_requirement("dev-master", "dev-master", None);
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_dev_installed_no_alias_unchanged() {
+ // Installed is dev-main without alias → None
+ let result = bump_requirement("^1.0", "dev-main", None);
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_dev_installed_with_alias() {
+ // Installed is "dev-main as 1.2.0" → bump based on alias
+ let result = bump_requirement("^1.0", "dev-main as 1.2.0", None);
+ assert_eq!(result, Some("^1.2".to_string()));
+ }
+
+ // ── Stability flags ───────────────────────────────────────────────────────
+
+ #[test]
+ fn test_stability_flag_preserved() {
+ // ^1.0@dev + 1.2.0 → ^1.2@dev
+ let result = bump_requirement("^1.0@dev", "1.2.0", Some("1.2.0.0"));
+ assert_eq!(result, Some("^1.2@dev".to_string()));
+ }
+
+ #[test]
+ fn test_stability_flag_beta_preserved() {
+ // ^1.0@beta + 1.2.1 → ^1.2.1@beta
+ let result = bump_requirement("^1.0@beta", "1.2.1", Some("1.2.1.0"));
+ assert_eq!(result, Some("^1.2.1@beta".to_string()));
+ }
+
+ // ── Edge cases ────────────────────────────────────────────────────────────
+
+ #[test]
+ fn test_exact_constraint_no_bump() {
+ // 1.2.3 → None (exact version, not bumped)
+ let result = bump_requirement("1.2.3", "1.3.0", Some("1.3.0.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_complex_range_no_bump() {
+ // >=1.0 <2.0 → None (complex range, not bumped)
+ let result = bump_requirement(">=1.0 <2.0", "1.5.0", Some("1.5.0.0"));
+ assert_eq!(result, None);
+ }
+
+ #[test]
+ fn test_parse_version_segments_basic() {
+ assert_eq!(parse_version_segments("1.2.3"), vec![1, 2, 3]);
+ assert_eq!(parse_version_segments("1.2"), vec![1, 2]);
+ assert_eq!(parse_version_segments("1"), vec![1]);
+ }
+
+ #[test]
+ fn test_parse_version_segments_with_prerelease() {
+ assert_eq!(parse_version_segments("1.2.3-beta1"), vec![1, 2, 3]);
+ }
+
+ #[test]
+ fn test_parse_version_segments_with_v_prefix() {
+ assert_eq!(parse_version_segments("v1.2.3"), vec![1, 2, 3]);
+ }
+
+ #[test]
+ fn test_parse_version_segments_alias() {
+ // "dev-master as 1.0.0" → segments of "1.0.0"
+ assert_eq!(parse_version_segments("dev-master as 1.0.0"), vec![1, 0, 0]);
+ }
+}