-
Notifications
You must be signed in to change notification settings - Fork 1
/
golden_files.go
228 lines (209 loc) · 7.82 KB
/
golden_files.go
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
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
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
package testutils
import (
"encoding/json"
"flag"
"fmt"
"io/ioutil"
"os"
"path/filepath"
"regexp"
"strings"
"testing"
"github.com/fabric8-services/fabric8-wit/log"
errs "github.com/pkg/errors"
uuid "github.com/satori/go.uuid"
"github.com/sergi/go-diff/diffmatchpatch"
)
var updateGoldenFiles = flag.Bool("update", false, "when set, rewrite the golden files")
// CompareOptions define how the comparison and golden file generation will take
// place
type CompareOptions struct {
// Whether or not to ignore UUIDs when comparing or writing the golden file
// to disk. When this is on we replace UUIDs in both strings (the golden
// file as well as in the actual object) before comparing the two strings.
// This should make the comparison UUID agnostic without loosing the
// locality comparison. In other words, that means we replace each UUID with
// a more generic "00000000-0000-0000-0000-000000000001",
// "00000000-0000-0000-0000-000000000002", ...,
// "00000000-0000-0000-0000-00000000000N" value.
UUIDAgnostic bool
// Whether or not to ignore date/times when comparing or writing the golden
// file to disk. We replace all RFC3339 time strings with
// "0001-01-01T00:00:00Z".
DateTimeAgnostic bool
// Whether or not to call JSON marshall on the actual object before
// comparing it against the content of the golden file or writing to the
// golden file. If this is false, then we will treat the actual object as a
// []byte or string.
MarshalInputAsJSON bool
}
// CompareWithGolden compares the actual object against the one from a
// golden file and let's you specify the options to be used for comparison and
// golden file production by hand. If the -update flag is given, that golden
// file is overwritten with the current actual object. When adding new tests you
// first must run them with the -update flag in order to create an initial
// golden version.
func CompareWithGolden(t *testing.T, goldenFile string, actualObj interface{}, opts CompareOptions) {
if err := testableCompareWithGolden(*updateGoldenFiles, goldenFile, actualObj, opts); err != nil {
t.Fatal(err)
}
}
func testableCompareWithGolden(update bool, goldenFile string, actualObj interface{}, opts CompareOptions) error {
absPath, err := filepath.Abs(goldenFile)
if err != nil {
return errs.WithStack(err)
}
var actual []byte
if opts.MarshalInputAsJSON {
var err error
actual, err = json.MarshalIndent(actualObj, "", " ")
if err != nil {
return errs.WithStack(err)
}
} else {
switch t := actualObj.(type) {
case []byte:
actual = t
case string:
actual = []byte(t)
default:
return errs.Errorf("don't know how to convert type of object %[1]T to string: %+[1]v (consider enabling MarshalInputAsJSON option)", actualObj)
}
}
if update {
// Make sure the directory exists where to write the file to
err := os.MkdirAll(filepath.Dir(absPath), os.FileMode(0777))
if err != nil {
return errs.Wrapf(err, "failed to create directory (and potential parents dirs) to write golden file to")
}
tmp := string(actual)
// Eliminate concrete UUIDs if requested. This makes adding changes to
// golden files much more easy in git.
if opts.UUIDAgnostic {
tmp, err = replaceUUIDs(tmp)
if err != nil {
return errs.Wrap(err, "failed to replace UUIDs with more generic ones")
}
}
if opts.DateTimeAgnostic {
tmp, err = replaceTimes(tmp)
if err != nil {
return errs.Wrap(err, "failed to replace RFC3339 times with default time")
}
}
err = ioutil.WriteFile(absPath, []byte(tmp), os.ModePerm)
if err != nil {
return errs.Wrapf(err, "failed to update golden file: %s", absPath)
}
}
expected, err := ioutil.ReadFile(absPath)
if err != nil {
return errs.Wrapf(err, "failed to read golden file: %s", absPath)
}
expectedStr := string(expected)
actualStr := string(actual)
if opts.UUIDAgnostic {
expectedStr, err = replaceUUIDs(expectedStr)
if err != nil {
return errs.Wrapf(err, "failed to replace UUIDs with more generic ones")
}
actualStr, err = replaceUUIDs(actualStr)
if err != nil {
return errs.Wrapf(err, "failed to replace UUIDs with more generic ones")
}
}
if opts.DateTimeAgnostic {
expectedStr, err = replaceTimes(expectedStr)
if err != nil {
return errs.Wrap(err, "failed to replace RFC3339 times with default time")
}
actualStr, err = replaceTimes(actualStr)
if err != nil {
return errs.Wrap(err, "failed to replace RFC3339 times with default time")
}
}
if expectedStr != actualStr {
log.Error(nil, nil, "testableCompareWithGolden: expected value %v", expectedStr)
log.Error(nil, nil, "testableCompareWithGolden: actual value %v", actualStr)
dmp := diffmatchpatch.New()
diffs := dmp.DiffMain(expectedStr, actualStr, false)
log.Error(nil, nil, "testableCompareWithGolden: mismatch of actual output and golden-file %s:\n %s \n", absPath, dmp.DiffPrettyText(diffs))
return errs.Errorf("mismatch of actual output and golden-file %s:\n %s \n", absPath, dmp.DiffPrettyText(diffs))
}
return nil
}
// findUUIDs returns an array of uniq UUIDs that have been found in the given
// string
func findUUIDs(str string) ([]uuid.UUID, error) {
pattern := "[0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[1-5][0-9a-fA-F]{3}-[89abAB][0-9a-fA-F]{3}-[0-9a-fA-F]{12}"
uuidRegexp, err := regexp.Compile(pattern)
if err != nil {
return nil, errs.Wrapf(err, "failed to compile UUID regex pattern: %s", pattern)
}
uniqIDs := map[uuid.UUID]struct{}{}
var res []uuid.UUID
for _, uuidStr := range uuidRegexp.FindAllString(str, -1) {
ID, err := uuid.FromString(uuidStr)
if err != nil {
return nil, errs.Wrapf(err, "failed to parse UUID %s", uuidStr)
}
_, alreadyInMap := uniqIDs[ID]
if !alreadyInMap {
uniqIDs[ID] = struct{}{}
// append to array
res = append(res, ID)
}
}
return res, nil
}
// replaceUUIDs finds all UUIDs in the given string and replaces them with
// "00000000-0000-0000-0000-000000000001,
// "00000000-0000-0000-0000-000000000002", ...,
// "00000000-0000-0000-0000-00000000000N"
func replaceUUIDs(str string) (string, error) {
replacementPattern := "00000000-0000-0000-0000-%012d"
ids, err := findUUIDs(str)
if err != nil {
return "", errs.Wrapf(err, "failed to find UUIDs in string %s", str)
}
newStr := str
for idx, id := range ids {
newStr = strings.Replace(newStr, id.String(), fmt.Sprintf(replacementPattern, idx+1), -1)
}
return newStr, nil
}
// replaceTimes finds all RFC3339 times and RFC7232 (section 2.2) times in the
// given string and replaces them with "0001-01-01T00:00:00Z" (for RFC3339) or
// "Mon, 01 Jan 0001 00:00:00 GMT" (for RFC7232) respectively.
func replaceTimes(str string) (string, error) {
year := "([0-9]+)"
month := "(0[1-9]|1[012])"
day := "(0[1-9]|[12][0-9]|3[01])"
datePattern := year + "-" + month + "-" + day
hour := "([01][0-9]|2[0-3])"
minute := "([0-5][0-9])"
second := "([0-5][0-9]|60)"
subSecond := "(\\.[0-9]+)?"
timePattern := hour + ":" + minute + ":" + second + subSecond
timeZoneOffset := "(([Zz])|([\\+|\\-]([01][0-9]|2[0-3]):[0-5][0-9]))"
pattern := datePattern + "[Tt]" + timePattern + timeZoneOffset
rfc3339Pattern, err := regexp.Compile(pattern)
if err != nil {
return "", errs.Wrapf(err, "failed to compile RFC3339 regex pattern: %s", pattern)
}
res := rfc3339Pattern.ReplaceAllString(str, `0001-01-01T00:00:00Z`)
dayName := "(Mon|Tue|Wed|Thu|Fri|Sat|Sun)"
day = "[0-9]{2}"
month = "(Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)"
year = "[0-9]{4}"
hour = "([01][0-9]|2[0-3])"
minute = "([0-5][0-9])"
second = "([0-5][0-9]|60)"
tz := "(GMT|CEST|UTC|IST|[A-Z]+)"
pattern = dayName + ", " + day + " " + month + " " + year + " " + hour + ":" + minute + ":" + second + " " + tz
lastModifiedPattern, err := regexp.Compile(pattern)
if err != nil {
return "", errs.Wrapf(err, "failed to compile RFC7232 last-modified regex pattern: %s", pattern)
}
return lastModifiedPattern.ReplaceAllString(res, `Mon, 01 Jan 0001 00:00:00 GMT`), nil
}