cargo/tests/testsuite/old_cargos.rs
Eric Huss 0d4137f4f5 Include the index version in the index cache.
This is intended to help prevent the following scenario from happening:

1. Old cargo builds an index cache.
2. Old cargo finds an index entry it cannot parse, skipping it,
   and saving the cache without the entry.
3. New cargo loads the cache with the missing entry, and never sees
   the new entries that it understands.

This may result in more cache thrashing, but that seems better than
having new cargos missing entries.
2021-02-19 10:51:38 -08:00

587 lines
20 KiB
Rust

//! 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::util::{ProcessBuilder, ProcessError};
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 semver::Version;
use std::fs;
fn tc_process(cmd: &str, toolchain: &str) -> ProcessBuilder {
if toolchain == "this" {
if cmd == "cargo" {
process(&cargo_exe())
} else {
process(cmd)
}
} else {
let mut cmd = process(cmd);
cmd.arg(format!("+{}", toolchain));
cmd
}
}
/// 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 = cargo::util::process("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<Version>,
baz: Option<Version>,
new_baz_dep: Option<Version>,
}
// Collect errors to print at the end. One entry per toolchain, a list of
// strings to print.
let mut unexpected_results: Vec<Vec<String>> = 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<Version> {
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<ToolchainBehavior> {
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<Version> = $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::<ProcessError>()
{
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();
}