1
2
3
4
5
6
7
8
9 package diff
10
11 import (
12 "context"
13 "errors"
14 "fmt"
15 "io/ioutil"
16 "os"
17 "os/exec"
18 "path/filepath"
19 "strings"
20 "time"
21
22 "go.uber.org/multierr"
23
24 "oss.terrastruct.com/util-go/xdefer"
25 "oss.terrastruct.com/util-go/xjson"
26 )
27
28
29
30
31
32
33
34
35
36
37
38 func Strings(exp, got string) (ds string, err error) {
39 defer xdefer.Errorf(&err, "failed to diff text")
40
41 if exp == got {
42 return "", nil
43 }
44
45 d, err := ioutil.TempDir("", "ts_d2_diff")
46 if err != nil {
47 return "", err
48 }
49
50 expPath := filepath.Join(d, "exp")
51 gotPath := filepath.Join(d, "got")
52
53 err = ioutil.WriteFile(expPath, []byte(exp), 0644)
54 if err != nil {
55 return "", err
56 }
57 err = ioutil.WriteFile(gotPath, []byte(got), 0644)
58 if err != nil {
59 return "", err
60 }
61
62 return Files(expPath, gotPath)
63 }
64
65
66
67
68 func Files(expPath, gotPath string) (ds string, err error) {
69 defer xdefer.Errorf(&err, "failed to diff files")
70
71 _, err = os.Stat(expPath)
72 if os.IsNotExist(err) {
73 expPath = "/dev/null"
74 }
75 _, err = os.Stat(gotPath)
76 if os.IsNotExist(err) {
77 gotPath = "/dev/null"
78 }
79
80 ctx, cancel := context.WithTimeout(context.Background(), time.Minute)
81 defer cancel()
82 cmd := exec.CommandContext(ctx, "git", "-c", "diff.color=always", "diff",
83
84 "--diff-algorithm=histogram",
85 "--ws-error-highlight=all",
86 "--no-index",
87 expPath, gotPath)
88 cmd.Env = append(cmd.Env, "GIT_CONFIG_NOSYSTEM=1", "HOME=")
89
90 diffBytes, err := cmd.CombinedOutput()
91 var ee *exec.ExitError
92 if err != nil && !errors.As(err, &ee) {
93 return "", fmt.Errorf("git diff failed: out=%q: %w", diffBytes, err)
94 }
95 ds = string(diffBytes)
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110 i := strings.Index(ds, "index")
111 if i > -1 {
112 j := strings.IndexByte(ds[i:], '\n')
113 if j > -1 {
114 ds = ds[i+j+1:]
115 }
116 }
117 return strings.TrimSpace(ds), nil
118 }
119
120
121
122
123 func Runes(exp, got string) error {
124 if exp == got {
125 return nil
126 }
127 expRunes := formatRunes(exp)
128 gotRunes := formatRunes(got)
129 ds, err := Strings(expRunes, gotRunes)
130 if err != nil {
131 return err
132 }
133 if ds != "" {
134 return errors.New(ds)
135 }
136 return nil
137 }
138
139 func formatRunes(s string) string {
140 return strings.Join(strings.Split(fmt.Sprintf("%#v", []rune(s)), ", "), "\n")
141 }
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197 func TestdataJSON(path string, got interface{}) error {
198 gotb := xjson.Marshal(got)
199 gotb = append(gotb, '\n')
200 return Testdata(path, ".json", gotb)
201 }
202
203
204 func Testdata(path, ext string, got []byte) error {
205 expPath := fmt.Sprintf("%s.exp%s", path, ext)
206 gotPath := fmt.Sprintf("%s.got%s", path, ext)
207
208 err := os.MkdirAll(filepath.Dir(gotPath), 0755)
209 if err != nil {
210 return err
211 }
212 err = ioutil.WriteFile(gotPath, []byte(got), 0600)
213 if err != nil {
214 return err
215 }
216
217 ds, err := Files(expPath, gotPath)
218 if err != nil {
219 return err
220 }
221
222 if ds != "" {
223 if os.Getenv("TESTDATA_ACCEPT") != "" || os.Getenv("TA") != "" {
224 return os.Rename(gotPath, expPath)
225 }
226 if os.Getenv("NO_DIFF") != "" || os.Getenv("ND") != "" {
227 ds = "diff hidden with $NO_DIFF=1 or $ND=1"
228 }
229 return fmt.Errorf("diff (rerun with $TESTDATA_ACCEPT=1 or $TA=1 to accept):\n%s", ds)
230 }
231 return os.Remove(gotPath)
232 }
233
234 func JSON(exp, got interface{}) (string, error) {
235 return Strings(string(xjson.Marshal(exp)), string(xjson.Marshal(got)))
236 }
237
238 func TestdataDir(testName, dir string) (err error) {
239 defer xdefer.Errorf(&err, "failed to commit testdata dir %v", dir)
240 testdataDir(&err, testName, dir)
241 return err
242 }
243
244 func testdataDir(errs *error, testName, dir string) {
245 ea, err := os.ReadDir(dir)
246 if err != nil {
247 *errs = multierr.Combine(*errs, err)
248 return
249 }
250
251 for _, e := range ea {
252 if e.IsDir() {
253 testdataDir(errs, filepath.Join(testName, e.Name()), filepath.Join(dir, e.Name()))
254 } else {
255 ext := filepath.Ext(e.Name())
256 name := strings.TrimSuffix(e.Name(), ext)
257 got, err := os.ReadFile(filepath.Join(dir, e.Name()))
258 if err != nil {
259 *errs = multierr.Combine(*errs, err)
260 continue
261 }
262 err = Testdata(filepath.Join(testName, name), ext, got)
263 if err != nil {
264 *errs = multierr.Combine(*errs, err)
265 }
266 }
267 }
268 }
269
View as plain text