1 package testutil
2
3 import (
4 "bytes"
5 "errors"
6 "fmt"
7 "io"
8 "os"
9 "path/filepath"
10 "testing"
11 "text/template"
12
13 "github.com/go-test/deep"
14 "github.com/sergi/go-diff/diffmatchpatch"
15 "gopkg.in/yaml.v2"
16 )
17
18
19 type TestDataDiffer struct {
20 PrettyDiff bool
21 UpdateFixtures bool
22 RejectPath string
23 }
24
25
26 func (td *TestDataDiffer) DiffTestYAML(path string, actualYAML string) error {
27 expectedYAML := ReadTestdata(path)
28 return td.diffTestYAML(path, actualYAML, expectedYAML)
29 }
30
31
32 func (td *TestDataDiffer) DiffTestYAMLTemplate(path string, actualYAML string, params any) error {
33 file := filepath.Join("testdata", path)
34 t, err := template.New(path).ParseFiles(file)
35 if err != nil {
36 return fmt.Errorf("Failed to read YAML template from %s: %w", path, err)
37 }
38 var buf bytes.Buffer
39 err = t.Execute(&buf, params)
40 if err != nil {
41 return fmt.Errorf("Failed to build YAML from template %s: %w", path, err)
42 }
43 return td.diffTestYAML(path, actualYAML, buf.String())
44 }
45
46 func (td *TestDataDiffer) diffTestYAML(path, actualYAML, expectedYAML string) error {
47 actual, err := unmarshalYAML([]byte(actualYAML))
48 if err != nil {
49 return fmt.Errorf("Failed to unmarshal generated YAML: %w", err)
50 }
51 expected, err := unmarshalYAML([]byte(expectedYAML))
52 if err != nil {
53 return fmt.Errorf("Failed to unmarshal expected YAML: %w", err)
54 }
55 diff := deep.Equal(expected, actual)
56 if diff == nil {
57 return nil
58 }
59 td.storeActual(path, []byte(actualYAML))
60 e := fmt.Sprintf("YAML mismatches %s:", path)
61 for _, d := range diff {
62 e += fmt.Sprintf("\n %s", d)
63 }
64 return errors.New(e)
65 }
66
67
68 func (td *TestDataDiffer) DiffTestdata(t *testing.T, path, actual string) {
69 t.Helper()
70 expected := ReadTestdata(path)
71 if actual == expected {
72 return
73 }
74 dmp := diffmatchpatch.New()
75 diffs := dmp.DiffMain(expected, actual, true)
76 diffs = dmp.DiffCleanupSemantic(diffs)
77 var diff string
78 if td.PrettyDiff {
79 diff = dmp.DiffPrettyText(diffs)
80 } else {
81 diff = dmp.PatchToText(dmp.PatchMake(diffs))
82 }
83 t.Errorf("mismatch: %s\n%s", path, diff)
84
85 td.storeActual(path, []byte(actual))
86 }
87
88 func (td *TestDataDiffer) storeActual(path string, actual []byte) {
89 if td.UpdateFixtures {
90 writeTestdata(path, actual)
91 }
92
93 if td.RejectPath != "" {
94 writeRejects(path, actual, td.RejectPath)
95 }
96 }
97
98
99 func ReadTestdata(fileName string) string {
100 file, err := os.Open(filepath.Join("testdata", fileName))
101 if err != nil {
102 panic(fmt.Sprintf("Failed to open expected output file: %v", err))
103 }
104
105 fixture, err := io.ReadAll(file)
106 if err != nil {
107 panic(fmt.Sprintf("Failed to read expected output file: %v", err))
108 }
109
110 return string(fixture)
111 }
112
113 func unmarshalYAML(data []byte) ([]interface{}, error) {
114 objs := make([]interface{}, 0)
115 rd := bytes.NewReader(data)
116 decoder := yaml.NewDecoder(rd)
117 for {
118 var obj interface{}
119 if err := decoder.Decode(&obj); err != nil {
120 if errors.Is(err, io.EOF) {
121 return objs, nil
122 }
123 return nil, err
124 }
125
126 objs = append(objs, obj)
127 }
128 }
129
130 func writeTestdata(fileName string, data []byte) {
131 p := filepath.Join("testdata", fileName)
132 if err := os.WriteFile(p, data, 0600); err != nil {
133 panic(err)
134 }
135 }
136
137 func writeRejects(origFileName string, data []byte, rejectPath string) {
138 p := filepath.Join(rejectPath, origFileName+".rej")
139 if err := os.WriteFile(p, data, 0600); err != nil {
140 panic(err)
141 }
142 }
143
View as plain text