build(rust): vendor googletest crate
This commit was merged in pull request #76.
This commit is contained in:
1 parent
a6fe84190e
commit
6887b104d6
576 files changed
+284290
-2
No files matched your search
@@ -0,0 +1,107 @@
|
||||
// Copyright 2024 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![doc(hidden)]
|
||||
|
||||
/// Macro that wraps the expression with `eq(...)` if the expression is
|
||||
/// not a matcher.
|
||||
///
|
||||
/// This is useful to let users pass expected value to macro matchers like
|
||||
/// `field!` and `property!`.
|
||||
///`
|
||||
/// **For internal use only. API stablility is not guaranteed!**
|
||||
/// If you are interested in using it in your matcher, please file an issue to
|
||||
/// stabilize this.
|
||||
#[macro_export]
|
||||
macro_rules! __auto_eq {
|
||||
($e:expr) => {{
|
||||
#[allow(unused_imports)]
|
||||
use $crate::matcher_support::__internal_unstable_do_not_depend_on_these::ExpectedKind as _;
|
||||
match $e {
|
||||
expected => {
|
||||
$crate::matcher_support::__internal_unstable_do_not_depend_on_these::Wrapper(
|
||||
&expected,
|
||||
)
|
||||
.kind()
|
||||
.matcher(expected)
|
||||
}
|
||||
}
|
||||
}};
|
||||
}
|
||||
|
||||
// This reimplements the pattern presented in
|
||||
// https://github.com/dtolnay/case-studies/issues/14
|
||||
pub mod internal {
|
||||
use crate::{
|
||||
matcher::MatcherBase,
|
||||
matchers::{eq, EqMatcher},
|
||||
};
|
||||
|
||||
pub struct Wrapper<T>(pub T);
|
||||
|
||||
impl<T: MatcherBase> Wrapper<&'_ T> {
|
||||
#[inline]
|
||||
pub fn kind(&self) -> MatcherTag {
|
||||
MatcherTag
|
||||
}
|
||||
}
|
||||
|
||||
pub trait ExpectedKind {
|
||||
#[inline]
|
||||
fn kind(&self) -> ExpectedTag {
|
||||
ExpectedTag
|
||||
}
|
||||
}
|
||||
|
||||
impl<T> ExpectedKind for Wrapper<T> {}
|
||||
|
||||
pub struct MatcherTag;
|
||||
|
||||
impl MatcherTag {
|
||||
#[inline]
|
||||
pub fn matcher<M>(self, matcher: M) -> M {
|
||||
matcher
|
||||
}
|
||||
}
|
||||
pub struct ExpectedTag;
|
||||
|
||||
impl ExpectedTag {
|
||||
#[inline]
|
||||
pub fn matcher<T>(self, expected: T) -> EqMatcher<T> {
|
||||
eq(expected)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use crate::prelude::*;
|
||||
use crate::Result;
|
||||
|
||||
#[test]
|
||||
fn auto_ref_matcher() -> Result<()> {
|
||||
verify_that!(123, __auto_eq!(ge(9)))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_ref_expected() -> Result<()> {
|
||||
verify_that!(123, __auto_eq!(123))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn auto_ref_on_ref_matcher() -> Result<()> {
|
||||
let matcher = eq(123);
|
||||
verify_that!(123, __auto_eq!(&matcher))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,79 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Counts the number of elements in `value`.
|
||||
///
|
||||
/// This uses [`Iterator::size_hint`] when that function returns an
|
||||
/// unambiguous answer, i.e., the upper bound exists and the lower and upper
|
||||
/// bounds agree. Otherwise it iterates through `value` and counts the
|
||||
/// elements.
|
||||
pub(crate) fn count_elements<ContainerT: IntoIterator>(value: ContainerT) -> usize {
|
||||
let iterator = value.into_iter();
|
||||
if let (lower, Some(higher)) = iterator.size_hint() {
|
||||
if lower == higher {
|
||||
return lower;
|
||||
}
|
||||
}
|
||||
iterator.count()
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
use crate::Result;
|
||||
|
||||
#[test]
|
||||
fn count_elements_vec() -> Result<()> {
|
||||
verify_that!(count_elements(vec![1, 2, 3]), eq(3))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_elements_with_imprecise_hint() -> Result<()> {
|
||||
struct FakeIterator;
|
||||
|
||||
impl Iterator for FakeIterator {
|
||||
type Item = ();
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
None
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, Some(123))
|
||||
}
|
||||
}
|
||||
|
||||
verify_that!(count_elements(FakeIterator), eq(0))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn count_elements_with_no_hint() -> Result<()> {
|
||||
struct FakeIterator;
|
||||
|
||||
impl Iterator for FakeIterator {
|
||||
type Item = ();
|
||||
|
||||
fn next(&mut self) -> Option<Self::Item> {
|
||||
None
|
||||
}
|
||||
|
||||
fn size_hint(&self) -> (usize, Option<usize>) {
|
||||
(0, None)
|
||||
}
|
||||
}
|
||||
|
||||
verify_that!(count_elements(FakeIterator), eq(0))
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,649 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
use std::fmt::Debug;
|
||||
|
||||
/// Maximum number of edits which can exist before [`edit_list`] falls back to a
|
||||
/// complete rewrite to produce the edit list.
|
||||
///
|
||||
/// Increasing this limit increases the accuracy of [`edit_list`] while
|
||||
/// quadratically increasing its worst-case runtime.
|
||||
const MAX_DISTANCE: i32 = 50;
|
||||
|
||||
/// The difference between two inputs as produced by [`edit_list`].
|
||||
#[derive(Debug)]
|
||||
pub(crate) enum Difference<T> {
|
||||
/// No differences were detected at all.
|
||||
Equal,
|
||||
|
||||
/// At most [`MAX_DISTANCE`] edits are required to convert one input to the
|
||||
/// other.
|
||||
///
|
||||
/// Contains the list of [`Edit`] to perform the transformation.
|
||||
Editable(Vec<Edit<T>>),
|
||||
|
||||
/// More than [`MAX_DISTANCE`] edits are required to convert one input to
|
||||
/// the other.
|
||||
///
|
||||
/// The inputs are therefore considered unrelated and no edit list is
|
||||
/// provided.
|
||||
Unrelated,
|
||||
}
|
||||
|
||||
/// An edit operation on two sequences of `T`.
|
||||
#[derive(Debug, Clone)]
|
||||
pub(crate) enum Edit<T> {
|
||||
/// An extra `T` was added to the actual sequence.
|
||||
ExtraActual(T),
|
||||
|
||||
/// An extra `T` was added to the expected sequence.
|
||||
ExtraExpected(T),
|
||||
|
||||
/// An element was added to each sequence.
|
||||
Both(T),
|
||||
|
||||
/// Additional (unlisted) elements are present in the actual sequence.
|
||||
///
|
||||
/// This is only output in the mode [`Mode::Prefix`]. Its presence precludes
|
||||
/// reconstructing the actual sequence from the expected sequence.
|
||||
AdditionalActual,
|
||||
}
|
||||
|
||||
/// Controls the termination condition of [`edit_list`].
|
||||
#[derive(Clone, Copy)]
|
||||
pub(crate) enum Mode {
|
||||
/// Indicates that the two arguments are intended to be equal.
|
||||
///
|
||||
/// The entire edit list to transform between `actual` and `expected` is
|
||||
/// returned.
|
||||
Exact,
|
||||
|
||||
/// Indicates that `expected` is inteded to be a prefix of `actual`.
|
||||
///
|
||||
/// Any additional parts of `actual` after the prefix `expected` are omitted
|
||||
/// from the output.
|
||||
Prefix,
|
||||
|
||||
/// Similar to [`Mode::Prefix`], except it is also assumed that `actual` has
|
||||
/// some number of initial lines which should not be in the output.
|
||||
///
|
||||
/// Any initial [`Edit::ExtraActual`] entries are replaced with
|
||||
/// [`Edit::AdditionalActual`] in the edit list. If the first entry which is
|
||||
/// not an [`Edit::ExtraActual`] is [`Edit::ExtraExpected`], then the last
|
||||
/// [`Edit::ExtraActual`] is actual in the output.
|
||||
Contains,
|
||||
}
|
||||
|
||||
/// Computes the edit list of `actual` and `expected`.
|
||||
///
|
||||
/// If `actual` and `expected` are equal, then this returns
|
||||
/// [`Difference::Equal`]. If they are different but have an
|
||||
/// [edit distance](https://en.wikipedia.org/wiki/Edit_distance)
|
||||
/// of at most [`MAX_DISTANCE`], this returns [`Difference::Editable`] with the
|
||||
/// sequence of [`Edit`] which can be applied to `actual` to obtain `expected`.
|
||||
/// Otherwise this returns [`Difference::Unrelated`].
|
||||
///
|
||||
/// This uses [Myers Algorithm](https://neil.fraser.name/writing/diff/myers.pdf)
|
||||
/// with a maximum edit distance of [`MAX_DISTANCE`]. Thus the worst-case
|
||||
/// runtime is linear in both the input length and [`MAX_DISTANCE`].
|
||||
pub(crate) fn edit_list<T: PartialEq + Copy>(
|
||||
actual: impl IntoIterator<Item = T>,
|
||||
expected: impl IntoIterator<Item = T>,
|
||||
mode: Mode,
|
||||
) -> Difference<T> {
|
||||
let actual: Vec<_> = actual.into_iter().collect();
|
||||
let expected: Vec<_> = expected.into_iter().collect();
|
||||
|
||||
let mut paths_last: Vec<Path<T>> = Vec::new();
|
||||
|
||||
for distance in 0..=MAX_DISTANCE {
|
||||
let mut paths_current = Vec::new();
|
||||
for k in (-distance..=distance).step_by(2) {
|
||||
// The following will be None when k is at the edges of the range,
|
||||
// since no paths have been created for k outside the range in the
|
||||
// previous iteration.
|
||||
let path_k_minus_1 = paths_last.get(index_of_k(k - 1, -distance + 1));
|
||||
let path_k_plus_1 = paths_last.get(index_of_k(k + 1, -distance + 1));
|
||||
|
||||
let (mut path, edit) = match (path_k_minus_1, path_k_plus_1) {
|
||||
// This the first (outer) iteration.
|
||||
(None, None) => (Path::default(), None),
|
||||
|
||||
// k = -distance. There is no previous parent path yet.
|
||||
(None, Some(path_k_plus_1)) => (
|
||||
path_k_plus_1.clone(),
|
||||
expected.get(path_k_plus_1.expected_endpoint).copied().map(Edit::ExtraExpected),
|
||||
),
|
||||
|
||||
// k = distance. There is no next parent path yet.
|
||||
(Some(path_k_minus_1), None) => (
|
||||
path_k_minus_1.extend_actual_endpoint(),
|
||||
actual.get(path_k_minus_1.actual_endpoint).copied().map(Edit::ExtraActual),
|
||||
),
|
||||
|
||||
// k is strictly between -distance and distance. Both parent paths were set in the
|
||||
// last iteration.
|
||||
(Some(path_k_minus_1), Some(path_k_plus_1)) => {
|
||||
// This decides whether the algorithm prefers to add an edit
|
||||
// from the actual or from the expected when the rows differ. We
|
||||
// alternate so that the elements of differing blocks
|
||||
// interleave rather than all elements of each respective
|
||||
// side being output in a single block.
|
||||
if (distance % 2 == 0
|
||||
&& path_k_plus_1.actual_endpoint > path_k_minus_1.actual_endpoint)
|
||||
|| (distance % 2 == 1
|
||||
&& path_k_plus_1.expected_endpoint > path_k_minus_1.expected_endpoint)
|
||||
{
|
||||
(
|
||||
path_k_plus_1.clone(),
|
||||
expected
|
||||
.get(path_k_plus_1.expected_endpoint)
|
||||
.copied()
|
||||
.map(Edit::ExtraExpected),
|
||||
)
|
||||
} else {
|
||||
(
|
||||
path_k_minus_1.extend_actual_endpoint(),
|
||||
actual
|
||||
.get(path_k_minus_1.actual_endpoint)
|
||||
.copied()
|
||||
.map(Edit::ExtraActual),
|
||||
)
|
||||
}
|
||||
}
|
||||
};
|
||||
path.edits.extend(edit);
|
||||
|
||||
// Advance through any common elements starting at the current path.
|
||||
let (mut actual_endpoint, mut expected_endpoint) =
|
||||
(path.actual_endpoint, (path.actual_endpoint as i32 - k) as usize);
|
||||
while actual_endpoint < actual.len()
|
||||
&& expected_endpoint < expected.len()
|
||||
&& actual[actual_endpoint] == expected[expected_endpoint]
|
||||
{
|
||||
path.edits.push(Edit::Both(actual[actual_endpoint]));
|
||||
(actual_endpoint, expected_endpoint) = (actual_endpoint + 1, expected_endpoint + 1);
|
||||
}
|
||||
|
||||
// If we have exhausted both inputs, we are done.
|
||||
if actual_endpoint == actual.len() && expected_endpoint == expected.len() {
|
||||
return if path.edits.iter().any(|v| !matches!(v, Edit::Both(_))) {
|
||||
if matches!(mode, Mode::Contains) {
|
||||
compress_prefix_and_suffix(&mut path.edits);
|
||||
}
|
||||
Difference::Editable(path.edits)
|
||||
} else {
|
||||
Difference::Equal
|
||||
};
|
||||
}
|
||||
|
||||
path.actual_endpoint = actual_endpoint;
|
||||
path.expected_endpoint = expected_endpoint;
|
||||
paths_current.push(path);
|
||||
}
|
||||
|
||||
if matches!(mode, Mode::Prefix) {
|
||||
if let Some(path) = paths_current
|
||||
.iter_mut()
|
||||
.filter(|p| p.expected_endpoint == expected.len())
|
||||
.max_by(|p1, p2| p1.edits.len().cmp(&p2.edits.len()))
|
||||
{
|
||||
// We've reached the end of the expected side but there could still be a
|
||||
// corresponding line on the actual which we haven't picked up into the edit
|
||||
// list. We'll just add it manually to the edit list. There's no
|
||||
// real harm doing so -- worst case is that there's an
|
||||
// additional line when there didn't have to be.
|
||||
if let Some(Edit::ExtraExpected(_)) = path.edits.last() {
|
||||
if path.actual_endpoint < actual.len() {
|
||||
// The edits from the actual should come before the corresponding one from
|
||||
// the expected, so we insert rather than push.
|
||||
path.edits.insert(
|
||||
path.edits.len() - 1,
|
||||
Edit::ExtraActual(actual[path.actual_endpoint]),
|
||||
);
|
||||
}
|
||||
}
|
||||
path.edits.push(Edit::AdditionalActual);
|
||||
return if path.edits.iter().any(|v| !matches!(v, Edit::Both(_))) {
|
||||
Difference::Editable(std::mem::take(&mut path.edits))
|
||||
} else {
|
||||
Difference::Equal
|
||||
};
|
||||
}
|
||||
}
|
||||
|
||||
paths_last = paths_current;
|
||||
}
|
||||
|
||||
Difference::Unrelated
|
||||
}
|
||||
|
||||
fn index_of_k(k: i32, k_min: i32) -> usize {
|
||||
((k - k_min) / 2) as usize
|
||||
}
|
||||
|
||||
fn compress_prefix_and_suffix<T>(edits: &mut Vec<Edit<T>>) {
|
||||
if let Some(mut first_non_extra_actual_edit) =
|
||||
edits.iter().position(|e| !matches!(e, Edit::ExtraActual(_)))
|
||||
{
|
||||
if first_non_extra_actual_edit > 1
|
||||
&& matches!(edits[first_non_extra_actual_edit], Edit::ExtraExpected(_))
|
||||
{
|
||||
first_non_extra_actual_edit -= 1;
|
||||
}
|
||||
edits.splice(..first_non_extra_actual_edit, [Edit::AdditionalActual]);
|
||||
}
|
||||
|
||||
if let Some(mut last_non_extra_actual_edit) =
|
||||
edits.iter().rposition(|e| !matches!(e, Edit::ExtraActual(_)))
|
||||
{
|
||||
if last_non_extra_actual_edit < edits.len() - 1
|
||||
&& matches!(edits[last_non_extra_actual_edit], Edit::ExtraExpected(_))
|
||||
{
|
||||
last_non_extra_actual_edit += 1;
|
||||
}
|
||||
edits.splice(last_non_extra_actual_edit + 1.., [Edit::AdditionalActual]);
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Clone)]
|
||||
struct Path<T: Clone> {
|
||||
actual_endpoint: usize,
|
||||
expected_endpoint: usize,
|
||||
edits: Vec<Edit<T>>,
|
||||
}
|
||||
|
||||
impl<T: Clone> Default for Path<T> {
|
||||
fn default() -> Self {
|
||||
Self { actual_endpoint: 0, expected_endpoint: 0, edits: vec![] }
|
||||
}
|
||||
}
|
||||
|
||||
impl<T: Clone> Path<T> {
|
||||
fn extend_actual_endpoint(&self) -> Self {
|
||||
Self {
|
||||
actual_endpoint: self.actual_endpoint + 1,
|
||||
expected_endpoint: self.expected_endpoint,
|
||||
edits: self.edits.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
use crate::Result;
|
||||
use quickcheck::{quickcheck, Arbitrary, TestResult};
|
||||
|
||||
#[test]
|
||||
fn returns_equal_when_strings_are_equal() -> Result<()> {
|
||||
let result = edit_list(["A string"], ["A string"], Mode::Exact);
|
||||
verify_that!(result, matches_pattern!(&Difference::Equal))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_sequence_of_two_common_parts() -> Result<()> {
|
||||
let result = edit_list(
|
||||
["A string (1)", "A string (2)"],
|
||||
["A string (1)", "A string (2)"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(result, matches_pattern!(&Difference::Equal))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_extra_actual_when_only_actual_has_content() -> Result<()> {
|
||||
let result = edit_list(["A string"], [], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![matches_pattern!(
|
||||
&Edit::ExtraActual(eq("A string"))
|
||||
)]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_extra_expected_when_only_expected_has_content() -> Result<()> {
|
||||
let result = edit_list([], ["A string"], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![matches_pattern!(
|
||||
&Edit::ExtraExpected(eq("A string"))
|
||||
)]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_extra_actual_followed_by_extra_expected_with_two_unequal_strings() -> Result<()> {
|
||||
let result = edit_list(["A string"], ["Another string"], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("A string"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Another string"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn interleaves_extra_actual_and_extra_expected_when_multiple_lines_differ() -> Result<()> {
|
||||
let result =
|
||||
edit_list(["A string", "A string"], ["Another string", "Another string"], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("A string"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Another string"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("A string"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Another string"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_difference_when_there_is_common_prefix() -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Common part", "Actual only"],
|
||||
["Common part", "Expected only"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_extra_actual_when_actual_has_extra_suffix() -> Result<()> {
|
||||
let result = edit_list(["Common part", "Actual only"], ["Common part"], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_extra_expected_when_expected_has_extra_suffix() -> Result<()> {
|
||||
let result = edit_list(["Common part"], ["Common part", "Expected only"], Mode::Exact);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_difference_plus_common_part_when_there_is_common_suffix() -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Actual only", "Common part"],
|
||||
["Expected only", "Common part"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_difference_plus_common_part_plus_difference_when_there_is_common_infix() -> Result<()>
|
||||
{
|
||||
let result = edit_list(
|
||||
["Actual only (1)", "Common part", "Actual only (2)"],
|
||||
["Expected only (1)", "Common part", "Expected only (2)"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only (1)"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only (1)"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only (2)"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only (2)"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_difference_plus_common_part_when_there_is_common_prefix_and_suffix(
|
||||
) -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Common part (1)", "Actual only", "Common part (2)"],
|
||||
["Common part (1)", "Expected only", "Common part (2)"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part (1)"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part (2)"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_extra_actual_plus_common_part_when_there_is_common_prefix_and_suffix(
|
||||
) -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Common part (1)", "Actual only", "Common part (2)"],
|
||||
["Common part (1)", "Common part (2)"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part (1)"))),
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part (2)"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_common_part_plus_extra_expected_plus_common_part_when_there_is_common_prefix_and_suffix(
|
||||
) -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Common part (1)", "Common part (2)"],
|
||||
["Common part (1)", "Expected only", "Common part (2)"],
|
||||
Mode::Exact,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::Both(eq("Common part (1)"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part (2)"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn skips_extra_parts_on_actual_at_end_in_prefix_mode() -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Common part", "Actual only"],
|
||||
["Expected only", "Common part"],
|
||||
Mode::Prefix,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref not(contains(matches_pattern!(
|
||||
&Edit::ExtraActual(eq("Actual only"))
|
||||
)))))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_skip_extra_parts_on_actual_in_prefix_mode_at_end_when_they_are_in_common(
|
||||
) -> Result<()> {
|
||||
let result = edit_list(
|
||||
["Actual only", "Common part"],
|
||||
["Expected only", "Common part"],
|
||||
Mode::Prefix,
|
||||
);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
matches_pattern!(&Edit::Both(eq("Common part"))),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn does_not_skip_corresponding_line_on_actual_when_actual_and_expected_differ_in_prefix_mode(
|
||||
) -> Result<()> {
|
||||
let result = edit_list(["Actual only"], ["Expected only"], Mode::Prefix);
|
||||
verify_that!(
|
||||
result,
|
||||
matches_pattern!(&Difference::Editable(ref elements_are![
|
||||
matches_pattern!(&Edit::ExtraActual(eq("Actual only"))),
|
||||
matches_pattern!(&Edit::ExtraExpected(eq("Expected only"))),
|
||||
matches_pattern!(&Edit::AdditionalActual),
|
||||
]))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn returns_unrelated_when_maximum_distance_exceeded() -> Result<()> {
|
||||
let result = edit_list(0..=50, 60..110, Mode::Exact);
|
||||
verify_that!(result, matches_pattern!(&Difference::Unrelated))
|
||||
}
|
||||
|
||||
quickcheck! {
|
||||
fn edit_list_edits_actual_to_expected(
|
||||
actual: Vec<Alphabet>,
|
||||
expected: Vec<Alphabet>
|
||||
) -> TestResult {
|
||||
match edit_list(actual.clone(), expected.clone(), Mode::Exact) {
|
||||
Difference::Equal => TestResult::from_bool(actual == expected),
|
||||
Difference::Editable(edit_list) => {
|
||||
TestResult::from_bool(apply_edits_to_actual(&edit_list, &actual) == expected)
|
||||
}
|
||||
Difference::Unrelated => {
|
||||
if actual == expected {
|
||||
TestResult::failed()
|
||||
} else {
|
||||
TestResult::discard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
quickcheck! {
|
||||
fn edit_list_edits_expected_to_actual(
|
||||
actual: Vec<Alphabet>,
|
||||
expected: Vec<Alphabet>
|
||||
) -> TestResult {
|
||||
match edit_list(actual.clone(), expected.clone(), Mode::Exact) {
|
||||
Difference::Equal => TestResult::from_bool(actual == expected),
|
||||
Difference::Editable(edit_list) => {
|
||||
TestResult::from_bool(apply_edits_to_expected(&edit_list, &expected) == actual)
|
||||
}
|
||||
Difference::Unrelated => {
|
||||
if actual == expected {
|
||||
TestResult::failed()
|
||||
} else {
|
||||
TestResult::discard()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||
enum Alphabet {
|
||||
A,
|
||||
B,
|
||||
C,
|
||||
}
|
||||
|
||||
impl Arbitrary for Alphabet {
|
||||
fn arbitrary(g: &mut quickcheck::Gen) -> Self {
|
||||
g.choose(&[Alphabet::A, Alphabet::B, Alphabet::C]).copied().unwrap()
|
||||
}
|
||||
}
|
||||
|
||||
fn apply_edits_to_actual<T: PartialEq + Debug + Copy>(
|
||||
edit_list: &[Edit<T>],
|
||||
actual: &[T],
|
||||
) -> Vec<T> {
|
||||
let mut result = Vec::new();
|
||||
let mut actual_iter = actual.iter();
|
||||
for edit in edit_list {
|
||||
match edit {
|
||||
Edit::ExtraActual(value) => {
|
||||
assert_that!(actual_iter.next(), some(eq(value)));
|
||||
}
|
||||
Edit::ExtraExpected(value) => {
|
||||
result.push(*value);
|
||||
}
|
||||
Edit::Both(value) => {
|
||||
assert_that!(actual_iter.next(), some(eq(value)));
|
||||
result.push(*value);
|
||||
}
|
||||
Edit::AdditionalActual => {
|
||||
fail!("Unexpected Edit::AdditionalActual").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_that!(actual_iter.next(), none());
|
||||
result
|
||||
}
|
||||
|
||||
fn apply_edits_to_expected<T: PartialEq + Debug + Copy>(
|
||||
edit_list: &[Edit<T>],
|
||||
expected: &[T],
|
||||
) -> Vec<T> {
|
||||
let mut result = Vec::new();
|
||||
let mut expected_iter = expected.iter();
|
||||
for edit in edit_list {
|
||||
match edit {
|
||||
Edit::ExtraActual(value) => {
|
||||
result.push(*value);
|
||||
}
|
||||
Edit::ExtraExpected(value) => {
|
||||
assert_that!(expected_iter.next(), some(eq(value)));
|
||||
}
|
||||
Edit::Both(value) => {
|
||||
assert_that!(expected_iter.next(), some(eq(value)));
|
||||
result.push(*value);
|
||||
}
|
||||
Edit::AdditionalActual => {
|
||||
fail!("Unexpected Edit::AdditionalActual").unwrap();
|
||||
}
|
||||
}
|
||||
}
|
||||
assert_that!(expected_iter.next(), none());
|
||||
result
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,518 @@
|
||||
// Copyright 2022 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#[doc(hidden)]
|
||||
pub mod internal {
|
||||
use crate::description::Description;
|
||||
use crate::matcher::{Matcher, MatcherResult};
|
||||
use crate::matcher_support::count_elements::count_elements;
|
||||
use std::collections::HashSet;
|
||||
use std::fmt::{Debug, Display};
|
||||
|
||||
/// The requirements of the mapping between matchers and actual values by
|
||||
/// which [`UnorderedElementsAre`] is deemed to match its input.
|
||||
///
|
||||
/// **For internal use only. API stablility is not guaranteed!**
|
||||
#[doc(hidden)]
|
||||
#[derive(Clone, Copy)]
|
||||
pub enum Requirements {
|
||||
/// There must be a 1:1 correspondence between the actual values and the
|
||||
/// matchers.
|
||||
PerfectMatch,
|
||||
|
||||
/// The mapping from matched actual values to their corresponding
|
||||
/// matchers must be surjective.
|
||||
Superset,
|
||||
|
||||
/// The mapping from matchers to matched actual values must be
|
||||
/// surjective.
|
||||
Subset,
|
||||
}
|
||||
|
||||
impl Requirements {
|
||||
pub fn explain_size_mismatch<ContainerT: IntoIterator + Copy>(
|
||||
&self,
|
||||
actual: ContainerT,
|
||||
expected_size: usize,
|
||||
) -> Option<Description> {
|
||||
let actual_size = count_elements(actual);
|
||||
match self {
|
||||
Requirements::PerfectMatch if actual_size != expected_size => {
|
||||
Some(format!("which has size {actual_size} (expected {expected_size})").into())
|
||||
}
|
||||
|
||||
Requirements::Superset if actual_size < expected_size => Some(
|
||||
format!("which has size {actual_size} (expected at least {expected_size})")
|
||||
.into(),
|
||||
),
|
||||
|
||||
Requirements::Subset if actual_size > expected_size => Some(
|
||||
format!("which has size {actual_size} (expected at most {expected_size})")
|
||||
.into(),
|
||||
),
|
||||
|
||||
_ => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for Requirements {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
Requirements::PerfectMatch => {
|
||||
write!(f, "perfect")
|
||||
}
|
||||
Requirements::Superset => {
|
||||
write!(f, "superset")
|
||||
}
|
||||
Requirements::Subset => {
|
||||
write!(f, "subset")
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// The bipartite matching graph between actual and expected elements.
|
||||
pub(crate) struct MatchMatrix {
|
||||
graph: Vec<Vec<MatcherResult>>, // graph[actual_idx][expected_idx]
|
||||
expected_len: usize,
|
||||
}
|
||||
|
||||
impl MatchMatrix {
|
||||
pub(crate) fn generate<'a, T: Debug + Copy + 'a>(
|
||||
actual: impl IntoIterator<Item = T>,
|
||||
expected: &[Box<dyn Matcher<T> + 'a>],
|
||||
) -> Self {
|
||||
let expected_len = expected.len();
|
||||
let graph = actual
|
||||
.into_iter()
|
||||
.map(|actual| {
|
||||
expected
|
||||
.iter()
|
||||
.map(|expected_matcher| expected_matcher.matches(actual))
|
||||
.collect()
|
||||
})
|
||||
.collect();
|
||||
MatchMatrix { graph, expected_len }
|
||||
}
|
||||
|
||||
pub(crate) fn is_match_for(&self, requirements: Requirements) -> bool {
|
||||
match requirements {
|
||||
Requirements::PerfectMatch => {
|
||||
!self.find_unmatchable_elements().has_unmatchable_elements()
|
||||
&& self.find_best_match().is_full_match()
|
||||
}
|
||||
Requirements::Superset => {
|
||||
!self.find_unmatched_expected().has_unmatchable_elements()
|
||||
&& self.find_best_match().is_superset_match()
|
||||
}
|
||||
Requirements::Subset => {
|
||||
!self.find_unmatched_actual().has_unmatchable_elements()
|
||||
&& self.find_best_match().is_subset_match()
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub(crate) fn explain_unmatchable(
|
||||
&self,
|
||||
requirements: Requirements,
|
||||
) -> Option<Description> {
|
||||
let unmatchable_elements = match requirements {
|
||||
Requirements::PerfectMatch => self.find_unmatchable_elements(),
|
||||
Requirements::Superset => self.find_unmatched_expected(),
|
||||
Requirements::Subset => self.find_unmatched_actual(),
|
||||
};
|
||||
unmatchable_elements.get_explanation()
|
||||
}
|
||||
|
||||
// Verifies that each actual matches at least one expected and that
|
||||
// each expected matches at least one actual.
|
||||
// This is a necessary condition but not sufficient. But it is faster
|
||||
// than `find_best_match()`.
|
||||
fn find_unmatchable_elements(&self) -> UnmatchableElements {
|
||||
let unmatchable_actual =
|
||||
self.graph.iter().map(|row| row.iter().all(|&e| e.is_no_match())).collect();
|
||||
let mut unmatchable_expected = vec![false; self.expected_len];
|
||||
for (col_idx, expected) in unmatchable_expected.iter_mut().enumerate() {
|
||||
*expected = self.graph.iter().map(|row| row[col_idx]).all(|e| e.is_no_match());
|
||||
}
|
||||
UnmatchableElements { unmatchable_actual, unmatchable_expected }
|
||||
}
|
||||
|
||||
fn find_unmatched_expected(&self) -> UnmatchableElements {
|
||||
let mut unmatchable_expected = vec![false; self.expected_len];
|
||||
for (col_idx, expected) in unmatchable_expected.iter_mut().enumerate() {
|
||||
*expected = self.graph.iter().map(|row| row[col_idx]).all(|e| e.is_no_match());
|
||||
}
|
||||
UnmatchableElements {
|
||||
unmatchable_actual: vec![false; self.expected_len],
|
||||
unmatchable_expected,
|
||||
}
|
||||
}
|
||||
|
||||
fn find_unmatched_actual(&self) -> UnmatchableElements {
|
||||
let unmatchable_actual =
|
||||
self.graph.iter().map(|row| row.iter().all(|e| e.is_no_match())).collect();
|
||||
UnmatchableElements {
|
||||
unmatchable_actual,
|
||||
unmatchable_expected: vec![false; self.expected_len],
|
||||
}
|
||||
}
|
||||
|
||||
// Verifies that a full match exists.
|
||||
//
|
||||
// Uses the well-known Ford-Fulkerson max flow method to find a maximum
|
||||
// bipartite matching. Flow is considered to be from actual to expected.
|
||||
// There is an implicit source node that is connected to all of the actual
|
||||
// nodes, and an implicit sink node that is connected to all of the
|
||||
// expected nodes. All edges have unit capacity.
|
||||
//
|
||||
// Neither the flow graph nor the residual flow graph are represented
|
||||
// explicitly. Instead, they are implied by the information in `self.graph` and
|
||||
// the local `actual_match : vec![Option<usize>; self.expected_len]` whose
|
||||
// elements are initialized to `None`. This represents the initial state
|
||||
// of the algorithm, where the flow graph is empty, and the residual
|
||||
// flow graph has the following edges:
|
||||
// - An edge from source to each actual element node
|
||||
// - An edge from each expected element node to sink
|
||||
// - An edge from each actual element node to each expected element node, if
|
||||
// the actual element matches the expected element, i.e.
|
||||
// `matches!(self.graph[actual_id][expected_id], Matches)`
|
||||
//
|
||||
// When the `try_augment(...)` method adds a flow, it sets `actual_match[l] =
|
||||
// Some(r)` for some nodes l and r. This induces the following changes:
|
||||
// - The edges (source, l), (l, r), and (r, sink) are added to the flow graph.
|
||||
// - The same three edges are removed from the residual flow graph.
|
||||
// - The reverse edges (l, source), (r, l), and (sink, r) are added to the
|
||||
// residual flow graph, which is a directional graph representing unused
|
||||
// flow capacity.
|
||||
//
|
||||
// When the method augments a flow (changing `actual_match[l]` from `Some(r1)`
|
||||
// to `Some(r2)`), this can be thought of as "undoing" the above steps
|
||||
// with respect to r1 and "redoing" them with respect to r2.
|
||||
//
|
||||
// It bears repeating that the flow graph and residual flow graph are
|
||||
// never represented explicitly, but can be derived by looking at the
|
||||
// information in 'self.graph' and in `actual_match`.
|
||||
//
|
||||
// As an optimization, there is a second local `expected_match:
|
||||
// vec![Option<usize>; self.expected_len]` which does not provide any
|
||||
// new information. Instead, it enables more efficient queries about
|
||||
// edges entering or leaving the expected elements nodes of the flow or
|
||||
// residual flow graphs. The following invariants are maintained:
|
||||
//
|
||||
// actual_match[a] == None or expected_match[actual_match[a].unwrap()] ==
|
||||
// Some(a)
|
||||
// expected_match[r] == None or actual_match[expected_match[e].unwrap()] ==
|
||||
// Some(e)
|
||||
//
|
||||
// | [ source ] |
|
||||
// | ||| |
|
||||
// | ||| |
|
||||
// | ||\-> actual_match[0]=Some(1) -\ expected_match[0]=None ---\ |
|
||||
// | || | | |
|
||||
// | |\--> actual_match[1]=None \-> expected_match[1]=Some(0) --\| |
|
||||
// | | || |
|
||||
// | \---> actual_match[2]=Some(2) --> expected_match[2]=Some(2) -\|| |
|
||||
// | ||| |
|
||||
// | elements matchers vvv |
|
||||
// | [ sink ] |
|
||||
//
|
||||
// See Also:
|
||||
// [1] Cormen, et al (2001). "Section 26.2: The Ford-Fulkerson method".
|
||||
// "Introduction to Algorithms (Second ed.)", pp. 651-664.
|
||||
// [2] "Ford-Fulkerson algorithm", Wikipedia,
|
||||
// 'http://en.wikipedia.org/wiki/Ford%E2%80%93Fulkerson_algorithm'
|
||||
pub(crate) fn find_best_match(&self) -> BestMatch {
|
||||
let mut actual_match = vec![None; self.graph.len()];
|
||||
let mut expected_match: Vec<Option<usize>> = vec![None; self.expected_len];
|
||||
// Searches the residual flow graph for a path from each actual node to
|
||||
// the sink in the residual flow graph, and if one is found, add this path
|
||||
// to the graph.
|
||||
// It's okay to search through the actual nodes once. The
|
||||
// edge from the implicit source node to each previously-visited actual
|
||||
// node will have flow if that actual node has any path to the sink
|
||||
// whatsoever. Subsequent augmentations can only add flow to the
|
||||
// network, and cannot take away that previous flow unit from the source.
|
||||
// Since the source-to-actual edge can only carry one flow unit (or,
|
||||
// each actual element can be matched to only one expected element), there is no
|
||||
// need to visit the actual nodes more than once looking for
|
||||
// augmented paths. The flow is known to be possible or impossible
|
||||
// by looking at the node once.
|
||||
for actual_idx in 0..self.graph.len() {
|
||||
assert!(actual_match[actual_idx].is_none());
|
||||
let mut seen = vec![false; self.expected_len];
|
||||
self.try_augment(actual_idx, &mut seen, &mut actual_match, &mut expected_match);
|
||||
}
|
||||
BestMatch::new(actual_match, self.expected_len)
|
||||
}
|
||||
|
||||
// Perform a depth-first search from actual node `actual_idx` to the sink by
|
||||
// searching for an unassigned expected node. If a path is found, flow
|
||||
// is added to the network by linking the actual and expected vector elements
|
||||
// corresponding each segment of the path. Returns true if a path to
|
||||
// sink was found, which means that a unit of flow was added to the
|
||||
// network. The 'seen' array elements correspond to expected nodes and are
|
||||
// marked to eliminate cycles from the search.
|
||||
//
|
||||
// Actual nodes will only be explored at most once because they
|
||||
// are accessible from at most one expected node in the residual flow
|
||||
// graph.
|
||||
//
|
||||
// Note that `actual_match[actual_idx]` is the only element of `actual_match`
|
||||
// that `try_augment(...)` will potentially transition from `None` to
|
||||
// `Some(...)`. Any other `actual_match` element holding `None` before
|
||||
// `try_augment(...)` will be holding it when `try_augment(...)`
|
||||
// returns.
|
||||
//
|
||||
fn try_augment(
|
||||
&self,
|
||||
actual_idx: usize,
|
||||
seen: &mut Vec<bool>,
|
||||
actual_match: &mut [Option<usize>],
|
||||
expected_match: &mut Vec<Option<usize>>,
|
||||
) -> bool {
|
||||
for expected_idx in 0..self.expected_len {
|
||||
if seen[expected_idx] {
|
||||
continue;
|
||||
}
|
||||
if self.graph[actual_idx][expected_idx].is_no_match() {
|
||||
continue;
|
||||
}
|
||||
// There is an edge between `actual_idx` and `expected_idx`.
|
||||
seen[expected_idx] = true;
|
||||
// Next a search is performed to determine whether
|
||||
// this edge is a dead end or leads to the sink.
|
||||
//
|
||||
// `expected_match[expected_idx].is_none()` means that there is residual flow
|
||||
// from expected node at index expected_idx to the sink, so we
|
||||
// can use that to finish this flow path and return success.
|
||||
//
|
||||
// Otherwise, we look for a residual flow starting from
|
||||
// `expected_match[expected_idx].unwrap()` by calling
|
||||
// ourselves recursively to see if this ultimately leads to
|
||||
// sink.
|
||||
if expected_match[expected_idx].is_none()
|
||||
|| self.try_augment(
|
||||
expected_match[expected_idx].unwrap(),
|
||||
seen,
|
||||
actual_match,
|
||||
expected_match,
|
||||
)
|
||||
{
|
||||
// We found a residual flow from source to sink. We thus need to add the new
|
||||
// edge to the current flow.
|
||||
// Note: this also remove the potential flow that existed by overwriting the
|
||||
// value in the `expected_match` and `actual_match`.
|
||||
expected_match[expected_idx] = Some(actual_idx);
|
||||
actual_match[actual_idx] = Some(expected_idx);
|
||||
return true;
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
}
|
||||
|
||||
/// The list of elements that do not match any element in the corresponding
|
||||
/// set.
|
||||
/// TODO - Use BitVec.
|
||||
pub(crate) struct UnmatchableElements {
|
||||
unmatchable_actual: Vec<bool>,
|
||||
unmatchable_expected: Vec<bool>,
|
||||
}
|
||||
|
||||
impl UnmatchableElements {
|
||||
fn has_unmatchable_elements(&self) -> bool {
|
||||
self.unmatchable_actual.iter().any(|b| *b)
|
||||
|| self.unmatchable_expected.iter().any(|b| *b)
|
||||
}
|
||||
|
||||
fn get_explanation(&self) -> Option<Description> {
|
||||
let unmatchable_actual = self.unmatchable_actual();
|
||||
let actual_idx = unmatchable_actual
|
||||
.iter()
|
||||
.map(|idx| format!("#{idx}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
let unmatchable_expected = self.unmatchable_expected();
|
||||
let expected_idx = unmatchable_expected
|
||||
.iter()
|
||||
.map(|idx| format!("#{idx}"))
|
||||
.collect::<Vec<_>>()
|
||||
.join(", ");
|
||||
match (unmatchable_actual.len(), unmatchable_expected.len()) {
|
||||
(0, 0) => None,
|
||||
(1, 0) => {
|
||||
Some(format!("whose element {actual_idx} does not match any expected elements").into())
|
||||
}
|
||||
(_, 0) => {
|
||||
Some(format!("whose elements {actual_idx} do not match any expected elements",).into())
|
||||
}
|
||||
(0, 1) => Some(format!(
|
||||
"which has no element matching the expected element {expected_idx}"
|
||||
).into()),
|
||||
(0, _) => Some(format!(
|
||||
"which has no elements matching the expected elements {expected_idx}"
|
||||
).into()),
|
||||
(1, 1) => Some(format!(
|
||||
"whose element {actual_idx} does not match any expected elements and no elements match the expected element {expected_idx}"
|
||||
).into()),
|
||||
(_, 1) => Some(format!(
|
||||
"whose elements {actual_idx} do not match any expected elements and no elements match the expected element {expected_idx}"
|
||||
).into()),
|
||||
(1, _) => Some(format!(
|
||||
"whose element {actual_idx} does not match any expected elements and no elements match the expected elements {expected_idx}"
|
||||
).into()),
|
||||
(_, _) => Some(format!(
|
||||
"whose elements {actual_idx} do not match any expected elements and no elements match the expected elements {expected_idx}"
|
||||
).into()),
|
||||
}
|
||||
}
|
||||
|
||||
fn unmatchable_actual(&self) -> Vec<usize> {
|
||||
self.unmatchable_actual
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, b)| if *b { Some(idx) } else { None })
|
||||
.collect()
|
||||
}
|
||||
|
||||
fn unmatchable_expected(&self) -> Vec<usize> {
|
||||
self.unmatchable_expected
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter_map(|(idx, b)| if *b { Some(idx) } else { None })
|
||||
.collect()
|
||||
}
|
||||
}
|
||||
|
||||
/// The representation of a match between actual and expected.
|
||||
/// The value at idx represents to which expected the actual at idx is
|
||||
/// matched with. For example, `BestMatch::new([Some(0), None, Some(1)],
|
||||
/// ..)` means:
|
||||
/// * The 0th element in actual matches the 0th element in expected.
|
||||
/// * The 1st element in actual does not match.
|
||||
/// * The 2nd element in actual matches the 1st element in expected.
|
||||
pub(crate) struct BestMatch {
|
||||
actual_match: Vec<Option<usize>>,
|
||||
expected_len: usize,
|
||||
}
|
||||
|
||||
impl BestMatch {
|
||||
fn new(actual_match: Vec<Option<usize>>, expected_len: usize) -> BestMatch {
|
||||
BestMatch { actual_match, expected_len }
|
||||
}
|
||||
|
||||
pub(crate) fn is_full_match(&self) -> bool {
|
||||
self.actual_match.iter().all(|o| o.is_some())
|
||||
}
|
||||
|
||||
pub(crate) fn is_subset_match(&self) -> bool {
|
||||
self.is_full_match()
|
||||
}
|
||||
|
||||
pub(crate) fn is_superset_match(&self) -> bool {
|
||||
self.get_unmatched_expected().is_empty()
|
||||
}
|
||||
|
||||
pub(crate) fn get_matches(&self) -> impl Iterator<Item = (usize, usize)> + '_ {
|
||||
self.actual_match.iter().enumerate().filter_map(|(actual_idx, maybe_expected_idx)| {
|
||||
maybe_expected_idx.map(|expected_idx| (actual_idx, expected_idx))
|
||||
})
|
||||
}
|
||||
|
||||
pub(crate) fn get_unmatched_actual(&self) -> impl Iterator<Item = usize> + '_ {
|
||||
self.actual_match
|
||||
.iter()
|
||||
.enumerate()
|
||||
.filter(|&(_, o)| o.is_none())
|
||||
.map(|(actual_idx, _)| actual_idx)
|
||||
}
|
||||
|
||||
pub(crate) fn get_unmatched_expected(&self) -> Vec<usize> {
|
||||
let matched_expected: HashSet<_> = self.actual_match.iter().flatten().collect();
|
||||
(0..self.expected_len)
|
||||
.filter(|expected_idx| !matched_expected.contains(expected_idx))
|
||||
.collect()
|
||||
}
|
||||
|
||||
pub(crate) fn get_explanation<'a, T: Debug + Copy>(
|
||||
&self,
|
||||
actual: impl IntoIterator<Item = T>,
|
||||
expected: &[Box<dyn Matcher<T> + 'a>],
|
||||
requirements: Requirements,
|
||||
) -> Option<Description> {
|
||||
let actual: Vec<_> = actual.into_iter().collect();
|
||||
if self.is_full_match() {
|
||||
return None;
|
||||
}
|
||||
let mut error_message =
|
||||
format!("which does not have a {requirements} match with the expected elements.");
|
||||
|
||||
error_message.push_str("\n The best match found was: ");
|
||||
|
||||
let matches = self.get_matches().map(|(actual_idx, expected_idx)|{
|
||||
format!(
|
||||
"Actual element {:?} at index {actual_idx} matched expected element `{}` at index {expected_idx}.",
|
||||
actual[actual_idx],
|
||||
expected[expected_idx].describe(MatcherResult::Match),
|
||||
)});
|
||||
|
||||
let unmatched_actual = self.get_unmatched_actual().map(|actual_idx| {
|
||||
format!(
|
||||
"Actual element {:#?} at index {actual_idx} did not match any remaining expected element.",
|
||||
actual[actual_idx]
|
||||
)
|
||||
});
|
||||
|
||||
let unmatched_expected = self.get_unmatched_expected().into_iter().map(|expected_idx|{format!(
|
||||
"Expected element `{}` at index {expected_idx} did not match any remaining actual element.",
|
||||
expected[expected_idx].describe(MatcherResult::Match)
|
||||
)});
|
||||
|
||||
let best_match = matches
|
||||
.chain(unmatched_actual)
|
||||
.chain(unmatched_expected)
|
||||
.collect::<Description>()
|
||||
.indent();
|
||||
Some(format!(
|
||||
"which does not have a {requirements} match with the expected elements. The best match found was:\n{best_match}"
|
||||
).into())
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::prelude::*;
|
||||
|
||||
#[test]
|
||||
fn match_matrix_generate() {
|
||||
let actual = vec![1, 2, 3, 4];
|
||||
let expected: Vec<Box<dyn Matcher<i32>>> =
|
||||
vec![Box::new(eq(1)), Box::new(eq(2)), Box::new(eq(3))];
|
||||
let matrix = MatchMatrix::generate(actual, &expected);
|
||||
|
||||
assert_eq!(
|
||||
matrix.graph,
|
||||
vec![
|
||||
vec![true.into(), false.into(), false.into()],
|
||||
vec![false.into(), true.into(), false.into()],
|
||||
vec![false.into(), false.into(), true.into()],
|
||||
vec![false.into(), false.into(), false.into()],
|
||||
]
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,31 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
//! Utilities to facilitate writing matchers.
|
||||
//!
|
||||
//! Tests normally do not need to import anything from this module. Some of
|
||||
//! these facilities could be useful to downstream users writing custom
|
||||
//! matchers.
|
||||
|
||||
mod auto_eq;
|
||||
pub(crate) mod count_elements;
|
||||
pub(crate) mod edit_distance;
|
||||
pub(crate) mod match_matrix;
|
||||
pub(crate) mod summarize_diff;
|
||||
pub(crate) mod zipped_iterator;
|
||||
|
||||
pub mod __internal_unstable_do_not_depend_on_these {
|
||||
pub use super::auto_eq::internal::{ExpectedKind, Wrapper};
|
||||
pub use crate::__auto_eq as auto_eq;
|
||||
}
|
||||
@@ -0,0 +1,591 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
#![doc(hidden)]
|
||||
|
||||
use crate::matcher_support::edit_distance;
|
||||
#[rustversion::since(1.70)]
|
||||
use std::io::IsTerminal;
|
||||
use std::{borrow::Cow, cell::Cell, fmt::Display};
|
||||
|
||||
/// Returns a string describing how the expected and actual lines differ.
|
||||
///
|
||||
/// This is included in a match explanation for [`EqMatcher`] and
|
||||
/// [`crate::matchers::str_matcher::StrMatcher`].
|
||||
///
|
||||
/// If the actual value has less than two lines, or the two differ by more than
|
||||
/// the maximum edit distance, then this returns the empty string. If the two
|
||||
/// are equal, it returns a simple statement that they are equal. Otherwise,
|
||||
/// this constructs a unified diff view of the actual and expected values.
|
||||
pub(crate) fn create_diff(
|
||||
actual_debug: &str,
|
||||
expected_debug: &str,
|
||||
diff_mode: edit_distance::Mode,
|
||||
) -> Cow<'static, str> {
|
||||
if actual_debug.lines().count() < 2 {
|
||||
// If the actual debug is only one line, then there is no point in doing a
|
||||
// line-by-line diff.
|
||||
return "".into();
|
||||
}
|
||||
|
||||
match edit_distance::edit_list(actual_debug.lines(), expected_debug.lines(), diff_mode) {
|
||||
edit_distance::Difference::Equal => {
|
||||
// str.lines() is oblivious to the last newline in a
|
||||
// string, so we need to check this to make sure we don't spuriously
|
||||
// claim that 'hello' and 'hello\n' are identical debug strings.
|
||||
//
|
||||
// Although we would have liked to resolve by replacing
|
||||
// str::lines() with str::split('\n'), the potentially
|
||||
// empty last element interferes with good diff output for
|
||||
// "contains" checks.
|
||||
let actual_newline_terminated = actual_debug.ends_with('\n');
|
||||
let expected_newline_terminated = expected_debug.ends_with('\n');
|
||||
if actual_newline_terminated && !expected_newline_terminated {
|
||||
"Actual includes a terminating newline that is absent from expected.".into()
|
||||
} else if !actual_newline_terminated && expected_newline_terminated {
|
||||
"Actual omits a terminating newline that is present in expected.".into()
|
||||
} else {
|
||||
"No difference found between debug strings.".into()
|
||||
}
|
||||
}
|
||||
edit_distance::Difference::Editable(edit_list) => {
|
||||
format!("{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
|
||||
.into()
|
||||
}
|
||||
edit_distance::Difference::Unrelated => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
/// Returns a string describing how the expected and actual differ after
|
||||
/// reversing the lines in each.
|
||||
///
|
||||
/// This is similar to [`create_diff`] except that it first reverses the lines
|
||||
/// in both the expected and actual values, then reverses the constructed edit
|
||||
/// list. When `diff_mode` is [`edit_distance::Mode::Prefix`], this becomes a
|
||||
/// diff of the suffix for use by [`ends_with`][crate::matchers::ends_with].
|
||||
pub(crate) fn create_diff_reversed(
|
||||
actual_debug: &str,
|
||||
expected_debug: &str,
|
||||
diff_mode: edit_distance::Mode,
|
||||
) -> Cow<'static, str> {
|
||||
if actual_debug.lines().count() < 2 {
|
||||
// If the actual debug is only one line, then there is no point in doing a
|
||||
// line-by-line diff.
|
||||
return "".into();
|
||||
}
|
||||
let mut actual_lines_reversed = actual_debug.lines().collect::<Vec<_>>();
|
||||
let mut expected_lines_reversed = expected_debug.lines().collect::<Vec<_>>();
|
||||
actual_lines_reversed.reverse();
|
||||
expected_lines_reversed.reverse();
|
||||
match edit_distance::edit_list(actual_lines_reversed, expected_lines_reversed, diff_mode) {
|
||||
edit_distance::Difference::Equal => "No difference found between debug strings.".into(),
|
||||
edit_distance::Difference::Editable(mut edit_list) => {
|
||||
edit_list.reverse();
|
||||
format!("{}{}", summary_header(), edit_list.into_iter().collect::<BufferedSummary>(),)
|
||||
.into()
|
||||
}
|
||||
edit_distance::Difference::Unrelated => "".into(),
|
||||
}
|
||||
}
|
||||
|
||||
// Produces the header, with or without coloring depending on
|
||||
// USE_COLOR
|
||||
fn summary_header() -> Cow<'static, str> {
|
||||
if USE_COLOR.with(Cell::get) {
|
||||
format!(
|
||||
"Difference(-{ACTUAL_ONLY_STYLE}actual{RESET_ALL} / +{EXPECTED_ONLY_STYLE}expected{RESET_ALL}):"
|
||||
).into()
|
||||
} else {
|
||||
"Difference(-actual / +expected):".into()
|
||||
}
|
||||
}
|
||||
|
||||
// Aggregator collecting the lines to be printed in the difference summary.
|
||||
//
|
||||
// This is buffered in order to allow a future line to potentially impact how
|
||||
// the current line would be printed.
|
||||
#[derive(Default)]
|
||||
struct BufferedSummary<'a> {
|
||||
summary: SummaryBuilder,
|
||||
buffer: Buffer<'a>,
|
||||
}
|
||||
|
||||
impl<'a> BufferedSummary<'a> {
|
||||
// Appends a new line which is common to both actual and expected.
|
||||
fn feed_common_lines(&mut self, common_line: &'a str) {
|
||||
if let Buffer::CommonLines(ref mut common_lines) = self.buffer {
|
||||
common_lines.push(common_line);
|
||||
} else {
|
||||
self.flush_buffer();
|
||||
self.buffer = Buffer::CommonLines(vec![common_line]);
|
||||
}
|
||||
}
|
||||
|
||||
// Appends a new line which is found only in the actual string.
|
||||
fn feed_extra_actual(&mut self, extra_actual: &'a str) {
|
||||
if let Buffer::ExtraExpectedLineChunk(extra_expected) = self.buffer {
|
||||
self.print_inline_diffs(extra_actual, extra_expected);
|
||||
self.buffer = Buffer::Empty;
|
||||
} else {
|
||||
self.flush_buffer();
|
||||
self.buffer = Buffer::ExtraActualLineChunk(extra_actual);
|
||||
}
|
||||
}
|
||||
|
||||
// Appends a new line which is found only in the expected string.
|
||||
fn feed_extra_expected(&mut self, extra_expected: &'a str) {
|
||||
if let Buffer::ExtraActualLineChunk(extra_actual) = self.buffer {
|
||||
self.print_inline_diffs(extra_actual, extra_expected);
|
||||
self.buffer = Buffer::Empty;
|
||||
} else {
|
||||
self.flush_buffer();
|
||||
self.buffer = Buffer::ExtraExpectedLineChunk(extra_expected);
|
||||
}
|
||||
}
|
||||
|
||||
// Appends a comment for the additional line at the start or the end of the
|
||||
// actual string which should be omitted.
|
||||
fn feed_additional_actual(&mut self) {
|
||||
self.flush_buffer();
|
||||
self.summary.new_line();
|
||||
self.summary.push_str_as_comment("<---- remaining lines omitted ---->");
|
||||
}
|
||||
|
||||
fn flush_buffer(&mut self) {
|
||||
self.buffer.flush(&mut self.summary);
|
||||
}
|
||||
|
||||
fn print_inline_diffs(&mut self, actual_line: &str, expected_line: &str) {
|
||||
let line_edits = edit_distance::edit_list(
|
||||
actual_line.chars(),
|
||||
expected_line.chars(),
|
||||
edit_distance::Mode::Exact,
|
||||
);
|
||||
|
||||
if let edit_distance::Difference::Editable(edit_list) = line_edits {
|
||||
let mut actual_summary = SummaryBuilder::default();
|
||||
actual_summary.new_line_for_actual();
|
||||
let mut expected_summary = SummaryBuilder::default();
|
||||
expected_summary.new_line_for_expected();
|
||||
for edit in &edit_list {
|
||||
match edit {
|
||||
edit_distance::Edit::ExtraActual(c) => actual_summary.push_actual_only(*c),
|
||||
edit_distance::Edit::ExtraExpected(c) => {
|
||||
expected_summary.push_expected_only(*c)
|
||||
}
|
||||
edit_distance::Edit::Both(c) => {
|
||||
actual_summary.push_actual_with_match(*c);
|
||||
expected_summary.push_expected_with_match(*c);
|
||||
}
|
||||
edit_distance::Edit::AdditionalActual => {
|
||||
// Calling edit_distance::edit_list(_, _, Mode::Exact) should never return
|
||||
// this enum
|
||||
panic!("This should not happen. This is a bug in gtest_rust")
|
||||
}
|
||||
}
|
||||
}
|
||||
actual_summary.reset_ansi();
|
||||
expected_summary.reset_ansi();
|
||||
self.summary.push_str(&actual_summary.summary);
|
||||
self.summary.push_str(&expected_summary.summary);
|
||||
} else {
|
||||
self.summary.new_line_for_actual();
|
||||
self.summary.push_str_actual_only(actual_line);
|
||||
self.summary.new_line_for_expected();
|
||||
self.summary.push_str_expected_only(expected_line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<'a> FromIterator<edit_distance::Edit<&'a str>> for BufferedSummary<'a> {
|
||||
fn from_iter<T: IntoIterator<Item = edit_distance::Edit<&'a str>>>(iter: T) -> Self {
|
||||
let mut buffered_summary = BufferedSummary::default();
|
||||
for edit in iter {
|
||||
match edit {
|
||||
edit_distance::Edit::Both(same) => {
|
||||
buffered_summary.feed_common_lines(same);
|
||||
}
|
||||
edit_distance::Edit::ExtraActual(actual) => {
|
||||
buffered_summary.feed_extra_actual(actual);
|
||||
}
|
||||
edit_distance::Edit::ExtraExpected(expected) => {
|
||||
buffered_summary.feed_extra_expected(expected);
|
||||
}
|
||||
edit_distance::Edit::AdditionalActual => {
|
||||
buffered_summary.feed_additional_actual();
|
||||
}
|
||||
};
|
||||
}
|
||||
buffered_summary.flush_buffer();
|
||||
buffered_summary.summary.reset_ansi();
|
||||
|
||||
buffered_summary
|
||||
}
|
||||
}
|
||||
|
||||
impl Display for BufferedSummary<'_> {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
if !matches!(self.buffer, Buffer::Empty) {
|
||||
panic!("Buffer is not empty. This is a bug in gtest_rust.")
|
||||
}
|
||||
if !self.summary.last_ansi_style.is_empty() {
|
||||
panic!("ANSI style has not been reset. This is a bug in gtest_rust.")
|
||||
}
|
||||
self.summary.summary.fmt(f)
|
||||
}
|
||||
}
|
||||
|
||||
enum Buffer<'a> {
|
||||
Empty,
|
||||
CommonLines(Vec<&'a str>),
|
||||
ExtraActualLineChunk(&'a str),
|
||||
ExtraExpectedLineChunk(&'a str),
|
||||
}
|
||||
|
||||
impl<'a> Buffer<'a> {
|
||||
fn flush(&mut self, summary: &mut SummaryBuilder) {
|
||||
match self {
|
||||
Buffer::Empty => {}
|
||||
Buffer::CommonLines(common_lines) => {
|
||||
Self::flush_common_lines(std::mem::take(common_lines), summary);
|
||||
}
|
||||
Buffer::ExtraActualLineChunk(extra_actual) => {
|
||||
summary.new_line_for_actual();
|
||||
summary.push_str_actual_only(extra_actual);
|
||||
}
|
||||
Buffer::ExtraExpectedLineChunk(extra_expected) => {
|
||||
summary.new_line_for_expected();
|
||||
summary.push_str_expected_only(extra_expected);
|
||||
}
|
||||
};
|
||||
*self = Buffer::Empty;
|
||||
}
|
||||
|
||||
fn flush_common_lines(common_lines: Vec<&'a str>, summary: &mut SummaryBuilder) {
|
||||
// The number of the lines kept before and after the compressed lines.
|
||||
const COMMON_LINES_CONTEXT_SIZE: usize = 2;
|
||||
|
||||
if common_lines.len() <= 2 * COMMON_LINES_CONTEXT_SIZE + 1 {
|
||||
for line in common_lines {
|
||||
summary.new_line();
|
||||
summary.push_str(line);
|
||||
}
|
||||
return;
|
||||
}
|
||||
|
||||
let start_context = &common_lines[0..COMMON_LINES_CONTEXT_SIZE];
|
||||
|
||||
for line in start_context {
|
||||
summary.new_line();
|
||||
summary.push_str(line);
|
||||
}
|
||||
|
||||
summary.new_line();
|
||||
summary.push_str_as_comment(&format!(
|
||||
"<---- {} common lines omitted ---->",
|
||||
common_lines.len() - 2 * COMMON_LINES_CONTEXT_SIZE,
|
||||
));
|
||||
|
||||
let end_context =
|
||||
&common_lines[common_lines.len() - COMMON_LINES_CONTEXT_SIZE..common_lines.len()];
|
||||
|
||||
for line in end_context {
|
||||
summary.new_line();
|
||||
summary.push_str(line);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl Default for Buffer<'_> {
|
||||
fn default() -> Self {
|
||||
Self::Empty
|
||||
}
|
||||
}
|
||||
|
||||
thread_local! {
|
||||
pub(crate) static USE_COLOR: Cell<bool> = Cell::new(stdout_supports_color());
|
||||
}
|
||||
|
||||
#[rustversion::since(1.70)]
|
||||
fn stdout_supports_color() -> bool {
|
||||
#[allow(clippy::incompatible_msrv)]
|
||||
match (is_env_var_set("NO_COLOR"), is_env_var_set("FORCE_COLOR")) {
|
||||
(true, _) => false,
|
||||
(false, true) => true,
|
||||
(false, false) => std::io::stdout().is_terminal(),
|
||||
}
|
||||
}
|
||||
|
||||
#[rustversion::not(since(1.70))]
|
||||
fn stdout_supports_color() -> bool {
|
||||
is_env_var_set("FORCE_COLOR") && !is_env_var_set("NO_COLOR")
|
||||
}
|
||||
|
||||
fn is_env_var_set(var: &'static str) -> bool {
|
||||
std::env::var(var).map(|s| !s.is_empty()).unwrap_or(false)
|
||||
}
|
||||
|
||||
// Font in italic
|
||||
const COMMENT_STYLE: &str = "\x1B[3m";
|
||||
// Font in green and bold
|
||||
const EXPECTED_ONLY_STYLE: &str = "\x1B[1;32m";
|
||||
// Font in red and bold
|
||||
const ACTUAL_ONLY_STYLE: &str = "\x1B[1;31m";
|
||||
// Font in green onlyh
|
||||
const EXPECTED_WITH_MATCH_STYLE: &str = "\x1B[32m";
|
||||
// Font in red only
|
||||
const ACTUAL_WITH_MATCH_STYLE: &str = "\x1B[31m";
|
||||
// Reset all ANSI formatting
|
||||
const RESET_ALL: &str = "\x1B[0m";
|
||||
|
||||
#[derive(Default)]
|
||||
struct SummaryBuilder {
|
||||
summary: String,
|
||||
last_ansi_style: &'static str,
|
||||
}
|
||||
|
||||
impl SummaryBuilder {
|
||||
fn push_str(&mut self, element: &str) {
|
||||
self.reset_ansi();
|
||||
self.summary.push_str(element);
|
||||
}
|
||||
|
||||
fn push_str_as_comment(&mut self, element: &str) {
|
||||
self.set_ansi(COMMENT_STYLE);
|
||||
self.summary.push_str(element);
|
||||
}
|
||||
|
||||
fn push_str_actual_only(&mut self, element: &str) {
|
||||
self.set_ansi(ACTUAL_ONLY_STYLE);
|
||||
self.summary.push_str(element);
|
||||
}
|
||||
|
||||
fn push_str_expected_only(&mut self, element: &str) {
|
||||
self.set_ansi(EXPECTED_ONLY_STYLE);
|
||||
self.summary.push_str(element);
|
||||
}
|
||||
|
||||
fn push_actual_only(&mut self, element: char) {
|
||||
self.set_ansi(ACTUAL_ONLY_STYLE);
|
||||
self.summary.push(element);
|
||||
}
|
||||
|
||||
fn push_expected_only(&mut self, element: char) {
|
||||
self.set_ansi(EXPECTED_ONLY_STYLE);
|
||||
self.summary.push(element);
|
||||
}
|
||||
|
||||
fn push_actual_with_match(&mut self, element: char) {
|
||||
self.set_ansi(ACTUAL_WITH_MATCH_STYLE);
|
||||
self.summary.push(element);
|
||||
}
|
||||
|
||||
fn push_expected_with_match(&mut self, element: char) {
|
||||
self.set_ansi(EXPECTED_WITH_MATCH_STYLE);
|
||||
self.summary.push(element);
|
||||
}
|
||||
|
||||
fn new_line(&mut self) {
|
||||
self.reset_ansi();
|
||||
self.summary.push_str("\n ");
|
||||
}
|
||||
|
||||
fn new_line_for_actual(&mut self) {
|
||||
self.reset_ansi();
|
||||
self.summary.push_str("\n-");
|
||||
}
|
||||
|
||||
fn new_line_for_expected(&mut self) {
|
||||
self.reset_ansi();
|
||||
self.summary.push_str("\n+");
|
||||
}
|
||||
|
||||
fn reset_ansi(&mut self) {
|
||||
if !self.last_ansi_style.is_empty() && USE_COLOR.with(Cell::get) {
|
||||
self.summary.push_str(RESET_ALL);
|
||||
self.last_ansi_style = "";
|
||||
}
|
||||
}
|
||||
|
||||
fn set_ansi(&mut self, ansi_style: &'static str) {
|
||||
if !USE_COLOR.with(Cell::get) || self.last_ansi_style == ansi_style {
|
||||
return;
|
||||
}
|
||||
if !self.last_ansi_style.is_empty() {
|
||||
self.summary.push_str(RESET_ALL);
|
||||
}
|
||||
self.summary.push_str(ansi_style);
|
||||
self.last_ansi_style = ansi_style;
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::{matcher_support::edit_distance::Mode, prelude::*, Result};
|
||||
use indoc::indoc;
|
||||
use std::fmt::Write;
|
||||
|
||||
// Make a long text with each element of the iterator on one line.
|
||||
// `collection` must contains at least one element.
|
||||
fn build_text<T: Display>(mut collection: impl Iterator<Item = T>) -> String {
|
||||
let mut text = String::new();
|
||||
write!(&mut text, "{}", collection.next().expect("Provided collection without elements"))
|
||||
.unwrap();
|
||||
for item in collection {
|
||||
write!(&mut text, "\n{item}").unwrap();
|
||||
}
|
||||
text
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_smaller_than_one_line() -> Result<()> {
|
||||
verify_that!(create_diff("One", "Two", Mode::Exact), eq(""))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_exact_same() -> Result<()> {
|
||||
let expected = indoc! {"
|
||||
One
|
||||
Two
|
||||
"};
|
||||
let actual = indoc! {"
|
||||
One
|
||||
Two
|
||||
"};
|
||||
verify_that!(
|
||||
create_diff(expected, actual, Mode::Exact),
|
||||
eq("No difference found between debug strings.")
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_multiline_diff() -> Result<()> {
|
||||
let expected = indoc! {"
|
||||
prefix
|
||||
Actual#1
|
||||
Actual#2
|
||||
Actual#3
|
||||
suffix"};
|
||||
let actual = indoc! {"
|
||||
prefix
|
||||
Expected@one
|
||||
Expected@two
|
||||
suffix"};
|
||||
// TODO: It would be better to have all the Actual together followed by all the
|
||||
// Expected together.
|
||||
verify_that!(
|
||||
create_diff(expected, actual, Mode::Exact),
|
||||
eq(indoc!(
|
||||
"
|
||||
Difference(-actual / +expected):
|
||||
prefix
|
||||
-Actual#1
|
||||
+Expected@one
|
||||
-Actual#2
|
||||
+Expected@two
|
||||
-Actual#3
|
||||
suffix"
|
||||
))
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_exact_unrelated() -> Result<()> {
|
||||
verify_that!(create_diff(&build_text(1..500), &build_text(501..1000), Mode::Exact), eq(""))
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_exact_small_difference() -> Result<()> {
|
||||
verify_that!(
|
||||
create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
|
||||
eq(indoc! {
|
||||
"
|
||||
Difference(-actual / +expected):
|
||||
1
|
||||
2
|
||||
<---- 45 common lines omitted ---->
|
||||
48
|
||||
49
|
||||
+50"
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_exact_small_difference_with_color() -> Result<()> {
|
||||
USE_COLOR.with(|cell| cell.set(true));
|
||||
|
||||
verify_that!(
|
||||
create_diff(&build_text(1..50), &build_text(1..51), Mode::Exact),
|
||||
eq(indoc! {
|
||||
"
|
||||
Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
|
||||
1
|
||||
2
|
||||
\x1B[3m<---- 45 common lines omitted ---->\x1B[0m
|
||||
48
|
||||
49
|
||||
+\x1B[1;32m50\x1B[0m"
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_exact_difference_with_inline_color() -> Result<()> {
|
||||
USE_COLOR.with(|cell| cell.set(true));
|
||||
|
||||
let actual = indoc!(
|
||||
"There is a home in Nouvelle Orleans
|
||||
They say, it is the rising sons
|
||||
And it has been the ruin of many a po'boy"
|
||||
);
|
||||
|
||||
let expected = indoc!(
|
||||
"There is a house way down in New Orleans
|
||||
They call the rising sun
|
||||
And it has been the ruin of many a poor boy"
|
||||
);
|
||||
|
||||
verify_that!(
|
||||
create_diff(actual, expected, Mode::Exact),
|
||||
eq(indoc! {
|
||||
"
|
||||
Difference(-\x1B[1;31mactual\x1B[0m / +\x1B[1;32mexpected\x1B[0m):
|
||||
-\x1B[31mThere is a ho\x1B[0m\x1B[1;31mm\x1B[0m\x1B[31me in N\x1B[0m\x1B[1;31mouv\x1B[0m\x1B[31me\x1B[0m\x1B[1;31mlle\x1B[0m\x1B[31m Orleans\x1B[0m
|
||||
+\x1B[32mThere is a ho\x1B[0m\x1B[1;32mus\x1B[0m\x1B[32me \x1B[0m\x1B[1;32mway down \x1B[0m\x1B[32min Ne\x1B[0m\x1B[1;32mw\x1B[0m\x1B[32m Orleans\x1B[0m
|
||||
-\x1B[31mThey \x1B[0m\x1B[1;31ms\x1B[0m\x1B[31ma\x1B[0m\x1B[1;31my,\x1B[0m\x1B[31m \x1B[0m\x1B[1;31mi\x1B[0m\x1B[31mt\x1B[0m\x1B[1;31m is t\x1B[0m\x1B[31mhe rising s\x1B[0m\x1B[1;31mo\x1B[0m\x1B[31mn\x1B[0m\x1B[1;31ms\x1B[0m
|
||||
+\x1B[32mThey \x1B[0m\x1B[1;32mc\x1B[0m\x1B[32ma\x1B[0m\x1B[1;32mll\x1B[0m\x1B[32m the rising s\x1B[0m\x1B[1;32mu\x1B[0m\x1B[32mn\x1B[0m
|
||||
-\x1B[31mAnd it has been the ruin of many a po\x1B[0m\x1B[1;31m'\x1B[0m\x1B[31mboy\x1B[0m
|
||||
+\x1B[32mAnd it has been the ruin of many a po\x1B[0m\x1B[1;32mor \x1B[0m\x1B[32mboy\x1B[0m"
|
||||
})
|
||||
)
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn create_diff_line_termination_diff() -> Result<()> {
|
||||
verify_that!(
|
||||
create_diff("1\n2\n3", "1\n2\n3\n", Mode::Exact),
|
||||
eq("Actual omits a terminating newline that is present in expected.")
|
||||
)?;
|
||||
verify_that!(
|
||||
create_diff("1\n2\n3\n", "1\n2\n3", Mode::Exact),
|
||||
eq("Actual includes a terminating newline that is absent from expected.")
|
||||
)?;
|
||||
verify_that!(
|
||||
create_diff("1\n2\n3\n", "1\n2\n3\n", Mode::Exact),
|
||||
eq("No difference found between debug strings.")
|
||||
)
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,85 @@
|
||||
// Copyright 2023 Google LLC
|
||||
//
|
||||
// Licensed under the Apache License, Version 2.0 (the "License");
|
||||
// you may not use this file except in compliance with the License.
|
||||
// You may obtain a copy of the License at
|
||||
//
|
||||
// http://www.apache.org/licenses/LICENSE-2.0
|
||||
//
|
||||
// Unless required by applicable law or agreed to in writing, software
|
||||
// distributed under the License is distributed on an "AS IS" BASIS,
|
||||
// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
||||
// See the License for the specific language governing permissions and
|
||||
// limitations under the License.
|
||||
|
||||
/// Zips up two iterators into a single iterator of pairs.
|
||||
///
|
||||
/// This is identical to [`Iterator::zip`] except that this version allows the
|
||||
/// caller to determine whether the two iterators had mismatching sizes using
|
||||
/// the method [`ZippedIterator::has_size_mismatch`].
|
||||
///
|
||||
/// [`Iterator::zip`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#method.zip
|
||||
pub(crate) fn zip<I1, I2>(left: I1, right: I2) -> ZippedIterator<I1, I2> {
|
||||
ZippedIterator { left, right, has_size_mismatch: false, consumed_elements: 0 }
|
||||
}
|
||||
|
||||
/// An iterator over pairs of the elements of two constituent iterators, which
|
||||
/// keeps track of whether the two iterators have the same size.
|
||||
///
|
||||
/// This is identical to [`Zip`] except that it allows the caller to determine
|
||||
/// whether the two iterators had mismatching sizes using the method
|
||||
/// [`ZippedIterator::has_size_mismatch`].
|
||||
///
|
||||
/// [`Zip`]: https://doc.rust-lang.org/std/iter/struct.Zip.html
|
||||
pub(crate) struct ZippedIterator<I1, I2> {
|
||||
left: I1,
|
||||
right: I2,
|
||||
has_size_mismatch: bool,
|
||||
consumed_elements: usize,
|
||||
}
|
||||
|
||||
impl<I1: Iterator, I2> ZippedIterator<I1, I2> {
|
||||
/// Returns whether a mismatch in the two sizes of the two iterators was
|
||||
/// detected during iteration.
|
||||
///
|
||||
/// This returns `true` if and only if, at some previous call to
|
||||
/// [`Iterator::next`] on this instance, one of the constituent iterators
|
||||
/// had a next element and the other did not.
|
||||
///
|
||||
/// [`Iterator::next`]: https://doc.rust-lang.org/std/iter/trait.Iterator.html#tymethod.next
|
||||
pub(crate) fn has_size_mismatch(&self) -> bool {
|
||||
self.has_size_mismatch
|
||||
}
|
||||
|
||||
/// Returns the number of elements in the left iterator.
|
||||
///
|
||||
/// This iterates through the remainder of the left iterator if necessary in
|
||||
/// order to get the true number of elements. It therefore consumes `self`.
|
||||
pub(crate) fn left_size(mut self) -> usize {
|
||||
self.consumed_elements + self.left.by_ref().count()
|
||||
}
|
||||
}
|
||||
|
||||
impl<I1: Iterator, I2: Iterator> Iterator for ZippedIterator<I1, I2> {
|
||||
type Item = (I1::Item, I2::Item);
|
||||
|
||||
fn next(&mut self) -> Option<(I1::Item, I2::Item)> {
|
||||
match (self.left.next(), self.right.next()) {
|
||||
(Some(v1), Some(v2)) => {
|
||||
self.consumed_elements += 1;
|
||||
Some((v1, v2))
|
||||
}
|
||||
(Some(_), None) => {
|
||||
// Consumed elements counts only elements from self.left
|
||||
self.consumed_elements += 1;
|
||||
self.has_size_mismatch = true;
|
||||
None
|
||||
}
|
||||
(None, Some(_)) => {
|
||||
self.has_size_mismatch = true;
|
||||
None
|
||||
}
|
||||
(None, None) => None,
|
||||
}
|
||||
}
|
||||
}
|
||||
Reference in new issue
Block a user