mirror of
https://github.com/rust-lang/cargo.git
synced 2025-09-28 11:20:36 +00:00

Git lets users define the default update/checkout strategy for a submodule by setting the `submodule.<name>.update` key in `.gitmodules` file. If the update strategy is `none`, the submodule will be skipped during update. It will not be fetched and checked out: 1. *foo* is a big git repo ``` /tmp $ git init foo Initialized empty Git repository in /tmp/foo/.git/ /tmp $ dd if=/dev/zero of=foo/big bs=1000M count=1 1+0 records in 1+0 records out 1048576000 bytes (1.0 GB, 1000 MiB) copied, 0.482087 s, 2.2 GB/s /tmp $ git -C foo add big /tmp $ git -C foo commit -m 'I am big' [main (root-commit) 84fb533] I am big 1 file changed, 0 insertions(+), 0 deletions(-) create mode 100644 big ``` 2. *bar* is a repo with a big submodule with `update=none` ``` /tmp $ git init bar Initialized empty Git repository in /tmp/bar/.git/ /tmp $ git -C bar submodule add file:///tmp/foo foo Cloning into '/tmp/bar/foo'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 1 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), 995.50 KiB | 338.00 KiB/s, done. /tmp $ git -C bar config --file .gitmodules submodule.foo.update none /tmp $ cat bar/.gitmodules [submodule "foo"] path = foo url = file:///tmp/foo update = none /tmp $ git -C bar commit --all -m 'I have a big submodule with update=none' [main (root-commit) 6c355ea] I have a big submodule not updated by default 2 files changed, 4 insertions(+) create mode 100644 .gitmodules create mode 160000 foo ``` 3. *baz* is a clone of *bar*, notice *foo* submodule gets skipped ``` /tmp $ git clone --recurse-submodules file:///tmp/bar baz Cloning into 'baz'... remote: Enumerating objects: 3, done. remote: Counting objects: 100% (3/3), done. remote: Compressing objects: 100% (3/3), done. remote: Total 3 (delta 0), reused 0 (delta 0), pack-reused 0 Receiving objects: 100% (3/3), done. Submodule 'foo' (file:///tmp/foo) registered for path 'foo' Skipping submodule 'foo' /tmp $ git -C baz submodule update --init Skipping submodule 'foo' /tmp $ ``` Cargo, on the other hand, ignores the submodule update strategy set in `.gitmodules` properties when updating dependencies. Such behavior can be considered against the wish of the crate publisher. 4. *bar* is now a lib with a big submodule with update disabled ``` /tmp $ cargo init --lib bar Created library package /tmp $ git -C bar add . /tmp $ git -C bar commit -m 'I am a lib with a big submodule but update=none' [main eb07cf7] I am a lib with a big submodule but update=none 3 files changed, 18 insertions(+) create mode 100644 .gitignore create mode 100644 Cargo.toml create mode 100644 src/lib.rs /tmp $ ``` 5. *qux* depends on *bar*, notice *bar*'s submodules are fetched ``` /tmp $ cargo init qux && cd qux Created binary (application) package /tmp/qux $ echo -e '[dependencies.bar]\ngit = "file:///tmp/bar"' >> Cargo.toml /tmp/qux $ time cargo update Updating git repository `file:///tmp/bar` Updating git submodule `file:///tmp/foo` real 0m22.182s user 0m20.402s sys 0m1.714s /tmp/qux $ ``` Fix it by checking if a Git repository submodule should be updated when cargo processes dependencies. 6. With the change applied, submodules with `update=none` are skipped ``` /tmp/qux $ cargo cache -a > /dev/null /tmp/qux $ time ~/src/cargo/target/debug/cargo update Updating git repository `file:///tmp/bar` Skipping git submodule `file:///tmp/foo` real 0m0.029s user 0m0.021s sys 0m0.008s /tmp/qux $ ``` Fixes #4247. Signed-off-by: Jakub Sitnicki <jakub@cloudflare.com>
637 lines
21 KiB
Rust
637 lines
21 KiB
Rust
//! 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;
|
|
|
|
/// Default `snapbox` Assertions
|
|
///
|
|
/// # Snapshots
|
|
///
|
|
/// Updating of snapshots is controlled with the `SNAPSHOTS` environment variable:
|
|
///
|
|
/// - `skip`: do not run the tests
|
|
/// - `ignore`: run the tests but ignore their failure
|
|
/// - `verify`: run the tests
|
|
/// - `overwrite`: update the snapshots based on the output of the tests
|
|
///
|
|
/// # Patterns
|
|
///
|
|
/// - `[..]` is a character wildcard, stopping at line breaks
|
|
/// - `\n...\n` is a multi-line wildcard
|
|
/// - `[EXE]` matches the exe suffix for the current platform
|
|
/// - `[ROOT]` matches [`paths::root()`][crate::paths::root]
|
|
/// - `[ROOTURL]` matches [`paths::root()`][crate::paths::root] as a URL
|
|
///
|
|
/// # Normalization
|
|
///
|
|
/// In addition to the patterns described above, text is normalized
|
|
/// in such a way to avoid unwanted differences. The normalizations are:
|
|
///
|
|
/// - 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.
|
|
pub fn assert() -> snapbox::Assert {
|
|
let root = paths::root();
|
|
// Use `from_file_path` instead of `from_dir_path` so the trailing slash is
|
|
// put in the users output, rather than hidden in the variable
|
|
let root_url = url::Url::from_file_path(&root).unwrap().to_string();
|
|
let root = root.display().to_string();
|
|
|
|
let mut subs = snapbox::Substitutions::new();
|
|
subs.extend([
|
|
(
|
|
"[EXE]",
|
|
std::borrow::Cow::Borrowed(std::env::consts::EXE_SUFFIX),
|
|
),
|
|
("[ROOT]", std::borrow::Cow::Owned(root)),
|
|
("[ROOTURL]", std::borrow::Cow::Owned(root_url)),
|
|
])
|
|
.unwrap();
|
|
snapbox::Assert::new()
|
|
.action_env(snapbox::DEFAULT_ACTION_ENV)
|
|
.substitutions(subs)
|
|
}
|
|
|
|
/// 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"),
|
|
("[EXECUTABLE]", " Executable"),
|
|
("[SKIPPING]", " Skipping"),
|
|
];
|
|
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: 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));
|
|
}
|
|
}
|