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;
pub fn assert_ui() -> snapbox::Assert {
let root = paths::root();
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)
}
fn normalize_actual(actual: &str, cwd: Option<&Path>) -> String {
let actual = actual.replace('\t', "<tab>");
if cfg!(windows) {
let actual = actual.replace('\r', "");
normalize_windows(&actual, cwd)
} else {
actual
}
}
fn normalize_expected(expected: &str, cwd: Option<&Path>) -> String {
let expected = replace_dirty_msvc(expected);
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
}
}
fn replace_dirty_msvc_impl(s: &str, is_msvc: bool) -> String {
if is_msvc {
s.replace("[DIRTY-MSVC]", "[DIRTY]")
} else {
use itertools::Itertools;
let mut new = s
.lines()
.filter(|it| !it.starts_with("[DIRTY-MSVC]"))
.join("\n");
if s.ends_with("\n") {
new.push_str("\n");
}
new
}
}
fn replace_dirty_msvc(s: &str) -> String {
replace_dirty_msvc_impl(s, cfg!(target_env = "msvc"))
}
fn normalize_windows(text: &str, cwd: Option<&Path>) -> String {
let text = text.replace('\\', "/");
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]"),
};
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"),
("[CREATING]", " Creating"),
("[CREDENTIAL]", " Credential"),
("[DOWNGRADING]", " Downgrading"),
("[FINISHED]", " Finished"),
("[ERROR]", "error:"),
("[WARNING]", "warning:"),
("[NOTE]", "note:"),
("[HELP]", "help:"),
("[DOCUMENTING]", " Documenting"),
("[SCRAPING]", " Scraping"),
("[FRESH]", " Fresh"),
("[DIRTY]", " Dirty"),
("[LOCKING]", " Locking"),
("[UPDATING]", " Updating"),
("[ADDING]", " Adding"),
("[REMOVING]", " Removing"),
("[REMOVED]", " Removed"),
("[UNCHANGED]", " Unchanged"),
("[DOCTEST]", " Doc-tests"),
("[PACKAGING]", " Packaging"),
("[PACKAGED]", " Packaged"),
("[DOWNLOADING]", " Downloading"),
("[DOWNLOADED]", " Downloaded"),
("[UPLOADING]", " Uploading"),
("[UPLOADED]", " Uploaded"),
("[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"),
("[WAITING]", " Waiting"),
("[PUBLISHED]", " Published"),
("[BLOCKING]", " Blocking"),
("[GENERATED]", " Generated"),
];
let mut result = input.to_owned();
for &(pat, subst) in ¯os {
result = result.replace(pat, subst)
}
result
}
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,
);
}
#[track_caller]
pub fn assert_match_exact(expected: &str, actual: &str) {
if let Err(e) = match_exact(expected, actual, "", "", None) {
crate::panic_error("", e);
}
}
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();
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(())
}
}
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
);
}
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(())
}
}
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
)
}
}
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")
),
}
}
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(())
}
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))
}
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 mut expected_entries = l.iter();
let mut actual_entries = r.iter();
loop {
match (expected_entries.next(), actual_entries.next()) {
(None, None) => return None,
(Some((expected_key, expected_value)), Some((actual_key, actual_value)))
if expected_key == actual_key =>
{
if let mismatch @ Some(_) =
find_json_mismatch_r(expected_value, actual_value, cwd)
{
return mismatch;
}
}
_ => return Some((expected, actual)),
}
}
}
(&Null, &Null) => None,
_ => Some((expected, actual)),
}
}
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));
}
}
#[test]
fn dirty_msvc() {
let case = |expected: &str, wild: &str, msvc: bool| {
assert_eq!(expected, &replace_dirty_msvc_impl(wild, msvc));
};
case("aa", "aa", false);
case("aa", "aa", true);
case(
"\
[DIRTY] a",
"\
[DIRTY-MSVC] a",
true,
);
case(
"",
"\
[DIRTY-MSVC] a",
false,
);
case(
"\
[DIRTY] a
[COMPILING] a",
"\
[DIRTY-MSVC] a
[COMPILING] a",
true,
);
case(
"\
[COMPILING] a",
"\
[DIRTY-MSVC] a
[COMPILING] a",
false,
);
case(
"\
A
B
", "\
A
B
", true,
);
case(
"\
A
B
", "\
A
B
", false,
);
case(
"\
A
B", "\
A
B", true,
);
case(
"\
A
B", "\
A
B", false,
);
case(
"\
[DIRTY] a
",
"\
[DIRTY-MSVC] a
",
true,
);
case(
"\n",
"\
[DIRTY-MSVC] a
",
false,
);
case(
"\
[DIRTY] a",
"\
[DIRTY-MSVC] a",
true,
);
case(
"",
"\
[DIRTY-MSVC] a",
false,
);
}