Auto merge of #9589 - ehuss:testsuite-diff, r=alexcrichton

Enhancements to testsuite error output.

This includes several changes to the testsuite error reporting in theory to help make it easier to see differences when the test output changes. The key change is to add a Myers diff with a little colored output to highlight the differences. Here is an example:

<img width="666" alt="image" src="https://user-images.githubusercontent.com/43198/122311381-e8d43580-cec6-11eb-81b4-e2675f10d6ba.png">

The rest of the changes here are various refactorings to try to clean up the diffing code.  It ended up being far more changes than I was intending, but I did try to split things into commits to separate them.
This commit is contained in:
bors 2021-06-17 14:50:47 +00:00
commit 40df0f17b5
21 changed files with 976 additions and 859 deletions

View File

@ -16,9 +16,11 @@ filetime = "0.2"
flate2 = { version = "1.0", default-features = false, features = ["zlib"] }
git2 = "0.13.16"
glob = "0.3"
itertools = "0.10.0"
lazy_static = "1.0"
remove_dir_all = "0.5"
serde_json = "1.0"
tar = { version = "0.4.18", default-features = false }
termcolor = "1.1.2"
toml = "0.5.7"
url = "2.2.2"

View File

@ -0,0 +1,583 @@
//! Routines for comparing and diffing output.
//!
//! # Patterns
//!
//! Many of these functions support special markup to assist with comparing
//! text that may vary or is otherwise uninteresting for the test at hand. The
//! supported patterns are:
//!
//! - `[..]` is a wildcard that matches 0 or more characters on the same line
//! (similar to `.*` in a regex). It is non-greedy.
//! - `[EXE]` optionally adds `.exe` on Windows (empty string on other
//! platforms).
//! - `[ROOT]` is the path to the test directory's root.
//! - `[CWD]` is the working directory of the process that was run.
//! - There is a wide range of substitutions (such as `[COMPILING]` or
//! `[WARNING]`) to match cargo's "status" output and allows you to ignore
//! the alignment. See the source of `substitute_macros` for a complete list
//! of substitutions.
//!
//! # Normalization
//!
//! In addition to the patterns described above, the strings are normalized
//! in such a way to avoid unwanted differences. The normalizations are:
//!
//! - Raw tab characters are converted to the string `<tab>`. This is helpful
//! so that raw tabs do not need to be written in the expected string, and
//! to avoid confusion of tabs vs spaces.
//! - Backslashes are converted to forward slashes to deal with Windows paths.
//! This helps so that all tests can be written assuming forward slashes.
//! Other heuristics are applied to try to ensure Windows-style paths aren't
//! a problem.
//! - Carriage returns are removed, which can help when running on Windows.
use crate::diff;
use crate::paths;
use anyhow::{bail, Context, Result};
use serde_json::Value;
use std::env;
use std::fmt;
use std::path::Path;
use std::str;
use url::Url;
/// Normalizes the output so that it can be compared against the expected value.
fn normalize_actual(actual: &str, cwd: Option<&Path>) -> String {
// It's easier to read tabs in outputs if they don't show up as literal
// hidden characters
let actual = actual.replace('\t', "<tab>");
if cfg!(windows) {
// Let's not deal with \r\n vs \n on windows...
let actual = actual.replace('\r', "");
normalize_windows(&actual, cwd)
} else {
actual
}
}
/// Normalizes the expected string so that it can be compared against the actual output.
fn normalize_expected(expected: &str, cwd: Option<&Path>) -> String {
let expected = substitute_macros(expected);
if cfg!(windows) {
normalize_windows(&expected, cwd)
} else {
let expected = match cwd {
None => expected,
Some(cwd) => expected.replace("[CWD]", &cwd.display().to_string()),
};
let expected = expected.replace("[ROOT]", &paths::root().display().to_string());
expected
}
}
/// Normalizes text for both actual and expected strings on Windows.
fn normalize_windows(text: &str, cwd: Option<&Path>) -> String {
// Let's not deal with / vs \ (windows...)
let text = text.replace('\\', "/");
// Weirdness for paths on Windows extends beyond `/` vs `\` apparently.
// Namely paths like `c:\` and `C:\` are equivalent and that can cause
// issues. The return value of `env::current_dir()` may return a
// lowercase drive name, but we round-trip a lot of values through `Url`
// which will auto-uppercase the drive name. To just ignore this
// distinction we try to canonicalize as much as possible, taking all
// forms of a path and canonicalizing them to one.
let replace_path = |s: &str, path: &Path, with: &str| {
let path_through_url = Url::from_file_path(path).unwrap().to_file_path().unwrap();
let path1 = path.display().to_string().replace('\\', "/");
let path2 = path_through_url.display().to_string().replace('\\', "/");
s.replace(&path1, with)
.replace(&path2, with)
.replace(with, &path1)
};
let text = match cwd {
None => text,
Some(p) => replace_path(&text, p, "[CWD]"),
};
// Similar to cwd above, perform similar treatment to the root path
// which in theory all of our paths should otherwise get rooted at.
let root = paths::root();
let text = replace_path(&text, &root, "[ROOT]");
text
}
fn substitute_macros(input: &str) -> String {
let macros = [
("[RUNNING]", " Running"),
("[COMPILING]", " Compiling"),
("[CHECKING]", " Checking"),
("[COMPLETED]", " Completed"),
("[CREATED]", " Created"),
("[FINISHED]", " Finished"),
("[ERROR]", "error:"),
("[WARNING]", "warning:"),
("[NOTE]", "note:"),
("[HELP]", "help:"),
("[DOCUMENTING]", " Documenting"),
("[FRESH]", " Fresh"),
("[UPDATING]", " Updating"),
("[ADDING]", " Adding"),
("[REMOVING]", " Removing"),
("[DOCTEST]", " Doc-tests"),
("[PACKAGING]", " Packaging"),
("[DOWNLOADING]", " Downloading"),
("[DOWNLOADED]", " Downloaded"),
("[UPLOADING]", " Uploading"),
("[VERIFYING]", " Verifying"),
("[ARCHIVING]", " Archiving"),
("[INSTALLING]", " Installing"),
("[REPLACING]", " Replacing"),
("[UNPACKING]", " Unpacking"),
("[SUMMARY]", " Summary"),
("[FIXED]", " Fixed"),
("[FIXING]", " Fixing"),
("[EXE]", env::consts::EXE_SUFFIX),
("[IGNORED]", " Ignored"),
("[INSTALLED]", " Installed"),
("[REPLACED]", " Replaced"),
("[BUILDING]", " Building"),
("[LOGIN]", " Login"),
("[LOGOUT]", " Logout"),
("[YANK]", " Yank"),
("[OWNER]", " Owner"),
("[MIGRATING]", " Migrating"),
];
let mut result = input.to_owned();
for &(pat, subst) in &macros {
result = result.replace(pat, subst)
}
result
}
/// Compares one string against another, checking that they both match.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
///
/// - `description` explains where the output is from (usually "stdout" or "stderr").
/// - `other_output` is other output to display in the error (usually stdout or stderr).
pub fn match_exact(
expected: &str,
actual: &str,
description: &str,
other_output: &str,
cwd: Option<&Path>,
) -> Result<()> {
let expected = normalize_expected(expected, cwd);
let actual = normalize_actual(actual, cwd);
let e: Vec<_> = expected.lines().map(WildStr::new).collect();
let a: Vec<_> = actual.lines().map(WildStr::new).collect();
if e == a {
return Ok(());
}
let diff = diff::colored_diff(&e, &a);
bail!(
"{} did not match:\n\
{}\n\n\
other output:\n\
{}\n",
description,
diff,
other_output,
);
}
/// Convenience wrapper around [`match_exact`] which will panic on error.
#[track_caller]
pub fn assert_match_exact(expected: &str, actual: &str) {
if let Err(e) = match_exact(expected, actual, "", "", None) {
crate::panic_error("", e);
}
}
/// Checks that the given string contains the given lines, ignoring the order
/// of the lines.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
pub fn match_unordered(expected: &str, actual: &str, cwd: Option<&Path>) -> Result<()> {
let expected = normalize_expected(expected, cwd);
let actual = normalize_actual(actual, cwd);
let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
let mut a: Vec<_> = actual.lines().map(|line| WildStr::new(line)).collect();
// match more-constrained lines first, although in theory we'll
// need some sort of recursive match here. This handles the case
// that you expect "a\n[..]b" and two lines are printed out,
// "ab\n"a", where technically we do match unordered but a naive
// search fails to find this. This simple sort at least gets the
// test suite to pass for now, but we may need to get more fancy
// if tests start failing again.
a.sort_by_key(|s| s.line.len());
let mut changes = Vec::new();
let mut a_index = 0;
let mut failure = false;
use crate::diff::Change;
for (e_i, e_line) in e.into_iter().enumerate() {
match a.iter().position(|a_line| e_line == *a_line) {
Some(index) => {
let a_line = a.remove(index);
changes.push(Change::Keep(e_i, index, a_line));
a_index += 1;
}
None => {
failure = true;
changes.push(Change::Remove(e_i, e_line));
}
}
}
for unmatched in a {
failure = true;
changes.push(Change::Add(a_index, unmatched));
a_index += 1;
}
if failure {
bail!(
"Expected lines did not match (ignoring order):\n{}\n",
diff::render_colored_changes(&changes)
);
} else {
Ok(())
}
}
/// Checks that the given string contains the given contiguous lines
/// somewhere.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
pub fn match_contains(expected: &str, actual: &str, cwd: Option<&Path>) -> Result<()> {
let expected = normalize_expected(expected, cwd);
let actual = normalize_actual(actual, cwd);
let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
let a: Vec<_> = actual.lines().map(|line| WildStr::new(line)).collect();
if e.len() == 0 {
bail!("expected length must not be zero");
}
for window in a.windows(e.len()) {
if window == e {
return Ok(());
}
}
bail!(
"expected to find:\n\
{}\n\n\
did not find in output:\n\
{}",
expected,
actual
);
}
/// Checks that the given string does not contain the given contiguous lines
/// anywhere.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
pub fn match_does_not_contain(expected: &str, actual: &str, cwd: Option<&Path>) -> Result<()> {
if match_contains(expected, actual, cwd).is_ok() {
bail!(
"expected not to find:\n\
{}\n\n\
but found in output:\n\
{}",
expected,
actual
);
} else {
Ok(())
}
}
/// Checks that the given string contains the given contiguous lines
/// somewhere, and should be repeated `number` times.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
pub fn match_contains_n(
expected: &str,
number: usize,
actual: &str,
cwd: Option<&Path>,
) -> Result<()> {
let expected = normalize_expected(expected, cwd);
let actual = normalize_actual(actual, cwd);
let e: Vec<_> = expected.lines().map(|line| WildStr::new(line)).collect();
let a: Vec<_> = actual.lines().map(|line| WildStr::new(line)).collect();
if e.len() == 0 {
bail!("expected length must not be zero");
}
let matches = a.windows(e.len()).filter(|window| *window == e).count();
if matches == number {
Ok(())
} else {
bail!(
"expected to find {} occurrences of:\n\
{}\n\n\
but found {} matches in the output:\n\
{}",
number,
expected,
matches,
actual
)
}
}
/// Checks that the given string has a line that contains the given patterns,
/// and that line also does not contain the `without` patterns.
///
/// See [Patterns](index.html#patterns) for more information on pattern matching.
///
/// See [`crate::Execs::with_stderr_line_without`] for an example and cautions
/// against using.
pub fn match_with_without(
actual: &str,
with: &[String],
without: &[String],
cwd: Option<&Path>,
) -> Result<()> {
let actual = normalize_actual(actual, cwd);
let norm = |s: &String| format!("[..]{}[..]", normalize_expected(s, cwd));
let with: Vec<_> = with.iter().map(norm).collect();
let without: Vec<_> = without.iter().map(norm).collect();
let with_wild: Vec<_> = with.iter().map(|w| WildStr::new(w)).collect();
let without_wild: Vec<_> = without.iter().map(|w| WildStr::new(w)).collect();
let matches: Vec<_> = actual
.lines()
.map(WildStr::new)
.filter(|line| with_wild.iter().all(|with| with == line))
.filter(|line| !without_wild.iter().any(|without| without == line))
.collect();
match matches.len() {
0 => bail!(
"Could not find expected line in output.\n\
With contents: {:?}\n\
Without contents: {:?}\n\
Actual stderr:\n\
{}\n",
with,
without,
actual
),
1 => Ok(()),
_ => bail!(
"Found multiple matching lines, but only expected one.\n\
With contents: {:?}\n\
Without contents: {:?}\n\
Matching lines:\n\
{}\n",
with,
without,
itertools::join(matches, "\n")
),
}
}
/// Checks that the given string of JSON objects match the given set of
/// expected JSON objects.
///
/// See [`crate::Execs::with_json`] for more details.
pub fn match_json(expected: &str, actual: &str, cwd: Option<&Path>) -> Result<()> {
let (exp_objs, act_objs) = collect_json_objects(expected, actual)?;
if exp_objs.len() != act_objs.len() {
bail!(
"expected {} json lines, got {}, stdout:\n{}",
exp_objs.len(),
act_objs.len(),
actual
);
}
for (exp_obj, act_obj) in exp_objs.iter().zip(act_objs) {
find_json_mismatch(exp_obj, &act_obj, cwd)?;
}
Ok(())
}
/// Checks that the given string of JSON objects match the given set of
/// expected JSON objects, ignoring their order.
///
/// See [`crate::Execs::with_json_contains_unordered`] for more details and
/// cautions when using.
pub fn match_json_contains_unordered(
expected: &str,
actual: &str,
cwd: Option<&Path>,
) -> Result<()> {
let (exp_objs, mut act_objs) = collect_json_objects(expected, actual)?;
for exp_obj in exp_objs {
match act_objs
.iter()
.position(|act_obj| find_json_mismatch(&exp_obj, act_obj, cwd).is_ok())
{
Some(index) => act_objs.remove(index),
None => {
bail!(
"Did not find expected JSON:\n\
{}\n\
Remaining available output:\n\
{}\n",
serde_json::to_string_pretty(&exp_obj).unwrap(),
itertools::join(
act_objs.iter().map(|o| serde_json::to_string(o).unwrap()),
"\n"
)
);
}
};
}
Ok(())
}
fn collect_json_objects(
expected: &str,
actual: &str,
) -> Result<(Vec<serde_json::Value>, Vec<serde_json::Value>)> {
let expected_objs: Vec<_> = expected
.split("\n\n")
.map(|expect| {
expect
.parse()
.with_context(|| format!("failed to parse expected JSON object:\n{}", expect))
})
.collect::<Result<_>>()?;
let actual_objs: Vec<_> = actual
.lines()
.filter(|line| line.starts_with('{'))
.map(|line| {
line.parse()
.with_context(|| format!("failed to parse JSON object:\n{}", line))
})
.collect::<Result<_>>()?;
Ok((expected_objs, actual_objs))
}
/// Compares JSON object for approximate equality.
/// You can use `[..]` wildcard in strings (useful for OS-dependent things such
/// as paths). You can use a `"{...}"` string literal as a wildcard for
/// arbitrary nested JSON (useful for parts of object emitted by other programs
/// (e.g., rustc) rather than Cargo itself).
pub fn find_json_mismatch(expected: &Value, actual: &Value, cwd: Option<&Path>) -> Result<()> {
match find_json_mismatch_r(expected, actual, cwd) {
Some((expected_part, actual_part)) => bail!(
"JSON mismatch\nExpected:\n{}\nWas:\n{}\nExpected part:\n{}\nActual part:\n{}\n",
serde_json::to_string_pretty(expected).unwrap(),
serde_json::to_string_pretty(&actual).unwrap(),
serde_json::to_string_pretty(expected_part).unwrap(),
serde_json::to_string_pretty(actual_part).unwrap(),
),
None => Ok(()),
}
}
fn find_json_mismatch_r<'a>(
expected: &'a Value,
actual: &'a Value,
cwd: Option<&Path>,
) -> Option<(&'a Value, &'a Value)> {
use serde_json::Value::*;
match (expected, actual) {
(&Number(ref l), &Number(ref r)) if l == r => None,
(&Bool(l), &Bool(r)) if l == r => None,
(&String(ref l), _) if l == "{...}" => None,
(&String(ref l), &String(ref r)) => {
if match_exact(l, r, "", "", cwd).is_err() {
Some((expected, actual))
} else {
None
}
}
(&Array(ref l), &Array(ref r)) => {
if l.len() != r.len() {
return Some((expected, actual));
}
l.iter()
.zip(r.iter())
.filter_map(|(l, r)| find_json_mismatch_r(l, r, cwd))
.next()
}
(&Object(ref l), &Object(ref r)) => {
let same_keys = l.len() == r.len() && l.keys().all(|k| r.contains_key(k));
if !same_keys {
return Some((expected, actual));
}
l.values()
.zip(r.values())
.filter_map(|(l, r)| find_json_mismatch_r(l, r, cwd))
.next()
}
(&Null, &Null) => None,
// Magic string literal `"{...}"` acts as wildcard for any sub-JSON.
_ => Some((expected, actual)),
}
}
/// A single line string that supports `[..]` wildcard matching.
pub struct WildStr<'a> {
has_meta: bool,
line: &'a str,
}
impl<'a> WildStr<'a> {
pub fn new(line: &'a str) -> WildStr<'a> {
WildStr {
has_meta: line.contains("[..]"),
line,
}
}
}
impl<'a> PartialEq for WildStr<'a> {
fn eq(&self, other: &Self) -> bool {
match (self.has_meta, other.has_meta) {
(false, false) => self.line == other.line,
(true, false) => meta_cmp(self.line, other.line),
(false, true) => meta_cmp(other.line, self.line),
(true, true) => panic!("both lines cannot have [..]"),
}
}
}
fn meta_cmp(a: &str, mut b: &str) -> bool {
for (i, part) in a.split("[..]").enumerate() {
match b.find(part) {
Some(j) => {
if i == 0 && j != 0 {
return false;
}
b = &b[j + part.len()..];
}
None => return false,
}
}
b.is_empty() || a.ends_with("[..]")
}
impl fmt::Display for WildStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
f.write_str(&self.line)
}
}
impl fmt::Debug for WildStr<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
write!(f, "{:?}", self.line)
}
}
#[test]
fn wild_str_cmp() {
for (a, b) in &[
("a b", "a b"),
("a[..]b", "a b"),
("a[..]", "a b"),
("[..]", "a b"),
("[..]b", "a b"),
] {
assert_eq!(WildStr::new(a), WildStr::new(b));
}
for (a, b) in &[("[..]b", "c"), ("b", "c"), ("b", "cb")] {
assert_ne!(WildStr::new(a), WildStr::new(b));
}
}

