mirror of https://github.com/tinygo-org/tinygo.git
Browse Source
Uses a vendored "internal/diff" from Big Go to show the differences between the expected and actual outputs when tests fail. Signed-off-by: L. Pereira <l.pereira@fastly.com>pull/3887/merge
L. Pereira
4 months ago
committed by
GitHub
2 changed files with 262 additions and 0 deletions
@ -0,0 +1,261 @@ |
|||
// Copyright 2022 The Go Authors. All rights reserved.
|
|||
// Use of this source code is governed by a BSD-style
|
|||
// license that can be found in the LICENSE file.
|
|||
|
|||
package main |
|||
|
|||
import ( |
|||
"bytes" |
|||
"fmt" |
|||
"sort" |
|||
"strings" |
|||
) |
|||
|
|||
// A pair is a pair of values tracked for both the x and y side of a diff.
|
|||
// It is typically a pair of line indexes.
|
|||
type pair struct{ x, y int } |
|||
|
|||
// Diff returns an anchored diff of the two texts old and new
|
|||
// in the “unified diff” format. If old and new are identical,
|
|||
// Diff returns a nil slice (no output).
|
|||
//
|
|||
// Unix diff implementations typically look for a diff with
|
|||
// the smallest number of lines inserted and removed,
|
|||
// which can in the worst case take time quadratic in the
|
|||
// number of lines in the texts. As a result, many implementations
|
|||
// either can be made to run for a long time or cut off the search
|
|||
// after a predetermined amount of work.
|
|||
//
|
|||
// In contrast, this implementation looks for a diff with the
|
|||
// smallest number of “unique” lines inserted and removed,
|
|||
// where unique means a line that appears just once in both old and new.
|
|||
// We call this an “anchored diff” because the unique lines anchor
|
|||
// the chosen matching regions. An anchored diff is usually clearer
|
|||
// than a standard diff, because the algorithm does not try to
|
|||
// reuse unrelated blank lines or closing braces.
|
|||
// The algorithm also guarantees to run in O(n log n) time
|
|||
// instead of the standard O(n²) time.
|
|||
//
|
|||
// Some systems call this approach a “patience diff,” named for
|
|||
// the “patience sorting” algorithm, itself named for a solitaire card game.
|
|||
// We avoid that name for two reasons. First, the name has been used
|
|||
// for a few different variants of the algorithm, so it is imprecise.
|
|||
// Second, the name is frequently interpreted as meaning that you have
|
|||
// to wait longer (to be patient) for the diff, meaning that it is a slower algorithm,
|
|||
// when in fact the algorithm is faster than the standard one.
|
|||
func Diff(oldName string, old []byte, newName string, new []byte) []byte { |
|||
if bytes.Equal(old, new) { |
|||
return nil |
|||
} |
|||
x := lines(old) |
|||
y := lines(new) |
|||
|
|||
// Print diff header.
|
|||
var out bytes.Buffer |
|||
fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) |
|||
fmt.Fprintf(&out, "--- %s\n", oldName) |
|||
fmt.Fprintf(&out, "+++ %s\n", newName) |
|||
|
|||
// Loop over matches to consider,
|
|||
// expanding each match to include surrounding lines,
|
|||
// and then printing diff chunks.
|
|||
// To avoid setup/teardown cases outside the loop,
|
|||
// tgs returns a leading {0,0} and trailing {len(x), len(y)} pair
|
|||
// in the sequence of matches.
|
|||
var ( |
|||
done pair // printed up to x[:done.x] and y[:done.y]
|
|||
chunk pair // start lines of current chunk
|
|||
count pair // number of lines from each side in current chunk
|
|||
ctext []string // lines for current chunk
|
|||
) |
|||
for _, m := range tgs(x, y) { |
|||
if m.x < done.x { |
|||
// Already handled scanning forward from earlier match.
|
|||
continue |
|||
} |
|||
|
|||
// Expand matching lines as far as possible,
|
|||
// establishing that x[start.x:end.x] == y[start.y:end.y].
|
|||
// Note that on the first (or last) iteration we may (or definitely do)
|
|||
// have an empty match: start.x==end.x and start.y==end.y.
|
|||
start := m |
|||
for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { |
|||
start.x-- |
|||
start.y-- |
|||
} |
|||
end := m |
|||
for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { |
|||
end.x++ |
|||
end.y++ |
|||
} |
|||
|
|||
// Emit the mismatched lines before start into this chunk.
|
|||
// (No effect on first sentinel iteration, when start = {0,0}.)
|
|||
for _, s := range x[done.x:start.x] { |
|||
ctext = append(ctext, "-"+s) |
|||
count.x++ |
|||
} |
|||
for _, s := range y[done.y:start.y] { |
|||
ctext = append(ctext, "+"+s) |
|||
count.y++ |
|||
} |
|||
|
|||
// If we're not at EOF and have too few common lines,
|
|||
// the chunk includes all the common lines and continues.
|
|||
const C = 3 // number of context lines
|
|||
if (end.x < len(x) || end.y < len(y)) && |
|||
(end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { |
|||
for _, s := range x[start.x:end.x] { |
|||
ctext = append(ctext, " "+s) |
|||
count.x++ |
|||
count.y++ |
|||
} |
|||
done = end |
|||
continue |
|||
} |
|||
|
|||
// End chunk with common lines for context.
|
|||
if len(ctext) > 0 { |
|||
n := end.x - start.x |
|||
if n > C { |
|||
n = C |
|||
} |
|||
for _, s := range x[start.x : start.x+n] { |
|||
ctext = append(ctext, " "+s) |
|||
count.x++ |
|||
count.y++ |
|||
} |
|||
done = pair{start.x + n, start.y + n} |
|||
|
|||
// Format and emit chunk.
|
|||
// Convert line numbers to 1-indexed.
|
|||
// Special case: empty file shows up as 0,0 not 1,0.
|
|||
if count.x > 0 { |
|||
chunk.x++ |
|||
} |
|||
if count.y > 0 { |
|||
chunk.y++ |
|||
} |
|||
fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) |
|||
for _, s := range ctext { |
|||
out.WriteString(s) |
|||
} |
|||
count.x = 0 |
|||
count.y = 0 |
|||
ctext = ctext[:0] |
|||
} |
|||
|
|||
// If we reached EOF, we're done.
|
|||
if end.x >= len(x) && end.y >= len(y) { |
|||
break |
|||
} |
|||
|
|||
// Otherwise start a new chunk.
|
|||
chunk = pair{end.x - C, end.y - C} |
|||
for _, s := range x[chunk.x:end.x] { |
|||
ctext = append(ctext, " "+s) |
|||
count.x++ |
|||
count.y++ |
|||
} |
|||
done = end |
|||
} |
|||
|
|||
return out.Bytes() |
|||
} |
|||
|
|||
// lines returns the lines in the file x, including newlines.
|
|||
// If the file does not end in a newline, one is supplied
|
|||
// along with a warning about the missing newline.
|
|||
func lines(x []byte) []string { |
|||
l := strings.SplitAfter(string(x), "\n") |
|||
if l[len(l)-1] == "" { |
|||
l = l[:len(l)-1] |
|||
} else { |
|||
// Treat last line as having a message about the missing newline attached,
|
|||
// using the same text as BSD/GNU diff (including the leading backslash).
|
|||
l[len(l)-1] += "\n\\ No newline at end of file\n" |
|||
} |
|||
return l |
|||
} |
|||
|
|||
// tgs returns the pairs of indexes of the longest common subsequence
|
|||
// of unique lines in x and y, where a unique line is one that appears
|
|||
// once in x and once in y.
|
|||
//
|
|||
// The longest common subsequence algorithm is as described in
|
|||
// Thomas G. Szymanski, “A Special Case of the Maximal Common
|
|||
// Subsequence Problem,” Princeton TR #170 (January 1975),
|
|||
// available at https://research.swtch.com/tgs170.pdf.
|
|||
func tgs(x, y []string) []pair { |
|||
// Count the number of times each string appears in a and b.
|
|||
// We only care about 0, 1, many, counted as 0, -1, -2
|
|||
// for the x side and 0, -4, -8 for the y side.
|
|||
// Using negative numbers now lets us distinguish positive line numbers later.
|
|||
m := make(map[string]int) |
|||
for _, s := range x { |
|||
if c := m[s]; c > -2 { |
|||
m[s] = c - 1 |
|||
} |
|||
} |
|||
for _, s := range y { |
|||
if c := m[s]; c > -8 { |
|||
m[s] = c - 4 |
|||
} |
|||
} |
|||
|
|||
// Now unique strings can be identified by m[s] = -1+-4.
|
|||
//
|
|||
// Gather the indexes of those strings in x and y, building:
|
|||
// xi[i] = increasing indexes of unique strings in x.
|
|||
// yi[i] = increasing indexes of unique strings in y.
|
|||
// inv[i] = index j such that x[xi[i]] = y[yi[j]].
|
|||
var xi, yi, inv []int |
|||
for i, s := range y { |
|||
if m[s] == -1+-4 { |
|||
m[s] = len(yi) |
|||
yi = append(yi, i) |
|||
} |
|||
} |
|||
for i, s := range x { |
|||
if j, ok := m[s]; ok && j >= 0 { |
|||
xi = append(xi, i) |
|||
inv = append(inv, j) |
|||
} |
|||
} |
|||
|
|||
// Apply Algorithm A from Szymanski's paper.
|
|||
// In those terms, A = J = inv and B = [0, n).
|
|||
// We add sentinel pairs {0,0}, and {len(x),len(y)}
|
|||
// to the returned sequence, to help the processing loop.
|
|||
J := inv |
|||
n := len(xi) |
|||
T := make([]int, n) |
|||
L := make([]int, n) |
|||
for i := range T { |
|||
T[i] = n + 1 |
|||
} |
|||
for i := 0; i < n; i++ { |
|||
k := sort.Search(n, func(k int) bool { |
|||
return T[k] >= J[i] |
|||
}) |
|||
T[k] = J[i] |
|||
L[i] = k + 1 |
|||
} |
|||
k := 0 |
|||
for _, v := range L { |
|||
if k < v { |
|||
k = v |
|||
} |
|||
} |
|||
seq := make([]pair, 2+k) |
|||
seq[1+k] = pair{len(x), len(y)} // sentinel at end
|
|||
lastj := n |
|||
for i := n - 1; i >= 0; i-- { |
|||
if L[i] == k && J[i] < lastj { |
|||
seq[k] = pair{xi[i], yi[J[i]]} |
|||
k-- |
|||
} |
|||
} |
|||
seq[0] = pair{0, 0} // sentinel at start
|
|||
return seq |
|||
} |
Loading…
Reference in new issue