use anyhow::Context;
use semver;
use serde::Serialize;
use std::path::PathBuf;
#[derive(Debug, Clone, PartialEq)]
pub enum VersionBump {
Major(i32),
Minor(i32),
Patch(i32),
}
#[derive(Debug, Clone, PartialEq)]
pub enum VersionField {
Absent,
Concrete(String),
Inherited,
}
#[derive(Debug, Clone, PartialEq)]
pub enum PackageSelection {
Specific(Vec<String>),
Workspace,
Default,
}
#[derive(Debug, Clone, Serialize)]
pub struct VersionChange {
pub package: String,
pub old_version: String,
pub new_version: String,
pub path: PathBuf,
}
#[derive(Debug, Serialize)]
pub struct OperationResult {
pub changes: Vec<VersionChange>,
pub operation: String,
}
impl OperationResult {
pub fn new(operation: String) -> Self {
Self {
changes: Vec::new(),
operation,
}
}
pub fn add_change(&mut self, change: VersionChange) {
self.changes.push(change);
}
pub fn has_changes(&self) -> bool {
!self.changes.is_empty()
}
}
#[derive(Debug, Clone)]
pub enum WorkspaceMember {
Cargo {
name: String,
path: PathBuf,
version: VersionField,
},
Node {
name: String,
path: PathBuf,
version: VersionField,
},
}
impl WorkspaceMember {
pub fn name(&self) -> &str {
match self {
WorkspaceMember::Cargo { name, .. } => name,
WorkspaceMember::Node { name, .. } => name,
}
}
pub fn path(&self) -> &PathBuf {
match self {
WorkspaceMember::Cargo { path, .. } => path,
WorkspaceMember::Node { path, .. } => path,
}
}
pub fn version(&self) -> &VersionField {
match self {
WorkspaceMember::Cargo { version, .. } => version,
WorkspaceMember::Node { version, .. } => version,
}
}
pub fn set_version(&mut self, new_version: VersionField) {
match self {
WorkspaceMember::Cargo { version, .. } => *version = new_version,
WorkspaceMember::Node { version, .. } => *version = new_version,
}
}
}
#[derive(Debug, Clone)]
pub struct Workspace {
pub members: Vec<WorkspaceMember>,
}
impl Workspace {
pub fn roll_version(
&mut self,
bump: VersionBump,
selection: &PackageSelection,
) -> anyhow::Result<OperationResult> {
let mut result = OperationResult::new(format!(
"roll {}",
match bump {
VersionBump::Major(amount) => format!("major {}", amount),
VersionBump::Minor(amount) => format!("minor {}", amount),
VersionBump::Patch(amount) => format!("patch {}", amount),
}
));
let indices = self.select_member_indices(selection)?;
for &index in &indices {
let member = &mut self.members[index];
let old_version = match member.version() {
VersionField::Concrete(version) => version.clone(),
_ => continue,
};
let new_version = bump.apply_to_version(&old_version)?;
if old_version != new_version {
result.add_change(VersionChange {
package: member.name().to_string(),
old_version: old_version.clone(),
new_version: new_version.clone(),
path: member.path().clone(),
});
member.set_version(VersionField::Concrete(new_version));
}
}
Ok(result)
}
pub fn set_version(
&mut self,
version: &str,
selection: &PackageSelection,
) -> anyhow::Result<OperationResult> {
let mut result = OperationResult::new(format!("set {}", version));
let indices = self.select_member_indices(selection)?;
for &index in &indices {
let member = &mut self.members[index];
let old_version = match member.version() {
VersionField::Concrete(version) => version.clone(),
_ => continue,
};
if old_version != version {
result.add_change(VersionChange {
package: member.name().to_string(),
old_version: old_version.clone(),
new_version: version.to_string(),
path: member.path().clone(),
});
member.set_version(VersionField::Concrete(version.to_string()));
}
}
Ok(result)
}
pub fn sync_version(&mut self, version: &str) -> anyhow::Result<OperationResult> {
let mut result = OperationResult::new(format!("sync {}", version));
for member in &mut self.members {
let old_version = match member.version() {
VersionField::Concrete(version) => version.clone(),
_ => continue,
};
if old_version != version {
result.add_change(VersionChange {
package: member.name().to_string(),
old_version: old_version.clone(),
new_version: version.to_string(),
path: member.path().clone(),
});
member.set_version(VersionField::Concrete(version.to_string()));
}
}
Ok(result)
}
pub fn show(&self, selection: &PackageSelection) -> anyhow::Result<String> {
let indices = self.select_member_indices(selection)?;
let mut members: Vec<&WorkspaceMember> =
indices.iter().map(|&i| &self.members[i]).collect();
members.sort_by(|a, b| a.name().cmp(b.name()));
let mut output = String::new();
for member in members {
let version = match member.version() {
VersionField::Concrete(version) => version.clone(),
_ => continue,
};
output.push_str(&format!("{}: {}\n", member.name(), version));
}
Ok(output)
}
pub fn lint(&self, selection: &PackageSelection) -> anyhow::Result<Vec<LintError>> {
let indices = self.select_member_indices(selection)?;
let mut members: Vec<&WorkspaceMember> =
indices.iter().map(|&i| &self.members[i]).collect();
members.sort_by(|a, b| a.name().cmp(b.name()));
let mut errors = Vec::new();
for member in members {
let version = match member.version() {
VersionField::Concrete(version) => version.clone(),
_ => continue,
};
if let Err(e) = semver::Version::parse(&version) {
errors.push(LintError {
member: member.name().to_string(),
message: format!("Invalid version '{}': {}", version, e),
});
}
}
Ok(errors)
}
#[cfg(test)]
pub fn selected_members(&self, selection: &PackageSelection) -> Vec<&WorkspaceMember> {
match self.select_member_indices(selection) {
Ok(indices) => {
let mut members: Vec<&WorkspaceMember> =
indices.iter().map(|&i| &self.members[i]).collect();
members.sort_by(|a, b| a.name().cmp(b.name()));
members
}
Err(_) => vec![], }
}
fn select_member_indices(&self, selection: &PackageSelection) -> anyhow::Result<Vec<usize>> {
match selection {
PackageSelection::Specific(packages) => {
let mut indices = Vec::new();
for package_name in packages {
match self.members.iter().position(|m| m.name() == *package_name) {
Some(index) => indices.push(index),
None => anyhow::bail!("Package '{}' not found in workspace", package_name),
}
}
Ok(indices)
}
PackageSelection::Workspace => Ok((0..self.members.len()).collect()),
PackageSelection::Default => {
if self.members.is_empty() {
anyhow::bail!("No packages found in workspace")
} else {
Ok(vec![0])
}
}
}
}
}
#[derive(Debug)]
pub struct LintError {
pub member: String,
pub message: String,
}
impl VersionBump {
pub fn apply_to_version(&self, current: &str) -> anyhow::Result<String> {
let mut version = semver::Version::parse(current)
.with_context(|| format!("Invalid semver version: '{}'", current))?;
match self {
VersionBump::Major(amount) => {
if *amount < 0 {
let abs_amount = amount.unsigned_abs() as u64;
if version.major < abs_amount {
anyhow::bail!(
"Cannot decrement major version by {} from {}: would result in negative version",
abs_amount, current
);
}
version.major -= abs_amount;
} else {
version.major += *amount as u64;
}
version.minor = 0;
version.patch = 0;
}
VersionBump::Minor(amount) => {
if *amount < 0 {
let abs_amount = amount.unsigned_abs() as u64;
if version.minor < abs_amount {
anyhow::bail!(
"Cannot decrement minor version by {} from {}: would result in negative version",
abs_amount, current
);
}
version.minor -= abs_amount;
} else {
version.minor += *amount as u64;
}
version.patch = 0;
}
VersionBump::Patch(amount) => {
if *amount < 0 {
let abs_amount = amount.unsigned_abs() as u64;
if version.patch < abs_amount {
anyhow::bail!(
"Cannot decrement patch version by {} from {}: would result in negative version",
abs_amount, current
);
}
version.patch -= abs_amount;
} else {
version.patch += *amount as u64;
}
}
}
Ok(version.to_string())
}
}
impl PackageSelection {
#[cfg(test)]
pub fn workspace() -> Self {
Self::Workspace
}
#[cfg(test)]
pub fn root_only() -> Self {
Self::Default
}
#[cfg(test)]
pub fn packages(packages: Vec<String>) -> Self {
Self::Specific(packages)
}
}
#[cfg(test)]
mod tests {
use super::*;
use std::path::PathBuf;
fn create_test_member(name: &str, version: VersionField) -> WorkspaceMember {
WorkspaceMember::Cargo {
name: name.to_string(),
path: PathBuf::from(format!("{}/Cargo.toml", name)),
version,
}
}
fn create_test_workspace(members: Vec<(&str, VersionField)>) -> Workspace {
Workspace {
members: members
.into_iter()
.map(|(name, version)| create_test_member(name, version))
.collect(),
}
}
#[test]
fn test_version_bump_patch() {
let bump = VersionBump::Patch(1);
assert_eq!(bump.apply_to_version("1.0.0").unwrap(), "1.0.1");
assert_eq!(bump.apply_to_version("0.5.9").unwrap(), "0.5.10");
}
#[test]
fn test_version_bump_patch_custom_amount() {
let bump = VersionBump::Patch(5);
assert_eq!(bump.apply_to_version("1.0.0").unwrap(), "1.0.5");
let bump = VersionBump::Patch(-2);
assert_eq!(bump.apply_to_version("1.0.5").unwrap(), "1.0.3");
}
#[test]
fn test_version_bump_minor() {
let bump = VersionBump::Minor(1);
assert_eq!(bump.apply_to_version("1.5.3").unwrap(), "1.6.0");
let bump = VersionBump::Minor(3);
assert_eq!(bump.apply_to_version("0.1.0").unwrap(), "0.4.0");
}
#[test]
fn test_version_bump_major() {
let bump = VersionBump::Major(1);
assert_eq!(bump.apply_to_version("1.5.3").unwrap(), "2.0.0");
let bump = VersionBump::Major(2);
assert_eq!(bump.apply_to_version("0.1.0").unwrap(), "2.0.0");
}
#[test]
fn test_version_bump_negative_errors() {
let bump = VersionBump::Patch(-10);
let result = bump.apply_to_version("1.0.3");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("would result in negative version"));
let bump = VersionBump::Minor(-5);
let result = bump.apply_to_version("1.2.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("would result in negative version"));
let bump = VersionBump::Major(-10);
let result = bump.apply_to_version("2.0.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("would result in negative version"));
}
#[test]
fn test_version_bump_patch_invalid_operations() {
let bump = VersionBump::Patch(-2);
let result = bump.apply_to_version("0.1.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 2 from 0.1.0"));
let bump = VersionBump::Patch(-1);
let result = bump.apply_to_version("0.1.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 1 from 0.1.0"));
let bump = VersionBump::Patch(2);
assert_eq!(bump.apply_to_version("0.1.0").unwrap(), "0.1.2");
}
#[test]
fn test_workspace_member_methods() {
let mut member =
create_test_member("test-crate", VersionField::Concrete("1.0.0".to_string()));
assert_eq!(member.name(), "test-crate");
assert_eq!(
member.version(),
&VersionField::Concrete("1.0.0".to_string())
);
member.set_version(VersionField::Concrete("2.0.0".to_string()));
assert_eq!(
member.version(),
&VersionField::Concrete("2.0.0".to_string())
);
}
#[test]
fn test_workspace_roll_version_default() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
]);
let selection = PackageSelection::root_only();
workspace
.roll_version(VersionBump::Patch(1), &selection)
.unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.1".to_string())
); assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.5.0".to_string())
); }
#[test]
fn test_workspace_roll_version_specific_packages() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
("utils", VersionField::Concrete("0.1.0".to_string())),
]);
let selection = PackageSelection::packages(vec!["lib".to_string(), "utils".to_string()]);
workspace
.roll_version(VersionBump::Minor(1), &selection)
.unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.0".to_string())
); assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.6.0".to_string())
); assert_eq!(
workspace.members[2].version(),
&VersionField::Concrete("0.2.0".to_string())
); }
#[test]
fn test_workspace_roll_version_workspace() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
("utils", VersionField::Concrete("0.1.0".to_string())),
]);
let selection = PackageSelection::workspace();
workspace
.roll_version(VersionBump::Patch(1), &selection)
.unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.1".to_string())
);
assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.5.1".to_string())
);
assert_eq!(
workspace.members[2].version(),
&VersionField::Concrete("0.1.1".to_string())
); }
#[test]
fn test_workspace_roll_version_workspace_with_exclude() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
("utils", VersionField::Concrete("0.1.0".to_string())),
]);
let selection = PackageSelection::Workspace;
workspace
.roll_version(VersionBump::Patch(1), &selection)
.unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.1".to_string())
); assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.5.1".to_string())
);
assert_eq!(
workspace.members[2].version(),
&VersionField::Concrete("0.1.1".to_string())
); }
#[test]
fn test_workspace_set_version() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
]);
let selection = PackageSelection::packages(vec!["lib".to_string()]);
workspace.set_version("2.0.0", &selection).unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.0".to_string())
);
assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("2.0.0".to_string())
); }
#[test]
fn test_workspace_sync_version() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
("utils", VersionField::Concrete("2.1.0".to_string())),
]);
workspace.sync_version("1.5.0").unwrap();
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.5.0".to_string())
);
assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("1.5.0".to_string())
);
assert_eq!(
workspace.members[2].version(),
&VersionField::Concrete("1.5.0".to_string())
);
}
#[test]
fn test_workspace_show() {
let workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
]);
let output = workspace.show(&PackageSelection::workspace()).unwrap();
assert_eq!(output, "app: 1.0.0\nlib: 0.5.0\n");
}
#[test]
fn test_workspace_lint_valid() {
let workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("0.5.0".to_string())),
]);
let errors = workspace.lint(&PackageSelection::root_only()).unwrap();
assert!(errors.is_empty());
}
#[test]
fn test_workspace_lint_invalid() {
let workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.0".to_string())),
("lib", VersionField::Concrete("invalid-version".to_string())),
]);
let errors = workspace.lint(&PackageSelection::workspace()).unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].member, "lib");
assert!(errors[0]
.message
.contains("Invalid version 'invalid-version'"));
}
#[test]
fn test_package_selection_not_found() {
let mut workspace =
create_test_workspace(vec![("app", VersionField::Concrete("1.0.0".to_string()))]);
let selection = PackageSelection::packages(vec!["nonexistent".to_string()]);
let result = workspace.roll_version(VersionBump::Patch(1), &selection);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Package 'nonexistent' not found"));
}
#[test]
fn test_workspace_roll_version_invalid_operation() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("0.1.0".to_string())),
("lib", VersionField::Concrete("0.1.0".to_string())),
]);
let selection = PackageSelection::workspace();
let result = workspace.roll_version(VersionBump::Patch(-2), &selection);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 2 from 0.1.0"));
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("0.1.0".to_string())
);
assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.1.0".to_string())
);
}
#[test]
fn test_version_bump_cross_component_boundaries() {
let bump = VersionBump::Patch(-1);
let result = bump.apply_to_version("1.2.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 1 from 1.2.0"));
let bump = VersionBump::Minor(-1);
let result = bump.apply_to_version("1.0.5");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement minor version by 1 from 1.0.5"));
}
#[test]
fn test_version_bump_absolute_minimum() {
let bump = VersionBump::Patch(-1);
let result = bump.apply_to_version("0.0.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 1 from 0.0.0"));
let bump = VersionBump::Minor(-1);
let result = bump.apply_to_version("0.0.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement minor version by 1 from 0.0.0"));
let bump = VersionBump::Major(-1);
let result = bump.apply_to_version("0.0.0");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement major version by 1 from 0.0.0"));
}
#[test]
fn test_workspace_roll_version_mixed_validity() {
let mut workspace = create_test_workspace(vec![
("app", VersionField::Concrete("1.0.2".to_string())), ("lib", VersionField::Concrete("0.1.0".to_string())), ("utils", VersionField::Concrete("2.5.3".to_string())), ]);
let selection = PackageSelection::workspace();
let result = workspace.roll_version(VersionBump::Patch(-2), &selection);
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 2 from 0.1.0"));
assert_eq!(
workspace.members[0].version(),
&VersionField::Concrete("1.0.0".to_string()),
"app was modified in-memory"
);
assert_eq!(
workspace.members[1].version(),
&VersionField::Concrete("0.1.0".to_string()),
"lib unchanged (caused error)"
);
assert_eq!(
workspace.members[2].version(),
&VersionField::Concrete("2.5.3".to_string()),
"utils unchanged (not processed)"
);
}
#[test]
fn test_version_bump_large_negative_numbers() {
let bump = VersionBump::Patch(-999999);
let result = bump.apply_to_version("1.0.5");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 999999 from 1.0.5"));
}
#[test]
fn test_version_bump_prerelease_versions() {
let bump = VersionBump::Patch(-1);
assert_eq!(bump.apply_to_version("1.0.1-alpha").unwrap(), "1.0.0-alpha");
let result = bump.apply_to_version("1.0.0-alpha");
assert!(result.is_err());
assert!(result
.unwrap_err()
.to_string()
.contains("Cannot decrement patch version by 1 from 1.0.0-alpha"));
let bump = VersionBump::Minor(-1);
assert_eq!(bump.apply_to_version("1.1.5-beta").unwrap(), "1.0.0-beta");
let bump = VersionBump::Major(-1);
assert_eq!(bump.apply_to_version("2.3.5-rc.1").unwrap(), "1.0.0-rc.1");
}
#[test]
fn test_selected_members_sorting() {
let workspace = create_test_workspace(vec![
("zebra", VersionField::Concrete("1.0.0".to_string())),
("apple", VersionField::Concrete("2.0.0".to_string())),
("banana", VersionField::Concrete("3.0.0".to_string())),
]);
let members = workspace.selected_members(&PackageSelection::workspace());
assert_eq!(members[0].name(), "apple");
assert_eq!(members[1].name(), "banana");
assert_eq!(members[2].name(), "zebra");
}
#[test]
fn test_selected_members_sorting_with_selection() {
let workspace = create_test_workspace(vec![
("zebra", VersionField::Concrete("1.0.0".to_string())),
("apple", VersionField::Concrete("2.0.0".to_string())),
("banana", VersionField::Concrete("3.0.0".to_string())),
]);
let selection = PackageSelection::packages(vec!["zebra".to_string(), "apple".to_string()]);
let members = workspace.selected_members(&selection);
assert_eq!(members[0].name(), "apple");
assert_eq!(members[1].name(), "zebra");
}
#[test]
fn test_version_bump_with_build_metadata() {
let bump = VersionBump::Patch(1);
let result = bump.apply_to_version("1.2.3+20130313144700").unwrap();
assert_eq!(result, "1.2.4+20130313144700");
}
#[test]
fn test_version_bump_with_prerelease_and_build() {
let bump = VersionBump::Patch(1);
let result = bump
.apply_to_version("1.2.3-beta.1+20130313144700")
.unwrap();
assert_eq!(result, "1.2.4-beta.1+20130313144700");
}
#[test]
fn test_version_bump_zero_amount() {
let bump = VersionBump::Patch(0);
let result = bump.apply_to_version("1.2.3").unwrap();
assert_eq!(result, "1.2.3");
}
#[test]
fn test_workspace_roll_version_preserves_inherited() {
let mut workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Inherited),
]);
let selection = PackageSelection::workspace();
let result = workspace
.roll_version(VersionBump::Patch(1), &selection)
.unwrap();
assert_eq!(result.changes.len(), 1);
assert_eq!(result.changes[0].package, "pkg1");
assert_eq!(result.changes[0].new_version, "1.0.1");
}
#[test]
fn test_workspace_set_version_preserves_inherited() {
let mut workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Inherited),
]);
let selection = PackageSelection::workspace();
let result = workspace.set_version("2.0.0", &selection).unwrap();
assert_eq!(result.changes.len(), 1);
assert_eq!(result.changes[0].package, "pkg1");
assert_eq!(result.changes[0].new_version, "2.0.0");
}
#[test]
fn test_workspace_sync_version_preserves_inherited() {
let mut workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Inherited),
("pkg3", VersionField::Concrete("2.0.0".to_string())),
]);
let result = workspace.sync_version("3.0.0").unwrap();
assert_eq!(result.changes.len(), 2);
assert!(result
.changes
.iter()
.any(|c| c.package == "pkg1" && c.new_version == "3.0.0"));
assert!(result
.changes
.iter()
.any(|c| c.package == "pkg3" && c.new_version == "3.0.0"));
}
#[test]
fn test_workspace_show_includes_inherited() {
let workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Inherited),
]);
let selection = PackageSelection::workspace();
let output = workspace.show(&selection).unwrap();
assert!(output.contains("pkg1: 1.0.0"));
assert!(!output.contains("pkg2")); }
#[test]
fn test_workspace_lint_skips_inherited() {
let workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Inherited),
("pkg3", VersionField::Concrete("invalid".to_string())),
]);
let selection = PackageSelection::workspace();
let errors = workspace.lint(&selection).unwrap();
assert_eq!(errors.len(), 1);
assert_eq!(errors[0].member, "pkg3");
}
#[test]
fn test_package_selection_default_selects_first() {
let workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Concrete("2.0.0".to_string())),
("pkg3", VersionField::Concrete("3.0.0".to_string())),
]);
let selection = PackageSelection::root_only();
let members = workspace.selected_members(&selection);
assert_eq!(members.len(), 1);
assert_eq!(members[0].name(), "pkg1");
}
#[test]
fn test_package_selection_specific_packages() {
let workspace = create_test_workspace(vec![
("pkg1", VersionField::Concrete("1.0.0".to_string())),
("pkg2", VersionField::Concrete("2.0.0".to_string())),
("pkg3", VersionField::Concrete("3.0.0".to_string())),
]);
let selection = PackageSelection::packages(vec!["pkg1".to_string(), "pkg2".to_string()]);
let members = workspace.selected_members(&selection);
assert_eq!(members.len(), 2);
assert_eq!(members[0].name(), "pkg1");
assert_eq!(members[1].name(), "pkg2");
}
#[test]
fn test_operation_result_has_changes() {
let mut result = OperationResult::new("test".to_string());
assert!(!result.has_changes());
result.add_change(VersionChange {
package: "pkg1".to_string(),
old_version: "1.0.0".to_string(),
new_version: "2.0.0".to_string(),
path: PathBuf::from("pkg1"),
});
assert!(result.has_changes());
}
}