// Part of the Carbon Language project, under the Apache License v2.0 with LLVM // Exceptions. See /LICENSE for license information. // SPDX-License-Identifier: Apache-2.0 WITH LLVM-exception #ifndef CARBON_TESTING_BASE_UNIFIED_DIFF_MATCHER_H_ #define CARBON_TESTING_BASE_UNIFIED_DIFF_MATCHER_H_ #include #include #include #include #include #include "common/check.h" #include "llvm/ADT/STLExtras.h" #include "llvm/ADT/Sequence.h" #include "llvm/ADT/SmallVector.h" namespace Carbon::Testing { // Matcher that compares the elements of two containers and produces a unified // diff on failure. template class UnifiedDiffMatcher { public: explicit UnifiedDiffMatcher(Container expected) : expected_(std::move(expected)) {} // Matches `actual` against `expected_`. Returns true on a match; returns // false and prints a unified diff to `listener` on a mismatch. template auto MatchAndExplain(const ActualContainer& actual, testing::MatchResultListener* listener) const -> bool; auto DescribeTo(std::ostream* os) const -> void { *os << "matches elements with unified diff"; } auto DescribeNegationTo(std::ostream* os) const -> void { *os << "does not match elements with unified diff"; } private: // A 2D array, stored contiguously. Rows correspond to `expected_`'s elements, // and columns correspond to the actual container's elements. template class Table; // The result of a `Matches` check between an expected and actual element. enum class MatchResult : uint8_t { Unknown, Matches, DoesNotMatch }; // Checks whether `actual_element` matches `expected_[expected_index]`. It // first checks whether a cached result exists. If not, it evaluates the // match and stores the result in `match_results`. template auto IsElementMatch(size_t expected_index, size_t actual_index, const ActualElement& actual_element, Table& match_results) const -> bool { MatchResult cached_result = match_results.Get(expected_index, actual_index); if (cached_result != MatchResult::Unknown) { return cached_result == MatchResult::Matches; } bool is_match = testing::MatcherCast(expected_[expected_index]) .Matches(actual_element); match_results.Set( expected_index, actual_index, is_match ? MatchResult::Matches : MatchResult::DoesNotMatch); return is_match; } // Returns true if every element in `expected_` matches the corresponding // element in `actual`. Stores comparisons in `match_results`. template auto IsEqual(const ActualContainer& actual, Table& match_results) const -> bool; // Populates `subsequences` with the longest common matching subsequences // found when comparing `actual` and `expected_`. Stores comparisons in // `match_results`. template auto GetLongestCommonSubsequences(const ActualContainer& actual, Table& match_results, Table& subsequences) const -> void; // Prints the unified diff. template auto PrintDiff(const ActualContainer& actual, Table& match_results, const Table& subsequences, testing::MatchResultListener* listener) const -> void; // The expected elements. Container expected_; }; // Returns a polymorphic matcher that acts similarly to // ElementsAreArray but produces a unified diff on failure. template auto ElementsAreArrayWithUnifiedDiff(Container expected) { return testing::MakePolymorphicMatcher( UnifiedDiffMatcher(std::move(expected))); } // ----------------------------------------------------------------------------- // Internal implementation details follow. // ----------------------------------------------------------------------------- template template class UnifiedDiffMatcher::Table { public: // Constructs a table with dimensions of expected_size and actual_size, // corresponding to the containers being compared. Table(int expected_size, int actual_size, T default_value) : actual_size_(actual_size), data_(expected_size * actual_size, default_value) {} // Sets the value at the given expected_index and actual_index. auto Set(int expected_index, int actual_index, T value) -> void { data_[expected_index * actual_size_ + actual_index] = std::move(value); } // Gets the value at the given expected_index and actual_index. auto Get(int expected_index, int actual_index) const -> T { return data_[expected_index * actual_size_ + actual_index]; } private: // The actual_size of the table. int actual_size_; // The contiguous data storage for the table. llvm::SmallVector data_; }; template template auto UnifiedDiffMatcher::MatchAndExplain( const ActualContainer& actual, testing::MatchResultListener* listener) const -> bool { Table match_results(expected_.size(), std::size(actual), MatchResult::Unknown); if (IsEqual(actual, match_results)) { return true; } if (listener->IsInterested()) { Table subsequences(expected_.size() + 1, std::size(actual) + 1, 0); GetLongestCommonSubsequences(actual, match_results, subsequences); PrintDiff(actual, match_results, subsequences, listener); } return false; } template template auto UnifiedDiffMatcher::IsEqual( const ActualContainer& actual, Table& match_results) const -> bool { if (expected_.size() != std::size(actual)) { return false; } for (auto [i, actual_element] : llvm::enumerate(actual)) { if (!IsElementMatch(i, i, actual_element, match_results)) { return false; } } return true; } template template auto UnifiedDiffMatcher::GetLongestCommonSubsequences( const ActualContainer& actual, Table& match_results, Table& subsequences) const -> void { for (auto expected_index : llvm::seq(expected_.size())) { for (auto [actual_index, actual_element] : llvm::enumerate(actual)) { int subsequence_value; if (IsElementMatch(expected_index, actual_index, actual_element, match_results)) { // If the elements match, the LCS length increases by 1 relative to // the prefixes where both elements are excluded. subsequence_value = subsequences.Get(expected_index, actual_index) + 1; } else { // Otherwise, the LCS length is the maximum of the LCS lengths // relative to the prefixes where one element is excluded. subsequence_value = std::max(subsequences.Get(expected_index, actual_index + 1), subsequences.Get(expected_index + 1, actual_index)); } subsequences.Set(expected_index + 1, actual_index + 1, subsequence_value); } } } template template auto UnifiedDiffMatcher::PrintDiff( const ActualContainer& actual, Table& match_results, const Table& subsequences, testing::MatchResultListener* listener) const -> void { // A line in the diff output. struct DiffLine { enum class Kind { Match, ActualOnly, ExpectedOnly }; Kind kind; // Only used for `Match` and `ActualOnly`. const ActualContainer::value_type* actual_value; int expected_index; }; llvm::SmallVector diff; // Reserve a quick upper bound of the size. diff.reserve(expected_.size() + std::size(actual)); // Reconstruct the diff by backtracking from the end of the table. int expected_index = expected_.size() - 1; int actual_index = std::size(actual) - 1; auto actual_it = std::end(actual) - 1; while (expected_index >= 0 || actual_index >= 0) { auto match_result = (expected_index >= 0 && actual_index >= 0) ? match_results.Get(expected_index, actual_index) : MatchResult::DoesNotMatch; CARBON_CHECK(match_result != MatchResult::Unknown); if (match_result == MatchResult::Matches) { // The element is in both lists for the diff. diff.push_back({.kind = DiffLine::Kind::Match, .actual_value = &*actual_it, .expected_index = expected_index}); --expected_index; --actual_index; --actual_it; } else if (actual_index >= 0 && (expected_index < 0 || subsequences.Get(expected_index + 1, actual_index) >= subsequences.Get(expected_index, actual_index + 1))) { // Dropping an element from `actual` preserves the LCS length, so treat it // as an insertion. diff.push_back({.kind = DiffLine::Kind::ActualOnly, .actual_value = &*actual_it, .expected_index = std::max(0, expected_index)}); --actual_index; --actual_it; } else { // Otherwise, treat it as a deletion from `expected`. diff.push_back({.kind = DiffLine::Kind::ExpectedOnly, .actual_value = nullptr, .expected_index = expected_index}); --expected_index; } } struct PrintRange { int begin; int end; }; llvm::SmallVector print_ranges; constexpr int ContextLines = 3; for (auto [i, line] : llvm::reverse(llvm::zip_equal(llvm::seq(diff.size()), diff))) { if (line.kind != DiffLine::Kind::Match) { PrintRange range = { .begin = std::max(0, i - ContextLines), .end = std::min(diff.size() - 1, i + ContextLines)}; if (print_ranges.empty() || print_ranges.back().begin > range.end + 1) { print_ranges.push_back(range); } else { // Merge diffs with overlapping context. print_ranges.back().begin = range.begin; } } } *listener << "unified diff (- expected, + actual):\n"; for (const auto& range : print_ranges) { *listener << "=== diff in expected elements " << diff[range.end].expected_index + 1 << " to " << diff[range.begin].expected_index + 1 << " (1-based index):\n"; for (auto i : llvm::reverse(llvm::seq_inclusive(range.begin, range.end))) { const auto& line = diff[i]; if (line.kind == DiffLine::Kind::Match) { *listener << " " << *line.actual_value << "\n"; } else if (line.kind == DiffLine::Kind::ActualOnly) { *listener << "+ " << *line.actual_value << "\n"; } else { *listener << "- "; expected_[line.expected_index].DescribeTo(listener->stream()); *listener << "\n"; } } } *listener << "=== diff end\n"; } } // namespace Carbon::Testing #endif // CARBON_TESTING_BASE_UNIFIED_DIFF_MATCHER_H_