-
Notifications
You must be signed in to change notification settings - Fork 23
/
Copy pathmain.go
358 lines (302 loc) · 8.07 KB
/
main.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
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
package main
import (
"encoding/json"
"flag"
"fmt"
"io"
"io/ioutil"
"os"
"path/filepath"
"reflect"
"regexp"
"strings"
jsonata "github.com/blues/jsonata-go"
types "github.com/blues/jsonata-go/jtypes"
)
type testCase struct {
Expr string
ExprFile string `json:"expr-file"`
Category string
Data interface{}
Dataset string
Description string
TimeLimit int
Depth int
Bindings map[string]interface{}
Result interface{}
Undefined bool
Error string `json:"code"`
Token string
Unordered bool
}
func main() {
var group string
var verbose bool
flag.BoolVar(&verbose, "verbose", false, "verbose output")
flag.StringVar(&group, "group", "", "restrict to one or more test groups")
flag.Parse()
if flag.NArg() != 1 {
fmt.Fprintln(os.Stderr, "Syntax: jsonata-test [options] <directory>")
os.Exit(1)
}
root := flag.Arg(0)
testdir := filepath.Join(root, "groups")
datadir := filepath.Join(root, "datasets")
err := run(testdir, datadir, group)
if err != nil {
fmt.Fprintf(os.Stderr, "Error while running: %s\n", err)
os.Exit(2)
}
fmt.Fprintln(os.Stdout, "OK")
}
// run runs all test cases
func run(testdir string, datadir string, filter string) error {
var numPassed, numFailed int
err := filepath.Walk(testdir, func(path string, info os.FileInfo, walkFnErr error) error {
var dirName string
if info.IsDir() {
if path == testdir {
return nil
}
dirName = filepath.Base(path)
if filter != "" && !strings.Contains(dirName, filter) {
return filepath.SkipDir
}
return nil
}
// Ignore files with names ending with .jsonata, these
// are not test cases
if filepath.Ext(path) == ".jsonata" {
return nil
}
testCases, err := loadTestCases(path)
if err != nil {
return fmt.Errorf("walk %s: %s", path, err)
}
for _, testCase := range testCases {
failed, err := runTest(testCase, datadir, path)
if err != nil {
return err
}
if failed {
numFailed++
} else {
numPassed++
}
}
return nil
})
if err != nil {
return fmt.Errorf("walk %s: ", err)
}
fmt.Fprintln(os.Stdout)
fmt.Fprintln(os.Stdout, numPassed, "passed", numFailed, "failed")
return nil
}
// runTest runs a single test case
func runTest(tc testCase, dataDir string, path string) (bool, error) {
// Some tests assume JavaScript-style object traversal,
// these are marked as unordered and can be skipped
// See https://github.com/jsonata-js/jsonata/issues/179
if tc.Unordered {
return false, nil
}
if tc.TimeLimit != 0 {
return false, nil
}
// If this test has an associated dataset, load it
data := tc.Data
if tc.Dataset != "" {
var dest interface{}
err := readJSONFile(filepath.Join(dataDir, tc.Dataset+".json"), &dest)
if err != nil {
return false, err
}
data = dest
}
var failed bool
expr, unQuoted := replaceQuotesInPaths(tc.Expr)
got, _ := eval(expr, tc.Bindings, data)
if !equalResults(got, tc.Result) {
failed = true
printTestCase(os.Stderr, tc, strings.TrimSuffix(filepath.Base(path), ".json"))
fmt.Fprintf(os.Stderr, "Test file: %s \n", path)
if tc.Category != "" {
fmt.Fprintf(os.Stderr, "Category: %s \n", tc.Category)
}
if tc.Description != "" {
fmt.Fprintf(os.Stderr, "Description: %s \n", tc.Description)
}
fmt.Fprintf(os.Stderr, "Expression: %s\n", expr)
if unQuoted {
fmt.Fprintf(os.Stderr, "Unquoted: %t\n", unQuoted)
}
fmt.Fprintf(os.Stderr, "Expected Result: %v [%T]\n", tc.Result, tc.Result)
fmt.Fprintf(os.Stderr, "Actual Result: %v [%T]\n", got, got)
}
// TODO this block is commented out to make staticcheck happy,
// but we should check that the error is the same as the js one
// var exp error
// if tc.Undefined {
// exp = jsonata.ErrUndefined
// } else {
// exp = convertError(tc.Error)
// }
// if !reflect.DeepEqual(err, exp) {
// TODO: Compare actual/expected errors
// }
return failed, nil
}
// loadTestExprFile loads a jsonata expression from a file and returns the
// expression
// For example, one test looks like this
// {
// "expr-file": "case000.jsonata",
// "dataset": null,
// "bindings": {},
// "result": 2
// }
//
// We want to load the expression from case000.jsonata so we can use it
// as an expression in the test case
func loadTestExprFile(testPath string, exprFileName string) (string, error) {
splitPath := strings.Split(testPath, "/")
splitPath[len(splitPath)-1] = exprFileName
exprFilePath := strings.Join(splitPath, "/")
content, err := ioutil.ReadFile(exprFilePath)
if err != nil {
return "", err
}
return string(content), nil
}
// loadTestCases loads all of the json data for tests and converts them to test cases
func loadTestCases(path string) ([]testCase, error) {
// Test cases are contained in json files. They consist of either
// one test case in the file or an array of test cases.
// Since we don't know which it will be until we load the file,
// first try to demarshall it a single case, and if there is an
// error, try again demarshalling it into an array of test cases
var tc testCase
err := readJSONFile(path, &tc)
if err != nil {
var tcs []testCase
err := readJSONFile(path, &tcs)
if err != nil {
return nil, err
}
// If any of the tests specify an expression file, load it from
// disk and add it to the test case
for _, testCase := range tcs {
if testCase.ExprFile != "" {
expr, err := loadTestExprFile(path, testCase.ExprFile)
if err != nil {
return nil, err
}
testCase.Expr = expr
}
}
return tcs, nil
}
// If we have gotten here then there was only one test specified in the
// tests file.
// If the test specifies an expression file, load it from
// disk and add it to the test case
if tc.ExprFile != "" {
expr, err := loadTestExprFile(path, tc.ExprFile)
if err != nil {
return nil, err
}
tc.Expr = expr
}
return []testCase{tc}, nil
}
func printTestCase(w io.Writer, tc testCase, name string) {
fmt.Fprintln(w)
fmt.Fprintf(w, "Failed Test Case: %s\n", name)
switch {
case tc.Data != nil:
fmt.Fprintf(w, "Data: %v\n", tc.Data)
case tc.Dataset != "":
fmt.Fprintf(w, "Dataset: %s\n", tc.Dataset)
default:
fmt.Fprintln(w, "Data: N/A")
}
if tc.Error != "" {
fmt.Fprintf(w, "Expected error code: %v\n", tc.Error)
}
if len(tc.Bindings) > 0 {
fmt.Fprintf(w, "Bindings: %v\n", tc.Bindings)
}
}
func eval(expression string, bindings map[string]interface{}, data interface{}) (interface{}, error) {
expr, err := jsonata.Compile(expression)
if err != nil {
return nil, err
}
err = expr.RegisterVars(bindings)
if err != nil {
return nil, err
}
return expr.Eval(data)
}
func equalResults(x, y interface{}) bool {
if reflect.DeepEqual(x, y) {
return true
}
vx := types.Resolve(reflect.ValueOf(x))
vy := types.Resolve(reflect.ValueOf(y))
if types.IsArray(vx) && types.IsArray(vy) {
if vx.Len() != vy.Len() {
return false
}
for i := 0; i < vx.Len(); i++ {
if !equalResults(vx.Index(i).Interface(), vy.Index(i).Interface()) {
return false
}
}
return true
}
ix, okx := types.AsNumber(vx)
iy, oky := types.AsNumber(vy)
if okx && oky && ix == iy {
return true
}
sx, okx := types.AsString(vx)
sy, oky := types.AsString(vy)
if okx && oky && sx == sy {
return true
}
bx, okx := types.AsBool(vx)
by, oky := types.AsBool(vy)
if okx && oky && bx == by {
return true
}
return false
}
func readJSONFile(path string, dest interface{}) error {
b, err := ioutil.ReadFile(path)
if err != nil {
return fmt.Errorf("ReadFile %s: %s", path, err)
}
err = json.Unmarshal(b, dest)
if err != nil {
return fmt.Errorf("unmarshal %s: %s", path, err)
}
return nil
}
var (
reQuotedPath = regexp.MustCompile(`([A-Za-z\$\\*\` + "`" + `])\.[\"']([ \.0-9A-Za-z]+?)[\"']`)
reQuotedPathStart = regexp.MustCompile(`^[\"']([ \.0-9A-Za-z]+?)[\"']\.([A-Za-z\$\*\"\'])`)
)
func replaceQuotesInPaths(s string) (string, bool) {
var changed bool
if reQuotedPathStart.MatchString(s) {
s = reQuotedPathStart.ReplaceAllString(s, "`$1`.$2")
changed = true
}
for reQuotedPath.MatchString(s) {
s = reQuotedPath.ReplaceAllString(s, "$1.`$2`")
changed = true
}
return s, changed
}