blob: 5b4f7294ab8892c41c1696795675bed75cc50ad2 [file] [log] [blame]
Marcel van Lohuizenb001da52018-12-10 15:34:30 +01001// Copyright 2018 The CUE Authors
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15package format
16
17// TODO: port more of the tests of go/printer
18
19import (
20 "bytes"
21 "errors"
22 "flag"
23 "fmt"
24 "io"
25 "io/ioutil"
26 "path/filepath"
27 "testing"
28 "time"
29
30 "cuelang.org/go/cue/ast"
31 "cuelang.org/go/cue/parser"
32 "cuelang.org/go/cue/token"
33)
34
35var (
36 defaultConfig = newConfig([]Option{FileSet(fset)})
37 Fprint = defaultConfig.fprint
38)
39
40const (
41 dataDir = "testdata"
42 tabwidth = 8
43)
44
45var update = flag.Bool("update", false, "update golden files")
46
47var fset = token.NewFileSet()
48
49type checkMode uint
50
51const (
52 export checkMode = 1 << iota
53 rawFormat
54 idempotent
55 simplify
56)
57
58// format parses src, prints the corresponding AST, verifies the resulting
59// src is syntactically correct, and returns the resulting src or an error
60// if any.
61func format(src []byte, mode checkMode) ([]byte, error) {
62 // parse src
63 var opts []Option
64 if mode&simplify != 0 {
65 opts = append(opts, Simplify())
66 }
67
68 res, err := Source(src, opts...)
69 if err != nil {
70 return nil, err
71 }
72
73 // make sure formatted output is syntactically correct
Marcel van Lohuizen2bf066f2018-12-16 11:43:49 +010074 if _, err := parser.ParseFile(fset, "", res, parser.AllErrors); err != nil {
Marcel van Lohuizenb001da52018-12-10 15:34:30 +010075 return nil, fmt.Errorf("re-parse: %s\n%s", err, res)
76 }
77
78 return res, nil
79}
80
81// lineAt returns the line in text starting at offset offs.
82func lineAt(text []byte, offs int) []byte {
83 i := offs
84 for i < len(text) && text[i] != '\n' {
85 i++
86 }
87 return text[offs:i]
88}
89
90// diff compares a and b.
91func diff(aname, bname string, a, b []byte) error {
92 var buf bytes.Buffer // holding long error message
93
94 // compare lengths
95 if len(a) != len(b) {
96 fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b))
97 }
98
99 // compare contents
100 line := 1
101 offs := 1
102 for i := 0; i < len(a) && i < len(b); i++ {
103 ch := a[i]
104 if ch != b[i] {
105 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs))
106 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs))
107 fmt.Fprintf(&buf, "\n\n")
108 break
109 }
110 if ch == '\n' {
111 line++
112 offs = i + 1
113 }
114 }
115
116 if buf.Len() > 0 {
117 return errors.New(buf.String())
118 }
119 return nil
120}
121
122func runcheck(t *testing.T, source, golden string, mode checkMode) {
123 src, err := ioutil.ReadFile(source)
124 if err != nil {
125 t.Error(err)
126 return
127 }
128
129 res, err := format(src, mode)
130 if err != nil {
131 t.Error(err)
132 return
133 }
134
135 // update golden files if necessary
136 if *update {
137 if err := ioutil.WriteFile(golden, res, 0644); err != nil {
138 t.Error(err)
139 }
140 return
141 }
142
143 // get golden
144 gld, err := ioutil.ReadFile(golden)
145 if err != nil {
146 t.Error(err)
147 return
148 }
149
150 // formatted source and golden must be the same
151 if err := diff(source, golden, res, gld); err != nil {
152 t.Error(err)
153 return
154 }
155
156 if mode&idempotent != 0 {
157 // formatting golden must be idempotent
158 // (This is very difficult to achieve in general and for now
159 // it is only checked for files explicitly marked as such.)
160 res, err = format(gld, mode)
161 if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil {
162 t.Errorf("golden is not idempotent: %s", err)
163 }
164 }
165}
166
167func check(t *testing.T, source, golden string, mode checkMode) {
168 // run the test
169 cc := make(chan int)
170 go func() {
171 runcheck(t, source, golden, mode)
172 cc <- 0
173 }()
174
175 // wait with timeout
176 select {
177 case <-time.After(100000 * time.Second): // plenty of a safety margin, even for very slow machines
178 // test running past time out
179 t.Errorf("%s: running too slowly", source)
180 case <-cc:
181 // test finished within allotted time margin
182 }
183}
184
185type entry struct {
186 source, golden string
187 mode checkMode
188}
189
190// Use go test -update to create/update the respective golden files.
191var data = []entry{
192 {"comments.input", "comments.golden", 0},
193 {"simplify.input", "simplify.golden", simplify},
194 {"expressions.input", "expressions.golden", 0},
195}
196
197func TestFiles(t *testing.T) {
198 t.Parallel()
199 for _, e := range data {
200 source := filepath.Join(dataDir, e.source)
201 golden := filepath.Join(dataDir, e.golden)
202 mode := e.mode
203 t.Run(e.source, func(t *testing.T) {
204 t.Parallel()
205 check(t, source, golden, mode)
206 // TODO(gri) check that golden is idempotent
207 //check(t, golden, golden, e.mode)
208 })
209 }
210}
211
212// Verify that the printer can be invoked during initialization.
213func init() {
214 const name = "foobar"
215 var buf bytes.Buffer
216 if err := Fprint(&buf, &ast.Ident{Name: name}); err != nil {
217 panic(err) // error in test
218 }
219 // in debug mode, the result contains additional information;
220 // ignore it
221 if s := buf.String(); !debug && s != name {
222 panic("got " + s + ", want " + name)
223 }
224}
225
226// Verify that the printer doesn't crash if the AST contains BadXXX nodes.
227func TestBadNodes(t *testing.T) {
228 const src = "package p\n("
229 const res = "package p\n\n(BadExpr)\n"
230 f, err := parser.ParseFile(fset, "", src, parser.ParseComments)
231 if err == nil {
232 t.Error("expected illegal program") // error in test
233 }
234 var buf bytes.Buffer
235 Fprint(&buf, f)
236 if buf.String() != res {
237 t.Errorf("got %q, expected %q", buf.String(), res)
238 }
239}
240
241// idents is an iterator that returns all idents in f via the result channel.
242func idents(f *ast.File) <-chan *ast.Ident {
243 v := make(chan *ast.Ident)
244 go func() {
245 ast.Walk(f, func(n ast.Node) bool {
246 if ident, ok := n.(*ast.Ident); ok {
247 v <- ident
248 }
249 return true
250 }, nil)
251 close(v)
252 }()
253 return v
254}
255
256// identCount returns the number of identifiers found in f.
257func identCount(f *ast.File) int {
258 n := 0
259 for range idents(f) {
260 n++
261 }
262 return n
263}
264
265// Verify that the SourcePos mode emits correct //line comments
266// by testing that position information for matching identifiers
267// is maintained.
268func TestSourcePos(t *testing.T) {
269 const src = `package p
270
271import (
272 "go/printer"
273 "math"
274 "regexp"
275)
276
277pi = 3.14 // TODO: allow on same line
278xx = 0
279t: {
280 x: int
281 y: int
282 z: int
283 u: number
284 v: number
285 w: number
286}
287e: a*t.x + b*t.y
288
289// two extra lines here // ...
290e2: c*t.z
291`
292
293 // parse original
294 f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments)
295 if err != nil {
296 t.Fatal(err)
297 }
298
299 // pretty-print original
300 var buf bytes.Buffer
301 err = (&config{UseSpaces: true, Tabwidth: 8}).fprint(&buf, f1)
302 if err != nil {
303 t.Fatal(err)
304 }
305
306 // parse pretty printed original
307 // (//line comments must be interpreted even w/o syntax.ParseComments set)
308 f2, err := parser.ParseFile(fset, "", buf.Bytes(),
Marcel van Lohuizen2bf066f2018-12-16 11:43:49 +0100309 parser.AllErrors, parser.ParseComments)
Marcel van Lohuizenb001da52018-12-10 15:34:30 +0100310 if err != nil {
311 t.Fatalf("%s\n%s", err, buf.Bytes())
312 }
313
314 // At this point the position information of identifiers in f2 should
315 // match the position information of corresponding identifiers in f1.
316
317 // number of identifiers must be > 0 (test should run) and must match
318 n1 := identCount(f1)
319 n2 := identCount(f2)
320 if n1 == 0 {
321 t.Fatal("got no idents")
322 }
323 if n2 != n1 {
324 t.Errorf("got %d idents; want %d", n2, n1)
325 }
326
327 // verify that all identifiers have correct line information
328 i2range := idents(f2)
329 for i1 := range idents(f1) {
330 i2 := <-i2range
331
332 if i2 == nil || i1 == nil {
333 t.Fatal("non nil identifiers")
334 }
335 if i2.Name != i1.Name {
336 t.Errorf("got ident %s; want %s", i2.Name, i1.Name)
337 }
338
339 l1 := fset.Position(i1.Pos()).Line
340 l2 := fset.Position(i2.Pos()).Line
341 if l2 != l1 {
342 t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name)
343 }
344 }
345
346 if t.Failed() {
347 t.Logf("\n%s", buf.Bytes())
348 }
349}
350
351var decls = []string{
352 `import "fmt"`,
353 "pi = 3.1415\ne = 2.71828\n\nx = pi",
354}
355
356func TestDeclLists(t *testing.T) {
357 for _, src := range decls {
358 file, err := parser.ParseFile(fset, "", "package p\n"+src, parser.ParseComments)
359 if err != nil {
360 panic(err) // error in test
361 }
362
363 var buf bytes.Buffer
364 err = Fprint(&buf, file.Decls) // only print declarations
365 if err != nil {
366 panic(err) // error in test
367 }
368
369 out := buf.String()
370
371 if out != src {
372 t.Errorf("\ngot : %q\nwant: %q\n", out, src)
373 }
374 }
375}
376
377var stmts = []string{
378 "i := 0",
379 "select {}\nvar a, b = 1, 2\nreturn a + b",
380 "go f()\ndefer func() {}()",
381}
382
383type limitWriter struct {
384 remaining int
385 errCount int
386}
387
388func (l *limitWriter) Write(buf []byte) (n int, err error) {
389 n = len(buf)
390 if n >= l.remaining {
391 n = l.remaining
392 err = io.EOF
393 l.errCount++
394 }
395 l.remaining -= n
396 return n, err
397}
398
399// TextX is a skeleton test that can be filled in for debugging one-off cases.
400// Do not remove.
401func TestX(t *testing.T) {
402 const src = `
403 { e: k <-
404 for a, v in s}
405 a: b
406
407`
408 b, err := format([]byte(src), 0)
409 if err != nil {
410 t.Error(err)
411 }
412 _ = b
413 // t.Error("\n", string(b))
414}