diff --git a/src/bin/cargo/commands/update.rs b/src/bin/cargo/commands/update.rs index 492be07c783..a0b4de87085 100644 --- a/src/bin/cargo/commands/update.rs +++ b/src/bin/cargo/commands/update.rs @@ -1,8 +1,11 @@ +use std::collections::HashMap; + use crate::command_prelude::*; use anyhow::anyhow; use cargo::ops::{self, UpdateOptions}; use cargo::util::print_available_packages; +use tracing::trace; pub fn cli() -> Command { subcommand("update") @@ -92,28 +95,39 @@ pub fn exec(gctx: &mut GlobalContext, args: &ArgMatches) -> CliResult { let update_opts = UpdateOptions { recursive: args.flag("recursive"), precise: args.get_one::("precise").map(String::as_str), + breaking: args.flag("breaking"), to_update, dry_run: args.dry_run(), workspace: args.flag("workspace"), gctx, }; - if args.flag("breaking") { - gctx.cli_unstable() - .fail_if_stable_opt("--breaking", 12425)?; - - let upgrades = ops::upgrade_manifests(&mut ws, &update_opts.to_update)?; - ops::resolve_ws(&ws, update_opts.dry_run)?; - ops::write_manifest_upgrades(&ws, &upgrades, update_opts.dry_run)?; + let breaking_update = update_opts.breaking + || (update_opts.precise.is_some() && gctx.cli_unstable().unstable_options); - if update_opts.dry_run { - update_opts - .gctx - .shell() - .warn("aborting update due to dry run")?; + // We are using the term "upgrade" here, which is the typical case, but it + // can also be a downgrade (in the case of a precise update). In general, it + // is a change to a version req matching a precise version. + let upgrades = if breaking_update { + if update_opts.breaking { + gctx.cli_unstable() + .fail_if_stable_opt("--breaking", 12425)?; } + + trace!("allowing breaking updates"); + ops::upgrade_manifests(&mut ws, &update_opts.to_update, &update_opts.precise)? } else { - ops::update_lockfile(&ws, &update_opts)?; + HashMap::new() + }; + + ops::update_lockfile(&ws, &update_opts, &upgrades)?; + ops::write_manifest_upgrades(&ws, &upgrades, update_opts.dry_run)?; + + if update_opts.dry_run { + update_opts + .gctx + .shell() + .warn("aborting update due to dry run")?; } Ok(()) diff --git a/src/cargo/ops/cargo_update.rs b/src/cargo/ops/cargo_update.rs index c3d367311e1..57f2f4fc45b 100644 --- a/src/cargo/ops/cargo_update.rs +++ b/src/cargo/ops/cargo_update.rs @@ -9,23 +9,38 @@ use crate::ops; use crate::sources::source::QueryKind; use crate::util::cache_lock::CacheLockMode; use crate::util::context::GlobalContext; +use crate::util::interning::InternedString; use crate::util::toml_mut::dependency::{MaybeWorkspace, Source}; use crate::util::toml_mut::manifest::LocalManifest; use crate::util::toml_mut::upgrade::upgrade_requirement; use crate::util::{style, OptVersionReq}; use crate::util::{CargoResult, VersionExt}; +use anyhow::Context as _; use itertools::Itertools; use semver::{Op, Version, VersionReq}; use std::cmp::Ordering; use std::collections::{BTreeMap, HashMap, HashSet}; use tracing::{debug, trace}; -pub type UpgradeMap = HashMap<(String, SourceId), Version>; +/// A map of all breaking upgrades which is filled in by +/// upgrade_manifests/upgrade_dependency when going through workspace member +/// manifests, and later used by write_manifest_upgrades in order to know which +/// upgrades to write to manifest files on disk. Also used by update_lockfile to +/// know which dependencies to keep unchanged if any have been upgraded (we will +/// do either breaking or non-breaking updates, but not both). +pub type UpgradeMap = HashMap< + // The key is a package identifier consisting of the name and the source id. + (InternedString, SourceId), + // The value is the original version requirement before upgrade, and the + // upgraded version. + (VersionReq, Version), +>; pub struct UpdateOptions<'a> { pub gctx: &'a GlobalContext, pub to_update: Vec, pub precise: Option<&'a str>, + pub breaking: bool, pub recursive: bool, pub dry_run: bool, pub workspace: bool, @@ -49,7 +64,11 @@ pub fn generate_lockfile(ws: &Workspace<'_>) -> CargoResult<()> { Ok(()) } -pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoResult<()> { +pub fn update_lockfile( + ws: &Workspace<'_>, + opts: &UpdateOptions<'_>, + upgrades: &UpgradeMap, +) -> CargoResult<()> { if opts.recursive && opts.precise.is_some() { anyhow::bail!("cannot specify both recursive and precise simultaneously") } @@ -90,8 +109,40 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes }; let mut registry = ws.package_registry()?; let mut to_avoid = HashSet::new(); + let breaking_update = !upgrades.is_empty(); - if opts.to_update.is_empty() { + if breaking_update { + // We don't necessarily want to update all specified packages. If we are + // doing a breaking update or precise upgrade, we don't want to touch + // any packages that have no breaking updates. So we want to only avoid + // all packages that got upgraded. + + for name in opts.to_update.iter() { + // We still want to query any specified package, for the sake of + // outputting errors if they don't exist. + previous_resolve.query(name)?; + } + + for ((name, source_id), (version_req, _)) in upgrades.iter() { + if let Some(matching_dep) = previous_resolve.iter().find(|dep| { + dep.name() == *name + && dep.source_id() == *source_id + && version_req.matches(dep.version()) + }) { + let spec = PackageIdSpec::new(name.to_string()) + .with_url(source_id.url().clone()) + .with_version(matching_dep.version().clone().into()) + .to_string(); + let pid = previous_resolve.query(&spec)?; + to_avoid.insert(pid); + } else { + // Should never happen + anyhow::bail!( + "no package named `{name}` with source `{source_id}` and version matching `{version_req}` in the previous lockfile", + ) + } + } + } else if opts.to_update.is_empty() { if !opts.workspace { to_avoid.extend(previous_resolve.iter()); to_avoid.extend(previous_resolve.unused_patches()); @@ -185,11 +236,7 @@ pub fn update_lockfile(ws: &Workspace<'_>, opts: &UpdateOptions<'_>) -> CargoRes opts.precise.is_some(), &mut registry, )?; - if opts.dry_run { - opts.gctx - .shell() - .warn("not updating lockfile due to dry run")?; - } else { + if !opts.dry_run { ops::write_pkg_lockfile(ws, &mut resolve)?; } Ok(()) @@ -217,6 +264,7 @@ pub fn print_lockfile_changes( pub fn upgrade_manifests( ws: &mut Workspace<'_>, to_update: &Vec, + precise: &Option<&str>, ) -> CargoResult { let gctx = ws.gctx(); let mut upgrades = HashMap::new(); @@ -245,6 +293,7 @@ pub fn upgrade_manifests( upgrade_dependency( &gctx, &to_update, + precise, &mut registry, &mut upgrades, &mut upgrade_messages, @@ -259,6 +308,7 @@ pub fn upgrade_manifests( fn upgrade_dependency( gctx: &GlobalContext, to_update: &Vec, + precise: &Option<&str>, registry: &mut PackageRegistry<'_>, upgrades: &mut UpgradeMap, upgrade_messages: &mut HashSet, @@ -316,7 +366,7 @@ fn upgrade_dependency( let query = crate::core::dependency::Dependency::parse(name, None, dependency.source_id().clone())?; - let possibilities = { + let possibilities = if precise.is_none() { loop { match registry.query_vec(&query, QueryKind::Exact) { std::task::Poll::Ready(res) => { @@ -325,6 +375,8 @@ fn upgrade_dependency( std::task::Poll::Pending => registry.block_until_ready()?, } } + } else { + Vec::new() }; let latest = if !possibilities.is_empty() { @@ -338,17 +390,23 @@ fn upgrade_dependency( None }; - let Some(latest) = latest else { + let new_version = if let Some(precise) = precise { + Version::parse(precise) + .with_context(|| format!("invalid version format for precise version `{precise}`"))? + } else if let Some(latest) = latest { + latest.clone() + } else { + // Breaking (not precise) upgrade did not find a latest version trace!("skipping dependency `{name}` without any published versions"); return Ok(dependency); }; - if current.matches(&latest) { + if current.matches(&new_version) { trace!("skipping dependency `{name}` without a breaking update available"); return Ok(dependency); } - let Some((new_req_string, _)) = upgrade_requirement(¤t.to_string(), latest)? else { + let Some((new_req_string, _)) = upgrade_requirement(¤t.to_string(), &new_version)? else { trace!("skipping dependency `{name}` because the version requirement didn't change"); return Ok(dependency); }; @@ -356,14 +414,36 @@ fn upgrade_dependency( let upgrade_message = format!("{name} {current} -> {new_req_string}"); trace!(upgrade_message); + let old_version = semver::Version::new( + comparator.major, + comparator.minor.unwrap_or_default(), + comparator.patch.unwrap_or_default(), + ); + let is_downgrade = new_version < old_version; + let status = if is_downgrade { + "Downgrading" + } else { + "Upgrading" + }; + if upgrade_messages.insert(upgrade_message.clone()) { gctx.shell() - .status_with_color("Upgrading", &upgrade_message, &style::GOOD)?; + .status_with_color(status, &upgrade_message, &style::WARN)?; } - upgrades.insert((name.to_string(), dependency.source_id()), latest.clone()); + upgrades.insert( + (name, dependency.source_id()), + (current.clone(), new_version.clone()), + ); + + let new_version_req = VersionReq::parse(&new_version.to_string())?; + + let req = if precise.is_some() { + OptVersionReq::Precise(new_version, new_version_req) + } else { + OptVersionReq::Req(new_version_req) + }; - let req = OptVersionReq::Req(VersionReq::parse(&latest.to_string())?); let mut dep = dependency.clone(); dep.set_version_req(req); Ok(dep) @@ -433,7 +513,7 @@ pub fn write_manifest_upgrades( continue; }; - let Some(latest) = upgrades.get(&(name.to_owned(), source_id)) else { + let Some((_, latest)) = upgrades.get(&(name.into(), source_id)) else { trace!("skipping dependency without an upgrade: {name}"); continue; }; diff --git a/src/cargo/util/semver_ext.rs b/src/cargo/util/semver_ext.rs index 06f3cd340fc..458b04afdfd 100644 --- a/src/cargo/util/semver_ext.rs +++ b/src/cargo/util/semver_ext.rs @@ -117,9 +117,15 @@ impl OptVersionReq { /// and we're not sure if this part of the functionality should be implemented in semver or cargo. pub fn matches_prerelease(&self, version: &Version) -> bool { if version.is_prerelease() { - let mut version = version.clone(); - version.pre = semver::Prerelease::EMPTY; - return self.matches(&version); + // Only in the case of "ordinary" version requirements with pre-release + // versions do we need to help the version matching. In the case of Any, + // Locked, or Precise, the `matches()` function is already doing the + // correct handling. + if let OptVersionReq::Req(_) = self { + let mut version = version.clone(); + version.pre = semver::Prerelease::EMPTY; + return self.matches(&version); + } } self.matches(version) } @@ -178,6 +184,8 @@ impl From for OptVersionReq { #[cfg(test)] mod matches_prerelease { + use semver::{Version, VersionReq}; + use super::OptVersionReq; #[test] @@ -238,4 +246,13 @@ mod matches_prerelease { assert_eq!(expected, matched, "req: {req}; ver: {ver}"); } } + + #[test] + fn precise_prerelease() { + let version_req = VersionReq::parse("1.2.3-pre").unwrap(); + let version = Version::parse("1.2.3-pre").unwrap(); + let matched = + OptVersionReq::Precise(version.clone(), version_req).matches_prerelease(&version); + assert!(matched, "a version must match its own precise requirement"); + } } diff --git a/src/cargo/util/toml_mut/upgrade.rs b/src/cargo/util/toml_mut/upgrade.rs index 13d5563450c..1cb49d60b6e 100644 --- a/src/cargo/util/toml_mut/upgrade.rs +++ b/src/cargo/util/toml_mut/upgrade.rs @@ -21,22 +21,18 @@ pub(crate) fn upgrade_requirement( .map(|p| set_comparator(p, version)) .collect(); let comparators = comparators?; - let new_req = semver::VersionReq { comparators }; + let mut new_req = semver::VersionReq { comparators }; + // Validate contract + if !new_req.matches(version) { + // If req is ^0.1 and version is 0.2.0-beta, new_req becomes ^0.2. + // This does not match version. In such cases, we should let new_req + // be ^version. + new_req = semver::VersionReq::parse(&format!("^{version}"))?; + } let mut new_req_text = new_req.to_string(); if new_req_text.starts_with('^') && !req.starts_with('^') { new_req_text.remove(0); } - // Validate contract - #[cfg(debug_assertions)] - { - assert!( - new_req.matches(version), - "New req {} is invalid, because {} does not match {}", - new_req_text, - new_req, - version - ) - } if new_req_text == req_text { Ok(None) } else { diff --git a/tests/testsuite/main.rs b/tests/testsuite/main.rs index 271d333e2ef..42f7d233f3c 100644 --- a/tests/testsuite/main.rs +++ b/tests/testsuite/main.rs @@ -175,6 +175,7 @@ mod tree; mod tree_graph_features; mod unit_graph; mod update; +mod update_duplicated_with_precise_breaking_feature; mod vendor; mod verify_project; mod version; diff --git a/tests/testsuite/precise_pre_release.rs b/tests/testsuite/precise_pre_release.rs index 70e5e3fa5f0..9afcfd6c9db 100644 --- a/tests/testsuite/precise_pre_release.rs +++ b/tests/testsuite/precise_pre_release.rs @@ -66,11 +66,12 @@ fn update_pre_release() { p.cargo("update my-dependency --precise 0.1.2-pre.0 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) .with_stderr_data(str![[r#" +[UPGRADING] my-dependency ^0.1.1 -> ^0.1.2-pre.0 [UPDATING] `dummy-registry` index -[UPDATING] my-dependency v0.1.1 -> v0.1.2-pre.0 "#]]) .run(); + let lockfile = p.read_lockfile(); assert!(lockfile.contains("\nname = \"my-dependency\"\nversion = \"0.1.2-pre.0\"")); } @@ -99,8 +100,8 @@ fn update_pre_release_differ() { p.cargo("update -p my-dependency --precise 0.1.2-pre.0 -Zunstable-options") .masquerade_as_nightly_cargo(&["precise-pre-release"]) .with_stderr_data(str![[r#" +[DOWNGRADING] my-dependency ^0.1.2 -> ^0.1.2-pre.0 [UPDATING] `dummy-registry` index -[DOWNGRADING] my-dependency v0.1.2 -> v0.1.2-pre.0 "#]]) .run(); diff --git a/tests/testsuite/update.rs b/tests/testsuite/update.rs index 1e74e32f93c..2ee2f05d99b 100644 --- a/tests/testsuite/update.rs +++ b/tests/testsuite/update.rs @@ -362,7 +362,7 @@ fn change_package_version() { } #[cargo_test] -fn update_precise() { +fn update_precise_downgrade() { Package::new("serde", "0.1.0").publish(); Package::new("serde", "0.2.1").publish(); @@ -925,7 +925,7 @@ fn dry_run_update() { [LOCKING] 1 package to latest compatible version [UPDATING] serde v0.1.0 -> v0.1.1 [NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest -[WARNING] not updating lockfile due to dry run +[WARNING] aborting update due to dry run "#]]) .run(); @@ -1390,103 +1390,1501 @@ fn update_precise_git_revisions() { assert!(p.read_lockfile().contains(&tag_commit_id)); assert!(!p.read_lockfile().contains(&head_id)); - // Now make a tag looks like an oid. - // It requires a git fetch, as the oid cannot be found in preexisting git db. - let arbitrary_tag: String = std::iter::repeat('a').take(head_id.len()).collect(); - git::tag(&git_repo, &arbitrary_tag); + // Now make a tag looks like an oid. + // It requires a git fetch, as the oid cannot be found in preexisting git db. + let arbitrary_tag: String = std::iter::repeat('a').take(head_id.len()).collect(); + git::tag(&git_repo, &arbitrary_tag); + + p.cargo("update git --precise") + .arg(&arbitrary_tag) + .with_stderr_data(format!( + "\ +[UPDATING] git repository `[ROOTURL]/git` +[UPDATING] git v0.5.0 ([ROOTURL]/git#[..]) -> #{} +", + &head_id[..8], + )) + .run(); + + assert!(p.read_lockfile().contains(&head_id)); + assert!(!p.read_lockfile().contains(&tag_commit_id)); +} + +#[cargo_test] +fn update_precise_yanked() { + Package::new("bar", "0.1.0").publish(); + Package::new("bar", "0.1.1").yanked(true).publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + + [dependencies] + bar = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + // Use non-yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); + + p.cargo("update --precise 0.1.1 bar") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[WARNING] selected package `bar@0.1.1` was yanked by the author +[NOTE] if possible, try a compatible non-yanked version +[UPDATING] bar v0.1.0 -> v0.1.1 + +"#]]) + .run(); + + // Use yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); +} + +#[cargo_test] +fn update_precise_yanked_multiple_presence() { + Package::new("bar", "0.1.0").publish(); + Package::new("bar", "0.1.1").yanked(true).publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + + [dependencies] + bar = "0.1" + baz = { package = "bar", version = "0.1" } + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + // Use non-yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); + + p.cargo("update --precise 0.1.1 bar") + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[WARNING] selected package `bar@0.1.1` was yanked by the author +[NOTE] if possible, try a compatible non-yanked version +[UPDATING] bar v0.1.0 -> v0.1.1 + +"#]]) + .run(); + + // Use yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); +} + +#[cargo_test] +fn update_precise_breaking_without_feature_flag() { + Package::new("incompatible", "1.0.0").publish(); + Package::new("incompatible", "2.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("update incompatible --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `incompatible = "^1.0"` +candidate versions found which didn't match: 2.0.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_dry_run() { + Package::new("incompatible", "1.0.0").publish(); + Package::new("ws", "1.0.0").publish(); + + let root_manifest = r#" + # Check if formatting is preserved. Nothing here should change, due to dry-run. + + [workspace] + members = ["foo"] + + [workspace.dependencies] + ws = "1.0" # Preserve formatting + "#; + + let crate_manifest = r#" + # Check if formatting is preserved. Nothing here should change, due to dry-run. + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" # Preserve formatting + ws.workspace = true # Preserve formatting + "#; + + let p = project() + .file("Cargo.toml", root_manifest) + .file("foo/Cargo.toml", crate_manifest) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + let lock_file = p.read_file("Cargo.lock"); + + Package::new("incompatible", "2.0.0").publish(); + Package::new("ws", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --dry-run incompatible ws --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^1.0 -> ^2.0 +[UPGRADING] ws ^1.0 -> ^2.0 +[UPDATING] `dummy-registry` index +[UPDATING] incompatible v1.0.0 -> v2.0.0 +[UPDATING] ws v1.0.0 -> v2.0.0 +[WARNING] aborting update due to dry run + +"#]]) + .run(); + + let root_manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&root_manifest_after, root_manifest); + + let crate_manifest_after = p.read_file("foo/Cargo.toml"); + assert_e2e().eq(&crate_manifest_after, crate_manifest); + + let lock_file_after = p.read_file("Cargo.lock"); + assert_e2e().eq(&lock_file_after, lock_file); +} + +#[cargo_test] +fn update_precise_breaking_incompatible() { + Package::new("incompatible", "0.1.0").publish(); + Package::new("incompatible", "0.2.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.1" # Comment + bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.2" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("incompatible", "0.3.0").publish(); + Package::new("incompatible", "0.3.1").publish(); + Package::new("incompatible", "0.4.5").publish(); + + p.cargo("update -Zunstable-options incompatible@0.1.0 --precise 0.3.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^0.1 -> ^0.3 +[UPDATING] `dummy-registry` index +[UPDATING] incompatible v0.1.0 -> v0.3.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.3" # Comment + bar = { path = "bar" } + +"#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_consistent_output() { + Package::new("compatible", "0.1.0").publish(); + Package::new("incompatible", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "0.1" + incompatible = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("compatible", "0.1.1").publish(); + Package::new("compatible", "0.1.2").publish(); + Package::new("incompatible", "0.2.0").publish(); + Package::new("incompatible", "0.2.1").publish(); + + p.cargo("update compatible --precise 0.1.1") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] compatible v0.1.0 -> v0.1.1 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + p.cargo("update -Zunstable-options incompatible --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[UPDATING] incompatible v0.1.0 -> v0.2.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_alternative() { + registry::alt_init(); + Package::new("alternative", "0.1.0") + .alternative(true) + .publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + alternative = { registry = "alternative", version = "0.1" } # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("alternative", "0.2.0") + .alternative(true) + .publish(); + + Package::new("alternative", "0.2.1") + .alternative(true) + .publish(); + + p.cargo("update -Zunstable-options alternative@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] alternative ^0.1 -> ^0.2 +[UPDATING] `alternative` index +[UPDATING] alternative v0.1.0 (registry `alternative`) -> v0.2.0 + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + alternative = { registry = "alternative", version = "0.2" } # Comment + "#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_pre_release() { + Package::new("pre", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + pre = "0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + Package::new("pre", "0.2.0-beta").publish(); + + p.cargo("update -Zunstable-options pre --precise 0.2.0-beta") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] pre ^0.1 -> ^0.2.0-beta +[UPDATING] `dummy-registry` index + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_pre_release_explicit_version_req() { + Package::new("pre", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + pre = "0.1.0" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("pre", "0.2.0-beta").publish(); + Package::new("pre", "0.2.0").publish(); + + p.cargo("update -Zunstable-options pre --precise 0.2.0-beta") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] pre ^0.1.0 -> ^0.2.0-beta +[UPDATING] `dummy-registry` index +[UPDATING] pre v0.1.0 -> v0.2.0-beta + +"#]]) + .run(); + + let manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + pre = "0.2.0-beta" # Comment + +"#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_upgrade_from_pre_release() { + Package::new("pre", "1.0.0-alpha").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + pre = "1.0.0-alpha" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("pre", "2.0.0").publish(); + Package::new("pre", "2.0.1").publish(); + + p.cargo("update -Zunstable-options pre --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] pre ^1.0.0-alpha -> ^2.0.0 +[UPDATING] `dummy-registry` index +[UPDATING] pre v1.0.0-alpha -> v2.0.0 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_yanked() { + Package::new("yanked", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + yanked = "0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("yanked", "0.2.0").yanked(true).publish(); + Package::new("yanked", "0.2.1").publish(); + + p.cargo("update -Zunstable-options yanked@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] yanked ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[WARNING] selected package `yanked@0.2.0` was yanked by the author +[NOTE] if possible, try a compatible non-yanked version +[UPDATING] yanked v0.1.0 -> v0.2.0 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_ws() { + Package::new("ws", "0.1.0").publish(); + + let crate_manifest = r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + ws.workspace = true # Comment + "#; + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["foo"] + + [workspace.dependencies] + ws = "0.1" # Comment + "#, + ) + .file("foo/Cargo.toml", crate_manifest) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("ws", "0.2.0").publish(); + Package::new("ws", "0.2.1").publish(); + + p.cargo("update -Zunstable-options ws@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] ws ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[UPDATING] ws v0.1.0 -> v0.2.0 + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + [workspace] + members = ["foo"] + + [workspace.dependencies] + ws = "0.2" # Comment + "#]], + ); + + let crate_manifest_after = p.read_file("foo/Cargo.toml"); + assert_e2e().eq(&crate_manifest_after, crate_manifest); +} + +#[cargo_test] +fn update_precise_breaking_shared_ws() { + Package::new("shared", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["foo", "bar"] + "#, + ) + .file( + "foo/Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.1" # Comment + bar = { path = "../bar" } + "#, + ) + .file("foo/src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.1" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("shared", "0.2.0").publish(); + Package::new("shared", "0.2.1").publish(); + + p.cargo("update -Zunstable-options shared@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] shared ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[UPDATING] shared v0.1.0 -> v0.2.0 + +"#]]) + .run(); + + let foo_manifest = p.read_file("foo/Cargo.toml"); + assert_e2e().eq( + &foo_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.2" # Comment + bar = { path = "../bar" } + "#]], + ); + + let bar_manifest = p.read_file("bar/Cargo.toml"); + assert_e2e().eq( + &bar_manifest, + str![[r#" + + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.2" + "#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_shared_non_ws() { + Package::new("shared", "0.1.0").publish(); + + let foo_manifest = r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.1" # Comment + bar = { path = "bar" } + "#; + + let bar_manifest = r#" + # Not part of the workspace, and won't get touched + + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + shared = "0.1" + "#; + + let p = project() + .file("Cargo.toml", foo_manifest) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", bar_manifest) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("shared", "0.2.0").publish(); + Package::new("shared", "0.2.1").publish(); + + p.cargo("update -Zunstable-options shared@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + // The lockfile update is failing for the same reason a non-breaking + // update would. There is still a shared@0.1.0 here that didn't get + // upgraded (the transitive dependency within bar), and we are now + // asking the lockfile update to update it to 0.2.0, which it cannot do. + .with_stderr_data(str![[r#" +[UPGRADING] shared ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `shared = "^0.1"` +candidate versions found which didn't match: 0.2.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `bar v0.0.1 ([ROOT]/foo/bar)` + ... which satisfies path dependency `bar` (locked to 0.0.1) of package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); + + let foo_manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&foo_manifest_after, foo_manifest); + + let bar_manifest_after = p.read_file("bar/Cargo.toml"); + assert_e2e().eq(&bar_manifest_after, bar_manifest); +} + +#[cargo_test] +fn update_precise_breaking_spec_version() { + Package::new("incompatible", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("incompatible", "0.3.0").publish(); + Package::new("incompatible", "0.3.1").publish(); + + // No spec + p.cargo("update -Zunstable-options incompatible --precise 0.3.1") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^0.1 -> ^0.3 +[UPDATING] `dummy-registry` index +[UPDATING] incompatible v0.1.0 -> v0.3.1 + +"#]]) + .run(); + + // Invalid spec + p.cargo("update -Zunstable-options incompatible@foo --precise 0.3.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] expected a version like "1.32" + +"#]]) + .run(); + + // Spec version not matching our current dependencies + p.cargo("update -Zunstable-options incompatible@2.0.0 --precise 0.3.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `incompatible@2.0.0` did not match any packages +Did you mean one of these? + + incompatible@0.3.1 + +"#]]) + .run(); + + // Spec source not matching our current dependencies + p.cargo("update -Zunstable-options https://alternative.com#incompatible@0.1.0 --precise 0.3.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `https://alternative.com/#incompatible@0.1.0` did not match any packages +Did you mean one of these? + + incompatible@0.3.1 + +"#]]) + .run(); + + // Accepted spec, full format + p.cargo("update -Zunstable-options https://github.com/rust-lang/crates.io-index#incompatible@0.3.1 --precise 0.1.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[DOWNGRADING] incompatible ^0.3 -> ^0.1 +[UPDATING] `dummy-registry` index +[DOWNGRADING] incompatible v0.3.1 -> v0.1.0 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_without_lock_file() { + Package::new("incompatible", "0.1.0").publish(); + Package::new("incompatible", "0.2.0").publish(); + Package::new("incompatible", "0.2.1").publish(); + + let manifest = r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.1" # Comment + "#; + + let p = project() + .file("Cargo.toml", manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("update -Zunstable-options incompatible@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + // Sort of makes sense, as there is no lock file with incompatible@0.1.0 + // in it. Although the hint isn't very helpful. + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^0.1 -> ^0.2 +[UPDATING] `dummy-registry` index +[ERROR] package ID specification `incompatible@0.1.0` did not match any packages +Did you mean one of these? + + incompatible@0.2.0 + +"#]]) + .run(); + + let manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&manifest_after, manifest); +} + +#[cargo_test] +fn update_precise_breaking_non_existing_version() { + Package::new("bar", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + bar = "0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + p.cargo("update -Zunstable-options bar@0.1.0 --precise 0.9.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPGRADING] bar ^0.1 -> ^0.9 +[UPDATING] `dummy-registry` index +[ERROR] no matching package named `bar` found +location searched: registry `crates-io` +required by package `foo v0.0.1 ([ROOT]/foo)` + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + bar = "0.1" # Comment + +"#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_compatible() { + Package::new("compatible", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("compatible", "0.1.1").publish(); + Package::new("compatible", "0.1.2").publish(); + + p.cargo("update -Zunstable-options compatible@0.1.0 --precise 0.1.1") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] compatible v0.1.0 -> v0.1.1 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_pinned() { + Package::new("pinned", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + pinned = "=0.1" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("pinned", "0.1.1").publish(); + Package::new("pinned", "0.2.0").publish(); + Package::new("pinned", "0.2.1").publish(); + + p.cargo("update -Zunstable-options pinned@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `pinned = "=0.1"` +candidate versions found which didn't match: 0.2.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_renamed() { + Package::new("renamed-from", "0.1.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + renamed-to = { package = "renamed-from", version = "0.1" } + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("renamed-from", "0.1.1").publish(); + Package::new("renamed-from", "0.2.0").publish(); + Package::new("renamed-from", "0.2.1").publish(); + + p.cargo("update -Zunstable-options renamed-from@0.1.0 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `renamed-from = "^0.1"` +candidate versions found which didn't match: 0.2.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_transitive() { + Package::new("incompatible", "0.1.0").publish(); + Package::new("incompatible", "0.2.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.1" # Comment + bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "0.2" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("incompatible", "0.3.0").publish(); + Package::new("incompatible", "0.3.1").publish(); + + // We cannot do this, as incompatible@0.2.0 is a transitive dependency, not + // a direct workspace dependency. + p.cargo("update -Zunstable-options incompatible@0.2.0 --precise 0.3.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `incompatible = "^0.2"` +candidate versions found which didn't match: 0.3.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `bar v0.0.1 ([ROOT]/foo/bar)` + ... which satisfies path dependency `bar` (locked to 0.0.1) of package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_breaking_direct_plus_transitive() { + Package::new("incompatible", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" # Comment + bar = { path = "bar" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + let lock1 = p.read_lockfile(); + + Package::new("incompatible", "2.0.0").publish(); + Package::new("incompatible", "2.0.1").publish(); + + p.cargo("update -Zunstable-options incompatible --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^1.0 -> ^2.0 +[UPDATING] `dummy-registry` index +[ADDING] incompatible v2.0.0 (latest: v2.0.1) +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest - p.cargo("update git --precise") - .arg(&arbitrary_tag) - .with_stderr_data(format!( - "\ -[UPDATING] git repository `[ROOTURL]/git` -[UPDATING] git v0.5.0 ([ROOTURL]/git#[..]) -> #{} -", - &head_id[..8], - )) +"#]]) .run(); - assert!(p.read_lockfile().contains(&head_id)); - assert!(!p.read_lockfile().contains(&tag_commit_id)); + let lock2 = p.read_lockfile(); + + assert_ne!(lock1, lock2); + assert!(lock2.contains("incompatible 1.0.0")); + assert!(lock2.contains("incompatible 2.0.0")); } #[cargo_test] -fn precise_yanked() { - Package::new("bar", "0.1.0").publish(); - Package::new("bar", "0.1.1").yanked(true).publish(); +fn update_precise_breaking_incompatible_downgrade() { + Package::new("incompatible", "1.0.0").publish(); + Package::new("incompatible", "2.0.0").publish(); + let p = project() .file( "Cargo.toml", r#" + # Check if formatting is preserved + [package] - name = "foo" + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] [dependencies] - bar = "0.1" + incompatible = "2.0" # Comment "#, ) .file("src/lib.rs", "") .build(); p.cargo("generate-lockfile").run(); - - // Use non-yanked version. - let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); - - p.cargo("update --precise 0.1.1 bar") + p.cargo("update -Zunstable-options incompatible --precise 1.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) .with_stderr_data(str![[r#" +[DOWNGRADING] incompatible ^2.0 -> ^1.0 [UPDATING] `dummy-registry` index -[WARNING] selected package `bar@0.1.1` was yanked by the author -[NOTE] if possible, try a compatible non-yanked version -[UPDATING] bar v0.1.0 -> v0.1.1 +[DOWNGRADING] incompatible v2.0.0 -> v1.0.0 "#]]) .run(); - // Use yanked version. - let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" # Comment + +"#]], + ); } #[cargo_test] -fn precise_yanked_multiple_presence() { - Package::new("bar", "0.1.0").publish(); - Package::new("bar", "0.1.1").yanked(true).publish(); +fn update_precise_breaking_incompatible_build_metadata() { + Package::new("incompatible", "1.0.0").publish(); + Package::new("incompatible", "2.0.0+a").publish(); + Package::new("incompatible", "2.0.0+b").publish(); + Package::new("incompatible", "2.0.0+c").publish(); + let p = project() .file( "Cargo.toml", r#" + # Check if formatting is preserved + [package] - name = "foo" + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] [dependencies] - bar = "0.1" - baz = { package = "bar", version = "0.1" } + incompatible = "1.0" # Comment "#, ) .file("src/lib.rs", "") .build(); p.cargo("generate-lockfile").run(); + p.cargo("update -Zunstable-options incompatible --precise 2.0.0+b") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPGRADING] incompatible ^1.0 -> ^2.0 +[UPDATING] `dummy-registry` index +[UPDATING] incompatible v1.0.0 -> v2.0.0+b - // Use non-yanked version. - let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); +"#]]) + .run(); - p.cargo("update --precise 0.1.1 bar") + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "2.0" # Comment + +"#]], + ); +} + +#[cargo_test] +fn update_precise_breaking_mixed_pinning_renaming() { + Package::new("mixed-pinned", "1.0.0").publish(); + Package::new("mixed-ws-pinned", "1.0.0").publish(); + Package::new("renamed-from", "1.0.0").publish(); + + let root_manifest = r#" + [workspace] + members = ["foo", "bar"] + + [workspace.dependencies] + mixed-ws-pinned = "=1.0" + "#; + + let foo_manifest = r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "=1.0" + mixed-ws-pinned.workspace = true + renamed-to = { package = "renamed-from", version = "1.0" } + "#; + + let bar_manifest = r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "1.0" + mixed-ws-pinned = "1.0" + renamed-from = "1.0" + "#; + + let p = project() + .file("Cargo.toml", root_manifest) + .file("foo/Cargo.toml", foo_manifest) + .file("foo/src/lib.rs", "") + .file("bar/Cargo.toml", bar_manifest) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("mixed-pinned", "2.0.0").publish(); + Package::new("mixed-ws-pinned", "2.0.0").publish(); + Package::new("renamed-from", "2.0.0").publish(); + Package::new("mixed-pinned", "2.0.1").publish(); + Package::new("mixed-ws-pinned", "2.0.1").publish(); + Package::new("renamed-from", "2.0.1").publish(); + + p.cargo("update -Zunstable-options mixed-pinned mixed-ws-pinned renamed-from --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + // The lockfile update is failing for the same reason a non-breaking + // update would. There is still a mixed-pinned@=1.0.0 here that didn't + // get upgraded, and we are now asking the lockfile update to update it + // to 2.0.0, which it cannot do. .with_stderr_data(str![[r#" +[UPGRADING] mixed-pinned ^1.0 -> ^2.0 +[UPGRADING] mixed-ws-pinned ^1.0 -> ^2.0 +[UPGRADING] renamed-from ^1.0 -> ^2.0 [UPDATING] `dummy-registry` index -[WARNING] selected package `bar@0.1.1` was yanked by the author -[NOTE] if possible, try a compatible non-yanked version -[UPDATING] bar v0.1.0 -> v0.1.1 +[ERROR] failed to select a version for the requirement `mixed-pinned = "=1.0"` +candidate versions found which didn't match: 2.0.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `foo v0.0.1 ([ROOT]/foo/foo)` +perhaps a crate was updated and forgotten to be re-vendored? "#]]) .run(); - // Use yanked version. - let lockfile = p.read_lockfile(); - assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); + let root_manifest_after = p.read_file("Cargo.toml"); + let foo_manifest_after = p.read_file("foo/Cargo.toml"); + let bar_manifest_after = p.read_file("bar/Cargo.toml"); + + assert_e2e().eq(&root_manifest_after, root_manifest); + assert_e2e().eq(&foo_manifest_after, foo_manifest); + assert_e2e().eq(&bar_manifest_after, bar_manifest); +} + +#[cargo_test] +fn update_precise_breaking_specific_packages_that_wont_upgrade() { + Package::new("renamed-from", "1.0.0").publish(); + Package::new("non-semver", "1.0.0").publish(); + Package::new("bar", "1.0.0") + .add_dep(Dependency::new("transitive-compatible", "1.0.0").build()) + .add_dep(Dependency::new("transitive-incompatible", "1.0.0").build()) + .publish(); + Package::new("transitive-compatible", "1.0.0").publish(); + Package::new("transitive-incompatible", "1.0.0").publish(); + + let crate_manifest = r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + renamed-to = { package = "renamed-from", version = "1.0" } # Comment + non-semver = "~1.0" # Comment + bar = "1.0" # Comment + "#; + + let p = project() + .file("Cargo.toml", crate_manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + let lock_file = p.read_file("Cargo.lock"); + + Package::new("renamed-from", "2.0.0").publish(); + Package::new("non-semver", "2.0.0").publish(); + Package::new("transitive-compatible", "1.0.1").publish(); + Package::new("transitive-incompatible", "2.0.0").publish(); + + p.cargo("update -Zunstable-options renamed-from non-semver transitive-compatible transitive-incompatible --precise 2.0.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + // This falls back to the non-breaking update, as no packages got upgraded. + // The non-breaking update consequently fails. + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `renamed-from = "^1.0"` +candidate versions found which didn't match: 2.0.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `foo v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .run(); + + let crate_manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&crate_manifest_after, crate_manifest); + + let lock_file_after = p.read_file("Cargo.lock"); + assert_e2e().eq(&lock_file_after, lock_file); } #[cargo_test] @@ -1524,7 +2922,7 @@ fn report_behind() { [LOCKING] 1 package to latest compatible version [UPDATING] breaking v0.1.0 -> v0.1.1 (latest: v0.2.0) [NOTE] pass `--verbose` to see 2 unchanged dependencies behind latest -[WARNING] not updating lockfile due to dry run +[WARNING] aborting update due to dry run "#]]) .run(); @@ -1537,7 +2935,7 @@ fn report_behind() { [UNCHANGED] pre v1.0.0-alpha.0 (latest: v1.0.0-alpha.1) [UNCHANGED] two-ver v0.1.0 (latest: v0.2.0) [NOTE] to see how you depend on a package, run `cargo tree --invert --package @` -[WARNING] not updating lockfile due to dry run +[WARNING] aborting update due to dry run "#]]) .run(); @@ -1549,7 +2947,7 @@ fn report_behind() { [UPDATING] `dummy-registry` index [LOCKING] 0 packages to latest compatible versions [NOTE] pass `--verbose` to see 3 unchanged dependencies behind latest -[WARNING] not updating lockfile due to dry run +[WARNING] aborting update due to dry run "#]]) .run(); @@ -1562,7 +2960,7 @@ fn report_behind() { [UNCHANGED] pre v1.0.0-alpha.0 (latest: v1.0.0-alpha.1) [UNCHANGED] two-ver v0.1.0 (latest: v0.2.0) [NOTE] to see how you depend on a package, run `cargo tree --invert --package @` -[WARNING] not updating lockfile due to dry run +[WARNING] aborting update due to dry run "#]]) .run(); @@ -1912,10 +3310,13 @@ fn update_breaking() { [UPDATING] multiple-registries v2.0.0 (registry `alternative`) -> v3.0.0 [UPDATING] multiple-registries v1.0.0 -> v2.0.0 [UPDATING] multiple-source-types v1.0.0 -> v2.0.0 +[REMOVING] multiple-versions v1.0.0 +[REMOVING] multiple-versions v2.0.0 [ADDING] multiple-versions v3.0.0 [UPDATING] platform-specific v1.0.0 -> v2.0.0 [UPDATING] shared v1.0.0 -> v2.0.0 [UPDATING] ws v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 4 unchanged dependencies behind latest "#]]) .run(); @@ -2108,6 +3509,7 @@ fn update_breaking_specific_packages() { [UPDATING] transitive-compatible v1.0.0 -> v1.0.1 [UPDATING] transitive-incompatible v1.0.0 -> v2.0.0 [UPDATING] ws v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); @@ -2162,7 +3564,9 @@ fn update_breaking_specific_packages_that_wont_update() { p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver transitive-compatible transitive-incompatible") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 5 unchanged dependencies behind latest "#]]) .run(); @@ -2177,7 +3581,7 @@ fn update_breaking_specific_packages_that_wont_update() { "update compatible renamed-from non-semver transitive-compatible transitive-incompatible", ) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [LOCKING] 5 packages to latest compatible versions [UPDATING] compatible v1.0.0 -> v1.0.1 [UPDATING] non-semver v1.0.0 -> v1.0.1 (latest: v2.0.0) @@ -2220,7 +3624,7 @@ fn update_breaking_without_lock_file() { p.cargo("update -Zunstable-options --breaking") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [UPGRADING] incompatible ^1.0 -> ^2.0 [LOCKING] 3 packages to latest compatible versions @@ -2271,23 +3675,38 @@ fn update_breaking_spec_version() { // Spec version not matching our current dependencies p.cargo("update -Zunstable-options --breaking incompatible@2.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) - .with_stderr_data(str![[r#""#]]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `incompatible@2.0.0` did not match any packages +Did you mean one of these? + + incompatible@1.0.0 + +"#]]) .run(); // Spec source not matching our current dependencies p.cargo("update -Zunstable-options --breaking https://alternative.com#incompatible@1.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) - .with_stderr_data(str![[r#""#]]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `https://alternative.com/#incompatible@1.0.0` did not match any packages +Did you mean one of these? + + incompatible@1.0.0 + +"#]]) .run(); // Accepted spec p.cargo("update -Zunstable-options --breaking incompatible@1.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [UPGRADING] incompatible ^1.0 -> ^2.0 [LOCKING] 1 package to latest compatible version [UPDATING] incompatible v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); @@ -2297,10 +3716,11 @@ fn update_breaking_spec_version() { p.cargo("update -Zunstable-options --breaking https://github.com/rust-lang/crates.io-index#incompatible@2.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [UPGRADING] incompatible ^2.0 -> ^3.0 [LOCKING] 1 package to latest compatible version [UPDATING] incompatible v2.0.0 -> v3.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); @@ -2309,7 +3729,9 @@ fn update_breaking_spec_version() { p.cargo("update -Zunstable-options --breaking compatible@1.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); @@ -2317,19 +3739,33 @@ fn update_breaking_spec_version() { // Non-existing versions p.cargo("update -Zunstable-options --breaking incompatible@9.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) - .with_stderr_data(str![[r#""#]]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `incompatible@9.0.0` did not match any packages +Did you mean one of these? + + incompatible@3.0.0 + +"#]]) .run(); p.cargo("update -Zunstable-options --breaking compatible@9.0.0") .masquerade_as_nightly_cargo(&["update-breaking"]) - .with_stderr_data(str![[r#""#]]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `compatible@9.0.0` did not match any packages +Did you mean one of these? + + compatible@1.0.0 + +"#]]) .run(); } #[cargo_test] fn update_breaking_spec_version_transitive() { Package::new("dep", "1.0.0").publish(); - Package::new("dep", "1.1.0").publish(); + Package::new("dep", "2.0.0").publish(); let p = project() .file( @@ -2357,7 +3793,7 @@ fn update_breaking_spec_version_transitive() { authors = [] [dependencies] - dep = "1.1" + dep = "2.0" "#, ) .file("bar/src/lib.rs", "") @@ -2365,36 +3801,39 @@ fn update_breaking_spec_version_transitive() { p.cargo("generate-lockfile").run(); - Package::new("dep", "1.1.1").publish(); - Package::new("dep", "2.0.0").publish(); + Package::new("dep", "2.0.1").publish(); + Package::new("dep", "3.0.0").publish(); // Will upgrade the direct dependency p.cargo("update -Zunstable-options --breaking dep@1.0") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index -[UPGRADING] dep ^1.0 -> ^2.0 +[UPDATING] `dummy-registry` index +[UPGRADING] dep ^1.0 -> ^3.0 [LOCKING] 1 package to latest compatible version -[ADDING] dep v2.0.0 +[UPDATING] dep v1.0.0 -> v3.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); // But not the transitive one, because bar is not a workspace member - p.cargo("update -Zunstable-options --breaking dep@1.1") + p.cargo("update -Zunstable-options --breaking dep@2.0") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest "#]]) .run(); // A non-breaking update is different, as it will update transitive dependencies - p.cargo("update dep@1.1") + p.cargo("update dep@2.0") .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [LOCKING] 1 package to latest compatible version -[UPDATING] dep v1.1.0 -> v1.1.1 (latest: v2.0.0) +[UPDATING] dep v2.0.0 -> v2.0.1 (latest: v3.0.0) "#]]) .run(); @@ -2450,9 +3889,11 @@ fn update_breaking_mixed_compatibility() { p.cargo("update -Zunstable-options --breaking") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [UPGRADING] mixed-compatibility ^1.0 -> ^2.0 [LOCKING] 1 package to latest compatible version +[REMOVING] mixed-compatibility v1.0.0 +[REMOVING] mixed-compatibility v2.0.0 [ADDING] mixed-compatibility v2.0.1 "#]]) @@ -2536,7 +3977,7 @@ fn update_breaking_mixed_pinning_renaming() { p.cargo("update -Zunstable-options --breaking") .masquerade_as_nightly_cargo(&["update-breaking"]) .with_stderr_data(str![[r#" -[UPDATING] `[..]` index +[UPDATING] `dummy-registry` index [UPGRADING] mixed-pinned ^1.0 -> ^2.0 [UPGRADING] mixed-ws-pinned ^1.0 -> ^2.0 [UPGRADING] renamed-from ^1.0 -> ^2.0 @@ -2544,6 +3985,7 @@ fn update_breaking_mixed_pinning_renaming() { [ADDING] mixed-pinned v2.0.0 [ADDING] mixed-ws-pinned v2.0.0 [ADDING] renamed-from v2.0.0 +[NOTE] pass `--verbose` to see 3 unchanged dependencies behind latest "#]]) .run(); diff --git a/tests/testsuite/update_duplicated_with_precise_breaking_feature.rs b/tests/testsuite/update_duplicated_with_precise_breaking_feature.rs new file mode 100644 index 00000000000..12236e538b8 --- /dev/null +++ b/tests/testsuite/update_duplicated_with_precise_breaking_feature.rs @@ -0,0 +1,2711 @@ +//! Duplicating tests for `cargo update --precise` with unstable-options +//! enabled. This will make sure we check backward compatibility when the +//! capability of making breaking changes has been implemented. When that +//! feature is stabilized, this file can be deleted. + +use cargo_test_support::compare::assert_e2e; +use cargo_test_support::prelude::*; +use cargo_test_support::registry::{self}; +use cargo_test_support::registry::{Dependency, Package}; +use cargo_test_support::{basic_lib_manifest, basic_manifest, git, project, str}; + +#[cargo_test] +fn minor_update_two_places() { + Package::new("log", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1" + foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + Package::new("log", "0.1.1").publish(); + + p.change_file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1.1" + "#, + ); + + p.cargo("check").run(); +} + +#[cargo_test] +fn transitive_minor_update() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.1.0").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + log = "0.1" + foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.1.1").dep("log", "0.1.1").publish(); + + // Note that `serde` isn't actually updated here! The default behavior for + // `update` right now is to as conservatively as possible attempt to satisfy + // an update. In this case we previously locked the dependency graph to `log + // 0.1.0`, but nothing on the command line says we're allowed to update + // that. As a result the update of `serde` here shouldn't update to `serde + // 0.1.1` as that would also force an update to `log 0.1.1`. + // + // Also note that this is probably counterintuitive and weird. We may wish + // to change this one day. + p.cargo("update -Zunstable-options serde") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 2 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn conservative() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.1.0").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + log = "0.1" + foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.1.1").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] serde v0.1.0 -> v0.1.1 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn update_via_new_dep() { + Package::new("log", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1" + # foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + Package::new("log", "0.1.1").publish(); + + p.uncomment_root_manifest(); + p.cargo("check").env("CARGO_LOG", "cargo=trace").run(); +} + +#[cargo_test] +fn update_via_new_member() { + Package::new("log", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [workspace] + # members = [ "foo" ] + + [dependencies] + log = "0.1" + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + Package::new("log", "0.1.1").publish(); + + p.uncomment_root_manifest(); + p.cargo("check").run(); +} + +#[cargo_test] +fn add_dep_deep_new_requirement() { + Package::new("log", "0.1.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + log = "0.1" + # bar = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("bar", "0.1.0").dep("log", "0.1.1").publish(); + + p.uncomment_root_manifest(); + p.cargo("check").run(); +} + +#[cargo_test] +fn everything_real_deep() { + Package::new("log", "0.1.0").publish(); + Package::new("foo", "0.1.0").dep("log", "0.1").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + foo = "0.1" + # bar = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("bar", "0.1.0").dep("log", "0.1.1").publish(); + + p.uncomment_root_manifest(); + p.cargo("check").run(); +} + +#[cargo_test] +fn change_package_version() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "a-foo" + version = "0.2.0-alpha" + edition = "2015" + authors = [] + + [dependencies] + bar = { path = "bar", version = "0.2.0-alpha" } + "#, + ) + .file("src/lib.rs", "") + .file("bar/Cargo.toml", &basic_manifest("bar", "0.2.0-alpha")) + .file("bar/src/lib.rs", "") + .file( + "Cargo.lock", + r#" + [[package]] + name = "foo" + version = "0.2.0" + dependencies = ["bar 0.2.0"] + + [[package]] + name = "bar" + version = "0.2.0" + "#, + ) + .build(); + + p.cargo("check").run(); +} + +#[cargo_test] +fn update_precise_downgrade() { + Package::new("serde", "0.1.0").publish(); + Package::new("serde", "0.2.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.2" + foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("serde", "0.2.0").publish(); + + p.cargo("update -Zunstable-options serde:0.2.1 --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[DOWNGRADING] serde v0.2.1 -> v0.2.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_mismatched() { + Package::new("serde", "1.2.0").publish(); + Package::new("serde", "1.2.1").publish(); + Package::new("serde", "1.6.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "~1.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + // `1.6.0` does not match `"~1.2"` + p.cargo("update -Zunstable-options serde:1.2 --precise 1.6.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] failed to select a version for the requirement `serde = "~1.2"` +candidate versions found which didn't match: 1.6.0 +location searched: `dummy-registry` index (which is replacing registry `crates-io`) +required by package `bar v0.0.1 ([ROOT]/foo)` +perhaps a crate was updated and forgotten to be re-vendored? + +"#]]) + .with_status(101) + .run(); + + // `1.9.0` does not exist + p.cargo("update -Zunstable-options serde:1.2 --precise 1.9.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + // This terrible error message has been the same for a long time. A fix is more than welcome! + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] no matching package named `serde` found +location searched: registry `crates-io` +required by package `bar v0.0.1 ([ROOT]/foo)` + +"#]]) + .with_status(101) + .run(); +} + +#[cargo_test] +fn update_precise_build_metadata() { + Package::new("serde", "0.0.1+first").publish(); + Package::new("serde", "0.0.1+second").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.0" + edition = "2015" + + [dependencies] + serde = "0.0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + p.cargo("update -Zunstable-options serde --precise 0.0.1+first") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .run(); + + p.cargo("update -Zunstable-options serde --precise 0.0.1+second") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] serde v0.0.1+first -> v0.0.1+second + +"#]]) + .run(); + + // This is not considered "Downgrading". Build metadata are not assumed to + // be ordered. + p.cargo("update -Zunstable-options serde --precise 0.0.1+first") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] serde v0.0.1+second -> v0.0.1+first + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_do_not_force_update_deps() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.2.1").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.2.2").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde:0.2.1 --precise 0.2.2") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] serde v0.2.1 -> v0.2.2 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn update_recursive() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.2.1").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.2.2").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde:0.2.1 --recursive") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[UPDATING] log v0.1.0 -> v0.1.1 +[UPDATING] serde v0.2.1 -> v0.2.2 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_aggressive_alias_for_recursive() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.2.1").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.2.2").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde:0.2.1 --aggressive") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 2 packages to latest compatible versions +[UPDATING] log v0.1.0 -> v0.1.1 +[UPDATING] serde v0.2.1 -> v0.2.2 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_recursive_conflicts_with_precise() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.2.1").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("check").run(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.2.2").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde:0.2.1 --precise 0.2.2 --recursive") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(1) + .with_stderr_data(str![[r#" +[ERROR] the argument '--precise ' cannot be used with '--recursive' + +Usage: cargo update -Z --precise ]> + +For more information, try '--help'. + +"#]]) + .run(); +} + +// cargo update should respect its arguments even without a lockfile. +// See issue "Running cargo update without a Cargo.lock ignores arguments" +// at . +#[cargo_test] +fn update_precise_first_run() { + Package::new("serde", "0.1.0").publish(); + Package::new("serde", "0.2.0").publish(); + Package::new("serde", "0.2.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + + [dependencies] + serde = "0.2" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("update -Zunstable-options serde --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[DOWNGRADING] serde v0.2.1 -> v0.2.0 + +"#]]) + .run(); + + // Assert `cargo metadata` shows serde 0.2.0 + p.cargo("metadata") + .with_stdout_data( + str![[r#" +{ + "metadata": null, + "packages": [ + { + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [ + { + "features": [], + "kind": null, + "name": "serde", + "optional": false, + "registry": null, + "rename": null, + "req": "^0.2", + "source": "registry+https://github.com/rust-lang/crates.io-index", + "target": null, + "uses_default_features": true + } + ], + "description": null, + "documentation": null, + "edition": "2015", + "features": {}, + "homepage": null, + "id": "path+[ROOTURL]/foo#bar@0.0.1", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[ROOT]/foo/Cargo.toml", + "metadata": null, + "name": "bar", + "publish": null, + "readme": null, + "repository": null, + "rust_version": null, + "source": null, + "targets": [ + { + "crate_types": [ + "lib" + ], + "doc": true, + "doctest": true, + "edition": "2015", + "kind": [ + "lib" + ], + "name": "bar", + "src_path": "[ROOT]/foo/src/lib.rs", + "test": true + } + ], + "version": "0.0.1" + }, + { + "authors": [], + "categories": [], + "default_run": null, + "dependencies": [], + "description": null, + "documentation": null, + "edition": "2015", + "features": {}, + "homepage": null, + "id": "registry+https://github.com/rust-lang/crates.io-index#serde@0.2.0", + "keywords": [], + "license": null, + "license_file": null, + "links": null, + "manifest_path": "[ROOT]/home/.cargo/registry/src/-[HASH]/serde-0.2.0/Cargo.toml", + "metadata": null, + "name": "serde", + "publish": null, + "readme": null, + "repository": null, + "rust_version": null, + "source": "registry+https://github.com/rust-lang/crates.io-index", + "targets": [ + { + "crate_types": [ + "lib" + ], + "doc": true, + "doctest": true, + "edition": "2015", + "kind": [ + "lib" + ], + "name": "serde", + "src_path": "[ROOT]/home/.cargo/registry/src/-[HASH]/serde-0.2.0/src/lib.rs", + "test": true + } + ], + "version": "0.2.0" + } + ], + "resolve": { + "nodes": [ + { + "dependencies": [ + "registry+https://github.com/rust-lang/crates.io-index#serde@0.2.0" + ], + "deps": [ + { + "dep_kinds": [ + { + "kind": null, + "target": null + } + ], + "name": "serde", + "pkg": "registry+https://github.com/rust-lang/crates.io-index#serde@0.2.0" + } + ], + "features": [], + "id": "path+[ROOTURL]/foo#bar@0.0.1" + }, + { + "dependencies": [], + "deps": [], + "features": [], + "id": "registry+https://github.com/rust-lang/crates.io-index#serde@0.2.0" + } + ], + "root": "path+[ROOTURL]/foo#bar@0.0.1" + }, + "target_directory": "[ROOT]/foo/target", + "version": 1, + "workspace_default_members": [ + "path+[ROOTURL]/foo#bar@0.0.1" + ], + "workspace_members": [ + "path+[ROOTURL]/foo#bar@0.0.1" + ], + "workspace_root": "[ROOT]/foo" +} +"#]] + .json(), + ) + .run(); + + p.cargo("update -Zunstable-options serde --precise 0.2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index + +"#]]) + .run(); +} + +#[cargo_test] +fn preserve_top_comment() { + let p = project().file("src/lib.rs", "").build(); + + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .run(); + + let lockfile = p.read_lockfile(); + assert!(lockfile.starts_with("# This file is automatically @generated by Cargo.\n# It is not intended for manual editing.\n")); + + let mut lines = lockfile.lines().collect::>(); + lines.insert(2, "# some other comment"); + let mut lockfile = lines.join("\n"); + lockfile.push('\n'); // .lines/.join loses the last newline + println!("saving Cargo.lock contents:\n{}", lockfile); + + p.change_file("Cargo.lock", &lockfile); + + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .run(); + + let lockfile2 = p.read_lockfile(); + println!("loaded Cargo.lock contents:\n{}", lockfile2); + + assert_eq!(lockfile, lockfile2); +} + +#[cargo_test] +fn dry_run_update() { + Package::new("log", "0.1.0").publish(); + Package::new("serde", "0.1.0").dep("log", "0.1").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + log = "0.1" + foo = { path = "foo" } + "#, + ) + .file("src/lib.rs", "") + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + serde = "0.1" + "#, + ) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("check").run(); + let old_lockfile = p.read_lockfile(); + + Package::new("log", "0.1.1").publish(); + Package::new("serde", "0.1.1").dep("log", "0.1").publish(); + + p.cargo("update -Zunstable-options serde --dry-run") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] serde v0.1.0 -> v0.1.1 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest +[WARNING] aborting update due to dry run + +"#]]) + .run(); + let new_lockfile = p.read_lockfile(); + assert_eq!(old_lockfile, new_lockfile) +} + +#[cargo_test] +fn workspace_only() { + let p = project().file("src/main.rs", "fn main() {}").build(); + p.cargo("generate-lockfile").run(); + let lock1 = p.read_lockfile(); + + p.change_file( + "Cargo.toml", + r#" + [package] + name = "foo" + authors = [] + version = "0.0.2" + edition = "2015" + "#, + ); + p.cargo("update -Zunstable-options --workspace") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .run(); + let lock2 = p.read_lockfile(); + + assert_ne!(lock1, lock2); + assert!(lock1.contains("0.0.1")); + assert!(lock2.contains("0.0.2")); + assert!(!lock1.contains("0.0.2")); + assert!(!lock2.contains("0.0.1")); +} + +#[cargo_test] +fn precise_with_build_metadata() { + // +foo syntax shouldn't be necessary with --precise + Package::new("bar", "0.1.0+extra-stuff.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + bar = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + p.cargo("generate-lockfile").run(); + Package::new("bar", "0.1.1+extra-stuff.1").publish(); + Package::new("bar", "0.1.2+extra-stuff.2").publish(); + + p.cargo("update -Zunstable-options bar --precise 0.1") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] invalid version format for precise version `0.1` + +Caused by: + unexpected end of input while parsing minor version number + +"#]]) + .run(); + + p.cargo("update -Zunstable-options bar --precise 0.1.1+does-not-match") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] no matching package named `bar` found +location searched: registry `crates-io` +required by package `foo v0.1.0 ([ROOT]/foo)` + +"#]]) + .run(); + + p.cargo("update -Zunstable-options bar --precise 0.1.1") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] bar v0.1.0+extra-stuff.0 -> v0.1.1+extra-stuff.1 + +"#]]) + .run(); + + Package::new("bar", "0.1.3").publish(); + p.cargo("update -Zunstable-options bar --precise 0.1.3+foo") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[ERROR] no matching package named `bar` found +location searched: registry `crates-io` +required by package `foo v0.1.0 ([ROOT]/foo)` + +"#]]) + .run(); + + p.cargo("update -Zunstable-options bar --precise 0.1.3") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPDATING] bar v0.1.1+extra-stuff.1 -> v0.1.3 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_only_members_order_one() { + let git_project = git::new("rustdns", |project| { + project + .file("Cargo.toml", &basic_lib_manifest("rustdns")) + .file("src/lib.rs", "pub fn bar() {}") + }); + + let workspace_toml = format!( + r#" +[workspace.package] +version = "2.29.8" +edition = "2021" +publish = false + +[workspace] +members = [ + "rootcrate", + "subcrate", +] +resolver = "2" + +[workspace.dependencies] +# Internal crates +subcrate = {{ version = "*", path = "./subcrate" }} + +# External dependencies +rustdns = {{ version = "0.5.0", default-features = false, git = "{}" }} + "#, + git_project.url() + ); + let p = project() + .file("Cargo.toml", &workspace_toml) + .file( + "rootcrate/Cargo.toml", + r#" +[package] +name = "rootcrate" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +subcrate.workspace = true +"#, + ) + .file("rootcrate/src/main.rs", "fn main() {}") + .file( + "subcrate/Cargo.toml", + r#" +[package] +name = "subcrate" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +rustdns.workspace = true +"#, + ) + .file("subcrate/src/lib.rs", "pub foo() {}") + .build(); + + // First time around we should compile both foo and bar + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[UPDATING] git repository `[ROOTURL]/rustdns` +[LOCKING] 3 packages to latest compatible versions + +"#]]) + .run(); + // Modify a file manually, shouldn't trigger a recompile + git_project.change_file("src/lib.rs", r#"pub fn bar() { println!("hello!"); }"#); + // Commit the changes and make sure we don't trigger a recompile because the + // lock file says not to change + let repo = git2::Repository::open(&git_project.root()).unwrap(); + git::add(&repo); + git::commit(&repo); + p.change_file("Cargo.toml", &workspace_toml.replace("2.29.8", "2.29.81")); + + p.cargo("update -Zunstable-options -p rootcrate") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[LOCKING] 2 packages to latest compatible versions +[UPDATING] rootcrate v2.29.8 ([ROOT]/foo/rootcrate) -> v2.29.81 +[UPDATING] subcrate v2.29.8 ([ROOT]/foo/subcrate) -> v2.29.81 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_only_members_order_two() { + let git_project = git::new("rustdns", |project| { + project + .file("Cargo.toml", &basic_lib_manifest("rustdns")) + .file("src/lib.rs", "pub fn bar() {}") + }); + + let workspace_toml = format!( + r#" +[workspace.package] +version = "2.29.8" +edition = "2021" +publish = false + +[workspace] +members = [ + "crate2", + "crate1", +] +resolver = "2" + +[workspace.dependencies] +# Internal crates +crate1 = {{ version = "*", path = "./crate1" }} + +# External dependencies +rustdns = {{ version = "0.5.0", default-features = false, git = "{}" }} + "#, + git_project.url() + ); + let p = project() + .file("Cargo.toml", &workspace_toml) + .file( + "crate2/Cargo.toml", + r#" +[package] +name = "crate2" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +crate1.workspace = true +"#, + ) + .file("crate2/src/main.rs", "fn main() {}") + .file( + "crate1/Cargo.toml", + r#" +[package] +name = "crate1" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +rustdns.workspace = true +"#, + ) + .file("crate1/src/lib.rs", "pub foo() {}") + .build(); + + // First time around we should compile both foo and bar + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[UPDATING] git repository `[ROOTURL]/rustdns` +[LOCKING] 3 packages to latest compatible versions + +"#]]) + .run(); + // Modify a file manually, shouldn't trigger a recompile + git_project.change_file("src/lib.rs", r#"pub fn bar() { println!("hello!"); }"#); + // Commit the changes and make sure we don't trigger a recompile because the + // lock file says not to change + let repo = git2::Repository::open(&git_project.root()).unwrap(); + git::add(&repo); + git::commit(&repo); + p.change_file("Cargo.toml", &workspace_toml.replace("2.29.8", "2.29.81")); + + p.cargo("update -Zunstable-options -p crate2") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[LOCKING] 2 packages to latest compatible versions +[UPDATING] crate1 v2.29.8 ([ROOT]/foo/crate1) -> v2.29.81 +[UPDATING] crate2 v2.29.8 ([ROOT]/foo/crate2) -> v2.29.81 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_only_members_with_workspace() { + let git_project = git::new("rustdns", |project| { + project + .file("Cargo.toml", &basic_lib_manifest("rustdns")) + .file("src/lib.rs", "pub fn bar() {}") + }); + + let workspace_toml = format!( + r#" +[workspace.package] +version = "2.29.8" +edition = "2021" +publish = false + +[workspace] +members = [ + "crate2", + "crate1", +] +resolver = "2" + +[workspace.dependencies] +# Internal crates +crate1 = {{ version = "*", path = "./crate1" }} + +# External dependencies +rustdns = {{ version = "0.5.0", default-features = false, git = "{}" }} + "#, + git_project.url() + ); + let p = project() + .file("Cargo.toml", &workspace_toml) + .file( + "crate2/Cargo.toml", + r#" +[package] +name = "crate2" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +crate1.workspace = true +"#, + ) + .file("crate2/src/main.rs", "fn main() {}") + .file( + "crate1/Cargo.toml", + r#" +[package] +name = "crate1" +version.workspace = true +edition.workspace = true +publish.workspace = true + +[dependencies] +rustdns.workspace = true +"#, + ) + .file("crate1/src/lib.rs", "pub foo() {}") + .build(); + + // First time around we should compile both foo and bar + p.cargo("generate-lockfile") + .with_stderr_data(str![[r#" +[UPDATING] git repository `[ROOTURL]/rustdns` +[LOCKING] 3 packages to latest compatible versions + +"#]]) + .run(); + // Modify a file manually, shouldn't trigger a recompile + git_project.change_file("src/lib.rs", r#"pub fn bar() { println!("hello!"); }"#); + // Commit the changes and make sure we don't trigger a recompile because the + // lock file says not to change + let repo = git2::Repository::open(&git_project.root()).unwrap(); + git::add(&repo); + git::commit(&repo); + p.change_file("Cargo.toml", &workspace_toml.replace("2.29.8", "2.29.81")); + + p.cargo("update -Zunstable-options --workspace") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[LOCKING] 2 packages to latest compatible versions +[UPDATING] crate1 v2.29.8 ([ROOT]/foo/crate1) -> v2.29.81 +[UPDATING] crate2 v2.29.8 ([ROOT]/foo/crate2) -> v2.29.81 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_precise_git_revisions() { + let (git_project, git_repo) = git::new_repo("git", |p| { + p.file("Cargo.toml", &basic_lib_manifest("git")) + .file("src/lib.rs", "") + }); + let tag_name = "Nazgûl"; + git::tag(&git_repo, tag_name); + let tag_commit_id = git_repo.head().unwrap().target().unwrap().to_string(); + + git_project.change_file("src/lib.rs", "fn f() {}"); + git::add(&git_repo); + let head_id = git::commit(&git_repo).to_string(); + let short_id = &head_id[..8]; + let url = git_project.url(); + + let p = project() + .file( + "Cargo.toml", + &format!( + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + git = {{ git = '{url}' }} + "# + ), + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("fetch") + .with_stderr_data(str![[r#" +[UPDATING] git repository `[ROOTURL]/git` +[LOCKING] 2 packages to latest compatible versions + +"#]]) + .run(); + + assert!(p.read_lockfile().contains(&head_id)); + + p.cargo("update -Zunstable-options git --precise") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .arg(tag_name) + .with_stderr_data(format!( + "\ +[UPDATING] git repository `[ROOTURL]/git` +[UPDATING] git v0.5.0 ([ROOTURL]/git#[..]) -> #{} +", + &tag_commit_id[..8], + )) + .run(); + + assert!(p.read_lockfile().contains(&tag_commit_id)); + assert!(!p.read_lockfile().contains(&head_id)); + + p.cargo("update -Zunstable-options git --precise") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .arg(short_id) + .with_stderr_data(format!( + "\ +[UPDATING] git repository `[ROOTURL]/git` +[UPDATING] git v0.5.0 ([ROOTURL]/git[..]) -> #{short_id} +", + )) + .run(); + + assert!(p.read_lockfile().contains(&head_id)); + assert!(!p.read_lockfile().contains(&tag_commit_id)); + + // updating back to tag still requires a git fetch, + // as the ref may change over time. + p.cargo("update -Zunstable-options git --precise") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .arg(tag_name) + .with_stderr_data(format!( + "\ +[UPDATING] git repository `[ROOTURL]/git` +[UPDATING] git v0.5.0 ([ROOTURL]/git#[..]) -> #{} +", + &tag_commit_id[..8], + )) + .run(); + + assert!(p.read_lockfile().contains(&tag_commit_id)); + assert!(!p.read_lockfile().contains(&head_id)); + + // Now make a tag looks like an oid. + // It requires a git fetch, as the oid cannot be found in preexisting git db. + let arbitrary_tag: String = std::iter::repeat('a').take(head_id.len()).collect(); + git::tag(&git_repo, &arbitrary_tag); + + p.cargo("update -Zunstable-options git --precise") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .arg(&arbitrary_tag) + .with_stderr_data(format!( + "\ +[UPDATING] git repository `[ROOTURL]/git` +[UPDATING] git v0.5.0 ([ROOTURL]/git#[..]) -> #{} +", + &head_id[..8], + )) + .run(); + + assert!(p.read_lockfile().contains(&head_id)); + assert!(!p.read_lockfile().contains(&tag_commit_id)); +} + +#[cargo_test] +fn update_precise_yanked() { + Package::new("bar", "0.1.0").publish(); + Package::new("bar", "0.1.1").yanked(true).publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + + [dependencies] + bar = "0.1" + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + // Use non-yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); + + p.cargo("update -Zunstable-options --precise 0.1.1 bar") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[WARNING] selected package `bar@0.1.1` was yanked by the author +[NOTE] if possible, try a compatible non-yanked version +[UPDATING] bar v0.1.0 -> v0.1.1 + +"#]]) + .run(); + + // Use yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); +} + +#[cargo_test] +fn update_precise_yanked_multiple_presence() { + Package::new("bar", "0.1.0").publish(); + Package::new("bar", "0.1.1").yanked(true).publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + + [dependencies] + bar = "0.1" + baz = { package = "bar", version = "0.1" } + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + // Use non-yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.0\"")); + + p.cargo("update -Zunstable-options --precise 0.1.1 bar") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[WARNING] selected package `bar@0.1.1` was yanked by the author +[NOTE] if possible, try a compatible non-yanked version +[UPDATING] bar v0.1.0 -> v0.1.1 + +"#]]) + .run(); + + // Use yanked version. + let lockfile = p.read_lockfile(); + assert!(lockfile.contains("\nname = \"bar\"\nversion = \"0.1.1\"")); +} + +#[cargo_test] +fn report_behind() { + Package::new("two-ver", "0.1.0").publish(); + Package::new("two-ver", "0.2.0").publish(); + Package::new("pre", "1.0.0-alpha.0").publish(); + Package::new("pre", "1.0.0-alpha.1").publish(); + Package::new("breaking", "0.1.0").publish(); + Package::new("breaking", "0.2.0").publish(); + Package::new("breaking", "0.2.1-alpha.0").publish(); + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + + [dependencies] + breaking = "0.1" + pre = "=1.0.0-alpha.0" + two-ver = "0.2.0" + two-ver-one = { version = "0.1.0", package = "two-ver" } + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + Package::new("breaking", "0.1.1").publish(); + + p.cargo("update -Zunstable-options --dry-run") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] breaking v0.1.0 -> v0.1.1 (latest: v0.2.0) +[NOTE] pass `--verbose` to see 2 unchanged dependencies behind latest +[WARNING] aborting update due to dry run + +"#]]) + .run(); + + p.cargo("update -Zunstable-options --dry-run --verbose") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] breaking v0.1.0 -> v0.1.1 (latest: v0.2.0) +[UNCHANGED] pre v1.0.0-alpha.0 (latest: v1.0.0-alpha.1) +[UNCHANGED] two-ver v0.1.0 (latest: v0.2.0) +[NOTE] to see how you depend on a package, run `cargo tree --invert --package @` +[WARNING] aborting update due to dry run + +"#]]) + .run(); + + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .run(); + + p.cargo("update -Zunstable-options --dry-run") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 3 unchanged dependencies behind latest +[WARNING] aborting update due to dry run + +"#]]) + .run(); + + p.cargo("update -Zunstable-options --dry-run --verbose") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[UNCHANGED] breaking v0.1.1 (latest: v0.2.0) +[UNCHANGED] pre v1.0.0-alpha.0 (latest: v1.0.0-alpha.1) +[UNCHANGED] two-ver v0.1.0 (latest: v0.2.0) +[NOTE] to see how you depend on a package, run `cargo tree --invert --package @` +[WARNING] aborting update due to dry run + +"#]]) + .run(); +} + +#[cargo_test] +fn update_with_missing_feature() { + // Attempting to update a package to a version with a missing feature + // should produce a warning. + Package::new("bar", "0.1.0").feature("feat1", &[]).publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.1.0" + edition = "2015" + + [dependencies] + bar = {version="0.1", features=["feat1"]} + "#, + ) + .file("src/lib.rs", "") + .build(); + p.cargo("generate-lockfile").run(); + + // Publish an update that is missing the feature. + Package::new("bar", "0.1.1").publish(); + + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // Publish a fixed version, should not warn. + Package::new("bar", "0.1.2").feature("feat1", &[]).publish(); + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] bar v0.1.0 -> v0.1.2 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_unstable() { + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("update --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] the `--breaking` flag is unstable, pass `-Z unstable-options` to enable it +See https://github.com/rust-lang/cargo/issues/12425 for more information about the `--breaking` flag. + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_dry_run() { + Package::new("incompatible", "1.0.0").publish(); + Package::new("ws", "1.0.0").publish(); + + let root_manifest = r#" + # Check if formatting is preserved. Nothing here should change, due to dry-run. + + [workspace] + members = ["foo"] + + [workspace.dependencies] + ws = "1.0" # Preserve formatting + "#; + + let crate_manifest = r#" + # Check if formatting is preserved. Nothing here should change, due to dry-run. + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + incompatible = "1.0" # Preserve formatting + ws.workspace = true # Preserve formatting + "#; + + let p = project() + .file("Cargo.toml", root_manifest) + .file("foo/Cargo.toml", crate_manifest) + .file("foo/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + let lock_file = p.read_file("Cargo.lock"); + + Package::new("incompatible", "1.0.1").publish(); + Package::new("ws", "1.0.1").publish(); + + Package::new("incompatible", "2.0.0").publish(); + Package::new("ws", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --dry-run --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] incompatible ^1.0 -> ^2.0 +[UPGRADING] ws ^1.0 -> ^2.0 +[LOCKING] 2 packages to latest compatible versions +[UPDATING] incompatible v1.0.0 -> v2.0.0 +[UPDATING] ws v1.0.0 -> v2.0.0 +[WARNING] aborting update due to dry run + +"#]]) + .run(); + + let root_manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&root_manifest_after, root_manifest); + + let crate_manifest_after = p.read_file("foo/Cargo.toml"); + assert_e2e().eq(&crate_manifest_after, crate_manifest); + + let lock_file_after = p.read_file("Cargo.lock"); + assert_e2e().eq(&lock_file_after, lock_file); +} + +#[cargo_test] +fn update_breaking() { + registry::alt_init(); + Package::new("compatible", "1.0.0").publish(); + Package::new("incompatible", "1.0.0").publish(); + Package::new("pinned", "1.0.0").publish(); + Package::new("less-than", "1.0.0").publish(); + Package::new("renamed-from", "1.0.0").publish(); + Package::new("pre-release", "1.0.0").publish(); + Package::new("yanked", "1.0.0").publish(); + Package::new("ws", "1.0.0").publish(); + Package::new("shared", "1.0.0").publish(); + Package::new("multiple-locations", "1.0.0").publish(); + Package::new("multiple-versions", "1.0.0").publish(); + Package::new("multiple-versions", "2.0.0").publish(); + Package::new("alternative-1", "1.0.0") + .alternative(true) + .publish(); + Package::new("alternative-2", "1.0.0") + .alternative(true) + .publish(); + Package::new("bar", "1.0.0").alternative(true).publish(); + Package::new("multiple-registries", "1.0.0").publish(); + Package::new("multiple-registries", "2.0.0") + .alternative(true) + .publish(); + Package::new("multiple-source-types", "1.0.0").publish(); + Package::new("platform-specific", "1.0.0").publish(); + Package::new("dev", "1.0.0").publish(); + Package::new("build", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + # Check if formatting is preserved + + [workspace] + members = ["foo", "bar"] + + [workspace.dependencies] + ws = "1.0" # This line gets partially rewritten + "#, + ) + .file( + "foo/Cargo.toml", + r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "1.0" # Comment + incompatible = "1.0" # Comment + pinned = "=1.0" # Comment + less-than = "<99.0" # Comment + renamed-to = { package = "renamed-from", version = "1.0" } # Comment + pre-release = "1.0" # Comment + yanked = "1.0" # Comment + ws.workspace = true # Comment + shared = "1.0" # Comment + multiple-locations = { path = "../multiple-locations", version = "1.0" } # Comment + multiple-versions = "1.0" # Comment + alternative-1 = { registry = "alternative", version = "1.0" } # Comment + multiple-registries = "1.0" # Comment + bar = { path = "../bar", registry = "alternative", version = "1.0.0" } # Comment + multiple-source-types = { path = "../multiple-source-types", version = "1.0.0" } # Comment + + [dependencies.alternative-2] # Comment + version = "1.0" # Comment + registry = "alternative" # Comment + + [target.'cfg(unix)'.dependencies] + platform-specific = "1.0" # Comment + + [dev-dependencies] + dev = "1.0" # Comment + + [build-dependencies] + build = "1.0" # Comment + "#, + ) + .file("foo/src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + authors = [] + + [dependencies] + shared = "1.0" + multiple-versions = "2.0" + multiple-registries = { registry = "alternative", version = "2.0" } # Comment + multiple-source-types = "1.0" # Comment + "#, + ) + .file("bar/src/lib.rs", "") + .file( + "multiple-locations/Cargo.toml", + r#" + [package] + name = "multiple-locations" + version = "1.0.0" + edition = "2015" + authors = [] + "#, + ) + .file("multiple-locations/src/lib.rs", "") + .file( + "multiple-source-types/Cargo.toml", + r#" + [package] + name = "multiple-source-types" + version = "1.0.0" + edition = "2015" + authors = [] + "#, + ) + .file("multiple-source-types/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("compatible", "1.0.1").publish(); + Package::new("incompatible", "1.0.1").publish(); + Package::new("pinned", "1.0.1").publish(); + Package::new("less-than", "1.0.1").publish(); + Package::new("renamed-from", "1.0.1").publish(); + Package::new("ws", "1.0.1").publish(); + Package::new("multiple-locations", "1.0.1").publish(); + Package::new("multiple-versions", "1.0.1").publish(); + Package::new("multiple-versions", "2.0.1").publish(); + Package::new("alternative-1", "1.0.1") + .alternative(true) + .publish(); + Package::new("alternative-2", "1.0.1") + .alternative(true) + .publish(); + Package::new("platform-specific", "1.0.1").publish(); + Package::new("dev", "1.0.1").publish(); + Package::new("build", "1.0.1").publish(); + + Package::new("incompatible", "2.0.0").publish(); + Package::new("pinned", "2.0.0").publish(); + Package::new("less-than", "2.0.0").publish(); + Package::new("renamed-from", "2.0.0").publish(); + Package::new("pre-release", "2.0.0-alpha").publish(); + Package::new("yanked", "2.0.0").yanked(true).publish(); + Package::new("ws", "2.0.0").publish(); + Package::new("shared", "2.0.0").publish(); + Package::new("multiple-locations", "2.0.0").publish(); + Package::new("multiple-versions", "3.0.0").publish(); + Package::new("alternative-1", "2.0.0") + .alternative(true) + .publish(); + Package::new("alternative-2", "2.0.0") + .alternative(true) + .publish(); + Package::new("bar", "2.0.0").alternative(true).publish(); + Package::new("multiple-registries", "2.0.0").publish(); + Package::new("multiple-registries", "3.0.0") + .alternative(true) + .publish(); + Package::new("multiple-source-types", "2.0.0").publish(); + Package::new("platform-specific", "2.0.0").publish(); + Package::new("dev", "2.0.0").publish(); + Package::new("build", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `alternative` index +[UPGRADING] multiple-registries ^2.0 -> ^3.0 +[UPDATING] `dummy-registry` index +[UPGRADING] multiple-source-types ^1.0 -> ^2.0 +[UPGRADING] multiple-versions ^2.0 -> ^3.0 +[UPGRADING] shared ^1.0 -> ^2.0 +[UPGRADING] alternative-1 ^1.0 -> ^2.0 +[UPGRADING] alternative-2 ^1.0 -> ^2.0 +[UPGRADING] incompatible ^1.0 -> ^2.0 +[UPGRADING] multiple-registries ^1.0 -> ^2.0 +[UPGRADING] multiple-versions ^1.0 -> ^3.0 +[UPGRADING] ws ^1.0 -> ^2.0 +[UPGRADING] dev ^1.0 -> ^2.0 +[UPGRADING] build ^1.0 -> ^2.0 +[UPGRADING] platform-specific ^1.0 -> ^2.0 +[LOCKING] 12 packages to latest compatible versions +[UPDATING] alternative-1 v1.0.0 (registry `alternative`) -> v2.0.0 +[UPDATING] alternative-2 v1.0.0 (registry `alternative`) -> v2.0.0 +[UPDATING] build v1.0.0 -> v2.0.0 +[UPDATING] dev v1.0.0 -> v2.0.0 +[UPDATING] incompatible v1.0.0 -> v2.0.0 +[UPDATING] multiple-registries v2.0.0 (registry `alternative`) -> v3.0.0 +[UPDATING] multiple-registries v1.0.0 -> v2.0.0 +[UPDATING] multiple-source-types v1.0.0 -> v2.0.0 +[REMOVING] multiple-versions v1.0.0 +[REMOVING] multiple-versions v2.0.0 +[ADDING] multiple-versions v3.0.0 +[UPDATING] platform-specific v1.0.0 -> v2.0.0 +[UPDATING] shared v1.0.0 -> v2.0.0 +[UPDATING] ws v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 4 unchanged dependencies behind latest + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + # Check if formatting is preserved + + [workspace] + members = ["foo", "bar"] + + [workspace.dependencies] + ws = "2.0" # This line gets partially rewritten + "#]], + ); + + let foo_manifest = p.read_file("foo/Cargo.toml"); + + assert_e2e().eq( + &foo_manifest, + str![[r#" + + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "1.0" # Comment + incompatible = "2.0" # Comment + pinned = "=1.0" # Comment + less-than = "<99.0" # Comment + renamed-to = { package = "renamed-from", version = "1.0" } # Comment + pre-release = "1.0" # Comment + yanked = "1.0" # Comment + ws.workspace = true # Comment + shared = "2.0" # Comment + multiple-locations = { path = "../multiple-locations", version = "1.0" } # Comment + multiple-versions = "3.0" # Comment + alternative-1 = { registry = "alternative", version = "2.0" } # Comment + multiple-registries = "2.0" # Comment + bar = { path = "../bar", registry = "alternative", version = "1.0.0" } # Comment + multiple-source-types = { path = "../multiple-source-types", version = "1.0.0" } # Comment + + [dependencies.alternative-2] # Comment + version = "2.0" # Comment + registry = "alternative" # Comment + + [target.'cfg(unix)'.dependencies] + platform-specific = "2.0" # Comment + + [dev-dependencies] + dev = "2.0" # Comment + + [build-dependencies] + build = "2.0" # Comment + "#]], + ); + + let bar_manifest = p.read_file("bar/Cargo.toml"); + + assert_e2e().eq( + &bar_manifest, + str![[r#" + + [package] + name = "bar" + version = "1.0.0" + edition = "2015" + authors = [] + + [dependencies] + shared = "2.0" + multiple-versions = "3.0" + multiple-registries = { registry = "alternative", version = "3.0" } # Comment + multiple-source-types = "2.0" # Comment + "#]], + ); + + p.cargo("update -Zunstable-options") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `alternative` index +[UPDATING] `dummy-registry` index +[LOCKING] 4 packages to latest compatible versions +[UPDATING] compatible v1.0.0 -> v1.0.1 +[UPDATING] less-than v1.0.0 -> v2.0.0 +[UPDATING] pinned v1.0.0 -> v1.0.1 (latest: v2.0.0) +[UPDATING] renamed-from v1.0.0 -> v1.0.1 (latest: v2.0.0) + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_specific_packages() { + Package::new("just-foo", "1.0.0") + .add_dep(Dependency::new("transitive-compatible", "1.0.0").build()) + .add_dep(Dependency::new("transitive-incompatible", "1.0.0").build()) + .publish(); + Package::new("just-bar", "1.0.0").publish(); + Package::new("shared", "1.0.0").publish(); + Package::new("ws", "1.0.0").publish(); + Package::new("transitive-compatible", "1.0.0").publish(); + Package::new("transitive-incompatible", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["foo", "bar"] + + [workspace.dependencies] + ws = "1.0" + "#, + ) + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + just-foo = "1.0" + shared = "1.0" + ws.workspace = true + "#, + ) + .file("foo/src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + just-bar = "1.0" + shared = "1.0" + ws.workspace = true + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("just-foo", "1.0.1") + .add_dep(Dependency::new("transitive-compatible", "1.0.0").build()) + .add_dep(Dependency::new("transitive-incompatible", "1.0.0").build()) + .publish(); + Package::new("just-bar", "1.0.1").publish(); + Package::new("shared", "1.0.1").publish(); + Package::new("ws", "1.0.1").publish(); + Package::new("transitive-compatible", "1.0.1").publish(); + Package::new("transitive-incompatible", "1.0.1").publish(); + + Package::new("just-foo", "2.0.0") + // Upgrading just-foo implies accepting an update of transitive-compatible. + .add_dep(Dependency::new("transitive-compatible", "1.0.1").build()) + // Upgrading just-foo implies accepting a major update of transitive-incompatible. + .add_dep(Dependency::new("transitive-incompatible", "2.0.0").build()) + .publish(); + Package::new("just-bar", "2.0.0").publish(); + Package::new("shared", "2.0.0").publish(); + Package::new("ws", "2.0.0").publish(); + Package::new("transitive-incompatible", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --breaking just-foo shared ws") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] shared ^1.0 -> ^2.0 +[UPGRADING] ws ^1.0 -> ^2.0 +[UPGRADING] just-foo ^1.0 -> ^2.0 +[LOCKING] 5 packages to latest compatible versions +[UPDATING] just-foo v1.0.0 -> v2.0.0 +[UPDATING] shared v1.0.0 -> v2.0.0 +[UPDATING] transitive-compatible v1.0.0 -> v1.0.1 +[UPDATING] transitive-incompatible v1.0.0 -> v2.0.0 +[UPDATING] ws v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_specific_packages_that_wont_update() { + Package::new("compatible", "1.0.0").publish(); + Package::new("renamed-from", "1.0.0").publish(); + Package::new("non-semver", "1.0.0").publish(); + Package::new("bar", "1.0.0") + .add_dep(Dependency::new("transitive-compatible", "1.0.0").build()) + .add_dep(Dependency::new("transitive-incompatible", "1.0.0").build()) + .publish(); + Package::new("transitive-compatible", "1.0.0").publish(); + Package::new("transitive-incompatible", "1.0.0").publish(); + + let crate_manifest = r#" + # Check if formatting is preserved + + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "1.0" # Comment + renamed-to = { package = "renamed-from", version = "1.0" } # Comment + non-semver = "~1.0" # Comment + bar = "1.0" # Comment + "#; + + let p = project() + .file("Cargo.toml", crate_manifest) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + let lock_file = p.read_file("Cargo.lock"); + + Package::new("compatible", "1.0.1").publish(); + Package::new("renamed-from", "1.0.1").publish(); + Package::new("non-semver", "1.0.1").publish(); + Package::new("transitive-compatible", "1.0.1").publish(); + Package::new("transitive-incompatible", "1.0.1").publish(); + + Package::new("renamed-from", "2.0.0").publish(); + Package::new("non-semver", "2.0.0").publish(); + Package::new("transitive-incompatible", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --breaking compatible renamed-from non-semver transitive-compatible transitive-incompatible") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 5 unchanged dependencies behind latest + +"#]]) + .run(); + + let crate_manifest_after = p.read_file("Cargo.toml"); + assert_e2e().eq(&crate_manifest_after, crate_manifest); + + let lock_file_after = p.read_file("Cargo.lock"); + assert_e2e().eq(&lock_file_after, lock_file); + + p.cargo( + "update compatible renamed-from non-semver transitive-compatible transitive-incompatible", + ) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 5 packages to latest compatible versions +[UPDATING] compatible v1.0.0 -> v1.0.1 +[UPDATING] non-semver v1.0.0 -> v1.0.1 (latest: v2.0.0) +[UPDATING] renamed-from v1.0.0 -> v1.0.1 (latest: v2.0.0) +[UPDATING] transitive-compatible v1.0.0 -> v1.0.1 +[UPDATING] transitive-incompatible v1.0.0 -> v1.0.1 (latest: v2.0.0) + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_without_lock_file() { + Package::new("compatible", "1.0.0").publish(); + Package::new("incompatible", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "1.0" # Comment + incompatible = "1.0" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + Package::new("compatible", "1.0.1").publish(); + Package::new("incompatible", "1.0.1").publish(); + + Package::new("incompatible", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] incompatible ^1.0 -> ^2.0 +[LOCKING] 3 packages to latest compatible versions + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_spec_version() { + Package::new("compatible", "1.0.0").publish(); + Package::new("incompatible", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + compatible = "1.0" # Comment + incompatible = "1.0" # Comment + "#, + ) + .file("src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("compatible", "1.0.1").publish(); + Package::new("incompatible", "1.0.1").publish(); + + Package::new("incompatible", "2.0.0").publish(); + + // Invalid spec + p.cargo("update -Zunstable-options --breaking incompatible@foo") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] expected a version like "1.32" + +"#]]) + .run(); + + // Spec version not matching our current dependencies + p.cargo("update -Zunstable-options --breaking incompatible@2.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `incompatible@2.0.0` did not match any packages +Did you mean one of these? + + incompatible@1.0.0 + +"#]]) + .run(); + + // Spec source not matching our current dependencies + p.cargo("update -Zunstable-options --breaking https://alternative.com#incompatible@1.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `https://alternative.com/#incompatible@1.0.0` did not match any packages +Did you mean one of these? + + incompatible@1.0.0 + +"#]]) + .run(); + + // Accepted spec + p.cargo("update -Zunstable-options --breaking incompatible@1.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] incompatible ^1.0 -> ^2.0 +[LOCKING] 1 package to latest compatible version +[UPDATING] incompatible v1.0.0 -> v2.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // Accepted spec, full format + Package::new("incompatible", "3.0.0").publish(); + p.cargo("update -Zunstable-options --breaking https://github.com/rust-lang/crates.io-index#incompatible@2.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] incompatible ^2.0 -> ^3.0 +[LOCKING] 1 package to latest compatible version +[UPDATING] incompatible v2.0.0 -> v3.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // Spec matches a dependency that will not be upgraded + p.cargo("update -Zunstable-options --breaking compatible@1.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // Non-existing versions + p.cargo("update -Zunstable-options --breaking incompatible@9.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `incompatible@9.0.0` did not match any packages +Did you mean one of these? + + incompatible@3.0.0 + +"#]]) + .run(); + + p.cargo("update -Zunstable-options --breaking compatible@9.0.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_status(101) + .with_stderr_data(str![[r#" +[ERROR] package ID specification `compatible@9.0.0` did not match any packages +Did you mean one of these? + + compatible@1.0.0 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_spec_version_transitive() { + Package::new("dep", "1.0.0").publish(); + Package::new("dep", "2.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + dep = "1.0" + bar = { path = "bar", version = "0.0.1" } + "#, + ) + .file("src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + dep = "2.0" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("dep", "2.0.1").publish(); + Package::new("dep", "3.0.0").publish(); + + // Will upgrade the direct dependency + p.cargo("update -Zunstable-options --breaking dep@1.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] dep ^1.0 -> ^3.0 +[LOCKING] 1 package to latest compatible version +[UPDATING] dep v1.0.0 -> v3.0.0 +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // But not the transitive one, because bar is not a workspace member + p.cargo("update -Zunstable-options --breaking dep@2.0") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 0 packages to latest compatible versions +[NOTE] pass `--verbose` to see 1 unchanged dependencies behind latest + +"#]]) + .run(); + + // A non-breaking update is different, as it will update transitive dependencies + p.cargo("update -Zunstable-options dep@2.0") + .masquerade_as_nightly_cargo(&["update-precise-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[LOCKING] 1 package to latest compatible version +[UPDATING] dep v2.0.0 -> v2.0.1 (latest: v3.0.0) + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_mixed_compatibility() { + Package::new("mixed-compatibility", "1.0.0").publish(); + Package::new("mixed-compatibility", "2.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["foo", "bar"] + "#, + ) + .file( + "foo/Cargo.toml", + r#" + [package] + name = "foo" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-compatibility = "1.0" + "#, + ) + .file("foo/src/lib.rs", "") + .file( + "bar/Cargo.toml", + r#" + [package] + name = "bar" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-compatibility = "2.0" + "#, + ) + .file("bar/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("mixed-compatibility", "2.0.1").publish(); + + p.cargo("update -Zunstable-options --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] mixed-compatibility ^1.0 -> ^2.0 +[LOCKING] 1 package to latest compatible version +[REMOVING] mixed-compatibility v1.0.0 +[REMOVING] mixed-compatibility v2.0.0 +[ADDING] mixed-compatibility v2.0.1 + +"#]]) + .run(); +} + +#[cargo_test] +fn update_breaking_mixed_pinning_renaming() { + Package::new("mixed-pinned", "1.0.0").publish(); + Package::new("mixed-ws-pinned", "1.0.0").publish(); + Package::new("renamed-from", "1.0.0").publish(); + + let p = project() + .file( + "Cargo.toml", + r#" + [workspace] + members = ["pinned", "unpinned", "mixed"] + + [workspace.dependencies] + mixed-ws-pinned = "=1.0" + "#, + ) + .file( + "pinned/Cargo.toml", + r#" + [package] + name = "pinned" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "=1.0" + mixed-ws-pinned.workspace = true + renamed-to = { package = "renamed-from", version = "1.0" } + "#, + ) + .file("pinned/src/lib.rs", "") + .file( + "unpinned/Cargo.toml", + r#" + [package] + name = "unpinned" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "1.0" + mixed-ws-pinned = "1.0" + renamed-from = "1.0" + "#, + ) + .file("unpinned/src/lib.rs", "") + .file( + "mixed/Cargo.toml", + r#" + [package] + name = "mixed" + version = "0.0.1" + edition = "2015" + authors = [] + + [target.'cfg(windows)'.dependencies] + mixed-pinned = "1.0" + + [target.'cfg(unix)'.dependencies] + mixed-pinned = "=1.0" + "#, + ) + .file("mixed/src/lib.rs", "") + .build(); + + p.cargo("generate-lockfile").run(); + + Package::new("mixed-pinned", "2.0.0").publish(); + Package::new("mixed-ws-pinned", "2.0.0").publish(); + Package::new("renamed-from", "2.0.0").publish(); + + p.cargo("update -Zunstable-options --breaking") + .masquerade_as_nightly_cargo(&["update-breaking"]) + .with_stderr_data(str![[r#" +[UPDATING] `dummy-registry` index +[UPGRADING] mixed-pinned ^1.0 -> ^2.0 +[UPGRADING] mixed-ws-pinned ^1.0 -> ^2.0 +[UPGRADING] renamed-from ^1.0 -> ^2.0 +[LOCKING] 3 packages to latest compatible versions +[ADDING] mixed-pinned v2.0.0 +[ADDING] mixed-ws-pinned v2.0.0 +[ADDING] renamed-from v2.0.0 +[NOTE] pass `--verbose` to see 3 unchanged dependencies behind latest + +"#]]) + .run(); + + let root_manifest = p.read_file("Cargo.toml"); + assert_e2e().eq( + &root_manifest, + str![[r#" + + [workspace] + members = ["pinned", "unpinned", "mixed"] + + [workspace.dependencies] + mixed-ws-pinned = "=1.0" + "#]], + ); + + let pinned_manifest = p.read_file("pinned/Cargo.toml"); + assert_e2e().eq( + &pinned_manifest, + str![[r#" + + [package] + name = "pinned" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "=1.0" + mixed-ws-pinned.workspace = true + renamed-to = { package = "renamed-from", version = "1.0" } + "#]], + ); + + let unpinned_manifest = p.read_file("unpinned/Cargo.toml"); + assert_e2e().eq( + &unpinned_manifest, + str![[r#" + + [package] + name = "unpinned" + version = "0.0.1" + edition = "2015" + authors = [] + + [dependencies] + mixed-pinned = "2.0" + mixed-ws-pinned = "2.0" + renamed-from = "2.0" + "#]], + ); + + let mixed_manifest = p.read_file("mixed/Cargo.toml"); + assert_e2e().eq( + &mixed_manifest, + str![[r#" + + [package] + name = "mixed" + version = "0.0.1" + edition = "2015" + authors = [] + + [target.'cfg(windows)'.dependencies] + mixed-pinned = "2.0" + + [target.'cfg(unix)'.dependencies] + mixed-pinned = "=1.0" + "#]], + ); +}