View File

@ -0,0 +1,174 @@
//! A simple Myers diff implementation.
//!
//! This focuses on being short and simple, and the expense of being
//! inefficient. A key characteristic here is that this supports cargotest's
//! `[..]` wildcard matching. That means things like hashing can't be used.
//! Since Cargo's output tends to be small, this should be sufficient.
use std::fmt;
use std::io::Write;
use termcolor::{Ansi, Color, ColorSpec, NoColor, WriteColor};
/// A single line change to be applied to the original.
#[derive(Debug, Eq, PartialEq)]
pub enum Change<T> {
Add(usize, T),
Remove(usize, T),
Keep(usize, usize, T),
}
pub fn diff<'a, T>(a: &'a [T], b: &'a [T]) -> Vec<Change<&'a T>>
where
T: PartialEq,
{
if a.is_empty() && b.is_empty() {
return vec![];
}
let mut diff = vec![];
for (prev_x, prev_y, x, y) in backtrack(&a, &b) {
if x == prev_x {
diff.push(Change::Add(prev_y + 1, &b[prev_y]));
} else if y == prev_y {
diff.push(Change::Remove(prev_x + 1, &a[prev_x]));
} else {
diff.push(Change::Keep(prev_x + 1, prev_y + 1, &a[prev_x]));
}
}
diff.reverse();
diff
}
fn shortest_edit<T>(a: &[T], b: &[T]) -> Vec<Vec<usize>>
where
T: PartialEq,
{
let max = a.len() + b.len();
let mut v = vec![0; 2 * max + 1];
let mut trace = vec![];
for d in 0..=max {
trace.push(v.clone());
for k in (0..=(2 * d)).step_by(2) {
let mut x = if k == 0 || (k != 2 * d && v[max - d + k - 1] < v[max - d + k + 1]) {
// Move down
v[max - d + k + 1]
} else {
// Move right
v[max - d + k - 1] + 1
};
let mut y = x + d - k;
// Step diagonally as far as possible.
while x < a.len() && y < b.len() && a[x] == b[y] {
x += 1;
y += 1;
}
v[max - d + k] = x;
// Return if reached the bottom-right position.
if x >= a.len() && y >= b.len() {
return trace;
}
}
}
panic!("finished without hitting end?");
}
fn backtrack<T>(a: &[T], b: &[T]) -> Vec<(usize, usize, usize, usize)>
where
T: PartialEq,
{
let mut result = vec![];
let mut x = a.len();
let mut y = b.len();
let max = x + y;
for (d, v) in shortest_edit(a, b).iter().enumerate().rev() {
let k = x + d - y;
let prev_k = if k == 0 || (k != 2 * d && v[max - d + k - 1] < v[max - d + k + 1]) {
k + 1
} else {
k - 1
};
let prev_x = v[max - d + prev_k];
let prev_y = (prev_x + d).saturating_sub(prev_k);
while x > prev_x && y > prev_y {
result.push((x - 1, y - 1, x, y));
x -= 1;
y -= 1;
}
if d > 0 {
result.push((prev_x, prev_y, x, y));
}
x = prev_x;
y = prev_y;
}
return result;
}
pub fn colored_diff<'a, T>(a: &'a [T], b: &'a [T]) -> String
where
T: PartialEq + fmt::Display,
{
let changes = diff(a, b);
render_colored_changes(&changes)
}
pub fn render_colored_changes<T: fmt::Display>(changes: &[Change<T>]) -> String {
// termcolor is not very ergonomic, but I don't want to bring in another dependency.
let mut red = ColorSpec::new();
red.set_fg(Some(Color::Red));
let mut green = ColorSpec::new();
green.set_fg(Some(Color::Green));
let mut dim = ColorSpec::new();
dim.set_dimmed(true);
let mut v = Vec::new();
let mut result: Box<dyn WriteColor> = if crate::is_ci() {
// Don't use color on CI. Even though GitHub can display colors, it
// makes reading the raw logs more difficult.
Box::new(NoColor::new(&mut v))
} else {
Box::new(Ansi::new(&mut v))
};
for change in changes {
let (nums, sign, color, text) = match change {
Change::Add(i, s) => (format!(" {:<4} ", i), '+', &green, s),
Change::Remove(i, s) => (format!("{:<4} ", i), '-', &red, s),
Change::Keep(x, y, s) => (format!("{:<4}{:<4} ", x, y), ' ', &dim, s),
};
result.set_color(&dim).unwrap();
write!(result, "{}", nums).unwrap();
let mut bold = color.clone();
bold.set_bold(true);
result.set_color(&bold).unwrap();
write!(result, "{}", sign).unwrap();
result.reset().unwrap();
result.set_color(&color).unwrap();
write!(result, "{}", text).unwrap();
result.reset().unwrap();
writeln!(result).unwrap();
}
drop(result);
String::from_utf8(v).unwrap()
}
#[cfg(test)]
pub fn compare(a: &str, b: &str) {
let a: Vec<_> = a.chars().collect();
let b: Vec<_> = b.chars().collect();
let changes = diff(&a, &b);
let mut result = vec![];
for change in changes {
match change {
Change::Add(_, s) => result.push(*s),
Change::Remove(_, _s) => {}
Change::Keep(_, _, s) => result.push(*s),
}
}
assert_eq!(b, result);
}
#[test]
fn basic_tests() {
compare("", "");
compare("A", "");
compare("", "B");
compare("ABCABBA", "CBABAC");
}

