aboutsummaryrefslogtreecommitdiffhomepage
diff options
context:
space:
mode:
authornsfisis <nsfisis@gmail.com>2026-02-22 17:49:49 +0900
committernsfisis <nsfisis@gmail.com>2026-02-22 17:49:49 +0900
commit07733b3b328f6e4ec23754fcb3504ddb196d65a3 (patch)
treed3891db598caacf961aa0e4f72a71e766f9ae758
parentb696eb7608d921ae0e14a4296e412c33340ceee8 (diff)
downloadphp-mozart-07733b3b328f6e4ec23754fcb3504ddb196d65a3.tar.gz
php-mozart-07733b3b328f6e4ec23754fcb3504ddb196d65a3.tar.zst
php-mozart-07733b3b328f6e4ec23754fcb3504ddb196d65a3.zip
fix(resolver): replace ComposerVersion(u16) with semver::Version(u64)
ComposerVersion used u16 segments (max 65535), causing overflow for PHP extensions like ext-dom (version 20031129). The extension became invisible to the resolver, failing create-project with "depends on ext-dom" errors. Use mozart_semver::Version (u64 segments) directly as pubgrub's version type, eliminating the overflow and redundant conversion layer. Also fixes Ord for patch pre-releases (patch1 > stable) and adds Display impl required by pubgrub. Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
-rw-r--r--crates/mozart-registry/src/resolver.rs999
-rw-r--r--crates/mozart-semver/src/lib.rs59
2 files changed, 351 insertions, 707 deletions
diff --git a/crates/mozart-registry/src/resolver.rs b/crates/mozart-registry/src/resolver.rs
index e240dcd..a683ef7 100644
--- a/crates/mozart-registry/src/resolver.rs
+++ b/crates/mozart-registry/src/resolver.rs
@@ -1,6 +1,6 @@
//! Dependency resolver using the pubgrub v0.3.0 algorithm.
//!
-//! This module converts Composer-style dependency constraints into pubgrub's `Ranges<ComposerVersion>`
+//! This module converts Composer-style dependency constraints into pubgrub's `Ranges<Version>`
//! and implements `DependencyProvider` for Mozart's package resolution.
use std::cell::RefCell;
@@ -16,250 +16,77 @@ use pubgrub::{
use crate::cache::Cache;
use crate::packagist;
use mozart_core::package::Stability;
-use mozart_semver::{Constraint, VersionConstraint};
+use mozart_semver::{Constraint, Version, VersionConstraint};
// ─────────────────────────────────────────────────────────────────────────────
-// Stability constants
+// Version helpers
// ─────────────────────────────────────────────────────────────────────────────
-const STABILITY_DEV: u16 = 0;
-const STABILITY_ALPHA_BASE: u16 = 1000;
-const STABILITY_BETA_BASE: u16 = 2000;
-const STABILITY_RC_BASE: u16 = 3000;
-const STABILITY_STABLE: u16 = 4000;
-const STABILITY_PATCH_BASE: u16 = 5000;
-
-// ─────────────────────────────────────────────────────────────────────────────
-// ComposerVersion
-// ─────────────────────────────────────────────────────────────────────────────
-
-/// A Composer version suitable for use with pubgrub.
-///
-/// Encodes a 4-segment Composer version plus stability into an ordered struct.
-/// Stability is encoded numerically so that higher values are more stable:
-/// - dev=0, alpha(N)=1000+N, beta(N)=2000+N, RC(N)=3000+N, stable=4000, patch(N)=5000+N
-///
-/// This ensures natural `Ord` comparison matches Composer's version ordering.
-/// Dev branches (dev-master, dev-*) are NOT representable and return `None` from `from_normalized`.
-#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
-pub struct ComposerVersion {
- pub major: u16,
- pub minor: u16,
- pub patch: u16,
- pub build: u16,
- /// Stability encoded as a comparable integer. Higher = more stable.
- pub stability: u16,
-}
-
-impl PartialOrd for ComposerVersion {
- fn partial_cmp(&self, other: &Self) -> Option<std::cmp::Ordering> {
- Some(self.cmp(other))
- }
-}
-
-impl Ord for ComposerVersion {
- fn cmp(&self, other: &Self) -> std::cmp::Ordering {
- (
- self.major,
- self.minor,
- self.patch,
- self.build,
- self.stability,
- )
- .cmp(&(
- other.major,
- other.minor,
- other.patch,
- other.build,
- other.stability,
- ))
- }
-}
-
-impl fmt::Display for ComposerVersion {
- fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
- write!(
- f,
- "{}.{}.{}.{}",
- self.major, self.minor, self.patch, self.build
- )?;
- let s = self.stability;
- if s == STABILITY_STABLE {
- // no suffix
- } else if s >= STABILITY_PATCH_BASE {
- write!(f, "-patch{}", s - STABILITY_PATCH_BASE)?;
- } else if s >= STABILITY_RC_BASE {
- write!(f, "-RC{}", s - STABILITY_RC_BASE)?;
- } else if s >= STABILITY_BETA_BASE {
- write!(f, "-beta{}", s - STABILITY_BETA_BASE)?;
- } else if s >= STABILITY_ALPHA_BASE {
- write!(f, "-alpha{}", s - STABILITY_ALPHA_BASE)?;
- } else {
- write!(f, "-dev")?;
+/// Determine the `Stability` of a `Version` from its pre_release string.
+fn version_stability(v: &Version) -> Stability {
+ match &v.pre_release {
+ None => Stability::Stable,
+ Some(pre) => {
+ let lower = pre.to_lowercase();
+ if lower.starts_with("dev") {
+ Stability::Dev
+ } else if lower.starts_with("alpha") || lower.starts_with('a') {
+ Stability::Alpha
+ } else if lower.starts_with("beta") || lower.starts_with('b') {
+ Stability::Beta
+ } else if lower.starts_with("rc") {
+ Stability::RC
+ } else {
+ // patch/pl/p and unknown → stable
+ Stability::Stable
+ }
}
- Ok(())
}
}
-impl ComposerVersion {
- /// Parse a branch alias target like "2.x-dev" or "1.0.x-dev" into a ComposerVersion
- /// with dev stability.
- ///
- /// Used to represent aliased dev branches in the resolver. The version number is taken
- /// from the numeric prefix (e.g. "2.x-dev" → major=2, minor=0, patch=0, build=0, stability=dev).
- /// This allows constraints like `^2.0` to match `dev-master` when it is aliased to `2.x-dev`.
- pub fn from_branch_alias_target(alias_target: &str) -> Option<ComposerVersion> {
- let s = alias_target.trim().to_lowercase();
- // Must end with "-dev" or ".x-dev"
- if !s.ends_with("-dev") {
- return None;
- }
- // Strip the trailing "-dev"
- let base = &s[..s.len() - 4];
- // Strip optional trailing ".x" segments (e.g. "2.x" → "2", "1.0.x" → "1.0")
- let base = base.trim_end_matches(".x");
- // Now parse whatever numeric segments remain
- let parts: Vec<&str> = base.split('.').collect();
- let major: u16 = parts.first().and_then(|p| p.parse().ok())?;
- let minor: u16 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
- let patch: u16 = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
- let build: u16 = parts.get(3).and_then(|p| p.parse().ok()).unwrap_or(0);
- Some(ComposerVersion {
- major,
- minor,
- patch,
- build,
- stability: STABILITY_DEV,
- })
- }
-
- /// Parse from a Packagist normalized version string like "1.2.3.0", "1.0.0.0-beta1", "1.0.0.0-RC2".
- /// Returns `None` for dev branches (dev-master, dev-*, *.x-dev).
- pub fn from_normalized(normalized: &str) -> Option<ComposerVersion> {
- let s = normalized.trim();
-
- // Reject dev branches
- if s.to_lowercase().starts_with("dev-") {
- return None;
- }
- // Reject *.x-dev style (e.g. "9999999.9999999.9999999.9999999-dev" from packagist sometimes)
- // Also reject anything like "2.1.x-dev"
- if s.to_lowercase().ends_with("-dev") && s.contains(".x") {
- return None;
- }
- // Packagist uses 9999999.9999999.9999999.9999999 for dev branches too
- if s.starts_with("9999999") {
- return None;
- }
-
- // Split on '-' for pre-release
- let (version_part, pre_part) = if let Some(pos) = s.find('-') {
- (&s[..pos], Some(&s[pos + 1..]))
- } else {
- (s, None)
- };
-
- let segments: Vec<&str> = version_part.split('.').collect();
- if segments.is_empty() || segments[0].is_empty() {
- return None;
- }
-
- let major: u16 = segments[0].parse().ok()?;
- let minor: u16 = segments.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
- let patch: u16 = segments.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
- let build: u16 = segments.get(3).and_then(|p| p.parse().ok()).unwrap_or(0);
-
- let stability = match pre_part {
- None => STABILITY_STABLE,
- Some(pre) => encode_pre_release_str(pre),
- };
+/// Parse a Packagist normalized version string like "1.2.3.0", "1.0.0.0-beta1".
+/// Returns `None` for dev branches (dev-master, dev-*, *.x-dev).
+fn parse_normalized(normalized: &str) -> Option<Version> {
+ let s = normalized.trim();
- Some(ComposerVersion {
- major,
- minor,
- patch,
- build,
- stability,
- })
+ // Reject dev branches
+ if s.to_lowercase().starts_with("dev-") {
+ return None;
}
-
- /// Construct a stable version from numeric segments.
- pub fn stable(major: u16, minor: u16, patch: u16, build: u16) -> ComposerVersion {
- ComposerVersion {
- major,
- minor,
- patch,
- build,
- stability: STABILITY_STABLE,
- }
+ // Reject *.x-dev style
+ if s.to_lowercase().ends_with("-dev") && s.contains(".x") {
+ return None;
}
-
- /// Get the `Stability` enum value for this version.
- pub fn stability_enum(&self) -> Stability {
- if self.stability < STABILITY_ALPHA_BASE {
- // Covers both STABILITY_DEV (0) and any value below ALPHA_BASE
- Stability::Dev
- } else if self.stability < STABILITY_BETA_BASE {
- Stability::Alpha
- } else if self.stability < STABILITY_RC_BASE {
- Stability::Beta
- } else if self.stability < STABILITY_STABLE {
- Stability::RC
- } else {
- // >= STABILITY_STABLE (includes patch)
- Stability::Stable
- }
+ // Packagist uses 9999999.9999999.9999999.9999999 for dev branches
+ if s.starts_with("9999999") {
+ return None;
}
-}
-fn encode_pre_release_str(pre: &str) -> u16 {
- let lower = pre.to_lowercase();
- if lower == "dev" {
- STABILITY_DEV
- } else if lower.starts_with("alpha") || lower.starts_with('a') {
- let n = extract_pre_release_number_from(
- &lower,
- if lower.starts_with("alpha") {
- "alpha"
- } else {
- "a"
- },
- );
- STABILITY_ALPHA_BASE + n
- } else if lower.starts_with("beta") || lower.starts_with('b') {
- let n = extract_pre_release_number_from(
- &lower,
- if lower.starts_with("beta") {
- "beta"
- } else {
- "b"
- },
- );
- STABILITY_BETA_BASE + n
- } else if lower.starts_with("rc") {
- let n = extract_pre_release_number_from(&lower, "rc");
- STABILITY_RC_BASE + n
- } else if lower.starts_with("patch") || lower.starts_with("pl") {
- let n = extract_pre_release_number_from(
- &lower,
- if lower.starts_with("patch") {
- "patch"
- } else {
- "pl"
- },
- );
- STABILITY_PATCH_BASE + n
- } else if lower == "p" {
- STABILITY_PATCH_BASE
- } else {
- STABILITY_STABLE
- }
+ Version::parse(s).ok()
}
-fn extract_pre_release_number_from(s: &str, prefix: &str) -> u16 {
- let after = &s[prefix.len()..];
- let digits: String = after.chars().filter(|c| c.is_ascii_digit()).collect();
- digits.parse().unwrap_or(0)
+/// Parse a branch alias target like "2.x-dev" or "1.0.x-dev" into a `Version` with dev pre-release.
+fn parse_branch_alias_target(alias_target: &str) -> Option<Version> {
+ let s = alias_target.trim().to_lowercase();
+ if !s.ends_with("-dev") {
+ return None;
+ }
+ let base = &s[..s.len() - 4];
+ let base = base.trim_end_matches(".x");
+ let parts: Vec<&str> = base.split('.').collect();
+ let major: u64 = parts.first().and_then(|p| p.parse().ok())?;
+ let minor: u64 = parts.get(1).and_then(|p| p.parse().ok()).unwrap_or(0);
+ let patch: u64 = parts.get(2).and_then(|p| p.parse().ok()).unwrap_or(0);
+ let build: u64 = parts.get(3).and_then(|p| p.parse().ok()).unwrap_or(0);
+ Some(Version {
+ major,
+ minor,
+ patch,
+ build,
+ pre_release: Some("dev".to_string()),
+ is_dev_branch: false,
+ dev_branch_name: None,
+ })
}
// ─────────────────────────────────────────────────────────────────────────────
@@ -305,13 +132,13 @@ impl PackageName {
// ─────────────────────────────────────────────────────────────────────────────
/// The version set type used throughout the resolver.
-pub type ComposerVS = Ranges<ComposerVersion>;
+pub type ComposerVS = Ranges<Version>;
// ─────────────────────────────────────────────────────────────────────────────
// Constraint-to-Ranges conversion
// ─────────────────────────────────────────────────────────────────────────────
-/// Convert a Composer version constraint string to a pubgrub `Ranges<ComposerVersion>`.
+/// Convert a Composer version constraint string to a pubgrub `Ranges<Version>`.
///
/// Supports: exact, >=, >, <=, <, !=, ^, ~, *, wildcards, hyphen ranges, AND, OR.
pub fn constraint_to_ranges(constraint: &str) -> Result<ComposerVS, String> {
@@ -343,76 +170,14 @@ fn version_constraint_to_ranges(vc: &VersionConstraint) -> Result<ComposerVS, St
fn single_constraint_to_ranges(c: &Constraint) -> Result<ComposerVS, String> {
match c {
Constraint::Any => Ok(Ranges::full()),
- Constraint::Exact(v) => {
- let cv = version_to_composer(v)?;
- Ok(Ranges::singleton(cv))
- }
- Constraint::GreaterThan(v) => {
- let cv = version_to_composer(v)?;
- Ok(Ranges::strictly_higher_than(cv))
- }
- Constraint::GreaterThanOrEqual(v) => {
- let cv = version_to_composer(v)?;
- Ok(Ranges::higher_than(cv))
- }
- Constraint::LessThan(v) => {
- let cv = version_to_composer(v)?;
- Ok(Ranges::strictly_lower_than(cv))
- }
+ Constraint::Exact(v) => Ok(Ranges::singleton(v.clone())),
+ Constraint::GreaterThan(v) => Ok(Ranges::strictly_higher_than(v.clone())),
+ Constraint::GreaterThanOrEqual(v) => Ok(Ranges::higher_than(v.clone())),
+ Constraint::LessThan(v) => Ok(Ranges::strictly_lower_than(v.clone())),
Constraint::LessThanOrEqual(v) => {
- let cv = version_to_composer(v)?;
- // No Ranges::lower_than in version-ranges 0.1.x, so use complement of strictly_higher_than
- Ok(Ranges::strictly_higher_than(cv).complement())
- }
- Constraint::NotEqual(v) => {
- let cv = version_to_composer(v)?;
- Ok(Ranges::singleton(cv).complement())
+ Ok(Ranges::strictly_higher_than(v.clone()).complement())
}
- }
-}
-
-/// Convert a `constraint::Version` to a `ComposerVersion`.
-fn version_to_composer(v: &mozart_semver::Version) -> Result<ComposerVersion, String> {
- // Dev branches cannot be represented as ComposerVersion
- if v.is_dev_branch {
- return Err(format!(
- "Dev branch versions cannot be used in Ranges (branch: {:?})",
- v.dev_branch_name
- ));
- }
-
- let major: u16 = v
- .major
- .try_into()
- .map_err(|_| format!("Major version {} too large for u16", v.major))?;
- let minor: u16 = v
- .minor
- .try_into()
- .map_err(|_| format!("Minor version {} too large for u16", v.minor))?;
- let patch: u16 = v
- .patch
- .try_into()
- .map_err(|_| format!("Patch version {} too large for u16", v.patch))?;
- let build: u16 = v
- .build
- .try_into()
- .map_err(|_| format!("Build version {} too large for u16", v.build))?;
-
- let stability = encode_pre_release(&v.pre_release);
-
- Ok(ComposerVersion {
- major,
- minor,
- patch,
- build,
- stability,
- })
-}
-
-fn encode_pre_release(pre: &Option<String>) -> u16 {
- match pre {
- None => STABILITY_STABLE,
- Some(s) => encode_pre_release_str(s),
+ Constraint::NotEqual(v) => Ok(Ranges::singleton(v.clone()).complement()),
}
}
@@ -441,35 +206,22 @@ impl PlatformConfig {
let detected = mozart_core::platform::detect_platform();
let mut packages = HashMap::new();
for pkg in detected {
- // Normalize version to four-component form (e.g. "8.2.1" → "8.2.1.0")
- let normalized = normalize_platform_version(&pkg.version);
- packages.insert(pkg.name, normalized);
+ packages.insert(pkg.name, pkg.version);
}
Self { packages }
}
- /// Parse platform packages into `ComposerVersion` values.
- pub fn to_versions(&self) -> HashMap<String, ComposerVersion> {
+ /// Parse platform packages into `Version` values.
+ pub fn to_versions(&self) -> HashMap<String, Version> {
self.packages
.iter()
.filter_map(|(name, version_str)| {
- ComposerVersion::from_normalized(version_str).map(|v| (name.clone(), v))
+ Version::parse(version_str).ok().map(|v| (name.clone(), v))
})
.collect()
}
}
-/// Pad a version string to four dot-separated components (e.g. "8.2.1" → "8.2.1.0").
-fn normalize_platform_version(version: &str) -> String {
- let parts: Vec<&str> = version.split('.').collect();
- match parts.len() {
- 1 => format!("{}.0.0.0", parts[0]),
- 2 => format!("{}.{}.0.0", parts[0], parts[1]),
- 3 => format!("{}.{}.{}.0", parts[0], parts[1], parts[2]),
- _ => version.to_string(),
- }
-}
-
// ─────────────────────────────────────────────────────────────────────────────
// Error types
// ─────────────────────────────────────────────────────────────────────────────
@@ -551,8 +303,8 @@ pub struct ResolverPriority {
/// Cached version data for a single package.
struct PackageVersions {
- /// All versions that pass the stability filter, sorted by ComposerVersion.
- versions: BTreeMap<ComposerVersion, VersionDependencies>,
+ /// All versions that pass the stability filter, sorted by Version.
+ versions: BTreeMap<Version, VersionDependencies>,
}
/// Dependencies of a specific package version.
@@ -591,7 +343,7 @@ pub struct MozartProvider {
repo_cache: Option<Cache>,
/// Platform packages (php, ext-*, lib-*) with their fixed versions.
- platform_packages: HashMap<String, ComposerVersion>,
+ platform_packages: HashMap<String, Version>,
/// Minimum stability threshold. Versions below this are excluded.
minimum_stability: Stability,
@@ -672,12 +424,12 @@ impl MozartProvider {
version_normalized,
};
- match ComposerVersion::from_normalized(&pv.version_normalized) {
- Some(cv) => {
+ match parse_normalized(&pv.version_normalized) {
+ Some(v) => {
// Regular (non-dev) version
- if self.passes_stability_filter(package_name, &cv) {
+ if self.passes_stability_filter(package_name, &v) {
let deps = make_deps(pv.version.clone(), pv.version_normalized.clone());
- versions.insert(cv, deps);
+ versions.insert(v, deps);
}
}
None => {
@@ -689,15 +441,15 @@ impl MozartProvider {
if branch.to_lowercase() != pv.version.to_lowercase() {
continue;
}
- if let Some(alias_cv) =
- ComposerVersion::from_branch_alias_target(alias_target)
- && self.passes_stability_filter(package_name, &alias_cv)
+ if let Some(alias_v) =
+ parse_branch_alias_target(alias_target)
+ && self.passes_stability_filter(package_name, &alias_v)
{
// Use the alias target as the normalized version string so
// that constraint matching works correctly.
let deps = make_deps(pv.version.clone(), alias_target.clone());
// Only insert if no real release already occupies this slot
- versions.entry(alias_cv).or_insert(deps);
+ versions.entry(alias_v).or_insert(deps);
}
}
}
@@ -711,7 +463,7 @@ impl MozartProvider {
}
/// Check if a version passes the minimum-stability filter for the given package.
- fn passes_stability_filter(&self, package_name: &str, version: &ComposerVersion) -> bool {
+ fn passes_stability_filter(&self, package_name: &str, version: &Version) -> bool {
// Per-package stability override takes precedence
let min_stability = self
.stability_flags
@@ -719,12 +471,12 @@ impl MozartProvider {
.copied()
.unwrap_or(self.minimum_stability);
- let version_stability = version.stability_enum();
+ let vs = version_stability(version);
// `Stability` enum: Stable=0, RC=5, Beta=10, Alpha=15, Dev=20
// Lower enum value = more stable.
- // version_stability must be <= min_stability (i.e., at least as stable as minimum).
- version_stability <= min_stability
+ // vs must be <= min_stability (i.e., at least as stable as minimum).
+ vs <= min_stability
}
/// Check whether a platform dependency should be skipped.
@@ -741,7 +493,7 @@ impl MozartProvider {
impl DependencyProvider for MozartProvider {
type P = PackageName;
- type V = ComposerVersion;
+ type V = Version;
type VS = ComposerVS;
type Priority = ResolverPriority;
type M = String;
@@ -751,10 +503,13 @@ impl DependencyProvider for MozartProvider {
&self,
package: &PackageName,
range: &ComposerVS,
- ) -> Result<Option<ComposerVersion>, ResolverError> {
- // Root package: always version 0.0.0.0-stable
+ ) -> Result<Option<Version>, ResolverError> {
+ // Root package: always version 0.0.0.0 (stable)
if package.is_root() {
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = Version {
+ major: 0, minor: 0, patch: 0, build: 0,
+ pre_release: None, is_dev_branch: false, dev_branch_name: None,
+ };
if range.contains(&root_v) {
return Ok(Some(root_v));
}
@@ -766,7 +521,7 @@ impl DependencyProvider for MozartProvider {
if let Some(v) = self.platform_packages.get(&package.0)
&& range.contains(v)
{
- return Ok(Some(*v));
+ return Ok(Some(v.clone()));
}
return Ok(None);
}
@@ -785,7 +540,7 @@ impl DependencyProvider for MozartProvider {
.versions
.keys()
.find(|v| range.contains(*v))
- .copied());
+ .cloned());
}
if self.prefer_stable {
@@ -794,9 +549,9 @@ impl DependencyProvider for MozartProvider {
.versions
.keys()
.rev()
- .find(|v| v.stability >= STABILITY_STABLE && range.contains(*v))
+ .find(|v| version_stability(v) == Stability::Stable && range.contains(*v))
{
- return Ok(Some(*v));
+ return Ok(Some(v.clone()));
}
}
@@ -806,7 +561,7 @@ impl DependencyProvider for MozartProvider {
.keys()
.rev()
.find(|v| range.contains(*v))
- .copied())
+ .cloned())
}
fn prioritize(
@@ -838,7 +593,7 @@ impl DependencyProvider for MozartProvider {
fn get_dependencies(
&self,
package: &PackageName,
- version: &ComposerVersion,
+ version: &Version,
) -> Result<Dependencies<PackageName, ComposerVS, String>, ResolverError> {
// Root package: return the configured root dependencies
if package.is_root() {
@@ -1049,7 +804,10 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
};
let root = PackageName::root();
- let root_version = ComposerVersion::stable(0, 0, 0, 0);
+ let root_version = Version {
+ major: 0, minor: 0, patch: 0, build: 0,
+ pre_release: None, is_dev_branch: false, dev_branch_name: None,
+ };
match pubgrub::resolve(&provider, root, root_version) {
Ok(solution) => {
@@ -1074,7 +832,7 @@ pub async fn resolve(request: &ResolveRequest) -> Result<Vec<ResolvedPackage>, R
name: pkg.0.clone(),
version: version_str,
version_normalized,
- is_dev: version.stability < STABILITY_ALPHA_BASE,
+ is_dev: version_stability(&version) == Stability::Dev,
});
}
Ok(result)
@@ -1131,83 +889,110 @@ mod tests {
.clone()
}
- // ──────────── ComposerVersion parsing ────────────
+ // ──────────── Version parsing helpers ────────────
+
+ fn v(major: u64, minor: u64, patch: u64, build: u64) -> Version {
+ Version {
+ major, minor, patch, build,
+ pre_release: None,
+ is_dev_branch: false,
+ dev_branch_name: None,
+ }
+ }
+
+ fn v_dev(major: u64, minor: u64, patch: u64, build: u64) -> Version {
+ Version {
+ major, minor, patch, build,
+ pre_release: Some("dev".to_string()),
+ is_dev_branch: false,
+ dev_branch_name: None,
+ }
+ }
+
+ fn v_pre(major: u64, minor: u64, patch: u64, build: u64, pre: &str) -> Version {
+ Version {
+ major, minor, patch, build,
+ pre_release: Some(pre.to_string()),
+ is_dev_branch: false,
+ dev_branch_name: None,
+ }
+ }
+
+ // ──────────── parse_normalized ────────────
#[test]
- fn test_composer_version_parse_stable() {
- let v = ComposerVersion::from_normalized("1.2.3.0").unwrap();
- assert_eq!(v.major, 1);
- assert_eq!(v.minor, 2);
- assert_eq!(v.patch, 3);
- assert_eq!(v.build, 0);
- assert_eq!(v.stability, STABILITY_STABLE);
+ fn test_parse_normalized_stable() {
+ let ver = parse_normalized("1.2.3.0").unwrap();
+ assert_eq!((ver.major, ver.minor, ver.patch, ver.build), (1, 2, 3, 0));
+ assert_eq!(ver.pre_release, None);
}
#[test]
- fn test_composer_version_parse_beta() {
- let v = ComposerVersion::from_normalized("1.0.0.0-beta1").unwrap();
- assert_eq!(v.major, 1);
- assert_eq!(v.minor, 0);
- assert_eq!(v.patch, 0);
- assert_eq!(v.build, 0);
- assert_eq!(v.stability, STABILITY_BETA_BASE + 1);
+ fn test_parse_normalized_beta() {
+ let ver = parse_normalized("1.0.0.0-beta1").unwrap();
+ assert_eq!(ver.major, 1);
+ assert_eq!(ver.pre_release, Some("beta1".to_string()));
}
#[test]
- fn test_composer_version_parse_rc() {
- let v = ComposerVersion::from_normalized("2.0.0.0-RC3").unwrap();
- assert_eq!(v.major, 2);
- assert_eq!(v.stability, STABILITY_RC_BASE + 3);
+ fn test_parse_normalized_rc() {
+ let ver = parse_normalized("2.0.0.0-RC3").unwrap();
+ assert_eq!(ver.major, 2);
+ assert_eq!(ver.pre_release, Some("RC3".to_string()));
}
#[test]
- fn test_composer_version_parse_alpha() {
- let v = ComposerVersion::from_normalized("1.0.0.0-alpha2").unwrap();
- assert_eq!(v.stability, STABILITY_ALPHA_BASE + 2);
+ fn test_parse_normalized_alpha() {
+ let ver = parse_normalized("1.0.0.0-alpha2").unwrap();
+ assert_eq!(ver.pre_release, Some("alpha2".to_string()));
}
#[test]
- fn test_composer_version_parse_dev() {
- let v = ComposerVersion::from_normalized("1.0.0.0-dev").unwrap();
- assert_eq!(v.stability, STABILITY_DEV);
+ fn test_parse_normalized_dev() {
+ let ver = parse_normalized("1.0.0.0-dev").unwrap();
+ assert_eq!(ver.pre_release, Some("dev".to_string()));
}
#[test]
- fn test_composer_version_parse_dev_branch() {
- let v = ComposerVersion::from_normalized("dev-master");
- assert!(
- v.is_none(),
- "dev-master should not parse as ComposerVersion"
- );
+ fn test_parse_normalized_dev_branch() {
+ let ver = parse_normalized("dev-master");
+ assert!(ver.is_none(), "dev-master should not parse as normalized version");
+ }
+
+ #[test]
+ fn test_parse_normalized_x_dev() {
+ let ver = parse_normalized("dev-feature/foo");
+ assert!(ver.is_none());
}
#[test]
- fn test_composer_version_parse_x_dev() {
- let v = ComposerVersion::from_normalized("dev-feature/foo");
- assert!(v.is_none());
+ fn test_parse_normalized_9999999_dev() {
+ let ver = parse_normalized("9999999.9999999.9999999.9999999-dev");
+ assert!(ver.is_none());
}
#[test]
- fn test_composer_version_parse_9999999_dev() {
- // Packagist sometimes uses 9999999.9999999.9999999.9999999 for dev
- let v = ComposerVersion::from_normalized("9999999.9999999.9999999.9999999-dev");
- assert!(v.is_none());
+ fn test_parse_normalized_large_version() {
+ // ext-dom version 20031129 — this was the u16 overflow bug
+ let ver = parse_normalized("20031129").unwrap();
+ assert_eq!(ver.major, 20031129);
+ assert_eq!(ver.pre_release, None);
}
#[test]
- fn test_composer_version_ordering_stable() {
- let v1 = ComposerVersion::from_normalized("2.0.0.0").unwrap();
- let v2 = ComposerVersion::from_normalized("1.0.0.0").unwrap();
+ fn test_version_ordering_stable() {
+ let v1 = parse_normalized("2.0.0.0").unwrap();
+ let v2 = parse_normalized("1.0.0.0").unwrap();
assert!(v1 > v2);
}
#[test]
- fn test_composer_version_ordering_stability() {
- let stable = ComposerVersion::from_normalized("1.0.0.0").unwrap();
- let rc = ComposerVersion::from_normalized("1.0.0.0-RC1").unwrap();
- let beta = ComposerVersion::from_normalized("1.0.0.0-beta1").unwrap();
- let alpha = ComposerVersion::from_normalized("1.0.0.0-alpha1").unwrap();
- let dev = ComposerVersion::from_normalized("1.0.0.0-dev").unwrap();
+ fn test_version_ordering_stability() {
+ let stable = parse_normalized("1.0.0.0").unwrap();
+ let rc = parse_normalized("1.0.0.0-RC1").unwrap();
+ let beta = parse_normalized("1.0.0.0-beta1").unwrap();
+ let alpha = parse_normalized("1.0.0.0-alpha1").unwrap();
+ let dev = parse_normalized("1.0.0.0-dev").unwrap();
assert!(stable > rc);
assert!(rc > beta);
assert!(beta > alpha);
@@ -1215,222 +1000,147 @@ mod tests {
}
#[test]
- fn test_composer_version_ordering_pre_number() {
- let beta2 = ComposerVersion::from_normalized("1.0.0.0-beta2").unwrap();
- let beta1 = ComposerVersion::from_normalized("1.0.0.0-beta1").unwrap();
+ fn test_version_ordering_pre_number() {
+ let beta2 = parse_normalized("1.0.0.0-beta2").unwrap();
+ let beta1 = parse_normalized("1.0.0.0-beta1").unwrap();
assert!(beta2 > beta1);
}
#[test]
- fn test_composer_version_display() {
- let stable = ComposerVersion::stable(1, 2, 3, 0);
+ fn test_version_display() {
+ let stable = v(1, 2, 3, 0);
assert_eq!(format!("{stable}"), "1.2.3.0");
- let beta1 = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_BETA_BASE + 1,
- };
+ let beta1 = v_pre(1, 0, 0, 0, "beta1");
assert_eq!(format!("{beta1}"), "1.0.0.0-beta1");
- let rc2 = ComposerVersion {
- major: 2,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_RC_BASE + 2,
- };
+ let rc2 = v_pre(2, 0, 0, 0, "RC2");
assert_eq!(format!("{rc2}"), "2.0.0.0-RC2");
- let dev = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_DEV,
- };
+ let dev = v_pre(1, 0, 0, 0, "dev");
assert_eq!(format!("{dev}"), "1.0.0.0-dev");
}
#[test]
- fn test_composer_version_stability_enum() {
- let stable = ComposerVersion::stable(1, 0, 0, 0);
- assert_eq!(stable.stability_enum(), Stability::Stable);
-
- let rc = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_RC_BASE,
- };
- assert_eq!(rc.stability_enum(), Stability::RC);
-
- let beta = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_BETA_BASE,
- };
- assert_eq!(beta.stability_enum(), Stability::Beta);
-
- let alpha = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_ALPHA_BASE,
- };
- assert_eq!(alpha.stability_enum(), Stability::Alpha);
-
- let dev = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_DEV,
- };
- assert_eq!(dev.stability_enum(), Stability::Dev);
+ fn test_version_stability_fn() {
+ assert_eq!(version_stability(&v(1, 0, 0, 0)), Stability::Stable);
+ assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "RC1")), Stability::RC);
+ assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "beta1")), Stability::Beta);
+ assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "alpha1")), Stability::Alpha);
+ assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "dev")), Stability::Dev);
+ // patch is treated as stable
+ assert_eq!(version_stability(&v_pre(1, 0, 0, 0, "patch1")), Stability::Stable);
}
// ──────────── Constraint conversion ────────────
- fn cv(major: u16, minor: u16, patch: u16, build: u16) -> ComposerVersion {
- ComposerVersion::stable(major, minor, patch, build)
- }
-
- fn cv_dev(major: u16, minor: u16, patch: u16, build: u16) -> ComposerVersion {
- ComposerVersion {
- major,
- minor,
- patch,
- build,
- stability: STABILITY_DEV,
- }
- }
-
#[test]
fn test_constraint_any() {
let range = constraint_to_ranges("*").unwrap();
- assert!(range.contains(&cv(1, 2, 3, 0)));
- assert!(range.contains(&cv(0, 0, 0, 0)));
+ assert!(range.contains(&v(1, 2, 3, 0)));
+ assert!(range.contains(&v(0, 0, 0, 0)));
}
#[test]
fn test_constraint_exact() {
let range = constraint_to_ranges("1.2.3").unwrap();
- // Exact "1.2.3" is parsed as Version { 1, 2, 3, 0, pre_release: None } → stable
- assert!(range.contains(&cv(1, 2, 3, 0)));
- assert!(!range.contains(&cv(1, 2, 4, 0)));
- assert!(!range.contains(&cv(1, 2, 2, 0)));
+ assert!(range.contains(&v(1, 2, 3, 0)));
+ assert!(!range.contains(&v(1, 2, 4, 0)));
+ assert!(!range.contains(&v(1, 2, 2, 0)));
}
#[test]
fn test_constraint_gte() {
let range = constraint_to_ranges(">=1.0").unwrap();
- // >=1.0 parses "1.0" as a stable version (no dev_boundary), so >= 1.0.0.0 (stable)
- assert!(range.contains(&cv(1, 0, 0, 0)));
- assert!(range.contains(&cv(2, 0, 0, 0)));
- // 0.9.0.0 should not be in range
- assert!(!range.contains(&cv(0, 9, 0, 0)));
- // 1.0.0.0-dev (stability=0) is LESS than 1.0.0.0 (stability=4000), so NOT in >=1.0
- assert!(!range.contains(&cv_dev(1, 0, 0, 0)));
+ assert!(range.contains(&v(1, 0, 0, 0)));
+ assert!(range.contains(&v(2, 0, 0, 0)));
+ assert!(!range.contains(&v(0, 9, 0, 0)));
+ // 1.0.0.0-dev is LESS than 1.0.0.0 (stable), so NOT in >=1.0
+ assert!(!range.contains(&v_dev(1, 0, 0, 0)));
}
#[test]
fn test_constraint_lt() {
let range = constraint_to_ranges("<2.0").unwrap();
- // <2.0 parses "2.0" as a stable version, so strictly < 2.0.0.0 (stable)
- // 2.0.0.0-dev (stability=0) is LESS than 2.0.0.0 (stability=4000), so IS in <2.0
- assert!(range.contains(&cv(1, 9, 9, 0)));
- assert!(range.contains(&cv_dev(2, 0, 0, 0))); // 2.0.0.0-dev < 2.0.0.0 (stable)
- // 2.0.0.0 (stable) and higher should not be in range
- assert!(!range.contains(&cv(2, 0, 0, 0)));
+ assert!(range.contains(&v(1, 9, 9, 0)));
+ assert!(range.contains(&v_dev(2, 0, 0, 0))); // 2.0.0.0-dev < 2.0.0.0 (stable)
+ assert!(!range.contains(&v(2, 0, 0, 0)));
}
#[test]
fn test_constraint_caret() {
// ^1.2 → >=1.2.0.0-dev <2.0.0.0-dev
let range = constraint_to_ranges("^1.2").unwrap();
- assert!(range.contains(&cv_dev(1, 2, 0, 0)));
- assert!(range.contains(&cv(1, 2, 0, 0)));
- assert!(range.contains(&cv(1, 9, 9, 0)));
- assert!(!range.contains(&cv_dev(2, 0, 0, 0)));
- assert!(!range.contains(&cv(2, 0, 0, 0)));
- // Below 1.2.0.0-dev should not match
- assert!(!range.contains(&cv(1, 1, 9, 0)));
+ assert!(range.contains(&v_dev(1, 2, 0, 0)));
+ assert!(range.contains(&v(1, 2, 0, 0)));
+ assert!(range.contains(&v(1, 9, 9, 0)));
+ assert!(!range.contains(&v_dev(2, 0, 0, 0)));
+ assert!(!range.contains(&v(2, 0, 0, 0)));
+ assert!(!range.contains(&v(1, 1, 9, 0)));
}
#[test]
fn test_constraint_caret_zero() {
// ^0.2.3 → >=0.2.3.0-dev <0.3.0.0-dev
let range = constraint_to_ranges("^0.2.3").unwrap();
- assert!(range.contains(&cv(0, 2, 3, 0)));
- assert!(range.contains(&cv(0, 2, 9, 0)));
- assert!(!range.contains(&cv_dev(0, 3, 0, 0)));
- assert!(!range.contains(&cv(1, 0, 0, 0)));
+ assert!(range.contains(&v(0, 2, 3, 0)));
+ assert!(range.contains(&v(0, 2, 9, 0)));
+ assert!(!range.contains(&v_dev(0, 3, 0, 0)));
+ assert!(!range.contains(&v(1, 0, 0, 0)));
}
#[test]
fn test_constraint_tilde() {
// ~1.2.3 → >=1.2.3.0-dev <1.3.0.0-dev
let range = constraint_to_ranges("~1.2.3").unwrap();
- assert!(range.contains(&cv(1, 2, 3, 0)));
- assert!(range.contains(&cv(1, 2, 9, 0)));
- assert!(!range.contains(&cv_dev(1, 3, 0, 0)));
+ assert!(range.contains(&v(1, 2, 3, 0)));
+ assert!(range.contains(&v(1, 2, 9, 0)));
+ assert!(!range.contains(&v_dev(1, 3, 0, 0)));
}
#[test]
fn test_constraint_wildcard() {
// 1.2.* → >=1.2.0.0-dev <1.3.0.0-dev
let range = constraint_to_ranges("1.2.*").unwrap();
- assert!(range.contains(&cv(1, 2, 0, 0)));
- assert!(range.contains(&cv(1, 2, 9, 0)));
- assert!(!range.contains(&cv_dev(1, 3, 0, 0)));
- assert!(!range.contains(&cv(1, 3, 0, 0)));
+ assert!(range.contains(&v(1, 2, 0, 0)));
+ assert!(range.contains(&v(1, 2, 9, 0)));
+ assert!(!range.contains(&v_dev(1, 3, 0, 0)));
+ assert!(!range.contains(&v(1, 3, 0, 0)));
}
#[test]
fn test_constraint_or() {
- // ^1.0 || ^2.0
let range = constraint_to_ranges("^1.0 || ^2.0").unwrap();
- assert!(range.contains(&cv(1, 5, 0, 0)));
- assert!(range.contains(&cv(2, 3, 0, 0)));
- assert!(!range.contains(&cv(3, 0, 0, 0)));
+ assert!(range.contains(&v(1, 5, 0, 0)));
+ assert!(range.contains(&v(2, 3, 0, 0)));
+ assert!(!range.contains(&v(3, 0, 0, 0)));
}
#[test]
fn test_constraint_and() {
- // >=1.0 <2.0: >=1.0 means >= 1.0.0.0 (stable); <2.0 means < 2.0.0.0 (stable)
let range = constraint_to_ranges(">=1.0 <2.0").unwrap();
- // 1.0.0.0-dev < 1.0.0.0 (stable), so NOT in >=1.0
- assert!(!range.contains(&cv_dev(1, 0, 0, 0)));
- assert!(range.contains(&cv(1, 0, 0, 0)));
- assert!(range.contains(&cv(1, 9, 9, 0)));
- // 2.0.0.0-dev < 2.0.0.0 (stable), so IS in <2.0 but overall intersection with >=1.0 is yes
- assert!(range.contains(&cv_dev(2, 0, 0, 0)));
- assert!(!range.contains(&cv(2, 0, 0, 0)));
+ assert!(!range.contains(&v_dev(1, 0, 0, 0)));
+ assert!(range.contains(&v(1, 0, 0, 0)));
+ assert!(range.contains(&v(1, 9, 9, 0)));
+ assert!(range.contains(&v_dev(2, 0, 0, 0)));
+ assert!(!range.contains(&v(2, 0, 0, 0)));
}
#[test]
fn test_constraint_not_equal() {
let range = constraint_to_ranges("!=1.5.0").unwrap();
- assert!(range.contains(&cv(1, 4, 0, 0)));
- assert!(!range.contains(&cv(1, 5, 0, 0)));
- assert!(range.contains(&cv(1, 6, 0, 0)));
+ assert!(range.contains(&v(1, 4, 0, 0)));
+ assert!(!range.contains(&v(1, 5, 0, 0)));
+ assert!(range.contains(&v(1, 6, 0, 0)));
}
#[test]
fn test_constraint_hyphen() {
- // "1.0 - 2.0" → >=1.0.0.0 <=2.0.0.0
let range = constraint_to_ranges("1.0 - 2.0").unwrap();
- assert!(range.contains(&cv(1, 0, 0, 0)));
- assert!(range.contains(&cv(1, 5, 0, 0)));
- assert!(range.contains(&cv(2, 0, 0, 0)));
- assert!(!range.contains(&cv(2, 1, 0, 0)));
+ assert!(range.contains(&v(1, 0, 0, 0)));
+ assert!(range.contains(&v(1, 5, 0, 0)));
+ assert!(range.contains(&v(2, 0, 0, 0)));
+ assert!(!range.contains(&v(2, 1, 0, 0)));
}
// ──────────── Provider tests (offline) ────────────
@@ -1454,7 +1164,6 @@ mod tests {
fn test_platform_config_to_versions() {
let config = PlatformConfig::new();
let versions = config.to_versions();
- // If PHP is available on the system, we should have detected it
if !config.packages.is_empty() {
assert!(
versions.contains_key("php"),
@@ -1465,11 +1174,7 @@ mod tests {
// ──────────── Integration tests (offline, using OfflineDependencyProvider) ────────────
- type TestVS = Ranges<ComposerVersion>;
-
- fn cv_stable(major: u16, minor: u16, patch: u16) -> ComposerVersion {
- ComposerVersion::stable(major, minor, patch, 0)
- }
+ type TestVS = Ranges<Version>;
/// Test simple resolution: root → foo ^1.0, foo 1.0 → bar ^2.0, bar 2.0 → (nothing)
#[test]
@@ -1477,23 +1182,20 @@ mod tests {
let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
let root = PackageName::root();
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = v(0, 0, 0, 0);
let foo = PackageName("foo/foo".to_string());
let bar = PackageName("bar/bar".to_string());
- let foo_1_0 = cv_stable(1, 0, 0);
- let bar_2_0 = cv_stable(2, 0, 0);
+ let foo_1_0 = v(1, 0, 0, 0);
+ let bar_2_0 = v(2, 0, 0, 0);
- // root depends on foo ^1.0
let foo_range = constraint_to_ranges("^1.0").unwrap();
- provider.add_dependencies(root.clone(), root_v, [(foo.clone(), foo_range)]);
+ provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
- // foo 1.0 depends on bar ^2.0
let bar_range = constraint_to_ranges("^2.0").unwrap();
- provider.add_dependencies(foo.clone(), foo_1_0, [(bar.clone(), bar_range)]);
+ provider.add_dependencies(foo.clone(), foo_1_0.clone(), [(bar.clone(), bar_range)]);
- // bar 2.0 has no dependencies
- provider.add_dependencies(bar.clone(), bar_2_0, []);
+ provider.add_dependencies(bar.clone(), bar_2_0.clone(), []);
let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
@@ -1507,34 +1209,30 @@ mod tests {
let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
let root = PackageName::root();
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = v(0, 0, 0, 0);
let foo = PackageName("foo/foo".to_string());
let bar = PackageName("bar/bar".to_string());
let dep = PackageName("dep/dep".to_string());
- let foo_1_0 = cv_stable(1, 0, 0);
- let bar_1_0 = cv_stable(1, 0, 0);
- let dep_1_0 = cv_stable(1, 0, 0);
- let dep_2_0 = cv_stable(2, 0, 0);
+ let foo_1_0 = v(1, 0, 0, 0);
+ let bar_1_0 = v(1, 0, 0, 0);
+ let dep_1_0 = v(1, 0, 0, 0);
+ let dep_2_0 = v(2, 0, 0, 0);
- // root depends on foo and bar
- let foo_range = Ranges::singleton(foo_1_0);
- let bar_range = Ranges::singleton(bar_1_0);
+ let foo_range = Ranges::singleton(foo_1_0.clone());
+ let bar_range = Ranges::singleton(bar_1_0.clone());
provider.add_dependencies(
root.clone(),
- root_v,
+ root_v.clone(),
[(foo.clone(), foo_range), (bar.clone(), bar_range)],
);
- // foo 1.0 requires dep ^1.0 (excludes 2.x)
let dep_range_1 = constraint_to_ranges("^1.0").unwrap();
provider.add_dependencies(foo.clone(), foo_1_0, [(dep.clone(), dep_range_1)]);
- // bar 1.0 requires dep ^2.0 (excludes 1.x)
let dep_range_2 = constraint_to_ranges("^2.0").unwrap();
provider.add_dependencies(bar.clone(), bar_1_0, [(dep.clone(), dep_range_2)]);
- // dep has versions 1.0 and 2.0
provider.add_dependencies(dep.clone(), dep_1_0, []);
provider.add_dependencies(dep.clone(), dep_2_0, []);
@@ -1542,27 +1240,14 @@ mod tests {
assert!(result.is_err(), "Expected no solution for conflicting deps");
}
- /// Test prefer-stable ordering: with prefer-stable, should pick stable over beta.
+ /// Test prefer-stable: stable version should have Stability::Stable.
#[test]
fn test_prefer_stable() {
- let stable = ComposerVersion::stable(1, 0, 0, 0);
- let beta = ComposerVersion {
- major: 1,
- minor: 1,
- patch: 0,
- build: 0,
- stability: STABILITY_BETA_BASE + 1,
- };
+ let stable = v(1, 0, 0, 0);
+ let beta = v_pre(1, 1, 0, 0, "beta1");
- // stable should have higher stability numeric value than beta
- assert!(
- stable.stability > beta.stability,
- "stable should be > beta numerically"
- );
- // But stable is 1.0.0.0 and beta is 1.1.0.0-beta1; when prefer-stable is on,
- // we first look for stable version and pick the highest stable
- assert!(stable.stability >= STABILITY_STABLE);
- assert!(beta.stability < STABILITY_STABLE);
+ assert_eq!(version_stability(&stable), Stability::Stable);
+ assert_eq!(version_stability(&beta), Stability::Beta);
}
/// Test stability filter: alpha versions should be excluded when minimum_stability = stable.
@@ -1583,35 +1268,11 @@ mod tests {
repo_cache: None,
};
- let stable_v = ComposerVersion::stable(1, 0, 0, 0);
- let alpha_v = ComposerVersion {
- major: 1,
- minor: 1,
- patch: 0,
- build: 0,
- stability: STABILITY_ALPHA_BASE,
- };
- let beta_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_BETA_BASE,
- };
- let rc_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_RC_BASE,
- };
- let dev_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_DEV,
- };
+ let stable_v = v(1, 0, 0, 0);
+ let alpha_v = v_pre(1, 1, 0, 0, "alpha1");
+ let beta_v = v_pre(1, 0, 0, 0, "beta1");
+ let rc_v = v_pre(1, 0, 0, 0, "RC1");
+ let dev_v = v_pre(1, 0, 0, 0, "dev");
assert!(provider.passes_stability_filter("foo/foo", &stable_v));
assert!(!provider.passes_stability_filter("foo/foo", &alpha_v));
@@ -1637,28 +1298,10 @@ mod tests {
repo_cache: None,
};
- let stable_v = ComposerVersion::stable(1, 0, 0, 0);
- let beta_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_BETA_BASE,
- };
- let alpha_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_ALPHA_BASE,
- };
- let dev_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_DEV,
- };
+ let stable_v = v(1, 0, 0, 0);
+ let beta_v = v_pre(1, 0, 0, 0, "beta1");
+ let alpha_v = v_pre(1, 0, 0, 0, "alpha1");
+ let dev_v = v_pre(1, 0, 0, 0, "dev");
assert!(provider.passes_stability_filter("foo/foo", &stable_v));
assert!(provider.passes_stability_filter("foo/foo", &beta_v));
@@ -1683,13 +1326,7 @@ mod tests {
repo_cache: None,
};
- let dev_v = ComposerVersion {
- major: 1,
- minor: 0,
- patch: 0,
- build: 0,
- stability: STABILITY_DEV,
- };
+ let dev_v = v_pre(1, 0, 0, 0, "dev");
assert!(provider.passes_stability_filter("foo/foo", &dev_v));
}
@@ -1756,7 +1393,7 @@ mod tests {
};
let root = PackageName::root();
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = v(0, 0, 0, 0);
let full_range: ComposerVS = Ranges::full();
let result = provider.choose_version(&root, &full_range).unwrap();
assert_eq!(result, Some(root_v));
@@ -1765,8 +1402,8 @@ mod tests {
#[test]
fn test_platform_choose_version() {
let mut platform = HashMap::new();
- let php_v = ComposerVersion::from_normalized("8.1.0.0").unwrap();
- platform.insert("php".to_string(), php_v);
+ let php_v = Version::parse("8.1.0").unwrap();
+ platform.insert("php".to_string(), php_v.clone());
let provider = MozartProvider {
handle: test_handle(),
@@ -1788,44 +1425,39 @@ mod tests {
let result = provider.choose_version(&php, &range).unwrap();
assert_eq!(result, Some(php_v));
- // Range that excludes 8.1
let too_new_range = constraint_to_ranges(">=9.0").unwrap();
let result2 = provider.choose_version(&php, &too_new_range).unwrap();
assert_eq!(result2, None);
}
- /// Test constraint_to_ranges produces correct range with version containment checks.
#[test]
fn test_constraint_contains_version() {
- // ^3.0 should contain 3.5.1.0 but not 4.0.0.0
let range = constraint_to_ranges("^3.0").unwrap();
- assert!(range.contains(&cv_stable(3, 5, 1)));
- assert!(!range.contains(&cv_stable(4, 0, 0)));
- assert!(!range.contains(&cv_stable(2, 9, 9)));
+ assert!(range.contains(&v(3, 5, 1, 0)));
+ assert!(!range.contains(&v(4, 0, 0, 0)));
+ assert!(!range.contains(&v(2, 9, 9, 0)));
}
// ──────────── Integration test with MozartProvider (no network) ────────────
- /// Test resolve() with root dependencies using offline provider
#[test]
fn test_resolve_with_offline_provider_simple() {
let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
let root = PackageName::root();
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = v(0, 0, 0, 0);
let foo = PackageName("foo/foo".to_string());
- let foo_1_0 = cv_stable(1, 0, 0);
- let foo_1_1 = cv_stable(1, 1, 0);
+ let foo_1_0 = v(1, 0, 0, 0);
+ let foo_1_1 = v(1, 1, 0, 0);
let foo_range = constraint_to_ranges("^1.0").unwrap();
- provider.add_dependencies(root.clone(), root_v, [(foo.clone(), foo_range)]);
+ provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
provider.add_dependencies(foo.clone(), foo_1_0, []);
- provider.add_dependencies(foo.clone(), foo_1_1, []);
+ provider.add_dependencies(foo.clone(), foo_1_1.clone(), []);
let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
- // Should pick highest version: 1.1.0
assert_eq!(*solution.get(&foo).unwrap(), foo_1_1);
}
@@ -1834,25 +1466,22 @@ mod tests {
let mut provider = OfflineDependencyProvider::<PackageName, TestVS>::new();
let root = PackageName::root();
- let root_v = ComposerVersion::stable(0, 0, 0, 0);
+ let root_v = v(0, 0, 0, 0);
let foo = PackageName("foo/foo".to_string());
- // foo has versions 1.5.0 and 2.3.0
- let foo_1_5 = cv_stable(1, 5, 0);
- let foo_2_3 = cv_stable(2, 3, 0);
+ let foo_1_5 = v(1, 5, 0, 0);
+ let foo_2_3 = v(2, 3, 0, 0);
- // root requires "^1.0 || ^2.0"
let foo_range = constraint_to_ranges("^1.0 || ^2.0").unwrap();
- provider.add_dependencies(root.clone(), root_v, [(foo.clone(), foo_range)]);
- provider.add_dependencies(foo.clone(), foo_1_5, []);
- provider.add_dependencies(foo.clone(), foo_2_3, []);
+ provider.add_dependencies(root.clone(), root_v.clone(), [(foo.clone(), foo_range)]);
+ provider.add_dependencies(foo.clone(), foo_1_5.clone(), []);
+ provider.add_dependencies(foo.clone(), foo_2_3.clone(), []);
let solution = pubgrub::resolve(&provider, root.clone(), root_v).unwrap();
- // Should pick the highest matching version: 2.3.0
- let picked = *solution.get(&foo).unwrap();
+ let picked = solution.get(&foo).unwrap();
assert!(
- picked == foo_1_5 || picked == foo_2_3,
+ *picked == foo_1_5 || *picked == foo_2_3,
"picked version should be one of the available versions"
);
}
@@ -1860,68 +1489,54 @@ mod tests {
// ──────────── Branch alias tests ────────────
#[test]
- fn test_from_branch_alias_target_x_dev() {
- let cv = ComposerVersion::from_branch_alias_target("2.x-dev").unwrap();
- assert_eq!(cv.major, 2);
- assert_eq!(cv.minor, 0);
- assert_eq!(cv.patch, 0);
- assert_eq!(cv.build, 0);
- assert_eq!(cv.stability, STABILITY_DEV);
+ fn test_parse_branch_alias_target_x_dev() {
+ let ver = parse_branch_alias_target("2.x-dev").unwrap();
+ assert_eq!((ver.major, ver.minor, ver.patch, ver.build), (2, 0, 0, 0));
+ assert_eq!(ver.pre_release, Some("dev".to_string()));
}
#[test]
- fn test_from_branch_alias_target_minor_x_dev() {
- let cv = ComposerVersion::from_branch_alias_target("1.5.x-dev").unwrap();
- assert_eq!(cv.major, 1);
- assert_eq!(cv.minor, 5);
- assert_eq!(cv.patch, 0);
- assert_eq!(cv.stability, STABILITY_DEV);
+ fn test_parse_branch_alias_target_minor_x_dev() {
+ let ver = parse_branch_alias_target("1.5.x-dev").unwrap();
+ assert_eq!((ver.major, ver.minor, ver.patch), (1, 5, 0));
+ assert_eq!(ver.pre_release, Some("dev".to_string()));
}
#[test]
- fn test_from_branch_alias_target_patch_x_dev() {
- let cv = ComposerVersion::from_branch_alias_target("1.0.2.x-dev").unwrap();
- assert_eq!(cv.major, 1);
- assert_eq!(cv.minor, 0);
- assert_eq!(cv.patch, 2);
- assert_eq!(cv.stability, STABILITY_DEV);
+ fn test_parse_branch_alias_target_patch_x_dev() {
+ let ver = parse_branch_alias_target("1.0.2.x-dev").unwrap();
+ assert_eq!((ver.major, ver.minor, ver.patch), (1, 0, 2));
+ assert_eq!(ver.pre_release, Some("dev".to_string()));
}
#[test]
- fn test_from_branch_alias_target_invalid() {
- // Must end with -dev
- assert!(ComposerVersion::from_branch_alias_target("dev-master").is_none());
- assert!(ComposerVersion::from_branch_alias_target("2.0.0").is_none());
- assert!(ComposerVersion::from_branch_alias_target("").is_none());
+ fn test_parse_branch_alias_target_invalid() {
+ assert!(parse_branch_alias_target("dev-master").is_none());
+ assert!(parse_branch_alias_target("2.0.0").is_none());
+ assert!(parse_branch_alias_target("").is_none());
}
- /// Test that a branch alias entry created from "dev-master" aliased to "2.x-dev"
- /// is contained in the ^2.0 constraint range.
#[test]
fn test_branch_alias_in_range() {
- // "2.x-dev" alias target → ComposerVersion { major: 2, stability: STABILITY_DEV }
- let aliased_cv = ComposerVersion::from_branch_alias_target("2.x-dev").unwrap();
- // ^2.0 → >=2.0.0.0-dev <3.0.0.0-dev
+ let aliased_v = parse_branch_alias_target("2.x-dev").unwrap();
let range = constraint_to_ranges("^2.0").unwrap();
assert!(
- range.contains(&aliased_cv),
+ range.contains(&aliased_v),
"dev-master aliased to 2.x-dev should satisfy ^2.0"
);
}
- /// Test that a branch alias entry for "1.0.x-dev" satisfies a ^1.0 constraint.
#[test]
fn test_branch_alias_1_x_in_range() {
- let aliased_cv = ComposerVersion::from_branch_alias_target("1.0.x-dev").unwrap();
+ let aliased_v = parse_branch_alias_target("1.0.x-dev").unwrap();
let range = constraint_to_ranges("^1.0").unwrap();
assert!(
- range.contains(&aliased_cv),
+ range.contains(&aliased_v),
"dev branch aliased to 1.0.x-dev should satisfy ^1.0"
);
- // But should NOT satisfy ^2.0
let range2 = constraint_to_ranges("^2.0").unwrap();
assert!(
- !range2.contains(&aliased_cv),
+ !range2.contains(&aliased_v),
"1.0.x-dev alias should not satisfy ^2.0"
);
}
diff --git a/crates/mozart-semver/src/lib.rs b/crates/mozart-semver/src/lib.rs
index 7c417ec..474b6ce 100644
--- a/crates/mozart-semver/src/lib.rs
+++ b/crates/mozart-semver/src/lib.rs
@@ -1,4 +1,5 @@
use std::cmp::Ordering;
+use std::fmt;
/// A parsed Composer version (always 4 numeric segments + optional stability suffix).
/// Composer normalizes all versions to `major.minor.patch.build[-stability[N]]`.
@@ -70,11 +71,26 @@ impl Ord for Version {
return num_cmp;
}
- // Compare pre-release: None (stable) > any pre-release
+ // Compare pre-release: None (stable) > most pre-releases,
+ // but patch/pl/p pre-releases (stability_rank 5) rank ABOVE stable.
match (&self.pre_release, &other.pre_release) {
(None, None) => Ordering::Equal,
- (None, Some(_)) => Ordering::Greater,
- (Some(_), None) => Ordering::Less,
+ (None, Some(b)) => {
+ if stability_rank(b) == 5 {
+ // patch pre-release ranks above stable
+ Ordering::Less
+ } else {
+ Ordering::Greater
+ }
+ }
+ (Some(a), None) => {
+ if stability_rank(a) == 5 {
+ // patch pre-release ranks above stable
+ Ordering::Greater
+ } else {
+ Ordering::Less
+ }
+ }
(Some(a), Some(b)) => {
let rank_a = stability_rank(a);
let rank_b = stability_rank(b);
@@ -92,6 +108,23 @@ impl Ord for Version {
}
}
+impl fmt::Display for Version {
+ fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
+ if self.is_dev_branch {
+ if let Some(ref name) = self.dev_branch_name {
+ return write!(f, "dev-{}", name);
+ }
+ // Numeric dev branch (e.g. "2.x-dev")
+ return write!(f, "{}.{}.{}.{}-dev", self.major, self.minor, self.patch, self.build);
+ }
+ write!(f, "{}.{}.{}.{}", self.major, self.minor, self.patch, self.build)?;
+ if let Some(ref pre) = self.pre_release {
+ write!(f, "-{}", pre)?;
+ }
+ Ok(())
+ }
+}
+
impl Version {
/// Parse a version string into a `Version` struct using Composer normalization rules.
///
@@ -1224,14 +1257,12 @@ mod tests {
}
#[test]
- fn test_ordering_stable_lt_patch() {
- // The Ord impl: (None, Some(_)) => Greater — stable (pre_release=None) beats any
- // pre_release including "patch1". Even though stability_rank("patch")=5 which is
- // higher than stable's implicit 0, that path is only reached when both sides are
- // Some(_). Since stable has pre_release=None, stable > patch version.
+ fn test_ordering_patch_gt_stable() {
+ // In Composer, patch/pl/p pre-releases rank ABOVE stable.
+ // e.g. 1.0.0-patch1 > 1.0.0
let stable = Version::parse("1.0.0").unwrap();
let patch = Version::parse("1.0.0-patch1").unwrap();
- assert!(stable > patch);
+ assert!(patch > stable);
}
#[test]
@@ -1757,15 +1788,13 @@ mod tests {
#[test]
fn test_stability_rank_patch_via_ordering() {
- // The Ord impl: (None, Some(_)) => Greater.
- // stable has pre_release=None; patch version has pre_release=Some("patch1").
- // The None arm wins unconditionally: stable is always Greater than any pre_release.
- // This means "patch" releases (post-release fixes) sort BELOW stable in this impl.
+ // In Composer, patch/pl/p pre-releases rank ABOVE stable.
+ // patch1 > stable for the same numeric version.
let patch_ver = Version::parse("1.0.0-patch1").unwrap();
let stable = Version::parse("1.0.0").unwrap();
assert!(
- stable > patch_ver,
- "stable (None pre_release) beats patch pre-release"
+ patch_ver > stable,
+ "patch pre-release ranks above stable"
);
}