blob: b0da92e1f371704b8d5828fd6b807912b4d24fe7 [file] [log] [blame]
// 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))
}