File diff suppressed because it is too large Load Diff

View File

@ -1,5 +1,5 @@
use crate::compare::{assert_match_exact, find_json_mismatch};
use crate::registry::{self, alt_api_path};
use crate::{find_json_mismatch, lines_match};
use flate2::read::GzDecoder;
use std::collections::{HashMap, HashSet};
use std::fs::File;
@ -151,16 +151,7 @@ pub fn validate_crate_contents(
let actual_contents = files
.get(&full_e_name)
.unwrap_or_else(|| panic!("file `{}` missing in archive", e_file_name));
if !lines_match(e_file_contents, actual_contents) {
panic!(
"Crate contents mismatch for {:?}:\n\
--- expected\n\
{}\n\
--- actual \n\
{}\n",
e_file_name, e_file_contents, actual_contents
);
}
assert_match_exact(e_file_contents, actual_contents);
}
}
}

View File

@ -58,11 +58,11 @@ p.cargo("run --bin foo")
This uses the [`Execs`] struct to build up a command to execute, along with
the expected output.
See [`support::lines_match`] for an explanation of the string pattern matching.
See [`support::compare`] for an explanation of the string pattern matching.
Patterns are used to make it easier to match against the expected output.
Browse the `pub` functions in the [`support`] crate for a variety of other
helpful utilities.
Browse the `pub` functions and modules in the [`support`] crate for a variety
of other helpful utilities.
### Testing Nightly Features
@ -127,6 +127,6 @@ dependency.
[`ProjectBuilder`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/lib.rs#L225-L231
[`Execs`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/lib.rs#L558-L579
[`support`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/lib.rs
[`support::lines_match`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/lib.rs#L1322-L1332
[`support::compare`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/compare.rs
[`support::registry::Package`]: https://github.com/rust-lang/cargo/blob/e4b65bdc80f2a293447f2f6a808fa7c84bf9a357/crates/cargo-test-support/src/registry.rs#L73-L149
[`support::git`]: https://github.com/rust-lang/cargo/blob/master/crates/cargo-test-support/src/git.rs

View File

@ -1391,16 +1391,16 @@ fn bad_target_cfg() {
.with_stderr(
"\
[ERROR] error in [..]/foo/.cargo/config: \
could not load config key `target.\"cfg(not(target_os = /\"none/\"))\".runner`
could not load config key `target.\"cfg(not(target_os = \\\"none\\\"))\".runner`
Caused by:
error in [..]/foo/.cargo/config: \
could not load config key `target.\"cfg(not(target_os = /\"none/\"))\".runner`
could not load config key `target.\"cfg(not(target_os = \\\"none\\\"))\".runner`
Caused by:
invalid configuration for key `target.\"cfg(not(target_os = /\"none/\"))\".runner`
invalid configuration for key `target.\"cfg(not(target_os = \\\"none\\\"))\".runner`
expected a string or array of strings, but found a boolean for \
`target.\"cfg(not(target_os = /\"none/\"))\".runner` in [..]/foo/.cargo/config
`target.\"cfg(not(target_os = \\\"none\\\"))\".runner` in [..]/foo/.cargo/config
",
)
.run();

View File

@ -346,12 +346,12 @@ fn cargo_bench_failing_test() {
[FINISHED] bench [optimized] target(s) in [..]
[RUNNING] [..] (target/release/deps/foo-[..][EXE])",
)
.with_either_contains(
.with_stdout_contains(
"[..]thread '[..]' panicked at 'assertion failed: `(left == right)`[..]",
)
.with_either_contains("[..]left: `\"hello\"`[..]")
.with_either_contains("[..]right: `\"nope\"`[..]")
.with_either_contains("[..]src/main.rs:15[..]")
.with_stdout_contains("[..]left: `\"hello\"`[..]")
.with_stdout_contains("[..]right: `\"nope\"`[..]")
.with_stdout_contains("[..]src/main.rs:15[..]")
.with_status(101)
.run();
}

View File

@ -6,13 +6,13 @@ use cargo::{
ops::CompileOptions,
Config,
};
use cargo_test_support::compare;
use cargo_test_support::paths::{root, CargoPathExt};
use cargo_test_support::registry::Package;
use cargo_test_support::tools;
use cargo_test_support::{
basic_bin_manifest, basic_lib_manifest, basic_manifest, cargo_exe, git, is_nightly,
lines_match_unordered, main_file, paths, process, project, rustc_host, sleep_ms,
symlink_supported, t, Execs, ProjectBuilder,
basic_bin_manifest, basic_lib_manifest, basic_manifest, cargo_exe, git, is_nightly, main_file,
paths, process, project, rustc_host, sleep_ms, symlink_supported, t, Execs, ProjectBuilder,
};
use cargo_util::paths::dylib_path_envvar;
use std::env;
@ -5320,7 +5320,7 @@ fn close_output() {
};
let stderr = spawn(false);
lines_match_unordered(
compare::match_unordered(
"\
[COMPILING] foo [..]
hello stderr!
@ -5329,13 +5329,14 @@ hello stderr!
[ERROR] [..]
",
&stderr,
None,
)
.unwrap();
// Try again with stderr.
p.build_dir().rm_rf();
let stdout = spawn(true);
lines_match_unordered("hello stdout!\n", &stdout).unwrap();
assert_eq!(stdout, "hello stdout!\n");
}
#[cargo_test]

View File

@ -1,8 +1,9 @@
//! Tests for build.rs scripts.
use cargo_test_support::compare::assert_match_exact;
use cargo_test_support::paths::CargoPathExt;
use cargo_test_support::registry::Package;
use cargo_test_support::{basic_manifest, cross_compile, is_coarse_mtime, project};
use cargo_test_support::{lines_match, paths::CargoPathExt};
use cargo_test_support::{rustc_host, sleep_ms, slow_cpu_multiplier, symlink_supported};
use cargo_util::paths::remove_dir_all;
use std::env;
@ -3037,25 +3038,9 @@ fn generate_good_d_files() {
println!("*.d file content*: {}", &dot_d);
#[cfg(windows)]
assert!(
lines_match(
"[..]\\target\\debug\\meow.exe: [..]\\awoo\\barkbarkbark [..]\\awoo\\build.rs[..]",
&dot_d
) || lines_match(
"[..]\\target\\debug\\meow.exe: [..]\\awoo\\build.rs [..]\\awoo\\barkbarkbark[..]",
&dot_d
)
);
#[cfg(not(windows))]
assert!(
lines_match(
"[..]/target/debug/meow: [..]/awoo/barkbarkbark [..]/awoo/build.rs[..]",
&dot_d
) || lines_match(
"[..]/target/debug/meow: [..]/awoo/build.rs [..]/awoo/barkbarkbark[..]",
&dot_d
)
assert_match_exact(
"[..]/target/debug/meow[EXE]: [..]/awoo/barkbarkbark [..]/awoo/build.rs[..]",
&dot_d,
);
// paths relative to dependency roots should not be allowed
@ -3076,25 +3061,9 @@ fn generate_good_d_files() {
println!("*.d file content with dep-info-basedir*: {}", &dot_d);
#[cfg(windows)]
assert!(
lines_match(
"target\\debug\\meow.exe: [..]awoo\\barkbarkbark [..]awoo\\build.rs[..]",
&dot_d
) || lines_match(
"target\\debug\\meow.exe: [..]awoo\\build.rs [..]awoo\\barkbarkbark[..]",
&dot_d
)
);
#[cfg(not(windows))]
assert!(
lines_match(
"target/debug/meow: [..]awoo/barkbarkbark [..]awoo/build.rs[..]",
&dot_d
) || lines_match(
"target/debug/meow: [..]awoo/build.rs [..]awoo/barkbarkbark[..]",
&dot_d
)
assert_match_exact(
"target/debug/meow[EXE]: awoo/barkbarkbark awoo/build.rs[..]",
&dot_d,
);
// paths relative to dependency roots should not be allowed

View File

@ -313,7 +313,7 @@ fn cargo_subcommand_args() {
r#"
fn main() {
let args: Vec<_> = ::std::env::args().collect();
println!("{:?}", args);
println!("{}", args.join(" "));
}
"#,
)
@ -329,9 +329,7 @@ fn cargo_subcommand_args() {
cargo_process("foo bar -v --help")
.env("PATH", &path)
.with_stdout(
r#"["[CWD]/cargo-foo/target/debug/cargo-foo[EXE]", "foo", "bar", "-v", "--help"]"#,
)
.with_stdout("[CWD]/cargo-foo/target/debug/cargo-foo[EXE] foo bar -v --help")
.run();
}

View File

@ -86,7 +86,7 @@ build.rustflags = [\"--flag-directory\", \"--flag-global\"]
extra-table.somekey = \"somevalue\"
profile.dev.opt-level = 3
profile.dev.package.foo.opt-level = 1
target.\"cfg(target_os = /\"linux/\")\".runner = \"runme\"
target.\"cfg(target_os = \\\"linux\\\")\".runner = \"runme\"
# The following environment variables may affect the loaded values.
# CARGO_ALIAS_BAR=[..]cat dog[..]
# CARGO_BUILD_JOBS=100
@ -263,7 +263,7 @@ build.rustflags = [
extra-table.somekey = \"somevalue\" # [ROOT]/home/.cargo/config.toml
profile.dev.opt-level = 3 # [ROOT]/home/.cargo/config.toml
profile.dev.package.foo.opt-level = 1 # [ROOT]/home/.cargo/config.toml
target.\"cfg(target_os = /\"linux/\")\".runner = \"runme\" # [ROOT]/home/.cargo/config.toml
target.\"cfg(target_os = \\\"linux\\\")\".runner = \"runme\" # [ROOT]/home/.cargo/config.toml
# The following environment variables may affect the loaded values.
# CARGO_HOME=[ROOT]/home/.cargo
",
@ -359,7 +359,7 @@ build.rustflags = [\"--flag-global\"]
extra-table.somekey = \"somevalue\"
profile.dev.opt-level = 3
profile.dev.package.foo.opt-level = 1
target.\"cfg(target_os = /\"linux/\")\".runner = \"runme\"
target.\"cfg(target_os = \\\"linux\\\")\".runner = \"runme\"
",
)
@ -513,7 +513,7 @@ build.rustflags = [\"--flag-global\"]
extra-table.somekey = \"somevalue\"
profile.dev.opt-level = 3
profile.dev.package.foo.opt-level = 1
target.\"cfg(target_os = /\"linux/\")\".runner = \"runme\"
target.\"cfg(target_os = \\\"linux\\\")\".runner = \"runme\"
",
)

View File

@ -5,7 +5,8 @@ use cargo::util::config::{self, Config, SslVersionConfig, StringList};
use cargo::util::interning::InternedString;
use cargo::util::toml::{self, VecStringOrBool as VSOB};
use cargo::CargoResult;
use cargo_test_support::{normalized_lines_match, paths, project, t};
use cargo_test_support::compare;
use cargo_test_support::{panic_error, paths, project, symlink_supported, t};
use serde::Deserialize;
use std::borrow::Borrow;
use std::collections::{BTreeMap, HashMap};
@ -151,28 +152,6 @@ fn write_config_toml(config: &str) {
write_config_at(paths::root().join(".cargo/config.toml"), config);
}
// Several test fail on windows if the user does not have permission to
// create symlinks (the `SeCreateSymbolicLinkPrivilege`). Instead of
// disabling these test on Windows, use this function to test whether we
// have permission, and return otherwise. This way, we still don't run these
// tests most of the time, but at least we do if the user has the right
// permissions.
// This function is derived from libstd fs tests.
pub fn got_symlink_permission() -> bool {
if cfg!(unix) {
return true;
}
let link = paths::root().join("some_hopefully_unique_link_name");
let target = paths::root().join("nonexisting_target");
match symlink_file(&target, &link) {
Ok(_) => true,
// ERROR_PRIVILEGE_NOT_HELD = 1314
Err(ref err) if err.raw_os_error() == Some(1314) => false,
Err(_) => true,
}
}
#[cfg(unix)]
fn symlink_file(target: &Path, link: &Path) -> io::Result<()> {
os::unix::fs::symlink(target, link)
@ -209,11 +188,8 @@ pub fn assert_error<E: Borrow<anyhow::Error>>(error: E, msgs: &str) {
#[track_caller]
pub fn assert_match(expected: &str, actual: &str) {
if !normalized_lines_match(expected, actual, None) {
panic!(
"Did not find expected:\n{}\nActual:\n{}\n",
expected, actual
);
if let Err(e) = compare::match_exact(expected, actual, "output", "", None) {
panic_error("", e);
}
}
@ -257,7 +233,7 @@ f1 = 1
fn config_ambiguous_filename_symlink_doesnt_warn() {
// Windows requires special permissions to create symlinks.
// If we don't have permission, just skip this test.
if !got_symlink_permission() {
if !symlink_supported() {
return;
};
@ -276,15 +252,7 @@ f1 = 1
// It should NOT have warned for the symlink.
let output = read_output(config);
let unexpected = "\
warning: Both `[..]/.cargo/config` and `[..]/.cargo/config.toml` exist. Using `[..]/.cargo/config`
";
if normalized_lines_match(unexpected, &output, None) {
panic!(
"Found unexpected:\n{}\nActual error:\n{}\n",
unexpected, output
);
}
assert_eq!(output, "");
}
#[cargo_test]

View File

@ -1470,8 +1470,8 @@ fn doc_message_format() {
"children": "{...}",
"code": "{...}",
"level": "error",
"message": "[..]",
"rendered": "[..]",
"message": "{...}",
"rendered": "{...}",
"spans": "{...}"
},
"package_id": "foo [..]",

View File

@ -1151,7 +1151,8 @@ fn publish_no_implicit() {
&["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
&[(
"Cargo.toml",
r#"[..]
&format!(
r#"{}
[package]
name = "foo"
version = "0.1.0"
@ -1169,6 +1170,8 @@ optional = true
[features]
feat = ["opt-dep1"]
"#,
cargo::core::package::MANIFEST_PREAMBLE
),
)],
);
}
@ -1255,7 +1258,8 @@ fn publish() {
&["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
&[(
"Cargo.toml",
r#"[..]
&format!(
r#"{}
[package]
name = "foo"
version = "0.1.0"
@ -1271,6 +1275,8 @@ feat1 = []
feat2 = ["dep:bar"]
feat3 = ["feat2"]
"#,
cargo::core::package::MANIFEST_PREAMBLE
),
)],
);
}

View File

@ -5,28 +5,22 @@ use cargo_test_support::project;
#[cargo_test]
fn simple() {
let p = project().build();
let root_manifest_path = p.root().join("Cargo.toml");
p.cargo("locate-project")
.with_stdout(format!(
r#"{{"root":"{}"}}"#,
root_manifest_path.to_str().unwrap()
))
.with_json(r#"{"root": "[ROOT]/foo/Cargo.toml"}"#)
.run();
}
#[cargo_test]
fn message_format() {
let p = project().build();
let root_manifest_path = p.root().join("Cargo.toml");
let root_str = root_manifest_path.to_str().unwrap();
p.cargo("locate-project --message-format plain")
.with_stdout(root_str)
.with_stdout("[ROOT]/foo/Cargo.toml")
.run();
p.cargo("locate-project --message-format json")
.with_stdout(format!(r#"{{"root":"{}"}}"#, root_str))
.with_json(r#"{"root": "[ROOT]/foo/Cargo.toml"}"#)
.run();
p.cargo("locate-project --message-format cryptic")
@ -61,28 +55,22 @@ fn workspace() {
.file("inner/src/lib.rs", "")
.build();
let outer_manifest = format!(
r#"{{"root":"{}"}}"#,
p.root().join("Cargo.toml").to_str().unwrap(),
);
let inner_manifest = format!(
r#"{{"root":"{}"}}"#,
p.root().join("inner").join("Cargo.toml").to_str().unwrap(),
);
let outer_manifest = r#"{"root": "[ROOT]/foo/Cargo.toml"}"#;
let inner_manifest = r#"{"root": "[ROOT]/foo/inner/Cargo.toml"}"#;
p.cargo("locate-project").with_stdout(&outer_manifest).run();
p.cargo("locate-project").with_json(outer_manifest).run();
p.cargo("locate-project")
.cwd("inner")
.with_stdout(&inner_manifest)
.with_json(inner_manifest)
.run();
p.cargo("locate-project --workspace")
.with_stdout(&outer_manifest)
.with_json(outer_manifest)
.run();
p.cargo("locate-project --workspace")
.cwd("inner")
.with_stdout(&outer_manifest)
.with_json(outer_manifest)
.run();
}

View File

@ -1,8 +1,9 @@
//! Tests for supporting older versions of the Cargo.lock file format.
use cargo_test_support::compare::assert_match_exact;
use cargo_test_support::git;
use cargo_test_support::registry::Package;
use cargo_test_support::{basic_lib_manifest, basic_manifest, lines_match, project};
use cargo_test_support::{basic_lib_manifest, basic_manifest, project};
#[cargo_test]
fn oldest_lockfile_still_works() {
@ -12,15 +13,6 @@ fn oldest_lockfile_still_works() {
}
}
#[track_caller]
fn assert_lockfiles_eq(expected: &str, actual: &str) {
for (l, r) in expected.lines().zip(actual.lines()) {
assert!(lines_match(l, r), "Lines differ:\n{}\n\n{}", l, r);
}
assert_eq!(expected.lines().count(), actual.lines().count());
}
fn oldest_lockfile_still_works_with_command(cargo_command: &str) {
Package::new("bar", "0.1.0").publish();
@ -76,7 +68,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
p.cargo(cargo_command).run();
let lock = p.read_lockfile();
assert_lockfiles_eq(expected_lockfile, &lock);
assert_match_exact(expected_lockfile, &lock);
}
#[cargo_test]
@ -122,7 +114,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
p.cargo("build --locked").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(&old_lockfile, &lock);
assert_match_exact(&old_lockfile, &lock);
}
#[cargo_test]
@ -169,7 +161,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index"
p.cargo("build").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(
assert_match_exact(
r#"# This file is automatically @generated by Cargo.
# It is not intended for manual editing.
version = 3
@ -427,7 +419,7 @@ dependencies = [
\"bar\",
]
";
assert_lockfiles_eq(expected, &actual);
assert_match_exact(expected, &actual);
}
#[cargo_test]
@ -471,7 +463,7 @@ dependencies = [
p.cargo("build").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(
assert_match_exact(
r#"# [..]
# [..]
version = 3
@ -568,7 +560,7 @@ dependencies = [
p.cargo("fetch").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(&lockfile, &lock);
assert_match_exact(&lockfile, &lock);
}
#[cargo_test]
@ -639,7 +631,7 @@ dependencies = [
p.cargo("fetch").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(&lockfile, &lock);
assert_match_exact(&lockfile, &lock);
}
#[cargo_test]
@ -695,7 +687,7 @@ dependencies = [
p.cargo("fetch").run();
let lock = p.read_lockfile();
assert_lockfiles_eq(&lockfile, &lock);
assert_match_exact(&lockfile, &lock);
}
#[cargo_test]

View File

@ -737,7 +737,7 @@ fn metabuild_failed_build_json() {
"code": "{...}",
"level": "error",
"message": "cannot find function `metabuild` in [..] `mb`",
"rendered": "[..]",
"rendered": "{...}",
"spans": "{...}"
},
"package_id": "foo [..]",

View File

@ -1213,22 +1213,41 @@ fn publish_git_with_version() {
(
"Cargo.toml",
// Check that only `version` is included in Cargo.toml.
"[..]\n\
[dependencies.dep1]\n\
version = \"1.0\"\n\
",
&format!(
"{}\n\
[package]\n\
edition = \"2018\"\n\
name = \"foo\"\n\
version = \"0.1.0\"\n\
authors = []\n\
description = \"foo\"\n\
license = \"MIT\"\n\
[dependencies.dep1]\n\
version = \"1.0\"\n\
",
cargo::core::package::MANIFEST_PREAMBLE
),
),
(
"Cargo.lock",
// The important check here is that it is 1.0.1 in the registry.
"[..]\n\
"# This file is automatically @generated by Cargo.\n\
# It is not intended for manual editing.\n\
version = 3\n\
\n\
[[package]]\n\
name = \"dep1\"\n\
version = \"1.0.1\"\n\
source = \"registry+https://github.com/rust-lang/crates.io-index\"\n\
checksum = \"[..]\"\n\
\n\
[[package]]\n\
name = \"foo\"\n\
version = \"0.1.0\"\n\
dependencies = [\n\
\x20\"dep1\",\n\
]\n\
[..]",
",
),
],
);
@ -1297,7 +1316,8 @@ fn publish_dev_dep_no_version() {
&["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
&[(
"Cargo.toml",
r#"[..]
&format!(
r#"{}
[package]
name = "foo"
version = "0.1.0"
@ -1310,6 +1330,8 @@ repository = "foo"
[dev-dependencies]
"#,
cargo::core::package::MANIFEST_PREAMBLE
),
)],
);
}

View File

@ -68,6 +68,6 @@ fn cargo_verify_project_honours_unstable_features() {
p.cargo("verify-project")
.with_status(1)
.with_stdout(r#"{"invalid":"failed to parse manifest at `[CWD]/Cargo.toml`"}"#)
.with_json(r#"{"invalid":"failed to parse manifest at `[CWD]/Cargo.toml`"}"#)
.run();
}

View File

@ -702,7 +702,8 @@ fn publish() {
&["Cargo.toml", "Cargo.toml.orig", "src/lib.rs"],
&[(
"Cargo.toml",
r#"[..]
&format!(
r#"{}
[package]
name = "foo"
version = "0.1.0"
@ -717,6 +718,8 @@ optional = true
feat1 = []
feat2 = ["bar?/feat"]
"#,
cargo::core::package::MANIFEST_PREAMBLE
),
)],
);
}