diff --git a/IntelliTect.TestTools.Console.Tests/EnhancedWildcardDiffTests.cs b/IntelliTect.TestTools.Console.Tests/EnhancedWildcardDiffTests.cs new file mode 100644 index 0000000..9506926 --- /dev/null +++ b/IntelliTect.TestTools.Console.Tests/EnhancedWildcardDiffTests.cs @@ -0,0 +1,130 @@ +using Microsoft.VisualStudio.TestTools.UnitTesting; +using System; +using System.Runtime.InteropServices; + +namespace IntelliTect.TestTools.Console.Tests; + +[TestClass] +public class EnhancedWildcardDiffTests +{ + [TestMethod] + public void ExpectLike_WithEnhancedDiff_ShowsDetailedOutput() + { + // Arrange: Create a test that will fail to see the enhanced output + string expected = @"PING *(* (::1)) 56 data bytes +64 bytes from * (::1): icmp_seq=1 ttl=64 time=* ms +Response time was * ms"; + + string actualOutput = @"PING localhost (::1) 56 data bytes +64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.018 ms +Extra line that shouldn't be here +Response time was 0.025 ms"; + + // Act & Assert + Exception exception = Assert.ThrowsException(() => + { + ConsoleAssert.ExpectLike(expected, () => + { + System.Console.Write(actualOutput); + }, DiffOptions.EnhancedWildcardDiff); + }); + + // Verify the enhanced output contains line-by-line information + string message = exception.Message; + StringAssert.Contains(message, "Line 1:"); + StringAssert.Contains(message, "Line 2:"); + StringAssert.Contains(message, "Line 3:"); + StringAssert.Contains(message, "✅"); // Should contain success markers + StringAssert.Contains(message, "❌"); // Should contain failure markers + StringAssert.Contains(message, "unexpected extra line"); // Should identify extra lines + StringAssert.Contains(message, "Summary:"); + } + + [TestMethod] + public void ExpectLike_WithoutEnhancedDiff_ShowsOriginalOutput() + { + // Arrange: Create a test that will fail to see the original output + string expected = @"PING *(* (::1)) 56 data bytes +64 bytes from * (::1): icmp_seq=1 ttl=64 time=* ms"; + + string actualOutput = @"PING localhost (::1) 56 data bytes +64 bytes from localhost (::1): icmp_seq=1 ttl=64 time=0.018 ms +Extra line"; + + // Act & Assert + Exception exception = Assert.ThrowsException(() => + { + ConsoleAssert.ExpectLike(expected, () => + { + System.Console.Write(actualOutput); + }); // Default behavior, no enhanced diff + }); + + // Verify the original output format + string message = exception.Message; + StringAssert.Contains(message, "Expected:"); + StringAssert.Contains(message, "Actual :"); + StringAssert.Contains(message, "-----------------------------------"); + + // Should NOT contain enhanced diff elements + Assert.IsFalse(message.Contains("Line 1:")); + Assert.IsFalse(message.Contains("✅")); + Assert.IsFalse(message.Contains("❌")); + } + + [TestMethod] + public void WildcardDiffAnalyzer_AnalyzeDiff_SimpleMatch() + { + // Arrange + string pattern = "Hello * world"; + string actual = "Hello beautiful world"; + + // Act + var result = WildcardDiffAnalyzer.AnalyzeDiff(pattern, actual); + + // Assert + Assert.IsTrue(result.OverallMatch); + Assert.AreEqual(1, result.LineResults.Count); + Assert.IsTrue(result.LineResults[0].IsMatch); + Assert.AreEqual(1, result.LineResults[0].WildcardMatches.Count); + Assert.AreEqual("beautiful", result.LineResults[0].WildcardMatches[0]); + } + + [TestMethod] + public void WildcardDiffAnalyzer_AnalyzeDiff_ExtraLines() + { + // Arrange + string pattern = "Line 1\nLine 2"; + string actual = "Line 1\nLine 2\nExtra Line 3"; + + // Act + var result = WildcardDiffAnalyzer.AnalyzeDiff(pattern, actual); + + // Assert + Assert.IsFalse(result.OverallMatch); + Assert.AreEqual(3, result.LineResults.Count); + Assert.IsTrue(result.LineResults[0].IsMatch); + Assert.IsTrue(result.LineResults[1].IsMatch); + Assert.IsFalse(result.LineResults[2].IsMatch); + Assert.AreEqual("unexpected extra line", result.LineResults[2].MismatchReason); + } + + [TestMethod] + public void WildcardDiffAnalyzer_AnalyzeDiff_MissingLines() + { + // Arrange + string pattern = "Line 1\nLine 2\nLine 3"; + string actual = "Line 1\nLine 2"; + + // Act + var result = WildcardDiffAnalyzer.AnalyzeDiff(pattern, actual); + + // Assert + Assert.IsFalse(result.OverallMatch); + Assert.AreEqual(3, result.LineResults.Count); + Assert.IsTrue(result.LineResults[0].IsMatch); + Assert.IsTrue(result.LineResults[1].IsMatch); + Assert.IsFalse(result.LineResults[2].IsMatch); + Assert.AreEqual("missing line", result.LineResults[2].MismatchReason); + } +} \ No newline at end of file diff --git a/IntelliTect.TestTools.Console.Tests/IntelliTect.TestTools.Console.Tests.csproj b/IntelliTect.TestTools.Console.Tests/IntelliTect.TestTools.Console.Tests.csproj index bcbcb71..d98f722 100644 --- a/IntelliTect.TestTools.Console.Tests/IntelliTect.TestTools.Console.Tests.csproj +++ b/IntelliTect.TestTools.Console.Tests/IntelliTect.TestTools.Console.Tests.csproj @@ -2,7 +2,7 @@ Exe - netcoreapp3.1 + net8.0 IntelliTect.TestTools.Console.Tests diff --git a/IntelliTect.TestTools.Console/ConsoleAssert.cs b/IntelliTect.TestTools.Console/ConsoleAssert.cs index 7d23f31..373aed7 100644 --- a/IntelliTect.TestTools.Console/ConsoleAssert.cs +++ b/IntelliTect.TestTools.Console/ConsoleAssert.cs @@ -248,6 +248,32 @@ private static string Expect( normalizeOptions, equivalentOperatorErrorMessage); } + /// + /// Performs a unit test on a console-based method. A "view" of + /// what a user would see in their console is provided as a string, + /// where their input (including line-breaks) is surrounded by double + /// less-than/greater-than signs, like so: "Input please: <<Input>>" + /// + /// Expected "view" to be seen on the console, + /// including both input and output + /// Method to be run + /// + /// Options to normalize input and expected output + /// A textual description of the message if the result of does not match the value + /// Options for controlling diff output behavior + private static string Expect( + string expected, Action action, Func comparisonOperator, + NormalizeOptions normalizeOptions, + string equivalentOperatorErrorMessage, + DiffOptions diffOptions) + { + (string input, string output) = Parse(expected); + + return Execute(input, output, action, + (left, right) => comparisonOperator(left, right), + normalizeOptions, equivalentOperatorErrorMessage, diffOptions); + } + /// /// Performs a unit test on a console-based method. A "view" of /// what a user would see in their console is provided as a string, @@ -336,6 +362,32 @@ public static string ExpectLike(string expected, "The values are not like (using wildcards) each other"); } + /// + /// Performs a unit test on a console-based method with enhanced wildcard diff output. A "view" of + /// what a user would see in their console is provided as a string, + /// where their input (including line-breaks) is surrounded by double + /// less-than/greater-than signs, like so: "Input please: <<Input>>" + /// + /// Expected "view" to be seen on the console, + /// including both input and output + /// Method to be run + /// Options for controlling diff output behavior + /// Options to normalize input and expected output + /// The escape character for the wildcard caracters. Default is '\'. + public static string ExpectLike(string expected, + Action action, + DiffOptions diffOptions, + NormalizeOptions normalizeLineEndings = NormalizeOptions.Default, + char escapeCharacter = '\\') + { + return Expect(expected, + action, + (pattern, output) => output.IsLike(pattern, escapeCharacter), + normalizeLineEndings, + "The values are not like (using wildcards) each other", + diffOptions); + } + /// /// Performs a unit test on a console-based method. A "view" of /// what a user would see in their console is provided as a string, @@ -410,6 +462,29 @@ private static string Execute(string givenInput, return CompareOutput(output, expectedOutput, normalizeOptions, areEquivalentOperator, equivalentOperatorErrorMessage); } + /// + /// Executes the unit test while providing console input. + /// + /// Input which will be given + /// The expected output + /// Action to be tested + /// delegate for comparing the expected from actual output. + /// Options to normalize input and expected output + /// A textual description of the message if the returns false + /// Options for controlling diff output behavior + private static string Execute(string givenInput, + string expectedOutput, + Action action, + Func areEquivalentOperator, + NormalizeOptions normalizeOptions, + string equivalentOperatorErrorMessage, + DiffOptions diffOptions) + { + string output = Execute(givenInput, action); + + return CompareOutput(output, expectedOutput, normalizeOptions, areEquivalentOperator, equivalentOperatorErrorMessage, diffOptions); + } + /// /// Executes the unit test while providing console input. /// @@ -438,6 +513,17 @@ private static string CompareOutput( NormalizeOptions normalizeOptions, Func areEquivalentOperator, string equivalentOperatorErrorMessage) + { + return CompareOutput(output, expectedOutput, normalizeOptions, areEquivalentOperator, equivalentOperatorErrorMessage, DiffOptions.Default); + } + + private static string CompareOutput( + string output, + string expectedOutput, + NormalizeOptions normalizeOptions, + Func areEquivalentOperator, + string equivalentOperatorErrorMessage, + DiffOptions diffOptions) { if ((normalizeOptions & NormalizeOptions.NormalizeLineEndings) != 0) { @@ -451,7 +537,7 @@ private static string CompareOutput( expectedOutput = StripAnsiEscapeCodes(expectedOutput); } - AssertExpectation(expectedOutput, output, areEquivalentOperator, equivalentOperatorErrorMessage); + AssertExpectation(expectedOutput, output, areEquivalentOperator, equivalentOperatorErrorMessage, diffOptions); return output; } @@ -464,11 +550,25 @@ private static string CompareOutput( /// A textual description of the message if the returns false private static void AssertExpectation(string expectedOutput, string output, Func areEquivalentOperator, string equivalentOperatorErrorMessage = null) + { + AssertExpectation(expectedOutput, output, areEquivalentOperator, equivalentOperatorErrorMessage, DiffOptions.Default); + } + + /// + /// Asserts whether the values are equivalent according to the "/> + /// + /// The expected value of the output. + /// The actual value output. + /// The operator used to compare equivalency. + /// A textual description of the message if the returns false + /// Options for controlling diff output behavior + private static void AssertExpectation(string expectedOutput, string output, Func areEquivalentOperator, + string equivalentOperatorErrorMessage, DiffOptions diffOptions) { bool failTest = !areEquivalentOperator(expectedOutput, output); if (failTest) { - throw new Exception(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage)); + throw new Exception(GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage, diffOptions)); } } @@ -557,6 +657,23 @@ public static async Task ExecuteAsync(string givenInput, Func acti private static string GetMessageText(string expectedOutput, string output, string equivalentOperatorErrorMessage = null) { + return GetMessageText(expectedOutput, output, equivalentOperatorErrorMessage, DiffOptions.Default); + } + + private static string GetMessageText(string expectedOutput, string output, string equivalentOperatorErrorMessage, DiffOptions diffOptions) + { + // Check if enhanced wildcard diff is enabled and the error message suggests wildcard usage + bool isWildcardError = equivalentOperatorErrorMessage != null && + equivalentOperatorErrorMessage.ToLowerInvariant().Contains("wildcard"); + + if ((diffOptions & DiffOptions.EnhancedWildcardDiff) != 0 && isWildcardError) + { + // Use enhanced wildcard diff output + var diffResult = WildcardDiffAnalyzer.AnalyzeDiff(expectedOutput, output); + return WildcardDiffFormatter.FormatEnhancedDiff(diffResult, equivalentOperatorErrorMessage); + } + + // Fall back to original implementation string result = ""; result += string.Join(Environment.NewLine, $"{equivalentOperatorErrorMessage}:- ", @@ -704,6 +821,27 @@ public static Process ExecuteProcess(string expected, string fileName, string ar /// Working directory to start the process in. public static Process ExecuteProcess(string expected, string fileName, string args, out string standardOutput, out string standardError, string workingDirectory = null) + { + return ExecuteProcess(expected, fileName, args, out standardOutput, out standardError, workingDirectory, DiffOptions.Default); + } + + /// + /// Performs a unit test on a console-based executable with enhanced diff options. A "view" of + /// what a user would see in their console is provided as a string, + /// where their input (including line-breaks) is surrounded by double + /// less-than/greater-than signs, like so: "Input please: <<Input>>" + /// + /// Expected "view" to be seen on the console, + /// including both input and output + /// The filename or executable to call to generate output + /// command line arguments + /// The output from the process to the standard output stream + /// The output from the process to the standard error stream + /// The working directory to start the process in + /// Options for controlling diff output behavior + /// The process that was executed + public static Process ExecuteProcess(string expected, string fileName, string args, + out string standardOutput, out string standardError, string workingDirectory, DiffOptions diffOptions) { ProcessStartInfo processStartInfo = new ProcessStartInfo(fileName, args) { @@ -718,7 +856,7 @@ public static Process ExecuteProcess(string expected, string fileName, string ar process.WaitForExit(); standardOutput = process.StandardOutput.ReadToEnd(); standardError = process.StandardError.ReadToEnd(); - AssertExpectation(expected, standardOutput, (left, right) => LikeOperator(left, right), "The values are not like (using wildcards) each other"); + AssertExpectation(expected, standardOutput, (left, right) => LikeOperator(left, right), "The values are not like (using wildcards) each other", diffOptions); return process; } } \ No newline at end of file diff --git a/IntelliTect.TestTools.Console/DiffOptions.cs b/IntelliTect.TestTools.Console/DiffOptions.cs new file mode 100644 index 0000000..e3b1926 --- /dev/null +++ b/IntelliTect.TestTools.Console/DiffOptions.cs @@ -0,0 +1,19 @@ +namespace IntelliTect.TestTools.Console; + +/// +/// Provides options for controlling diff output behavior when wildcard matching fails. +/// +[Flags] +public enum DiffOptions +{ + /// + /// Use the default diff output format. + /// + Default = 0, + + /// + /// Enable enhanced line-by-line diff output with wildcard match tracking for wildcard patterns. + /// This provides detailed information about what each wildcard matched and where mismatches occurred. + /// + EnhancedWildcardDiff = 1, +} \ No newline at end of file diff --git a/IntelliTect.TestTools.Console/WildcardDiffAnalyzer.cs b/IntelliTect.TestTools.Console/WildcardDiffAnalyzer.cs new file mode 100644 index 0000000..4590577 --- /dev/null +++ b/IntelliTect.TestTools.Console/WildcardDiffAnalyzer.cs @@ -0,0 +1,256 @@ +using System; +using System.Collections.Generic; +using System.Linq; +using System.Text; +using System.Text.RegularExpressions; + +namespace IntelliTect.TestTools.Console; + +/// +/// Provides detailed analysis of wildcard pattern matching for enhanced diff output. +/// +internal class WildcardDiffAnalyzer +{ + /// + /// Represents the result of matching a single line with a wildcard pattern. + /// + public class LineMatchResult + { + public bool IsMatch { get; set; } + public string ExpectedLine { get; set; } = string.Empty; + public string ActualLine { get; set; } = string.Empty; + public List WildcardMatches { get; set; } = new List(); + public string MismatchReason { get; set; } + } + + /// + /// Represents the overall result of comparing expected and actual output with wildcards. + /// + public class DiffResult + { + public List LineResults { get; set; } = new List(); + public List ExtraActualLines { get; set; } = new List(); + public List MissingExpectedLines { get; set; } = new List(); + public bool OverallMatch { get; set; } + } + + /// + /// Analyzes the difference between expected pattern and actual output using wildcard matching. + /// + /// The expected pattern with wildcards + /// The actual output to match against + /// The escape character for wildcards (default is backslash) + /// Detailed analysis of the matching process + public static DiffResult AnalyzeDiff(string expectedPattern, string actualOutput, char escapeCharacter = '\\') + { + var result = new DiffResult(); + + var expectedLines = SplitIntoLines(expectedPattern); + var actualLines = SplitIntoLines(actualOutput); + + int expectedIndex = 0; + int actualIndex = 0; + + while (expectedIndex < expectedLines.Count || actualIndex < actualLines.Count) + { + if (expectedIndex >= expectedLines.Count) + { + // Extra actual lines + result.ExtraActualLines.Add(actualLines[actualIndex]); + result.LineResults.Add(new LineMatchResult + { + IsMatch = false, + ExpectedLine = "", + ActualLine = actualLines[actualIndex], + MismatchReason = "unexpected extra line" + }); + actualIndex++; + } + else if (actualIndex >= actualLines.Count) + { + // Missing expected lines + result.MissingExpectedLines.Add(expectedLines[expectedIndex]); + result.LineResults.Add(new LineMatchResult + { + IsMatch = false, + ExpectedLine = expectedLines[expectedIndex], + ActualLine = "", + MismatchReason = "missing line" + }); + expectedIndex++; + } + else + { + // Try to match current lines + var lineResult = MatchLineWithWildcards(expectedLines[expectedIndex], actualLines[actualIndex], escapeCharacter); + result.LineResults.Add(lineResult); + + expectedIndex++; + actualIndex++; + } + } + + result.OverallMatch = result.LineResults.All(lr => lr.IsMatch); + return result; + } + + /// + /// Matches a single line against a wildcard pattern and tracks what each wildcard matched. + /// + private static LineMatchResult MatchLineWithWildcards(string expectedPattern, string actualLine, char escapeCharacter) + { + var result = new LineMatchResult + { + ExpectedLine = expectedPattern, + ActualLine = actualLine + }; + + // Use the existing IsLike method to check if it matches + bool isMatch = actualLine.IsLike(expectedPattern, escapeCharacter); + result.IsMatch = isMatch; + + if (isMatch) + { + // Try to extract what each wildcard matched, but if it fails, just indicate matches exist + try + { + result.WildcardMatches = ExtractWildcardMatches(expectedPattern, actualLine, escapeCharacter); + } + catch (Exception) + { + // If extraction fails, just count the wildcards and provide placeholder text + int wildcardCount = CountWildcards(expectedPattern, escapeCharacter); + result.WildcardMatches = new List(); + for (int i = 0; i < wildcardCount; i++) + { + result.WildcardMatches.Add(""); + } + } + } + else + { + result.MismatchReason = "pattern does not match"; + } + + return result; + } + + /// + /// Counts the number of wildcards in a pattern. + /// + private static int CountWildcards(string pattern, char escapeCharacter) + { + int count = 0; + for (int i = 0; i < pattern.Length; i++) + { + if (pattern[i] == '*' && (i == 0 || pattern[i - 1] != escapeCharacter)) + { + count++; + } + } + return count; + } + + /// + /// Extracts what each wildcard (*) in the pattern matched in the actual string. + /// + private static List ExtractWildcardMatches(string pattern, string actual, char escapeCharacter) + { + var matches = new List(); + + // Convert wildcard pattern to regex to capture groups + try + { + var regexPattern = ConvertWildcardToRegex(pattern, escapeCharacter); + var regex = new Regex(regexPattern, RegexOptions.Singleline); + var match = regex.Match(actual); + + if (match.Success) + { + // Skip group 0 (the entire match) and collect captured groups + for (int i = 1; i < match.Groups.Count; i++) + { + matches.Add(match.Groups[i].Value); + } + } + else + { + // Fallback: count wildcards and provide placeholders + int wildcardCount = CountWildcards(pattern, escapeCharacter); + for (int i = 0; i < wildcardCount; i++) + { + matches.Add(""); + } + } + } + catch (Exception) + { + // If regex conversion fails, fall back to basic tracking + int wildcardCount = CountWildcards(pattern, escapeCharacter); + for (int i = 0; i < wildcardCount; i++) + { + matches.Add(""); + } + } + + return matches; + } + + /// + /// Converts a wildcard pattern to a regex pattern with capture groups for each wildcard. + /// + private static string ConvertWildcardToRegex(string wildcardPattern, char escapeCharacter) + { + var result = new StringBuilder(); + result.Append("^"); + + for (int i = 0; i < wildcardPattern.Length; i++) + { + char c = wildcardPattern[i]; + + if (c == '*' && (i == 0 || wildcardPattern[i - 1] != escapeCharacter)) + { + // Unescaped wildcard - convert to capturing group + result.Append("(.*?)"); + } + else if (c == '?' && (i == 0 || wildcardPattern[i - 1] != escapeCharacter)) + { + // Unescaped single character wildcard + result.Append("."); + } + else if (c == escapeCharacter && i + 1 < wildcardPattern.Length) + { + // Escaped character - add the next character literally + i++; // Skip the escape character + result.Append(Regex.Escape(wildcardPattern[i].ToString())); + } + else + { + // Regular character - escape for regex + result.Append(Regex.Escape(c.ToString())); + } + } + + result.Append("$"); + return result.ToString(); + } + + /// + /// Splits text into lines, preserving empty lines. + /// + private static List SplitIntoLines(string text) + { + if (string.IsNullOrEmpty(text)) + return new List(); + + var lines = text.Split(new[] { '\r', '\n' }, StringSplitOptions.None).ToList(); + + // Remove trailing empty line if it's the last one and was created by a trailing newline + if (lines.Count > 0 && string.IsNullOrEmpty(lines[lines.Count - 1])) + { + lines.RemoveAt(lines.Count - 1); + } + + return lines; + } +} \ No newline at end of file diff --git a/IntelliTect.TestTools.Console/WildcardDiffFormatter.cs b/IntelliTect.TestTools.Console/WildcardDiffFormatter.cs new file mode 100644 index 0000000..7c47189 --- /dev/null +++ b/IntelliTect.TestTools.Console/WildcardDiffFormatter.cs @@ -0,0 +1,113 @@ +using System; +using System.Linq; +using System.Text; + +namespace IntelliTect.TestTools.Console; + +/// +/// Formats enhanced diff output for wildcard pattern matching failures. +/// +internal static class WildcardDiffFormatter +{ + /// + /// Formats a detailed diff output showing line-by-line comparison with wildcard matches. + /// + /// The result from WildcardDiffAnalyzer + /// The error message prefix + /// Formatted diff output string + public static string FormatEnhancedDiff(WildcardDiffAnalyzer.DiffResult diffResult, string equivalentOperatorErrorMessage) + { + var result = new StringBuilder(); + + result.AppendLine($"{equivalentOperatorErrorMessage}: Output does not match expected pattern using wildcards."); + result.AppendLine(); + + int lineNumber = 1; + + foreach (var lineResult in diffResult.LineResults) + { + result.AppendLine("────────────────────────────────────────────────────────────"); + result.AppendLine($"Line {lineNumber}:"); + + if (string.IsNullOrEmpty(lineResult.ExpectedLine) && !string.IsNullOrEmpty(lineResult.ActualLine)) + { + // Extra line in actual + result.AppendLine($"Expected: "); + result.AppendLine($"Actual : {lineResult.ActualLine}"); + result.AppendLine($"Match : ❌ (unexpected extra line)"); + } + else if (!string.IsNullOrEmpty(lineResult.ExpectedLine) && string.IsNullOrEmpty(lineResult.ActualLine)) + { + // Missing line in actual + result.AppendLine($"Expected: {lineResult.ExpectedLine}"); + result.AppendLine($"Actual : "); + result.AppendLine($"Match : ❌ (missing line)"); + } + else + { + // Regular line comparison + result.AppendLine($"Expected: {lineResult.ExpectedLine}"); + result.AppendLine($"Actual : {lineResult.ActualLine}"); + + if (lineResult.IsMatch) + { + result.AppendLine("Match : ✅"); + + // Show what wildcards matched if there are any + if (lineResult.WildcardMatches.Count > 0) + { + result.AppendLine(); + result.AppendLine("Wildcard matches:"); + for (int i = 0; i < lineResult.WildcardMatches.Count; i++) + { + result.AppendLine($" * => \"{lineResult.WildcardMatches[i]}\""); + } + } + } + else + { + result.AppendLine("Match : ❌"); + + if (!string.IsNullOrEmpty(lineResult.MismatchReason)) + { + result.AppendLine($"Reason : {lineResult.MismatchReason}"); + } + } + } + + result.AppendLine(); + lineNumber++; + } + + // Summary + result.AppendLine("────────────────────────────────────────────────────────────"); + result.AppendLine("Summary:"); + + int matchedLines = diffResult.LineResults.Count(lr => lr.IsMatch); + int totalLines = diffResult.LineResults.Count; + int extraLines = diffResult.ExtraActualLines.Count; + int missingLines = diffResult.MissingExpectedLines.Count; + + if (matchedLines > 0) + { + result.AppendLine($"✅ {matchedLines} lines matched"); + } + + if (extraLines > 0) + { + result.AppendLine($"❌ {extraLines} unexpected lines in actual output"); + } + + if (missingLines > 0) + { + result.AppendLine($"❌ {missingLines} missing lines in expected output"); + } + + if (matchedLines == 0 && extraLines == 0 && missingLines == 0) + { + result.AppendLine("❌ No lines matched"); + } + + return result.ToString(); + } +} \ No newline at end of file