| // Copyright 2018 The CUE Authors |
| // |
| // Licensed under the Apache License, Version 2.0 (the "License"); |
| // you may not use this file except in compliance with the License. |
| // You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, software |
| // distributed under the License is distributed on an "AS IS" BASIS, |
| // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. |
| // See the License for the specific language governing permissions and |
| // limitations under the License. |
| |
| package format |
| |
| // TODO: port more of the tests of go/printer |
| |
| import ( |
| "bytes" |
| "flag" |
| "fmt" |
| "io/ioutil" |
| "path/filepath" |
| "testing" |
| "time" |
| |
| "cuelang.org/go/cue/ast" |
| "cuelang.org/go/cue/errors" |
| "cuelang.org/go/cue/parser" |
| "cuelang.org/go/cue/token" |
| "cuelang.org/go/internal" |
| ) |
| |
| var ( |
| defaultConfig = newConfig([]Option{}) |
| Fprint = defaultConfig.fprint |
| ) |
| |
| const ( |
| dataDir = "testdata" |
| ) |
| |
| var update = flag.Bool("update", false, "update golden files") |
| |
| type checkMode uint |
| |
| const ( |
| _ checkMode = 1 << iota |
| idempotent |
| simplify |
| sortImps |
| ) |
| |
| // format parses src, prints the corresponding AST, verifies the resulting |
| // src is syntactically correct, and returns the resulting src or an error |
| // if any. |
| func format(src []byte, mode checkMode) ([]byte, error) { |
| // parse src |
| opts := []Option{TabIndent(true)} |
| if mode&simplify != 0 { |
| opts = append(opts, Simplify()) |
| } |
| if mode&sortImps != 0 { |
| opts = append(opts, sortImportsOption()) |
| } |
| |
| res, err := Source(src, opts...) |
| if err != nil { |
| return nil, err |
| } |
| |
| // make sure formatted output is syntactically correct |
| if _, err := parser.ParseFile("", res, parser.AllErrors); err != nil { |
| return nil, errors.Append(err.(errors.Error), |
| errors.Newf(token.NoPos, "re-parse failed: %s", res)) |
| } |
| |
| return res, nil |
| } |
| |
| // lineAt returns the line in text starting at offset offs. |
| func lineAt(text []byte, offs int) []byte { |
| i := offs |
| for i < len(text) && text[i] != '\n' { |
| i++ |
| } |
| return text[offs:i] |
| } |
| |
| // diff compares a and b. |
| func diff(aname, bname string, a, b []byte) error { |
| var buf bytes.Buffer // holding long error message |
| |
| // compare lengths |
| if len(a) != len(b) { |
| fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b)) |
| } |
| |
| // compare contents |
| line := 1 |
| offs := 1 |
| for i := 0; i < len(a) && i < len(b); i++ { |
| ch := a[i] |
| if ch != b[i] { |
| fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs)) |
| fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs)) |
| fmt.Fprintf(&buf, "\n\n") |
| break |
| } |
| if ch == '\n' { |
| line++ |
| offs = i + 1 |
| } |
| } |
| |
| if buf.Len() > 0 { |
| return errors.New(buf.String()) |
| } |
| return nil |
| } |
| |
| func runcheck(t *testing.T, source, golden string, mode checkMode) { |
| src, err := ioutil.ReadFile(source) |
| if err != nil { |
| t.Error(err) |
| return |
| } |
| |
| res, err := format(src, mode) |
| if err != nil { |
| b := &bytes.Buffer{} |
| errors.Print(b, err, nil) |
| t.Error(b.String()) |
| return |
| } |
| |
| // update golden files if necessary |
| if *update { |
| if err := ioutil.WriteFile(golden, res, 0644); err != nil { |
| t.Error(err) |
| } |
| return |
| } |
| |
| // get golden |
| gld, err := ioutil.ReadFile(golden) |
| if err != nil { |
| t.Error(err) |
| return |
| } |
| |
| // formatted source and golden must be the same |
| if err := diff(source, golden, res, gld); err != nil { |
| t.Error(err) |
| return |
| } |
| |
| if mode&idempotent != 0 { |
| // formatting golden must be idempotent |
| // (This is very difficult to achieve in general and for now |
| // it is only checked for files explicitly marked as such.) |
| res, err = format(gld, mode) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil { |
| t.Errorf("golden is not idempotent: %s", err) |
| } |
| } |
| } |
| |
| func check(t *testing.T, source, golden string, mode checkMode) { |
| // run the test |
| cc := make(chan int) |
| go func() { |
| runcheck(t, source, golden, mode) |
| cc <- 0 |
| }() |
| |
| // wait with timeout |
| select { |
| case <-time.After(100000 * time.Second): // plenty of a safety margin, even for very slow machines |
| // test running past time out |
| t.Errorf("%s: running too slowly", source) |
| case <-cc: |
| // test finished within allotted time margin |
| } |
| } |
| |
| type entry struct { |
| source, golden string |
| mode checkMode |
| } |
| |
| // Use go test -update to create/update the respective golden files. |
| var data = []entry{ |
| {"comments.input", "comments.golden", simplify}, |
| {"simplify.input", "simplify.golden", simplify}, |
| {"expressions.input", "expressions.golden", 0}, |
| {"values.input", "values.golden", 0}, |
| {"imports.input", "imports.golden", sortImps}, |
| } |
| |
| func TestFiles(t *testing.T) { |
| t.Parallel() |
| for _, e := range data { |
| source := filepath.Join(dataDir, e.source) |
| golden := filepath.Join(dataDir, e.golden) |
| mode := e.mode |
| t.Run(e.source, func(t *testing.T) { |
| t.Parallel() |
| check(t, source, golden, mode) |
| // TODO(gri) check that golden is idempotent |
| //check(t, golden, golden, e.mode) |
| }) |
| } |
| } |
| |
| // Verify that the printer can be invoked during initialization. |
| func init() { |
| const name = "foobar" |
| b, err := Fprint(&ast.Ident{Name: name}) |
| if err != nil { |
| panic(err) // error in test |
| } |
| // in debug mode, the result contains additional information; |
| // ignore it |
| if s := string(b); !debug && s != name { |
| panic("got " + s + ", want " + name) |
| } |
| } |
| |
| // TestNodes tests nodes that are that are invalid CUE, but are accepted by |
| // format. |
| func TestNodes(t *testing.T) { |
| testCases := []struct { |
| name string |
| in ast.Node |
| out string |
| }{{ |
| name: "old-style octal numbers", |
| in: ast.NewLit(token.INT, "0123"), |
| out: "0o123", |
| }, { |
| name: "labels with multi-line strings", |
| in: &ast.Field{ |
| Label: ast.NewLit(token.STRING, |
| `""" |
| foo |
| bar |
| """`, |
| ), |
| Value: ast.NewIdent("goo"), |
| }, |
| out: `"foo\nbar": goo`, |
| }, { |
| name: "foo", |
| in: func() ast.Node { |
| st := ast.NewStruct("version", ast.NewString("foo")) |
| st = ast.NewStruct("info", st) |
| ast.AddComment(st.Elts[0], internal.NewComment(true, "FOO")) |
| return st |
| }(), |
| out: `{ |
| // FOO |
| info: { |
| version: "foo" |
| } |
| }`, |
| }} |
| for _, tc := range testCases { |
| t.Run(tc.name, func(t *testing.T) { |
| b, err := Node(tc.in, Simplify()) |
| if err != nil { |
| t.Fatal(err) |
| } |
| if got := string(b); got != tc.out { |
| t.Errorf("\ngot: %v; want: %v", got, tc.out) |
| } |
| }) |
| } |
| |
| } |
| |
| // Verify that the printer doesn't crash if the AST contains BadXXX nodes. |
| func TestBadNodes(t *testing.T) { |
| const src = "package p\n(" |
| const res = "package p\n\n(BadExpr)\n" |
| f, err := parser.ParseFile("", src, parser.ParseComments) |
| if err == nil { |
| t.Error("expected illegal program") // error in test |
| } |
| b, _ := Fprint(f) |
| if string(b) != res { |
| t.Errorf("got %q, expected %q", string(b), res) |
| } |
| } |
| func TestPackage(t *testing.T) { |
| f := &ast.File{ |
| Decls: []ast.Decl{ |
| &ast.Package{Name: ast.NewIdent("foo")}, |
| &ast.EmbedDecl{ |
| Expr: &ast.BasicLit{ |
| Kind: token.INT, |
| ValuePos: token.NoSpace.Pos(), |
| Value: "1", |
| }, |
| }, |
| }, |
| } |
| b, err := Node(f) |
| if err != nil { |
| t.Fatal(err) |
| } |
| const want = "package foo\n\n1\n" |
| if got := string(b); got != want { |
| t.Errorf("got %q, expected %q", got, want) |
| } |
| } |
| |
| // idents is an iterator that returns all idents in f via the result channel. |
| func idents(f *ast.File) <-chan *ast.Ident { |
| v := make(chan *ast.Ident) |
| go func() { |
| ast.Walk(f, func(n ast.Node) bool { |
| if ident, ok := n.(*ast.Ident); ok { |
| v <- ident |
| } |
| return true |
| }, nil) |
| close(v) |
| }() |
| return v |
| } |
| |
| // identCount returns the number of identifiers found in f. |
| func identCount(f *ast.File) int { |
| n := 0 |
| for range idents(f) { |
| n++ |
| } |
| return n |
| } |
| |
| // Verify that the SourcePos mode emits correct //line comments |
| // by testing that position information for matching identifiers |
| // is maintained. |
| func TestSourcePos(t *testing.T) { |
| const src = `package p |
| |
| import ( |
| "go/printer" |
| "math" |
| "regexp" |
| ) |
| |
| let pi = 3.14 |
| let xx = 0 |
| t: { |
| x: int |
| y: int |
| z: int |
| u: number |
| v: number |
| w: number |
| } |
| e: a*t.x + b*t.y |
| |
| // two extra lines here // ... |
| e2: c*t.z |
| ` |
| |
| // parse original |
| f1, err := parser.ParseFile("src", src, parser.ParseComments) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // pretty-print original |
| b, err := (&config{UseSpaces: true, Tabwidth: 8}).fprint(f1) |
| if err != nil { |
| t.Fatal(err) |
| } |
| |
| // parse pretty printed original |
| // (//line comments must be interpreted even w/o syntax.ParseComments set) |
| f2, err := parser.ParseFile("", b, parser.AllErrors, parser.ParseComments) |
| if err != nil { |
| t.Fatalf("%s\n%s", err, b) |
| } |
| |
| // At this point the position information of identifiers in f2 should |
| // match the position information of corresponding identifiers in f1. |
| |
| // number of identifiers must be > 0 (test should run) and must match |
| n1 := identCount(f1) |
| n2 := identCount(f2) |
| if n1 == 0 { |
| t.Fatal("got no idents") |
| } |
| if n2 != n1 { |
| t.Errorf("got %d idents; want %d", n2, n1) |
| } |
| |
| // verify that all identifiers have correct line information |
| i2range := idents(f2) |
| for i1 := range idents(f1) { |
| i2 := <-i2range |
| |
| if i2 == nil || i1 == nil { |
| t.Fatal("non nil identifiers") |
| } |
| if i2.Name != i1.Name { |
| t.Errorf("got ident %s; want %s", i2.Name, i1.Name) |
| } |
| |
| l1 := i1.Pos().Line() |
| l2 := i2.Pos().Line() |
| if l2 != l1 { |
| t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name) |
| } |
| } |
| |
| if t.Failed() { |
| t.Logf("\n%s", b) |
| } |
| } |
| |
| var decls = []string{ |
| "package p\n\n" + `import "fmt"`, |
| "package p\n\n" + "let pi = 3.1415\nlet e = 2.71828\n\nlet x = pi", |
| } |
| |
| func TestDeclLists(t *testing.T) { |
| for _, src := range decls { |
| file, err := parser.ParseFile("", src, parser.ParseComments) |
| if err != nil { |
| panic(err) // error in test |
| } |
| |
| b, err := Fprint(file.Decls) // only print declarations |
| if err != nil { |
| panic(err) // error in test |
| } |
| |
| out := string(b) |
| |
| if out != src { |
| t.Errorf("\ngot : %q\nwant: %q\n", out, src) |
| } |
| } |
| } |
| |
| func TestIncorrectIdent(t *testing.T) { |
| testCases := []struct { |
| ident string |
| out string |
| }{ |
| {"foo", "foo"}, |
| {"a.b.c", `"a.b.c"`}, |
| {"for", "for"}, |
| } |
| for _, tc := range testCases { |
| t.Run(tc.ident, func(t *testing.T) { |
| b, _ := Node(&ast.Field{Label: ast.NewIdent(tc.ident), Value: ast.NewIdent("A")}) |
| if got, want := string(b), tc.out+`: A`; got != want { |
| t.Errorf("got %q; want %q", got, want) |
| } |
| }) |
| } |
| } |
| |
| // TextX is a skeleton test that can be filled in for debugging one-off cases. |
| // Do not remove. |
| func TestX(t *testing.T) { |
| t.Skip() |
| const src = ` |
| |
| ` |
| b, err := format([]byte(src), simplify) |
| if err != nil { |
| t.Error(err) |
| } |
| _ = b |
| t.Error("\n", string(b)) |
| } |