build(rust): vendor googletest crate

This commit was merged in pull request #76.
This commit is contained in:
alexion committed 2026-09-19 14:53:22 -04:00
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,
}
}
}