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")
|
||||||
|
}
|
||||||
|
}
|
||||||
+7
-11
@@ -44,6 +44,7 @@ type testFixtureFiles struct {
|
|||||||
type testMismatchError struct {
|
type testMismatchError struct {
|
||||||
expected []byte
|
expected []byte
|
||||||
actual []byte
|
actual []byte
|
||||||
|
lineNumber int
|
||||||
}
|
}
|
||||||
|
|
||||||
const (
|
const (
|
||||||
@@ -52,6 +53,10 @@ const (
|
|||||||
)
|
)
|
||||||
|
|
||||||
func (e *testMismatchError) Error() string {
|
func (e *testMismatchError) Error() string {
|
||||||
|
if e.lineNumber > 0 {
|
||||||
|
return fmt.Sprintf("output differed at line %d", e.lineNumber)
|
||||||
|
}
|
||||||
|
|
||||||
return "output differed"
|
return "output differed"
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -302,26 +307,17 @@ func replayScenario(scenario testScenario, shellPath string, _ testIO, sandboxCo
|
|||||||
}
|
}
|
||||||
|
|
||||||
if !bytes.Equal(got, want) {
|
if !bytes.Equal(got, want) {
|
||||||
|
details := firstMismatchingLine(want, got)
|
||||||
return &testMismatchError{
|
return &testMismatchError{
|
||||||
expected: want,
|
expected: want,
|
||||||
actual: got,
|
actual: got,
|
||||||
|
lineNumber: details.lineNumber,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
return nil
|
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) {
|
func trimReplayOutputToMarker(data []byte, shellPath string) ([]byte, error) {
|
||||||
idx := bytes.Index(data, []byte(compareOutputMarker))
|
idx := bytes.Index(data, []byte(compareOutputMarker))
|
||||||
if idx == -1 {
|
if idx == -1 {
|
||||||
|
|||||||
Reference in New Issue
Block a user