//! Tests for checking behavior of old cargos. //! //! These tests are ignored because it is intended to be run on a developer //! system with a bunch of toolchains installed. This requires `rustup` to be //! installed. It will iterate over installed toolchains, and run some tests //! over each one, producing a report at the end. As of this writing, I have //! tested 1.0 to 1.51. Run this with: //! //! ```console //! cargo test --test testsuite -- old_cargos --nocapture --ignored //! ``` use cargo::CargoResult; use cargo_test_support::paths::CargoPathExt; use cargo_test_support::registry::{self, Dependency, Package}; use cargo_test_support::{cargo_exe, execs, paths, process, project, rustc_host}; use cargo_util::{ProcessBuilder, ProcessError}; use semver::Version; use std::fs; fn tc_process(cmd: &str, toolchain: &str) -> ProcessBuilder { let mut p = if toolchain == "this" { if cmd == "cargo" { process(&cargo_exe()) } else { process(cmd) } } else { let mut cmd = process(cmd); cmd.arg(format!("+{}", toolchain)); cmd }; // Reset PATH since `process` modifies it to remove rustup. p.env("PATH", std::env::var_os("PATH").unwrap()); p } /// Returns a sorted list of all toolchains. /// /// The returned value includes the parsed version, and the rustup toolchain /// name as a string. fn collect_all_toolchains() -> Vec<(Version, String)> { let rustc_version = |tc| { let mut cmd = tc_process("rustc", tc); cmd.arg("-V"); let output = cmd.exec_with_output().expect("rustc installed"); let version = std::str::from_utf8(&output.stdout).unwrap(); let parts: Vec<_> = version.split_whitespace().collect(); assert_eq!(parts[0], "rustc"); assert!(parts[1].starts_with("1.")); Version::parse(parts[1]).expect("valid version") }; // Provide a way to override the list. if let Ok(tcs) = std::env::var("OLD_CARGO") { return tcs .split(',') .map(|tc| (rustc_version(tc), tc.to_string())) .collect(); } let host = rustc_host(); // I tend to have lots of toolchains installed, but I don't want to test // all of them (like dated nightlies, or toolchains for non-host targets). let valid_names = &[ format!("stable-{}", host), format!("beta-{}", host), format!("nightly-{}", host), ]; let output = ProcessBuilder::new("rustup") .args(&["toolchain", "list"]) .exec_with_output() .expect("rustup should be installed"); let stdout = std::str::from_utf8(&output.stdout).unwrap(); let mut toolchains: Vec<_> = stdout .lines() .map(|line| { // Some lines say things like (default), just get the version. line.split_whitespace().next().expect("non-empty line") }) .filter(|line| { line.ends_with(&host) && (line.starts_with("1.") || valid_names.iter().any(|name| name == line)) }) .map(|line| (rustc_version(line), line.to_string())) .collect(); // Also include *this* cargo. toolchains.push((rustc_version("this"), "this".to_string())); toolchains.sort_by(|a, b| a.0.cmp(&b.0)); toolchains } // This is a test for exercising the behavior of older versions of cargo with // the new feature syntax. // // The test involves a few dependencies with different feature requirements: // // * `bar` 1.0.0 is the base version that does not use the new syntax. // * `bar` 1.0.1 has a feature with the new syntax, but the feature is unused. // The optional dependency `new-baz-dep` should not be activated. // * `bar` 1.0.2 has a dependency on `baz` that *requires* the new feature // syntax. #[ignore] #[cargo_test] fn new_features() { if std::process::Command::new("rustup").output().is_err() { panic!("old_cargos requires rustup to be installed"); } Package::new("new-baz-dep", "1.0.0").publish(); Package::new("baz", "1.0.0").publish(); let baz101_cksum = Package::new("baz", "1.0.1") .add_dep(Dependency::new("new-baz-dep", "1.0").optional(true)) .feature("new-feat", &["dep:new-baz-dep"]) .publish(); let bar100_cksum = Package::new("bar", "1.0.0") .add_dep(Dependency::new("baz", "1.0").optional(true)) .feature("feat", &["baz"]) .publish(); let bar101_cksum = Package::new("bar", "1.0.1") .add_dep(Dependency::new("baz", "1.0").optional(true)) .feature("feat", &["dep:baz"]) .publish(); let bar102_cksum = Package::new("bar", "1.0.2") .add_dep(Dependency::new("baz", "1.0").enable_features(&["new-feat"])) .publish(); let p = project() .file( "Cargo.toml", r#" [package] name = "foo" version = "0.1.0" [dependencies] bar = "1.0" "#, ) .file("src/lib.rs", "") .build(); let lock_bar_to = |toolchain_version: &Version, bar_version| { let lock = if toolchain_version < &Version::new(1, 12, 0) { let url = registry::registry_url(); match bar_version { 100 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.0 (registry+{url})", ] [[package]] name = "bar" version = "1.0.0" source = "registry+{url}" "#, url = url ), 101 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.1 (registry+{url})", ] [[package]] name = "bar" version = "1.0.1" source = "registry+{url}" "#, url = url ), 102 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.2 (registry+{url})", ] [[package]] name = "bar" version = "1.0.2" source = "registry+{url}" dependencies = [ "baz 1.0.1 (registry+{url})", ] [[package]] name = "baz" version = "1.0.1" source = "registry+{url}" "#, url = url ), _ => panic!("unexpected version"), } } else { match bar_version { 100 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "bar" version = "1.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum bar 1.0.0 (registry+https://github.com/rust-lang/crates.io-index)" = "{}" "#, bar100_cksum ), 101 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "bar" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum bar 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{}" "#, bar101_cksum ), 102 => format!( r#" [root] name = "foo" version = "0.1.0" dependencies = [ "bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "bar" version = "1.0.2" source = "registry+https://github.com/rust-lang/crates.io-index" dependencies = [ "baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)", ] [[package]] name = "baz" version = "1.0.1" source = "registry+https://github.com/rust-lang/crates.io-index" [metadata] "checksum bar 1.0.2 (registry+https://github.com/rust-lang/crates.io-index)" = "{bar102_cksum}" "checksum baz 1.0.1 (registry+https://github.com/rust-lang/crates.io-index)" = "{baz101_cksum}" "#, bar102_cksum = bar102_cksum, baz101_cksum = baz101_cksum ), _ => panic!("unexpected version"), } }; p.change_file("Cargo.lock", &lock); }; let toolchains = collect_all_toolchains(); let config_path = paths::home().join(".cargo/config"); let lock_path = p.root().join("Cargo.lock"); struct ToolchainBehavior { bar: Option, baz: Option, new_baz_dep: Option, } // Collect errors to print at the end. One entry per toolchain, a list of // strings to print. let mut unexpected_results: Vec> = Vec::new(); for (version, toolchain) in &toolchains { let mut tc_result = Vec::new(); // Write a config appropriate for this version. if version < &Version::new(1, 12, 0) { fs::write( &config_path, format!( r#" [registry] index = "{}" "#, registry::registry_url() ), ) .unwrap(); } else { fs::write( &config_path, format!( " [source.crates-io] registry = 'https://wut' # only needed by 1.12 replace-with = 'dummy-registry' [source.dummy-registry] registry = '{}' ", registry::registry_url() ), ) .unwrap(); } // Fetches the version of a package in the lock file. let pkg_version = |pkg| -> Option { let output = tc_process("cargo", toolchain) .args(&["pkgid", pkg]) .cwd(p.root()) .exec_with_output() .ok()?; let stdout = std::str::from_utf8(&output.stdout).unwrap(); let version = stdout .trim() .rsplitn(2, ':') .next() .expect("version after colon"); Some(Version::parse(version).expect("parseable version")) }; // Runs `cargo build` and returns the versions selected in the lock. let run_cargo = || -> CargoResult { match tc_process("cargo", toolchain) .args(&["build", "--verbose"]) .cwd(p.root()) .exec_with_output() { Ok(_output) => { eprintln!("{} ok", toolchain); let bar = pkg_version("bar"); let baz = pkg_version("baz"); let new_baz_dep = pkg_version("new-baz-dep"); Ok(ToolchainBehavior { bar, baz, new_baz_dep, }) } Err(e) => { eprintln!("{} err {}", toolchain, e); Err(e) } } }; macro_rules! check_lock { ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, None) => { check_lock!(= $tc_result, $pkg, $which, $actual, None); }; ($tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => { check_lock!(= $tc_result, $pkg, $which, $actual, Some(Version::parse($expected).unwrap())); }; (= $tc_result:ident, $pkg:expr, $which:expr, $actual:expr, $expected:expr) => { let exp: Option = $expected; if $actual != $expected { $tc_result.push(format!( "{} for {} saw {:?} but expected {:?}", $which, $pkg, $actual, exp )); } }; } let check_err_contains = |tc_result: &mut Vec<_>, err: anyhow::Error, contents| { if let Some(ProcessError { stderr: Some(stderr), .. }) = err.downcast_ref::() { let stderr = std::str::from_utf8(stderr).unwrap(); if !stderr.contains(contents) { tc_result.push(format!( "{} expected to see error contents:\n{}\nbut saw:\n{}", toolchain, contents, stderr )); } } else { panic!("{} unexpected error {}", toolchain, err); } }; // Unlocked behavior. let which = "unlocked"; lock_path.rm_rf(); p.build_dir().rm_rf(); match run_cargo() { Ok(behavior) => { // TODO: Switch to 51 after backport. if version < &Version::new(1, 52, 0) && toolchain != "this" { check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2"); check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1"); check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None); } else { check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0"); check_lock!(tc_result, "baz", which, behavior.baz, None); check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None); } } Err(e) => { tc_result.push(format!("unlocked build failed: {}", e)); } } let which = "locked bar 1.0.0"; lock_bar_to(version, 100); match run_cargo() { Ok(behavior) => { check_lock!(tc_result, "bar", which, behavior.bar, "1.0.0"); check_lock!(tc_result, "baz", which, behavior.baz, None); check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None); } Err(e) => { tc_result.push(format!("bar 1.0.0 locked build failed: {}", e)); } } let which = "locked bar 1.0.1"; lock_bar_to(version, 101); match run_cargo() { Ok(behavior) => { check_lock!(tc_result, "bar", which, behavior.bar, "1.0.1"); check_lock!(tc_result, "baz", which, behavior.baz, None); check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None); } Err(e) => { if toolchain == "this" { // 1.0.1 can't be used without -Znamespaced-features // It gets filtered out of the index. check_err_contains(&mut tc_result, e, "error: failed to select a version for the requirement `bar = \"=1.0.1\"`\n\ candidate versions found which didn't match: 1.0.2, 1.0.0" ); } else { tc_result.push(format!("bar 1.0.1 locked build failed: {}", e)); } } } let which = "locked bar 1.0.2"; lock_bar_to(version, 102); match run_cargo() { Ok(behavior) => { check_lock!(tc_result, "bar", which, behavior.bar, "1.0.2"); check_lock!(tc_result, "baz", which, behavior.baz, "1.0.1"); check_lock!(tc_result, "new-baz-dep", which, behavior.new_baz_dep, None); } Err(e) => { if toolchain == "this" { // baz can't lock to 1.0.1, it requires -Znamespaced-features check_err_contains(&mut tc_result, e, "error: failed to select a version for the requirement `baz = \"=1.0.1\"`\n\ candidate versions found which didn't match: 1.0.0" ); } else { tc_result.push(format!("bar 1.0.2 locked build failed: {}", e)); } } } unexpected_results.push(tc_result); } // Generate a report. let mut has_err = false; for ((tc_vers, tc_name), errs) in toolchains.iter().zip(unexpected_results) { if errs.is_empty() { continue; } eprintln!("error: toolchain {} (version {}):", tc_name, tc_vers); for err in errs { eprintln!(" {}", err); } has_err = true; } if has_err { panic!("at least one toolchain did not run as expected"); } } #[cargo_test] #[ignore] fn index_cache_rebuild() { // Checks that the index cache gets rebuilt. // // 1.48 will not cache entries with features with the same name as a // dependency. If the cache does not get rebuilt, then running with // `-Znamespaced-features` would prevent the new cargo from seeing those // entries. The index cache version was changed to prevent this from // happening, and switching between versions should work correctly // (although it will thrash the cash, that's better than not working // correctly. Package::new("baz", "1.0.0").publish(); Package::new("bar", "1.0.0").publish(); Package::new("bar", "1.0.1") .add_dep(Dependency::new("baz", "1.0").optional(true)) .feature("baz", &["dep:baz"]) .publish(); let p = project() .file( "Cargo.toml", r#" [package] name = "foo" version = "0.1.0" [dependencies] bar = "1.0" "#, ) .file("src/lib.rs", "") .build(); // This version of Cargo errors on index entries that have overlapping // feature names, so 1.0.1 will be missing. execs() .with_process_builder(tc_process("cargo", "1.48.0")) .arg("check") .cwd(p.root()) .with_stderr( "\ [UPDATING] [..] [DOWNLOADING] crates ... [DOWNLOADED] bar v1.0.0 [..] [CHECKING] bar v1.0.0 [CHECKING] foo v0.1.0 [..] [FINISHED] [..] ", ) .run(); fs::remove_file(p.root().join("Cargo.lock")).unwrap(); // This should rebuild the cache and use 1.0.1. p.cargo("check -Znamespaced-features") .masquerade_as_nightly_cargo() .with_stderr( "\ [UPDATING] [..] [DOWNLOADING] crates ... [DOWNLOADED] bar v1.0.1 [..] [CHECKING] bar v1.0.1 [CHECKING] foo v0.1.0 [..] [FINISHED] [..] ", ) .run(); fs::remove_file(p.root().join("Cargo.lock")).unwrap(); // Verify 1.48 can still resolve, and is at 1.0.0. execs() .with_process_builder(tc_process("cargo", "1.48.0")) .arg("tree") .cwd(p.root()) .with_stdout( "\ foo v0.1.0 [..] └── bar v1.0.0 ", ) .run(); }