From 93a92cf2da8dc9afe1189e73f39772e6791e4d06 Mon Sep 17 00:00:00 2001 From: kabrahamAMD Date: Sat, 25 Oct 2025 16:22:41 +0200 Subject: [PATCH] [CK_BUILDER] Add inline string diff for tests (#3067) Adds new testing functionality: an inline diff for string comparison. Example usage: EXPECT_THAT("Actual string", ck_tile::test::StringEqWithDiff("Expected string")); Failure message: Value of: "Actual string" Expected: "Expected string" Actual: "Actual string" (of type char [14]), Diff: "[Expe|A]ct[ed|ual] string" The inline-diff function uses the Wagner-Fischer algorithm to find the minimum edit distance and generate diff markers, which has O(N^2) complexity. It has optional color codes that are enabled with the matcher. [ROCm/composable_kernel commit: e576992dca14eab85be6a6db555cc76cfabbf577] --- experimental/builder/test/CMakeLists.txt | 5 +- .../builder/test/test_inline_diff.cpp | 52 +++++ experimental/builder/test/testing_utils.cpp | 219 ++++++++++++++++++ experimental/builder/test/testing_utils.hpp | 43 ++++ 4 files changed, 318 insertions(+), 1 deletion(-) create mode 100644 experimental/builder/test/test_inline_diff.cpp create mode 100644 experimental/builder/test/testing_utils.cpp create mode 100644 experimental/builder/test/testing_utils.hpp diff --git a/experimental/builder/test/CMakeLists.txt b/experimental/builder/test/CMakeLists.txt index 04b63b7823..f77219d019 100644 --- a/experimental/builder/test/CMakeLists.txt +++ b/experimental/builder/test/CMakeLists.txt @@ -17,7 +17,10 @@ endfunction() add_ck_builder_test(test_conv_builder test_conv_builder.cpp - test_instance_traits.cpp) + test_instance_traits.cpp + testing_utils.cpp) add_ck_builder_test(test_get_instance_string test_get_instance_string.cpp) + +add_ck_builder_test(test_inline_diff test_inline_diff.cpp testing_utils.cpp) diff --git a/experimental/builder/test/test_inline_diff.cpp b/experimental/builder/test/test_inline_diff.cpp new file mode 100644 index 0000000000..41692fb40e --- /dev/null +++ b/experimental/builder/test/test_inline_diff.cpp @@ -0,0 +1,52 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +#include + +#include "testing_utils.hpp" + +namespace ck_tile::builder { +namespace { + +TEST(InlineDiff, simpleColorDiff) +{ + std::string str1{"hello"}; + std::string str2{"hello"}; + std::string str3{"world"}; + + // some easy tests + // you can veryfy the ungodly strings are meaningful by running echo -e "" + EXPECT_THAT(test::inlineDiff(str1, str2, true), "hello"); + EXPECT_THAT(test::inlineDiff(str1, str3, true), + "[\x1B[36mwor\x1B[0m|\x1B[35mhel\x1B[0m]l[\x1B[36md\x1B[0m|\x1B[35mo\x1B[0m]"); +} + +TEST(InlineDiff, noColorDiff) +{ + std::string str1{"hello"}; + std::string str2{"hello"}; + std::string str3{"world"}; + + // some easy tests without color + EXPECT_THAT(test::inlineDiff(str1, str2, false), "hello"); + EXPECT_THAT(test::inlineDiff(str1, str3, false), "[wor|hel]l[d|o]"); +} + +TEST(InlineDiff, complexColorDiff) +{ + + // now something more interesting + std::string str4{"this part has changed, this part has been left out, this part, this part has " + "an extra letter"}; + std::string str5{ + "this part has degeahc, this part has, this part added, this part has ana extra letter"}; + + EXPECT_THAT( + test::inlineDiff(str5, str4, true), + "this part has [\x1B[36mchanged\x1B[0m|\x1B[35mdegeahc\x1B[0m], this part has[\x1B[36m " + "been left out\x1B[0m|\x1B[35m\x1B[0m], this part[\x1B[36m\x1B[0m|\x1B[35m added\x1B[0m], " + "this part has an[\x1B[36m\x1B[0m|\x1B[35ma\x1B[0m] extra letter"); +}; + +} // namespace +} // namespace ck_tile::builder diff --git a/experimental/builder/test/testing_utils.cpp b/experimental/builder/test/testing_utils.cpp new file mode 100644 index 0000000000..c99d56ef56 --- /dev/null +++ b/experimental/builder/test/testing_utils.cpp @@ -0,0 +1,219 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +#include +#include +#include +#include +#include +#include +#include +#include "testing_utils.hpp" + +namespace ck_tile::test { + +namespace { + +} // namespace + +// Wagner-Fischer Algorithm for Computing Edit Distance and Inline Diff +// +// OUTPUT FORMAT: [expected|actual] for differences, plain text for matches +// Example: "hello world" vs "hello earth" → "hello [world|earth]" +// +// This function implements the Wagner-Fischer algorithm (1974), which is the classic +// dynamic programming solution for computing the minimum edit distance (Levenshtein distance) +// between two strings. The algorithm has O(n*m) time and space complexity. +// +// ALGORITHM OVERVIEW: +// 1. Build a 2D DP table where dp[i][j] represents the minimum edit distance +// between the first i characters of 'expected' and first j characters of 'actual' +// 2. Fill the table using the recurrence relation: +// dp[i][j] = min( +// dp[i-1][j] + 1, // deletion (remove char from expected) +// dp[i][j-1] + 1, // insertion (add char to expected) +// dp[i-1][j-1] + cost // substitution (cost=0 if chars match, 1 if different) +// ) +// 3. Backtrack through the table to reconstruct the optimal edit sequence +// +// REFERENCES: +// - Wagner, R. A.; Fischer, M. J. (1974). "The String-to-String Correction Problem" +// - Also known as: Levenshtein distance, edit distance, string alignment +// - Similar to sequence alignment algorithms used in bioinformatics (Needleman-Wunsch) +std::string inlineDiff(const std::string& actual, const std::string& expected, bool use_color) +{ + + const char* EXPECTED_COLOR = use_color ? "\033[36m" : ""; // Cyan + const char* ACTUAL_COLOR = use_color ? "\033[35m" : ""; // Magenta + const char* RESET = use_color ? "\033[0m" : ""; + + const size_t n = expected.length(); // Length of expected string + const size_t m = actual.length(); // Length of actual string + + // PHASE 1: Build the Dynamic Programming Table + // dp[i][j] = minimum edit distance between expected[0..i-1] and actual[0..j-1] + std::vector> dp(n + 1, std::vector(m + 1)); + + // Base cases: transforming empty string to/from prefixes + for(size_t i = 0; i <= n; ++i) + { + dp[i][0] = i; // Delete i characters from expected to get empty string + } + for(size_t j = 0; j <= m; ++j) + { + dp[0][j] = j; // Insert j characters to empty string to get actual[0..j-1] + } + + // Fill the DP table using the Wagner-Fischer recurrence relation + for(size_t i = 1; i <= n; ++i) + { + for(size_t j = 1; j <= m; ++j) + { + // Cost is 0 if characters match, 1 if they need substitution + int cost = (expected[i - 1] == actual[j - 1]) ? 0 : 1; + + // Choose the minimum cost operation: + dp[i][j] = std::min({ + dp[i - 1][j] + 1, // Deletion: remove expected[i-1] + dp[i][j - 1] + 1, // Insertion: add actual[j-1] + dp[i - 1][j - 1] + cost // Substitution/Match + }); + } + } + + // PHASE 2: Backtrack to Reconstruct the Optimal Edit Sequence + // We trace back from dp[n][m] to dp[0][0] to find which operations were used + std::vector operations; // 'M'atch, 'S'ubstitution, 'I'nsertion, 'D'eletion + std::vector> diff_chars; // Character pairs for each operation + + size_t i = n, j = m; // Start from bottom-right corner of DP table + while(i > 0 || j > 0) + { + // Determine which operation led to the current cell's value + int cost = (i > 0 && j > 0 && expected[i - 1] == actual[j - 1]) ? 0 : 1; + + // Check if we came from diagonal (substitution/match) + if(i > 0 && j > 0 && dp[i][j] == dp[i - 1][j - 1] + cost) + { + if(cost == 0) + { + operations.push_back('M'); // Characters match + diff_chars.push_back({expected[i - 1], actual[j - 1]}); + } + else + { + operations.push_back('S'); // Substitution needed + diff_chars.push_back({expected[i - 1], actual[j - 1]}); + } + --i; + --j; // Move diagonally up-left + } + // Check if we came from left (insertion) + else if(j > 0 && dp[i][j] == dp[i][j - 1] + 1) + { + operations.push_back('I'); // Insertion: actual has extra character + diff_chars.push_back({'\0', actual[j - 1]}); + --j; // Move left + } + // Must have come from above (deletion) + else if(i > 0 && dp[i][j] == dp[i - 1][j] + 1) + { + operations.push_back('D'); // Deletion: expected has extra character + diff_chars.push_back({expected[i - 1], '\0'}); + --i; // Move up + } + } + + // PHASE 3: Reverse and Build the Human-Readable Diff String + // Backtracking gives us operations in reverse order, so we reverse to get forward order + std::reverse(operations.begin(), operations.end()); + std::reverse(diff_chars.begin(), diff_chars.end()); + + // Build the final diff string with color highlighting + std::ostringstream diff; + std::string expected_diff, actual_diff; // Accumulate consecutive differences + bool in_diff = false; // Track whether we're inside a diff section + + for(size_t k = 0; k < operations.size(); ++k) + { + char op = operations[k]; + char exp_char = diff_chars[k].first; // Expected character ('\0' for insertions) + char act_char = diff_chars[k].second; // Actual character ('\0' for deletions) + + if(op == 'M') // Match - characters are identical + { + if(in_diff) + { + // Close the current diff section and output it + diff << "[" << EXPECTED_COLOR << expected_diff << RESET << "|" << ACTUAL_COLOR + << actual_diff << RESET << "]"; + expected_diff.clear(); + actual_diff.clear(); + in_diff = false; + } + diff << exp_char; // Output the matching character as-is + } + else // Difference (substitution, insertion, or deletion) + { + in_diff = true; + // Accumulate characters for the diff section + if(exp_char != '\0') + expected_diff += exp_char; // Add to expected side + if(act_char != '\0') + actual_diff += act_char; // Add to actual side + } + } + + // Close any remaining diff section at the end + if(in_diff) + { + diff << "[" << EXPECTED_COLOR << expected_diff << RESET << "|" << ACTUAL_COLOR + << actual_diff << RESET << "]"; + } + + return diff.str(); +} + +std::string formatInlineDiff(const std::string& actual, const std::string& expected) +{ + return std::string("Inline diff: \"") + inlineDiff(actual, expected) + "\""; +} + +// StringEqWithDiffMatcher implementation +StringEqWithDiffMatcher::StringEqWithDiffMatcher(const std::string& expected) : expected_(expected) +{ +} + +bool StringEqWithDiffMatcher::MatchAndExplain(std::string actual, + ::testing::MatchResultListener* listener) const +{ + if(actual == expected_) + { + return true; + } + + // On failure, provide detailed diff information + if(listener->IsInterested()) + { + *listener << "\n Diff: \"" << inlineDiff(actual, expected_) << "\""; + } + return false; +} + +void StringEqWithDiffMatcher::DescribeTo(std::ostream* os) const +{ + *os << "\"" << expected_ << "\""; +} + +void StringEqWithDiffMatcher::DescribeNegationTo(std::ostream* os) const +{ + *os << "is not equal to \"" << expected_ << "\""; +} + +// Factory function for the StringEqWithDiff matcher +::testing::Matcher StringEqWithDiff(const std::string& expected) +{ + return ::testing::MakeMatcher(new StringEqWithDiffMatcher(expected)); +} + +} // namespace ck_tile::test diff --git a/experimental/builder/test/testing_utils.hpp b/experimental/builder/test/testing_utils.hpp new file mode 100644 index 0000000000..3e8772a080 --- /dev/null +++ b/experimental/builder/test/testing_utils.hpp @@ -0,0 +1,43 @@ +// SPDX-License-Identifier: MIT +// Copyright (c) 2018-2025, Advanced Micro Devices, Inc. All rights reserved. + +#include +#include +#include +#include + +namespace ck_tile::test { + +static bool isTerminalOutput() { return isatty(fileno(stdout)) || isatty(fileno(stderr)); } + +// Returns a string highlighting differences between actual and expected. +// Differences are enclosed in brackets with actual and expected parts separated by '|'. +std::string inlineDiff(const std::string& actual, + const std::string& expected, + bool use_color = isTerminalOutput()); + +// A convenience alias for inlineDiff to improve readability in test assertions. +// Note that the function has O(n^2) complexity both in compute and in memory - do not use for very +// long strings +std::string formatInlineDiff(const std::string& actual, const std::string& expected); + +// Gmock matcher for string equality with inline diff output on failure +class StringEqWithDiffMatcher : public ::testing::MatcherInterface +{ + public: + explicit StringEqWithDiffMatcher(const std::string& expected); + + bool MatchAndExplain(std::string actual, + ::testing::MatchResultListener* listener) const override; + + void DescribeTo(std::ostream* os) const override; + void DescribeNegationTo(std::ostream* os) const override; + + private: + std::string expected_; +}; + +// Factory function for the StringEqWithDiff matcher +::testing::Matcher StringEqWithDiff(const std::string& expected); + +} // namespace ck_tile::test