fix: improved diff message on mismatch

This commit is contained in:
2026-03-24 18:09:08 +00:00
parent 12cb12991a
commit ecd11767cd
3 changed files with 219 additions and 15 deletions
+96
View File
@@ -0,0 +1,96 @@
package mire
import (
"fmt"
"io"
"strings"
"mire/internal/output"
)
const missingMismatchLine = "<no line>"
type mismatchLine struct {
text string
present bool
}
type mismatchDetails struct {
lineNumber int
expected mismatchLine
actual mismatchLine
}
func writeScenarioMismatch(w io.Writer, mismatch *testMismatchError) {
details := firstMismatchingLine(mismatch.expected, mismatch.actual)
fmt.Fprintf(w, "%s\n%s\n", italicLabel("Expected", output.Pass), formatMismatchLine(details.expected))
fmt.Fprintf(w, "%s\n%s\n", italicLabel("Actual", output.Fail), formatMismatchLine(details.actual))
}
func firstMismatchingLine(expected, actual []byte) mismatchDetails {
expectedLines := splitMismatchLines(expected)
actualLines := splitMismatchLines(actual)
maxLines := len(expectedLines)
if len(actualLines) > maxLines {
maxLines = len(actualLines)
}
for i := 0; i < maxLines; i++ {
expectedLine := mismatchLineAt(expectedLines, i)
actualLine := mismatchLineAt(actualLines, i)
if expectedLine != actualLine {
return mismatchDetails{
lineNumber: i + 1,
expected: expectedLine,
actual: actualLine,
}
}
}
return mismatchDetails{
expected: mismatchLineAt(expectedLines, len(expectedLines)-1),
actual: mismatchLineAt(actualLines, len(actualLines)-1),
}
}
func splitMismatchLines(data []byte) []string {
if len(data) == 0 {
return nil
}
lines := strings.Split(string(data), "\n")
if data[len(data)-1] == '\n' {
lines = lines[:len(lines)-1]
}
for i := range lines {
lines[i] = strings.TrimSuffix(lines[i], "\r")
}
return lines
}
func mismatchLineAt(lines []string, index int) mismatchLine {
if index < 0 || index >= len(lines) {
return mismatchLine{}
}
return mismatchLine{
text: lines[index],
present: true,
}
}
func formatMismatchLine(line mismatchLine) string {
if !line.present {
return missingMismatchLine
}
return line.text
}
func italicLabel(text string, color output.Color) string {
return output.NewStyle().Italic().Apply(output.Label(text, color))
}
+112
View File
@@ -0,0 +1,112 @@
package mire
import (
"bytes"
"strings"
"testing"
)
func TestFirstMismatchingLineReturnsFirstChangedLine(t *testing.T) {
got := firstMismatchingLine(
[]byte("$ echo x\nx\n$ \nexit\n"),
[]byte("$ echo a\na\n$ \nexit\n"),
)
if got.lineNumber != 1 {
t.Fatalf("lineNumber = %d, want 1", got.lineNumber)
}
if got.expected != (mismatchLine{text: "$ echo x", present: true}) {
t.Fatalf("expected line = %#v, want first changed expected line", got.expected)
}
if got.actual != (mismatchLine{text: "$ echo a", present: true}) {
t.Fatalf("actual line = %#v, want first changed actual line", got.actual)
}
}
func TestFirstMismatchingLineMarksMissingLine(t *testing.T) {
got := firstMismatchingLine(
[]byte("alpha\nbeta\n"),
[]byte("alpha\n"),
)
if got.lineNumber != 2 {
t.Fatalf("lineNumber = %d, want 2", got.lineNumber)
}
if got.expected != (mismatchLine{text: "beta", present: true}) {
t.Fatalf("expected line = %#v, want missing expected line content", got.expected)
}
if got.actual != (mismatchLine{}) {
t.Fatalf("actual line = %#v, want missing line", got.actual)
}
}
func TestWriteScenarioMismatchPrintsOnlyFirstMismatch(t *testing.T) {
t.Setenv("NO_COLOR", "1")
var buf bytes.Buffer
writeScenarioMismatch(&buf, &testMismatchError{
expected: []byte("$ echo x\nx\n$ \nexit\n"),
actual: []byte("$ echo a\na\n$ \nexit\n"),
})
got := buf.String()
for _, want := range []string{
"Expected\n",
"$ echo x\n",
"Actual\n",
"$ echo a\n",
} {
if !strings.Contains(got, want) {
t.Fatalf("writeScenarioMismatch() output = %q, want substring %q", got, want)
}
}
if strings.Contains(got, "mire Expected:\n") || strings.Contains(got, "mire Actual:\n") {
t.Fatalf("writeScenarioMismatch() output = %q, want mismatch lines without mire prefix", got)
}
for _, unwanted := range []string{"\nx\n", "\na\n", "\nexit\n"} {
if strings.Contains(got, unwanted) {
t.Fatalf("writeScenarioMismatch() output = %q, want only first mismatching line", got)
}
}
}
func TestWriteScenarioMismatchFormatsMissingLine(t *testing.T) {
t.Setenv("NO_COLOR", "1")
var buf bytes.Buffer
writeScenarioMismatch(&buf, &testMismatchError{
expected: []byte("alpha\nbeta\n"),
actual: []byte("alpha\n"),
})
got := buf.String()
if !strings.Contains(got, "beta\n") {
t.Fatalf("writeScenarioMismatch() output = %q, want expected mismatching line", got)
}
if !strings.Contains(got, missingMismatchLine+"\n") {
t.Fatalf("writeScenarioMismatch() output = %q, want missing-line placeholder", got)
}
}
func TestWriteScenarioMismatchItalicizesLabels(t *testing.T) {
t.Setenv("NO_COLOR", "")
var buf bytes.Buffer
writeScenarioMismatch(&buf, &testMismatchError{
expected: []byte("alpha\n"),
actual: []byte("beta\n"),
})
got := buf.String()
if !strings.Contains(got, "\x1b[3m") {
t.Fatalf("writeScenarioMismatch() output = %q, want italic styling", got)
}
}
func TestTestMismatchErrorIncludesLineNumber(t *testing.T) {
err := &testMismatchError{lineNumber: 7}
if got := err.Error(); got != "output differed at line 7" {
t.Fatalf("Error() = %q, want %q", got, "output differed at line 7")
}
}
+7 -11
View File
@@ -44,6 +44,7 @@ type testFixtureFiles struct {
type testMismatchError struct {
expected []byte
actual []byte
lineNumber int
}
const (
@@ -52,6 +53,10 @@ const (
)
func (e *testMismatchError) Error() string {
if e.lineNumber > 0 {
return fmt.Sprintf("output differed at line %d", e.lineNumber)
}
return "output differed"
}
@@ -302,26 +307,17 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
}
if !bytes.Equal(got, want) {
details := firstMismatchingLine(want, got)
return &testMismatchError{
expected: want,
actual: got,
lineNumber: details.lineNumber,
}
}
return nil
}
func writeScenarioMismatch(w io.Writer, mismatch *testMismatchError) {
output.Fprintf(w, "Expected:\n%s", string(mismatch.expected))
if len(mismatch.expected) == 0 || mismatch.expected[len(mismatch.expected)-1] != '\n' {
output.Fprintf(w, "\n")
}
output.Fprintf(w, "Actual:\n%s", string(mismatch.actual))
if len(mismatch.actual) == 0 || mismatch.actual[len(mismatch.actual)-1] != '\n' {
output.Fprintf(w, "\n")
}
}
func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
idx := bytes.Index(data, []byte(compareOutputMarker))
if idx == -1 {