fix: improved diff message on mismatch
This commit is contained in:
@@ -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))
|
||||
}
|
||||
@@ -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")
|
||||
}
|
||||
}
|
||||
+11
-15
@@ -42,8 +42,9 @@ type testFixtureFiles struct {
|
||||
}
|
||||
|
||||
type testMismatchError struct {
|
||||
expected []byte
|
||||
actual []byte
|
||||
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,
|
||||
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 {
|
||||
|
||||
Reference in New Issue
Block a user