A lightweight test helper package
0

Configure Feed

Select the types of activity you want to include in your feed.

Vendor Go's internal diff to remove a dependency (#45)

* Vendor Go's internal diff to remove a dependency

* Include diff tests in coverage

* Use filepath join for windows

* Moar debug

* Try replacing line endings

* Yep, that worked, stupid windows

authored by

Tom Fleet and committed by
GitHub
(Nov 16, 2024, 4:11 PM UTC) f6e8ffb7 dfcced39

+663 -14
+1 -1
codecov.yml
··· 1 1 ignore: 2 - - internal 2 + - internal/colour
+4 -2
go.mod
··· 1 1 module github.com/FollowTheProcess/test 2 2 3 - go 1.22 3 + go 1.22.0 4 + 5 + toolchain go1.23.3 4 6 5 7 require github.com/google/go-cmp v0.6.0 6 8 7 - require github.com/aymanbagabas/go-udiff v0.2.0 9 + require golang.org/x/tools v0.27.0
+2 -2
go.sum
··· 1 - github.com/aymanbagabas/go-udiff v0.2.0 h1:TK0fH4MteXUDspT88n8CKzvK0X9O2xu9yQjWpi6yML8= 2 - github.com/aymanbagabas/go-udiff v0.2.0/go.mod h1:RE4Ex0qsGkTAJoQdQQCA0uG+nAzJO/pI/QwceO5fgrA= 3 1 github.com/google/go-cmp v0.6.0 h1:ofyhxvXcZhMsU5ulbFiLKl/XBFqE1GSq7atu8tAmTRI= 4 2 github.com/google/go-cmp v0.6.0/go.mod h1:17dUlkBOakJ0+DkrSSNjCkIjxS6bF9zb3elmeNGIjoY= 3 + golang.org/x/tools v0.27.0 h1:qEKojBykQkQ4EynWy4S8Weg69NumxKdn40Fce3uc/8o= 4 + golang.org/x/tools v0.27.0/go.mod h1:sUi0ZgbwW9ZPAq26Ekut+weQPR5eIM6GQLQ1Yjm1H0Q=
+3 -5
test.go
··· 18 18 "testing" 19 19 20 20 "github.com/FollowTheProcess/test/internal/colour" 21 - "github.com/aymanbagabas/go-udiff" 21 + "github.com/FollowTheProcess/test/internal/diff" 22 22 "github.com/google/go-cmp/cmp" 23 23 ) 24 24 ··· 334 334 335 335 contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n")) 336 336 337 - if diff := udiff.Unified(f, "got", string(contents), got); diff != "" { 338 - tb.Fatalf("\nMismatch\n--------\n%s\n", prettyDiff(diff)) 337 + if diff := diff.Diff(f, contents, "got", []byte(got)); diff != nil { 338 + tb.Fatalf("\nMismatch\n--------\n%s\n", prettyDiff(string(diff))) 339 339 } 340 340 } 341 341 ··· 471 471 472 472 // prettyDiff takes a string diff in unified diff format and colourises it for easier viewing. 473 473 func prettyDiff(diff string) string { 474 - // TODO(@FollowTheProcess): I don't like parsing the string directly but 475 - // it's the simplest way I think 476 474 lines := strings.Split(diff, "\n") 477 475 for i := 0; i < len(lines); i++ { 478 476 trimmed := strings.TrimSpace(lines[i])
-4
test_test.go
··· 578 578 test.Equal(t, stderr, "") 579 579 }) 580 580 } 581 - 582 - func TestSomething(t *testing.T) { 583 - test.Equal(t, "apples", "oranges") // Fruits are not equal 584 - }
+271
internal/diff/diff.go
··· 1 + // Taken from Go's internal/diff with only very minor tweaks; those being: 2 + // - Adding an extra space between the diff character (+/-) and the line so we can easily colour it 3 + // - Lint ignores 4 + // - Renaming diff test package to diff_test 5 + // 6 + // Copyright 2022 The Go Authors. All rights reserved. 7 + // Use of this source code is governed by a BSD-style 8 + // license that can be found in the LICENSE file. 9 + 10 + package diff 11 + 12 + import ( 13 + "bytes" 14 + "fmt" 15 + "sort" 16 + "strings" 17 + ) 18 + 19 + // A pair is a pair of values tracked for both the x and y side of a diff. 20 + // It is typically a pair of line indexes. 21 + type pair struct{ x, y int } 22 + 23 + // Diff returns an anchored diff of the two texts old and new 24 + // in the “unified diff” format. If old and new are identical, 25 + // Diff returns a nil slice (no output). 26 + // 27 + // Unix diff implementations typically look for a diff with 28 + // the smallest number of lines inserted and removed, 29 + // which can in the worst case take time quadratic in the 30 + // number of lines in the texts. As a result, many implementations 31 + // either can be made to run for a long time or cut off the search 32 + // after a predetermined amount of work. 33 + // 34 + // In contrast, this implementation looks for a diff with the 35 + // smallest number of “unique” lines inserted and removed, 36 + // where unique means a line that appears just once in both old and new. 37 + // We call this an “anchored diff” because the unique lines anchor 38 + // the chosen matching regions. An anchored diff is usually clearer 39 + // than a standard diff, because the algorithm does not try to 40 + // reuse unrelated blank lines or closing braces. 41 + // The algorithm also guarantees to run in O(n log n) time 42 + // instead of the standard O(n²) time. 43 + // 44 + // Some systems call this approach a “patience diff,” named for 45 + // the “patience sorting” algorithm, itself named for a solitaire card game. 46 + // We avoid that name for two reasons. First, the name has been used 47 + // for a few different variants of the algorithm, so it is imprecise. 48 + // Second, the name is frequently interpreted as meaning that you have 49 + // to wait longer (to be patient) for the diff, meaning that it is a slower algorithm, 50 + // when in fact the algorithm is faster than the standard one. 51 + func Diff( //nolint: gocyclo 52 + oldName string, 53 + old []byte, 54 + newName string, 55 + new []byte, //nolint: predeclared 56 + ) []byte { 57 + if bytes.Equal(old, new) { 58 + return nil 59 + } 60 + x := lines(old) 61 + y := lines(new) 62 + 63 + // Print diff header. 64 + var out bytes.Buffer 65 + fmt.Fprintf(&out, "diff %s %s\n", oldName, newName) 66 + fmt.Fprintf(&out, "--- %s\n", oldName) 67 + fmt.Fprintf(&out, "+++ %s\n", newName) 68 + 69 + // Loop over matches to consider, 70 + // expanding each match to include surrounding lines, 71 + // and then printing diff chunks. 72 + // To avoid setup/teardown cases outside the loop, 73 + // tgs returns a leading {0,0} and trailing {len(x), len(y)} pair 74 + // in the sequence of matches. 75 + var ( 76 + done pair // printed up to x[:done.x] and y[:done.y] 77 + chunk pair // start lines of current chunk 78 + count pair // number of lines from each side in current chunk 79 + ctext []string // lines for current chunk 80 + ) 81 + for _, m := range tgs(x, y) { 82 + if m.x < done.x { 83 + // Already handled scanning forward from earlier match. 84 + continue 85 + } 86 + 87 + // Expand matching lines as far possible, 88 + // establishing that x[start.x:end.x] == y[start.y:end.y]. 89 + // Note that on the first (or last) iteration we may (or definitey do) 90 + // have an empty match: start.x==end.x and start.y==end.y. 91 + start := m 92 + for start.x > done.x && start.y > done.y && x[start.x-1] == y[start.y-1] { 93 + start.x-- 94 + start.y-- 95 + } 96 + end := m 97 + for end.x < len(x) && end.y < len(y) && x[end.x] == y[end.y] { 98 + end.x++ 99 + end.y++ 100 + } 101 + 102 + // Emit the mismatched lines before start into this chunk. 103 + // (No effect on first sentinel iteration, when start = {0,0}.) 104 + for _, s := range x[done.x:start.x] { 105 + ctext = append(ctext, "- "+s) 106 + count.x++ 107 + } 108 + for _, s := range y[done.y:start.y] { 109 + ctext = append(ctext, "+ "+s) 110 + count.y++ 111 + } 112 + 113 + // If we're not at EOF and have too few common lines, 114 + // the chunk includes all the common lines and continues. 115 + const C = 3 // number of context lines 116 + if (end.x < len(x) || end.y < len(y)) && 117 + (end.x-start.x < C || (len(ctext) > 0 && end.x-start.x < 2*C)) { 118 + for _, s := range x[start.x:end.x] { 119 + ctext = append(ctext, " "+s) 120 + count.x++ 121 + count.y++ 122 + } 123 + done = end 124 + continue 125 + } 126 + 127 + // End chunk with common lines for context. 128 + if len(ctext) > 0 { 129 + n := end.x - start.x 130 + if n > C { 131 + n = C 132 + } 133 + for _, s := range x[start.x : start.x+n] { 134 + ctext = append(ctext, " "+s) 135 + count.x++ 136 + count.y++ 137 + } 138 + done = pair{start.x + n, start.y + n} 139 + 140 + // Format and emit chunk. 141 + // Convert line numbers to 1-indexed. 142 + // Special case: empty file shows up as 0,0 not 1,0. 143 + if count.x > 0 { 144 + chunk.x++ 145 + } 146 + if count.y > 0 { 147 + chunk.y++ 148 + } 149 + fmt.Fprintf(&out, "@@ -%d,%d +%d,%d @@\n", chunk.x, count.x, chunk.y, count.y) 150 + for _, s := range ctext { 151 + out.WriteString(s) 152 + } 153 + count.x = 0 154 + count.y = 0 155 + ctext = ctext[:0] 156 + } 157 + 158 + // If we reached EOF, we're done. 159 + if end.x >= len(x) && end.y >= len(y) { 160 + break 161 + } 162 + 163 + // Otherwise start a new chunk. 164 + chunk = pair{end.x - C, end.y - C} 165 + for _, s := range x[chunk.x:end.x] { 166 + ctext = append(ctext, " "+s) 167 + count.x++ 168 + count.y++ 169 + } 170 + done = end 171 + } 172 + 173 + return out.Bytes() 174 + } 175 + 176 + // lines returns the lines in the file x, including newlines. 177 + // If the file does not end in a newline, one is supplied 178 + // along with a warning about the missing newline. 179 + func lines(x []byte) []string { 180 + l := strings.SplitAfter(string(x), "\n") 181 + if l[len(l)-1] == "" { 182 + l = l[:len(l)-1] 183 + } else { 184 + // Treat last line as having a message about the missing newline attached, 185 + // using the same text as BSD/GNU diff (including the leading backslash). 186 + l[len(l)-1] += "\n\\ No newline at end of file\n" 187 + } 188 + return l 189 + } 190 + 191 + // tgs returns the pairs of indexes of the longest common subsequence 192 + // of unique lines in x and y, where a unique line is one that appears 193 + // once in x and once in y. 194 + // 195 + // The longest common subsequence algorithm is as described in 196 + // Thomas G. Szymanski, “A Special Case of the Maximal Common 197 + // Subsequence Problem,” Princeton TR #170 (January 1975), 198 + // available at https://research.swtch.com/tgs170.pdf. 199 + func tgs(x, y []string) []pair { 200 + // Count the number of times each string appears in a and b. 201 + // We only care about 0, 1, many, counted as 0, -1, -2 202 + // for the x side and 0, -4, -8 for the y side. 203 + // Using negative numbers now lets us distinguish positive line numbers later. 204 + m := make(map[string]int) 205 + for _, s := range x { 206 + if c := m[s]; c > -2 { 207 + m[s] = c - 1 208 + } 209 + } 210 + for _, s := range y { 211 + if c := m[s]; c > -8 { 212 + m[s] = c - 4 //nolint: mnd 213 + } 214 + } 215 + 216 + // Now unique strings can be identified by m[s] = -1+-4. 217 + // 218 + // Gather the indexes of those strings in x and y, building: 219 + // xi[i] = increasing indexes of unique strings in x. 220 + // yi[i] = increasing indexes of unique strings in y. 221 + // inv[i] = index j such that x[xi[i]] = y[yi[j]]. 222 + var xi, yi, inv []int 223 + for i, s := range y { 224 + if m[s] == -1+-4 { 225 + m[s] = len(yi) 226 + yi = append(yi, i) 227 + } 228 + } 229 + for i, s := range x { 230 + if j, ok := m[s]; ok && j >= 0 { 231 + xi = append(xi, i) 232 + inv = append(inv, j) 233 + } 234 + } 235 + 236 + // Apply Algorithm A from Szymanski's paper. 237 + // In those terms, A = J = inv and B = [0, n). 238 + // We add sentinel pairs {0,0}, and {len(x),len(y)} 239 + // to the returned sequence, to help the processing loop. 240 + J := inv 241 + n := len(xi) 242 + T := make([]int, n) 243 + L := make([]int, n) 244 + for i := range T { 245 + T[i] = n + 1 246 + } 247 + for i := 0; i < n; i++ { 248 + k := sort.Search(n, func(k int) bool { 249 + return T[k] >= J[i] 250 + }) 251 + T[k] = J[i] 252 + L[i] = k + 1 253 + } 254 + k := 0 255 + for _, v := range L { 256 + if k < v { 257 + k = v 258 + } 259 + } 260 + seq := make([]pair, 2+k) //nolint:mnd 261 + seq[1+k] = pair{len(x), len(y)} // sentinel at end 262 + lastj := n 263 + for i := n - 1; i >= 0; i-- { 264 + if L[i] == k && J[i] < lastj { 265 + seq[k] = pair{xi[i], yi[J[i]]} 266 + k-- 267 + } 268 + } 269 + seq[0] = pair{0, 0} // sentinel at start 270 + return seq 271 + }
+57
internal/diff/diff_test.go
··· 1 + // Copyright 2022 The Go Authors. All rights reserved. 2 + // Use of this source code is governed by a BSD-style 3 + // license that can be found in the LICENSE file. 4 + 5 + package diff_test 6 + 7 + import ( 8 + "bytes" 9 + "os" 10 + "path/filepath" 11 + "testing" 12 + 13 + "github.com/FollowTheProcess/test/internal/diff" 14 + "golang.org/x/tools/txtar" 15 + ) 16 + 17 + func clean(text []byte) []byte { 18 + text = bytes.ReplaceAll(text, []byte("$\n"), []byte("\n")) 19 + text = bytes.TrimSuffix(text, []byte("^D\n")) 20 + return text 21 + } 22 + 23 + func Test(t *testing.T) { 24 + files, err := filepath.Glob(filepath.Join("testdata", "*.txtar")) 25 + if err != nil { 26 + t.Fatalf("could not glob txtar files: %v", err) 27 + } 28 + if len(files) == 0 { 29 + t.Fatalf("no testdata") 30 + } 31 + 32 + for _, file := range files { 33 + t.Run(filepath.Base(file), func(t *testing.T) { 34 + contents, err := os.ReadFile(file) 35 + if err != nil { 36 + t.Fatalf("could not read %s: %v", file, err) 37 + } 38 + // Stupid windows 39 + contents = bytes.ReplaceAll(contents, []byte("\r\n"), []byte("\n")) 40 + archive := txtar.Parse(contents) 41 + if len(archive.Files) != 3 || archive.Files[2].Name != "diff" { 42 + t.Fatalf("%s: want three files, third named \"diff\", got: %v", file, archive.Files) 43 + } 44 + diffs := diff.Diff( 45 + archive.Files[0].Name, 46 + clean(archive.Files[0].Data), 47 + archive.Files[1].Name, 48 + clean(archive.Files[1].Data), 49 + ) 50 + want := clean(archive.Files[2].Data) 51 + if !bytes.Equal(diffs, want) { 52 + t.Fatalf("%s: have:\n%s\nwant:\n%s\n%s", file, 53 + diffs, want, diff.Diff("have", diffs, "want", want)) 54 + } 55 + }) 56 + } 57 + }
+13
internal/diff/testdata/allnew.txtar
··· 1 + -- old -- 2 + -- new -- 3 + a 4 + b 5 + c 6 + -- diff -- 7 + diff old new 8 + --- old 9 + +++ new 10 + @@ -0,0 +1,3 @@ 11 + + a 12 + + b 13 + + c
+13
internal/diff/testdata/allold.txtar
··· 1 + -- old -- 2 + a 3 + b 4 + c 5 + -- new -- 6 + -- diff -- 7 + diff old new 8 + --- old 9 + +++ new 10 + @@ -1,3 +0,0 @@ 11 + - a 12 + - b 13 + - c
+35
internal/diff/testdata/basic.txtar
··· 1 + # Example from Hunt and McIlroy, “An Algorithm for Differential File Comparison.” 2 + # https://www.cs.dartmouth.edu/~doug/diff.pdf 3 + 4 + -- old -- 5 + a 6 + b 7 + c 8 + d 9 + e 10 + f 11 + g 12 + -- new -- 13 + w 14 + a 15 + b 16 + x 17 + y 18 + z 19 + e 20 + -- diff -- 21 + diff old new 22 + --- old 23 + +++ new 24 + @@ -1,7 +1,7 @@ 25 + + w 26 + a 27 + b 28 + - c 29 + - d 30 + + x 31 + + y 32 + + z 33 + e 34 + - f 35 + - g
+40
internal/diff/testdata/dups.txtar
··· 1 + -- old -- 2 + a 3 + 4 + b 5 + 6 + c 7 + 8 + d 9 + 10 + e 11 + 12 + f 13 + -- new -- 14 + a 15 + 16 + B 17 + 18 + C 19 + 20 + d 21 + 22 + e 23 + 24 + f 25 + -- diff -- 26 + diff old new 27 + --- old 28 + +++ new 29 + @@ -1,8 +1,8 @@ 30 + a 31 + $ 32 + - b 33 + - 34 + - c 35 + + B 36 + + 37 + + C 38 + $ 39 + d 40 + $
+38
internal/diff/testdata/end.txtar
··· 1 + -- old -- 2 + 1 3 + 2 4 + 3 5 + 4 6 + 5 7 + 6 8 + 7 9 + eight 10 + nine 11 + ten 12 + eleven 13 + -- new -- 14 + 1 15 + 2 16 + 3 17 + 4 18 + 5 19 + 6 20 + 7 21 + 8 22 + 9 23 + 10 24 + -- diff -- 25 + diff old new 26 + --- old 27 + +++ new 28 + @@ -5,7 +5,6 @@ 29 + 5 30 + 6 31 + 7 32 + - eight 33 + - nine 34 + - ten 35 + - eleven 36 + + 8 37 + + 9 38 + + 10
+9
internal/diff/testdata/eof.txtar
··· 1 + -- old -- 2 + a 3 + b 4 + c^D 5 + -- new -- 6 + a 7 + b 8 + c^D 9 + -- diff --
+18
internal/diff/testdata/eof1.txtar
··· 1 + -- old -- 2 + a 3 + b 4 + c 5 + -- new -- 6 + a 7 + b 8 + c^D 9 + -- diff -- 10 + diff old new 11 + --- old 12 + +++ new 13 + @@ -1,3 +1,3 @@ 14 + a 15 + b 16 + - c 17 + + c 18 + \ No newline at end of file
+18
internal/diff/testdata/eof2.txtar
··· 1 + -- old -- 2 + a 3 + b 4 + c^D 5 + -- new -- 6 + a 7 + b 8 + c 9 + -- diff -- 10 + diff old new 11 + --- old 12 + +++ new 13 + @@ -1,3 +1,3 @@ 14 + a 15 + b 16 + - c 17 + \ No newline at end of file 18 + + c
+62
internal/diff/testdata/long.txtar
··· 1 + -- old -- 2 + 1 3 + 2 4 + 3 5 + 4 6 + 5 7 + 6 8 + 7 9 + 8 10 + 9 11 + 10 12 + 11 13 + 12 14 + 13 15 + 14 16 + 14½ 17 + 15 18 + 16 19 + 17 20 + 18 21 + 19 22 + 20 23 + -- new -- 24 + 1 25 + 2 26 + 3 27 + 4 28 + 5 29 + 6 30 + 8 31 + 9 32 + 10 33 + 11 34 + 12 35 + 13 36 + 14 37 + 17 38 + 18 39 + 19 40 + 20 41 + -- diff -- 42 + diff old new 43 + --- old 44 + +++ new 45 + @@ -4,7 +4,6 @@ 46 + 4 47 + 5 48 + 6 49 + - 7 50 + 8 51 + 9 52 + 10 53 + @@ -12,9 +11,6 @@ 54 + 12 55 + 13 56 + 14 57 + - 14½ 58 + - 15 59 + - 16 60 + 17 61 + 18 62 + 19
+5
internal/diff/testdata/same.txtar
··· 1 + -- old -- 2 + hello world 3 + -- new -- 4 + hello world 5 + -- diff --
+34
internal/diff/testdata/start.txtar
··· 1 + -- old -- 2 + e 3 + pi 4 + 4 5 + 5 6 + 6 7 + 7 8 + 8 9 + 9 10 + 10 11 + -- new -- 12 + 1 13 + 2 14 + 3 15 + 4 16 + 5 17 + 6 18 + 7 19 + 8 20 + 9 21 + 10 22 + -- diff -- 23 + diff old new 24 + --- old 25 + +++ new 26 + @@ -1,5 +1,6 @@ 27 + - e 28 + - pi 29 + + 1 30 + + 2 31 + + 3 32 + 4 33 + 5 34 + 6
+40
internal/diff/testdata/triv.txtar
··· 1 + # Another example from Hunt and McIlroy, 2 + # “An Algorithm for Differential File Comparison.” 3 + # https://www.cs.dartmouth.edu/~doug/diff.pdf 4 + 5 + # Anchored diff gives up on finding anything, 6 + # since there are no unique lines. 7 + 8 + -- old -- 9 + a 10 + b 11 + c 12 + a 13 + b 14 + b 15 + a 16 + -- new -- 17 + c 18 + a 19 + b 20 + a 21 + b 22 + c 23 + -- diff -- 24 + diff old new 25 + --- old 26 + +++ new 27 + @@ -1,7 +1,6 @@ 28 + - a 29 + - b 30 + - c 31 + - a 32 + - b 33 + - b 34 + - a 35 + + c 36 + + a 37 + + b 38 + + a 39 + + b 40 + + c