diff --git a/crates/cargo-test-support/Cargo.toml b/crates/cargo-test-support/Cargo.toml index 8fe6268a4..99f8d2717 100644 --- a/crates/cargo-test-support/Cargo.toml +++ b/crates/cargo-test-support/Cargo.toml @@ -16,6 +16,7 @@ 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" diff --git a/crates/cargo-test-support/src/compare.rs b/crates/cargo-test-support/src/compare.rs new file mode 100644 index 000000000..4fa21e9c2 --- /dev/null +++ b/crates/cargo-test-support/src/compare.rs @@ -0,0 +1,597 @@ +//! 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 ``. 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::paths; +use anyhow::{bail, Context, Result}; +use serde_json::Value; +use std::env; +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', ""); + // Let's not deal with \r\n vs \n on windows... + let actual = actual.replace('\r', ""); + normalize_common(&actual, cwd) +} + +/// 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); + normalize_common(&expected, cwd) +} + +/// Normalizes text for both actual and expected strings. +fn normalize_common(text: &str, cwd: Option<&Path>) -> String { + // Let's not deal with / vs \ (windows...) + // First replace backslash-escaped backslashes with forward slashes + // which can occur in, for example, JSON output + let text = text.replace("\\\\", "/").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 ¯os { + 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 = expected.lines(); + let a = actual.lines(); + + let diffs = diff_lines(a, e, false); + if diffs.is_empty() { + Ok(()) + } else { + bail!( + "{} did not match:\n\ + {}\n\n\ + other output:\n\ + `{}`", + description, + diffs.join("\n"), + other_output, + ) + } +} + +/// 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 mut a = actual.lines().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.len()); + let mut failures = Vec::new(); + + for e_line in expected.lines() { + match a.iter().position(|a_line| lines_match(e_line, a_line)) { + Some(index) => { + a.remove(index); + } + None => failures.push(e_line), + } + } + if !failures.is_empty() { + bail!( + "Did not find expected line(s):\n{}\n\ + Remaining available output:\n{}\n", + failures.join("\n"), + a.join("\n") + ); + } + if !a.is_empty() { + bail!( + "Output included extra lines:\n\ + {}\n", + a.join("\n") + ) + } 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 = expected.lines(); + let mut a = actual.lines(); + + let mut diffs = diff_lines(a.clone(), e.clone(), true); + while a.next().is_some() { + let a = diff_lines(a.clone(), e.clone(), true); + if a.len() < diffs.len() { + diffs = a; + } + } + if diffs.is_empty() { + Ok(()) + } else { + 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 = expected.lines(); + let mut a = actual.lines(); + + let mut matches = 0; + + while let Some(..) = { + if diff_lines(a.clone(), e.clone(), true).is_empty() { + matches += 1; + } + a.next() + } {} + + if matches == number { + Ok(()) + } else { + bail!( + "expected to find {} occurrences:\n\ + {}\n\n\ + did not find in output:\n\ + {}", + number, + expected, + 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 contains = |s, line| { + let mut s = normalize_expected(s, cwd); + s.insert_str(0, "[..]"); + s.push_str("[..]"); + lines_match(&s, line) + }; + let matches: Vec<&str> = actual + .lines() + .filter(|line| with.iter().all(|with| contains(with, line))) + .filter(|line| !without.iter().any(|without| contains(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, + matches.join("\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, Vec)> { + let expected_objs: Vec<_> = expected + .split("\n\n") + .map(|expect| { + expect + .parse() + .with_context(|| format!("failed to parse expected JSON object:\n{}", expect)) + }) + .collect::>()?; + 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::>()?; + Ok((expected_objs, actual_objs)) +} + +fn diff_lines<'a>(actual: str::Lines<'a>, expected: str::Lines<'a>, partial: bool) -> Vec { + let actual = actual.take(if partial { + expected.clone().count() + } else { + usize::MAX + }); + zip_all(actual, expected) + .enumerate() + .filter_map(|(i, (a, e))| match (a, e) { + (Some(a), Some(e)) => { + if lines_match(e, a) { + None + } else { + Some(format!("{:3} - |{}|\n + |{}|\n", i, e, a)) + } + } + (Some(a), None) => Some(format!("{:3} -\n + |{}|\n", i, a)), + (None, Some(e)) => Some(format!("{:3} - |{}|\n +\n", i, e)), + (None, None) => unreachable!(), + }) + .collect() +} + +struct ZipAll { + first: I1, + second: I2, +} + +impl, I2: Iterator> Iterator for ZipAll { + type Item = (Option, Option); + fn next(&mut self) -> Option<(Option, Option)> { + let first = self.first.next(); + let second = self.second.next(); + + match (first, second) { + (None, None) => None, + (a, b) => Some((a, b)), + } + } +} + +/// Returns an iterator, similar to `zip`, but exhausts both iterators. +/// +/// Each element is `(Option, Option)` where `None` indicates an +/// iterator ended early. +fn zip_all, I2: Iterator>(a: I1, b: I2) -> ZipAll { + ZipAll { + first: a, + second: b, + } +} + +/// Compares a line with an expected pattern. +/// - Use `[..]` as a wildcard to match 0 or more characters on the same line +/// (similar to `.*` in a regex). It is non-greedy. +/// - Use `[EXE]` to optionally add `.exe` on Windows (empty string on other +/// platforms). +/// - There is a wide range of macros (such as `[COMPILING]` or `[WARNING]`) +/// to match cargo's "status" output and allows you to ignore the alignment. +/// See `substitute_macros` for a complete list of macros. +/// - `[ROOT]` the path to the test directory's root +/// - `[CWD]` is the working directory of the process that was run. +pub fn lines_match(expected: &str, mut actual: &str) -> bool { + for (i, part) in expected.split("[..]").enumerate() { + match actual.find(part) { + Some(j) => { + if i == 0 && j != 0 { + return false; + } + actual = &actual[j + part.len()..]; + } + None => return false, + } + } + actual.is_empty() || expected.ends_with("[..]") +} + +/// Variant of `lines_match` that applies normalization to the strings. +pub fn normalized_lines_match(expected: &str, actual: &str, cwd: Option<&Path>) -> bool { + let expected = normalize_expected(expected, cwd); + let actual = normalize_actual(actual, cwd); + lines_match(&expected, &actual) +} + +/// 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)) => { + let l = normalize_expected(l, cwd); + let r = normalize_actual(r, cwd); + if lines_match(&l, &r) { + None + } else { + Some((expected, actual)) + } + } + (&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)), + } +} + +#[test] +fn lines_match_works() { + assert!(lines_match("a b", "a b")); + assert!(lines_match("a[..]b", "a b")); + assert!(lines_match("a[..]", "a b")); + assert!(lines_match("[..]", "a b")); + assert!(lines_match("[..]b", "a b")); + + assert!(!lines_match("[..]b", "c")); + assert!(!lines_match("b", "c")); + assert!(!lines_match("b", "cb")); +} diff --git a/crates/cargo-test-support/src/lib.rs b/crates/cargo-test-support/src/lib.rs index 487d5910f..0f82f1aee 100644 --- a/crates/cargo-test-support/src/lib.rs +++ b/crates/cargo-test-support/src/lib.rs @@ -1,6 +1,6 @@ //! # Cargo test support. //! -//! See https://rust-lang.github.io/cargo/contrib/ for a guide on writing tests. +//! See for a guide on writing tests. #![allow(clippy::all)] #![warn(clippy::needless_borrow)] @@ -16,9 +16,9 @@ use std::process::{Command, Output}; use std::str; use std::time::{self, Duration}; -use anyhow::{bail, format_err, Result}; +use anyhow::{bail, Result}; use cargo_util::{is_ci, ProcessBuilder, ProcessError}; -use serde_json::{self, Value}; +use serde_json; use url::Url; use self::paths::CargoPathExt; @@ -50,8 +50,10 @@ pub fn panic_error(what: &str, err: impl Into) -> ! { pub use cargo_test_macro::cargo_test; +pub mod compare; pub mod cross_compile; pub mod git; +pub mod install; pub mod paths; pub mod publish; pub mod registry; @@ -449,12 +451,6 @@ pub fn cargo_exe() -> PathBuf { cargo_dir().join(format!("cargo{}", env::consts::EXE_SUFFIX)) } -/* - * - * ===== Matchers ===== - * - */ - /// This is the raw output from the process. /// /// This is similar to `std::process::Output`, however the `status` is @@ -484,10 +480,9 @@ pub struct Execs { expect_stdout_not_contains: Vec, expect_stderr_not_contains: Vec, expect_stderr_unordered: Vec, - expect_neither_contains: Vec, expect_stderr_with_without: Vec<(Vec, Vec)>, - expect_json: Option>, - expect_json_contains_unordered: Vec, + expect_json: Option, + expect_json_contains_unordered: Option, stream_output: bool, } @@ -498,14 +493,14 @@ impl Execs { } /// Verifies that stdout is equal to the given lines. - /// See `lines_match` for supported patterns. + /// See [`compare`] for supported patterns. pub fn with_stdout(&mut self, expected: S) -> &mut Self { self.expect_stdout = Some(expected.to_string()); self } /// Verifies that stderr is equal to the given lines. - /// See `lines_match` for supported patterns. + /// See [`compare`] for supported patterns. pub fn with_stderr(&mut self, expected: S) -> &mut Self { self.expect_stderr = Some(expected.to_string()); self @@ -529,7 +524,8 @@ impl Execs { /// Verifies that stdout contains the given contiguous lines somewhere in /// its output. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. pub fn with_stdout_contains(&mut self, expected: S) -> &mut Self { self.expect_stdout_contains.push(expected.to_string()); self @@ -537,7 +533,8 @@ impl Execs { /// Verifies that stderr contains the given contiguous lines somewhere in /// its output. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. pub fn with_stderr_contains(&mut self, expected: S) -> &mut Self { self.expect_stderr_contains.push(expected.to_string()); self @@ -545,7 +542,8 @@ impl Execs { /// Verifies that either stdout or stderr contains the given contiguous /// lines somewhere in its output. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. pub fn with_either_contains(&mut self, expected: S) -> &mut Self { self.expect_either_contains.push(expected.to_string()); self @@ -553,7 +551,8 @@ impl Execs { /// Verifies that stdout contains the given contiguous lines somewhere in /// its output, and should be repeated `number` times. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. pub fn with_stdout_contains_n(&mut self, expected: S, number: usize) -> &mut Self { self.expect_stdout_contains_n .push((expected.to_string(), number)); @@ -561,15 +560,18 @@ impl Execs { } /// Verifies that stdout does not contain the given contiguous lines. - /// See `lines_match` for supported patterns. - /// See note on `with_stderr_does_not_contain`. + /// + /// See [`compare`] for supported patterns. + /// + /// See note on [`Self::with_stderr_does_not_contain`]. pub fn with_stdout_does_not_contain(&mut self, expected: S) -> &mut Self { self.expect_stdout_not_contains.push(expected.to_string()); self } /// Verifies that stderr does not contain the given contiguous lines. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. /// /// Care should be taken when using this method because there is a /// limitless number of possible things that *won't* appear. A typo means @@ -583,7 +585,9 @@ impl Execs { /// Verifies that all of the stderr output is equal to the given lines, /// ignoring the order of the lines. - /// See `lines_match` for supported patterns. + /// + /// See [`compare`] for supported patterns. + /// /// This is useful when checking the output of `cargo build -v` since /// the order of the output is not always deterministic. /// Recommend use `with_stderr_contains` instead unless you really want to @@ -593,8 +597,10 @@ impl Execs { /// with multiple lines that might match, and this is not smart enough to /// do anything like longest-match. For example, avoid something like: /// - /// [RUNNING] `rustc [..] - /// [RUNNING] `rustc --crate-name foo [..] + /// ```text + /// [RUNNING] `rustc [..] + /// [RUNNING] `rustc --crate-name foo [..] + /// ``` /// /// This will randomly fail if the other crate name is `bar`, and the /// order changes. @@ -635,28 +641,28 @@ impl Execs { } /// Verifies the JSON output matches the given JSON. - /// Typically used when testing cargo commands that emit JSON. + /// + /// This is typically used when testing cargo commands that emit JSON. /// Each separate JSON object should be separated by a blank line. /// Example: - /// assert_that( - /// p.cargo("metadata"), - /// execs().with_json(r#" - /// {"example": "abc"} /// - /// {"example": "def"} - /// "#) - /// ); - /// Objects should match in the order given. - /// The order of arrays is ignored. - /// Strings support patterns described in `lines_match`. - /// Use `{...}` to match any object. + /// ```rust,ignore + /// assert_that( + /// p.cargo("metadata"), + /// execs().with_json(r#" + /// {"example": "abc"} + /// + /// {"example": "def"} + /// "#) + /// ); + /// ``` + /// + /// - Objects should match in the order given. + /// - The order of arrays is ignored. + /// - Strings support patterns described in [`compare`]. + /// - Use `"{...}"` to match any object. pub fn with_json(&mut self, expected: &str) -> &mut Self { - self.expect_json = Some( - expected - .split("\n\n") - .map(|line| line.to_string()) - .collect(), - ); + self.expect_json = Some(expected.to_string()); self } @@ -670,8 +676,13 @@ impl Execs { /// /// See `with_json` for more detail. pub fn with_json_contains_unordered(&mut self, expected: &str) -> &mut Self { - self.expect_json_contains_unordered - .extend(expected.split("\n\n").map(|line| line.to_string())); + match &mut self.expect_json_contains_unordered { + None => self.expect_json_contains_unordered = Some(expected.to_string()), + Some(e) => { + e.push_str("\n\n"); + e.push_str(expected); + } + } self } @@ -703,6 +714,10 @@ impl Execs { self } + fn get_cwd(&self) -> Option<&Path> { + self.process_builder.as_ref().and_then(|p| p.get_cwd()) + } + pub fn env>(&mut self, key: &str, val: T) -> &mut Self { if let Some(ref mut p) = self.process_builder { p.env(key, val); @@ -779,7 +794,7 @@ impl Execs { #[track_caller] pub fn run_output(&mut self, output: &Output) { self.ran = true; - if let Err(e) = self.match_output(output) { + if let Err(e) = self.match_output(output.status.code(), &output.stdout, &output.stderr) { panic_error("process did not return the expected result", e) } } @@ -796,10 +811,9 @@ impl Execs { && self.expect_stdout_not_contains.is_empty() && self.expect_stderr_not_contains.is_empty() && self.expect_stderr_unordered.is_empty() - && self.expect_neither_contains.is_empty() && self.expect_stderr_with_without.is_empty() && self.expect_json.is_none() - && self.expect_json_contains_unordered.is_empty() + && self.expect_json_contains_unordered.is_none() { panic!( "`with_status()` is used, but no output is checked.\n\ @@ -834,7 +848,7 @@ impl Execs { match res { Ok(out) => { - self.match_output(&out)?; + self.match_output(out.status.code(), &out.stdout, &out.stderr)?; return Ok(RawOutput { stdout: out.stdout, stderr: out.stderr, @@ -849,9 +863,7 @@ impl Execs { .. }) = e.downcast_ref::() { - self.match_status(*code, stdout, stderr) - .and(self.match_stdout(stdout, stderr)) - .and(self.match_stderr(stdout, stderr))?; + self.match_output(*code, stdout, stderr)?; return Ok(RawOutput { stdout: stdout.to_vec(), stderr: stderr.to_vec(), @@ -863,376 +875,78 @@ impl Execs { } } - fn match_output(&self, actual: &Output) -> Result<()> { - self.match_status(actual.status.code(), &actual.stdout, &actual.stderr) - .and(self.match_stdout(&actual.stdout, &actual.stderr)) - .and(self.match_stderr(&actual.stdout, &actual.stderr)) - } - - fn match_status(&self, code: Option, stdout: &[u8], stderr: &[u8]) -> Result<()> { + fn match_output(&self, code: Option, stdout: &[u8], stderr: &[u8]) -> Result<()> { self.verify_checks_output(stdout, stderr); + let stdout = str::from_utf8(stdout).expect("stdout is not utf8"); + let stderr = str::from_utf8(stderr).expect("stderr is not utf8"); + let cwd = self.get_cwd(); + match self.expect_exit_code { - None => Ok(()), - Some(expected) if code == Some(expected) => Ok(()), + None => {} + Some(expected) if code == Some(expected) => {} Some(expected) => bail!( "process exited with code {} (expected {})\n--- stdout\n{}\n--- stderr\n{}", code.unwrap_or(-1), expected, - String::from_utf8_lossy(stdout), - String::from_utf8_lossy(stderr) + stdout, + stderr ), } - } - fn match_stdout(&self, stdout: &[u8], stderr: &[u8]) -> Result<()> { - self.match_std( - self.expect_stdout.as_ref(), - stdout, - "stdout", - stderr, - MatchKind::Exact, - )?; + if let Some(expect_stdout) = &self.expect_stdout { + compare::match_exact(expect_stdout, stdout, "stdout", stderr, cwd)?; + } + if let Some(expect_stderr) = &self.expect_stderr { + compare::match_exact(expect_stderr, stderr, "stderr", stdout, cwd)?; + } for expect in self.expect_stdout_contains.iter() { - self.match_std(Some(expect), stdout, "stdout", stderr, MatchKind::Partial)?; + compare::match_contains(expect, stdout, cwd)?; } for expect in self.expect_stderr_contains.iter() { - self.match_std(Some(expect), stderr, "stderr", stdout, MatchKind::Partial)?; + compare::match_contains(expect, stderr, cwd)?; } for &(ref expect, number) in self.expect_stdout_contains_n.iter() { - self.match_std( - Some(expect), - stdout, - "stdout", - stderr, - MatchKind::PartialN(number), - )?; + compare::match_contains_n(expect, number, stdout, cwd)?; } for expect in self.expect_stdout_not_contains.iter() { - self.match_std( - Some(expect), - stdout, - "stdout", - stderr, - MatchKind::NotPresent, - )?; + compare::match_does_not_contain(expect, stdout, cwd)?; } for expect in self.expect_stderr_not_contains.iter() { - self.match_std( - Some(expect), - stderr, - "stderr", - stdout, - MatchKind::NotPresent, - )?; + compare::match_does_not_contain(expect, stderr, cwd)?; } for expect in self.expect_stderr_unordered.iter() { - self.match_std(Some(expect), stderr, "stderr", stdout, MatchKind::Unordered)?; + compare::match_unordered(expect, stderr, cwd)?; } - for expect in self.expect_neither_contains.iter() { - self.match_std( - Some(expect), - stdout, - "stdout", - stdout, - MatchKind::NotPresent, - )?; - - self.match_std( - Some(expect), - stderr, - "stderr", - stderr, - MatchKind::NotPresent, - )?; - } - for expect in self.expect_either_contains.iter() { - let match_std = - self.match_std(Some(expect), stdout, "stdout", stdout, MatchKind::Partial); - let match_err = - self.match_std(Some(expect), stderr, "stderr", stderr, MatchKind::Partial); - + let match_std = compare::match_contains(expect, stdout, cwd); + let match_err = compare::match_contains(expect, stderr, cwd); if let (Err(_), Err(_)) = (match_std, match_err) { bail!( "expected to find:\n\ {}\n\n\ - did not find in either output.", - expect + did not find in either output. + --- stdout\n{}\n + --- stderr\n{}\n", + expect, + stdout, + stderr, ); } } for (with, without) in self.expect_stderr_with_without.iter() { - self.match_with_without(stderr, with, without)?; + compare::match_with_without(stderr, with, without, cwd)?; } - if let Some(ref objects) = self.expect_json { - let stdout = - str::from_utf8(stdout).map_err(|_| format_err!("stdout was not utf8 encoded"))?; - let lines = stdout - .lines() - .filter(|line| line.starts_with('{')) - .collect::>(); - if lines.len() != objects.len() { - bail!( - "expected {} json lines, got {}, stdout:\n{}", - objects.len(), - lines.len(), - stdout - ); - } - for (obj, line) in objects.iter().zip(lines) { - self.match_json(obj, line)?; - } + if let Some(ref expect_json) = self.expect_json { + compare::match_json(expect_json, stdout, cwd)?; } - if !self.expect_json_contains_unordered.is_empty() { - let stdout = - str::from_utf8(stdout).map_err(|_| format_err!("stdout was not utf8 encoded"))?; - let mut lines = stdout - .lines() - .filter(|line| line.starts_with('{')) - .collect::>(); - for obj in &self.expect_json_contains_unordered { - match lines - .iter() - .position(|line| self.match_json(obj, line).is_ok()) - { - Some(index) => lines.remove(index), - None => { - bail!( - "Did not find expected JSON:\n\ - {}\n\ - Remaining available output:\n\ - {}\n", - serde_json::to_string_pretty(obj).unwrap(), - lines.join("\n") - ); - } - }; - } + if let Some(ref expected) = self.expect_json_contains_unordered { + compare::match_json_contains_unordered(expected, stdout, cwd)?; } Ok(()) } - - fn match_stderr(&self, stdout: &[u8], stderr: &[u8]) -> Result<()> { - self.match_std( - self.expect_stderr.as_ref(), - stderr, - "stderr", - stdout, - MatchKind::Exact, - ) - } - - fn normalize_actual(&self, description: &str, actual: &[u8]) -> Result { - let actual = match str::from_utf8(actual) { - Err(..) => bail!("{} was not utf8 encoded", description), - Ok(actual) => actual, - }; - Ok(self.normalize_matcher(actual)) - } - - fn normalize_matcher(&self, matcher: &str) -> String { - normalize_matcher( - matcher, - self.process_builder.as_ref().and_then(|p| p.get_cwd()), - ) - } - - fn match_std( - &self, - expected: Option<&String>, - actual: &[u8], - description: &str, - extra: &[u8], - kind: MatchKind, - ) -> Result<()> { - let out = match expected { - Some(out) => self.normalize_matcher(out), - None => return Ok(()), - }; - - let actual = self.normalize_actual(description, actual)?; - - match kind { - MatchKind::Exact => { - let a = actual.lines(); - let e = out.lines(); - - let diffs = self.diff_lines(a, e, false); - if diffs.is_empty() { - Ok(()) - } else { - bail!( - "{} did not match:\n\ - {}\n\n\ - other output:\n\ - `{}`", - description, - diffs.join("\n"), - String::from_utf8_lossy(extra) - ) - } - } - MatchKind::Partial => { - let mut a = actual.lines(); - let e = out.lines(); - - let mut diffs = self.diff_lines(a.clone(), e.clone(), true); - while a.next().is_some() { - let a = self.diff_lines(a.clone(), e.clone(), true); - if a.len() < diffs.len() { - diffs = a; - } - } - if diffs.is_empty() { - Ok(()) - } else { - bail!( - "expected to find:\n\ - {}\n\n\ - did not find in output:\n\ - {}", - out, - actual - ) - } - } - MatchKind::PartialN(number) => { - let mut a = actual.lines(); - let e = out.lines(); - - let mut matches = 0; - - while let Some(..) = { - if self.diff_lines(a.clone(), e.clone(), true).is_empty() { - matches += 1; - } - a.next() - } {} - - if matches == number { - Ok(()) - } else { - bail!( - "expected to find {} occurrences:\n\ - {}\n\n\ - did not find in output:\n\ - {}", - number, - out, - actual - ) - } - } - MatchKind::NotPresent => { - let mut a = actual.lines(); - let e = out.lines(); - - let mut diffs = self.diff_lines(a.clone(), e.clone(), true); - while a.next().is_some() { - let a = self.diff_lines(a.clone(), e.clone(), true); - if a.len() < diffs.len() { - diffs = a; - } - } - if diffs.is_empty() { - bail!( - "expected not to find:\n\ - {}\n\n\ - but found in output:\n\ - {}", - out, - actual - ) - } else { - Ok(()) - } - } - MatchKind::Unordered => lines_match_unordered(&out, &actual), - } - } - - fn match_with_without(&self, actual: &[u8], with: &[String], without: &[String]) -> Result<()> { - let actual = self.normalize_actual("stderr", actual)?; - let contains = |s, line| { - let mut s = self.normalize_matcher(s); - s.insert_str(0, "[..]"); - s.push_str("[..]"); - lines_match(&s, line) - }; - let matches: Vec<&str> = actual - .lines() - .filter(|line| with.iter().all(|with| contains(with, line))) - .filter(|line| !without.iter().any(|without| contains(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, - matches.join("\n") - ), - } - } - - fn match_json(&self, expected: &str, line: &str) -> Result<()> { - let actual = match line.parse() { - Err(e) => bail!("invalid json, {}:\n`{}`", e, line), - Ok(actual) => actual, - }; - let expected = match expected.parse() { - Err(e) => bail!("invalid json, {}:\n`{}`", e, line), - Ok(expected) => expected, - }; - - let cwd = self.process_builder.as_ref().and_then(|p| p.get_cwd()); - find_json_mismatch(&expected, &actual, cwd) - } - - fn diff_lines<'a>( - &self, - actual: str::Lines<'a>, - expected: str::Lines<'a>, - partial: bool, - ) -> Vec { - let actual = actual.take(if partial { - expected.clone().count() - } else { - usize::MAX - }); - zip_all(actual, expected) - .enumerate() - .filter_map(|(i, (a, e))| match (a, e) { - (Some(a), Some(e)) => { - if lines_match(e, a) { - None - } else { - Some(format!("{:3} - |{}|\n + |{}|\n", i, e, a)) - } - } - (Some(a), None) => Some(format!("{:3} -\n + |{}|\n", i, a)), - (None, Some(e)) => Some(format!("{:3} - |{}|\n +\n", i, e)), - (None, None) => panic!("Cannot get here"), - }) - .collect() - } } impl Drop for Execs { @@ -1243,227 +957,6 @@ impl Drop for Execs { } } -#[derive(Debug, PartialEq, Eq, Clone, Copy)] -enum MatchKind { - Exact, - Partial, - PartialN(usize), - NotPresent, - Unordered, -} - -/// Compares a line with an expected pattern. -/// - Use `[..]` as a wildcard to match 0 or more characters on the same line -/// (similar to `.*` in a regex). It is non-greedy. -/// - Use `[EXE]` to optionally add `.exe` on Windows (empty string on other -/// platforms). -/// - There is a wide range of macros (such as `[COMPILING]` or `[WARNING]`) -/// to match cargo's "status" output and allows you to ignore the alignment. -/// See `substitute_macros` for a complete list of macros. -/// - `[ROOT]` the path to the test directory's root -/// - `[CWD]` is the working directory of the process that was run. -pub fn lines_match(expected: &str, mut actual: &str) -> bool { - let expected = substitute_macros(expected); - for (i, part) in expected.split("[..]").enumerate() { - match actual.find(part) { - Some(j) => { - if i == 0 && j != 0 { - return false; - } - actual = &actual[j + part.len()..]; - } - None => return false, - } - } - actual.is_empty() || expected.ends_with("[..]") -} - -pub fn lines_match_unordered(expected: &str, actual: &str) -> Result<()> { - let mut a = actual.lines().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.len()); - let mut failures = Vec::new(); - - for e_line in expected.lines() { - match a.iter().position(|a_line| lines_match(e_line, a_line)) { - Some(index) => { - a.remove(index); - } - None => failures.push(e_line), - } - } - if !failures.is_empty() { - bail!( - "Did not find expected line(s):\n{}\n\ - Remaining available output:\n{}\n", - failures.join("\n"), - a.join("\n") - ); - } - if !a.is_empty() { - bail!( - "Output included extra lines:\n\ - {}\n", - a.join("\n") - ) - } else { - Ok(()) - } -} - -/// Variant of `lines_match` that applies normalization to the strings. -pub fn normalized_lines_match(expected: &str, actual: &str, cwd: Option<&Path>) -> bool { - let expected = normalize_matcher(expected, cwd); - let actual = normalize_matcher(actual, cwd); - lines_match(&expected, &actual) -} - -fn normalize_matcher(matcher: &str, cwd: Option<&Path>) -> String { - // Let's not deal with / vs \ (windows...) - let matcher = matcher.replace("\\\\", "/").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) - }; - - // Do the template replacements on the expected string. - let matcher = match cwd { - None => matcher, - Some(p) => replace_path(&matcher, 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 matcher = replace_path(&matcher, &root, "[ROOT]"); - - // Let's not deal with \r\n vs \n on windows... - let matcher = matcher.replace("\r", ""); - - // It's easier to read tabs in outputs if they don't show up as literal - // hidden characters - matcher.replace("\t", "") -} - -#[test] -fn lines_match_works() { - assert!(lines_match("a b", "a b")); - assert!(lines_match("a[..]b", "a b")); - assert!(lines_match("a[..]", "a b")); - assert!(lines_match("[..]", "a b")); - assert!(lines_match("[..]b", "a b")); - - assert!(!lines_match("[..]b", "c")); - assert!(!lines_match("b", "c")); - assert!(!lines_match("b", "cb")); -} - -/// 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)) => { - let normalized = normalize_matcher(r, cwd); - if lines_match(l, &normalized) { - None - } else { - Some((expected, actual)) - } - } - (&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)), - } -} - -struct ZipAll { - first: I1, - second: I2, -} - -impl, I2: Iterator> Iterator for ZipAll { - type Item = (Option, Option); - fn next(&mut self) -> Option<(Option, Option)> { - let first = self.first.next(); - let second = self.second.next(); - - match (first, second) { - (None, None) => None, - (a, b) => Some((a, b)), - } - } -} - -fn zip_all, I2: Iterator>(a: I1, b: I2) -> ZipAll { - ZipAll { - first: a, - second: b, - } -} - pub fn execs() -> Execs { Execs { ran: false, @@ -1479,10 +972,9 @@ pub fn execs() -> Execs { expect_stdout_not_contains: Vec::new(), expect_stderr_not_contains: Vec::new(), expect_stderr_unordered: Vec::new(), - expect_neither_contains: Vec::new(), expect_stderr_with_without: Vec::new(), expect_json: None, - expect_json_contains_unordered: Vec::new(), + expect_json_contains_unordered: None, stream_output: false, } } @@ -1537,56 +1029,6 @@ pub fn path2url>(p: P) -> Url { Url::from_file_path(p).ok().unwrap() } -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 ¯os { - result = result.replace(pat, subst) - } - result -} - -pub mod install; - struct RustcInfo { verbose_version: String, host: String, diff --git a/crates/cargo-test-support/src/publish.rs b/crates/cargo-test-support/src/publish.rs index 6a4549f15..a0b31be21 100644 --- a/crates/cargo-test-support/src/publish.rs +++ b/crates/cargo-test-support/src/publish.rs @@ -1,5 +1,5 @@ +use crate::compare::{find_json_mismatch, lines_match}; 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; diff --git a/src/doc/contrib/src/tests/writing.md b/src/doc/contrib/src/tests/writing.md index 3343f1e65..42d1b5c79 100644 --- a/src/doc/contrib/src/tests/writing.md +++ b/src/doc/contrib/src/tests/writing.md @@ -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 diff --git a/tests/testsuite/build.rs b/tests/testsuite/build.rs index 5b2f3786b..1dfc8d994 100644 --- a/tests/testsuite/build.rs +++ b/tests/testsuite/build.rs @@ -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] diff --git a/tests/testsuite/build_script.rs b/tests/testsuite/build_script.rs index 44e9295f6..46ccb703e 100644 --- a/tests/testsuite/build_script.rs +++ b/tests/testsuite/build_script.rs @@ -1,8 +1,9 @@ //! Tests for build.rs scripts. +use cargo_test_support::compare::lines_match; +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; diff --git a/tests/testsuite/config.rs b/tests/testsuite/config.rs index e066a7927..b416b687e 100644 --- a/tests/testsuite/config.rs +++ b/tests/testsuite/config.rs @@ -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::normalized_lines_match; +use cargo_test_support::{paths, project, t}; use serde::Deserialize; use std::borrow::Borrow; use std::collections::{BTreeMap, HashMap}; diff --git a/tests/testsuite/lockfile_compat.rs b/tests/testsuite/lockfile_compat.rs index 77d874c5a..8955beaae 100644 --- a/tests/testsuite/lockfile_compat.rs +++ b/tests/testsuite/lockfile_compat.rs @@ -1,8 +1,9 @@ //! Tests for supporting older versions of the Cargo.lock file format. +use cargo_test_support::compare::lines_match; 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() {