...

Source file src/cuelang.org/go/cue/format/format_test.go

Documentation: cuelang.org/go/cue/format

     1  // 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  
    15  package format
    16  
    17  // TODO: port more of the tests of go/printer
    18  
    19  import (
    20  	"bytes"
    21  	"fmt"
    22  	"os"
    23  	"path/filepath"
    24  	"testing"
    25  	"time"
    26  
    27  	"cuelang.org/go/cue/ast"
    28  	"cuelang.org/go/cue/errors"
    29  	"cuelang.org/go/cue/parser"
    30  	"cuelang.org/go/cue/token"
    31  	"cuelang.org/go/internal"
    32  	"cuelang.org/go/internal/cuetest"
    33  )
    34  
    35  var (
    36  	defaultConfig = newConfig([]Option{})
    37  	Fprint        = defaultConfig.fprint
    38  )
    39  
    40  const (
    41  	dataDir = "testdata"
    42  )
    43  
    44  type checkMode uint
    45  
    46  const (
    47  	_ checkMode = 1 << iota
    48  	idempotent
    49  	simplify
    50  	sortImps
    51  )
    52  
    53  // format parses src, prints the corresponding AST, verifies the resulting
    54  // src is syntactically correct, and returns the resulting src or an error
    55  // if any.
    56  func format(src []byte, mode checkMode) ([]byte, error) {
    57  	// parse src
    58  	opts := []Option{TabIndent(true)}
    59  	if mode&simplify != 0 {
    60  		opts = append(opts, Simplify())
    61  	}
    62  	if mode&sortImps != 0 {
    63  		opts = append(opts, sortImportsOption())
    64  	}
    65  
    66  	res, err := Source(src, opts...)
    67  	if err != nil {
    68  		return nil, err
    69  	}
    70  
    71  	// make sure formatted output is syntactically correct
    72  	if _, err := parser.ParseFile("", res, parser.AllErrors); err != nil {
    73  		return nil, errors.Append(err.(errors.Error),
    74  			errors.Newf(token.NoPos, "re-parse failed: %s", res))
    75  	}
    76  
    77  	return res, nil
    78  }
    79  
    80  // lineAt returns the line in text starting at offset offs.
    81  func lineAt(text []byte, offs int) []byte {
    82  	i := offs
    83  	for i < len(text) && text[i] != '\n' {
    84  		i++
    85  	}
    86  	return text[offs:i]
    87  }
    88  
    89  // diff compares a and b.
    90  func diff(aname, bname string, a, b []byte) error {
    91  	var buf bytes.Buffer // holding long error message
    92  
    93  	// compare lengths
    94  	if len(a) != len(b) {
    95  		fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b))
    96  	}
    97  
    98  	// compare contents
    99  	line := 1
   100  	offs := 1
   101  	for i := 0; i < len(a) && i < len(b); i++ {
   102  		ch := a[i]
   103  		if ch != b[i] {
   104  			fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs))
   105  			fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs))
   106  			fmt.Fprintf(&buf, "\n\n")
   107  			break
   108  		}
   109  		if ch == '\n' {
   110  			line++
   111  			offs = i + 1
   112  		}
   113  	}
   114  
   115  	if buf.Len() > 0 {
   116  		return errors.New(buf.String())
   117  	}
   118  	return nil
   119  }
   120  
   121  func runcheck(t *testing.T, source, golden string, mode checkMode) {
   122  	src, err := os.ReadFile(source)
   123  	if err != nil {
   124  		t.Error(err)
   125  		return
   126  	}
   127  
   128  	res, err := format(src, mode)
   129  	if err != nil {
   130  		b := &bytes.Buffer{}
   131  		errors.Print(b, err, nil)
   132  		t.Error(b.String())
   133  		return
   134  	}
   135  
   136  	// update golden files if necessary
   137  	if cuetest.UpdateGoldenFiles {
   138  		if err := os.WriteFile(golden, res, 0644); err != nil {
   139  			t.Error(err)
   140  		}
   141  		return
   142  	}
   143  
   144  	// get golden
   145  	gld, err := os.ReadFile(golden)
   146  	if err != nil {
   147  		t.Error(err)
   148  		return
   149  	}
   150  
   151  	// formatted source and golden must be the same
   152  	if err := diff(source, golden, res, gld); err != nil {
   153  		t.Error(err)
   154  		return
   155  	}
   156  
   157  	if mode&idempotent != 0 {
   158  		// formatting golden must be idempotent
   159  		// (This is very difficult to achieve in general and for now
   160  		// it is only checked for files explicitly marked as such.)
   161  		res, err = format(gld, mode)
   162  		if err != nil {
   163  			t.Fatal(err)
   164  		}
   165  		if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil {
   166  			t.Errorf("golden is not idempotent: %s", err)
   167  		}
   168  	}
   169  }
   170  
   171  func check(t *testing.T, source, golden string, mode checkMode) {
   172  	// run the test
   173  	cc := make(chan int)
   174  	go func() {
   175  		runcheck(t, source, golden, mode)
   176  		cc <- 0
   177  	}()
   178  
   179  	// wait with timeout
   180  	select {
   181  	case <-time.After(100000 * time.Second): // plenty of a safety margin, even for very slow machines
   182  		// test running past time out
   183  		t.Errorf("%s: running too slowly", source)
   184  	case <-cc:
   185  		// test finished within allotted time margin
   186  	}
   187  }
   188  
   189  type entry struct {
   190  	source, golden string
   191  	mode           checkMode
   192  }
   193  
   194  // Set CUE_UPDATE=1 to create/update the respective golden files.
   195  var data = []entry{
   196  	{"comments.input", "comments.golden", simplify},
   197  	{"simplify.input", "simplify.golden", simplify},
   198  	{"expressions.input", "expressions.golden", 0},
   199  	{"values.input", "values.golden", 0},
   200  	{"imports.input", "imports.golden", sortImps},
   201  }
   202  
   203  func TestFiles(t *testing.T) {
   204  	t.Parallel()
   205  	for _, e := range data {
   206  		source := filepath.Join(dataDir, e.source)
   207  		golden := filepath.Join(dataDir, e.golden)
   208  		mode := e.mode
   209  		t.Run(e.source, func(t *testing.T) {
   210  			t.Parallel()
   211  			check(t, source, golden, mode)
   212  			// TODO(gri) check that golden is idempotent
   213  			//check(t, golden, golden, e.mode)
   214  		})
   215  	}
   216  }
   217  
   218  // Verify that the printer can be invoked during initialization.
   219  func init() {
   220  	const name = "foobar"
   221  	b, err := Fprint(&ast.Ident{Name: name})
   222  	if err != nil {
   223  		panic(err) // error in test
   224  	}
   225  	// in debug mode, the result contains additional information;
   226  	// ignore it
   227  	if s := string(b); !debug && s != name {
   228  		panic("got " + s + ", want " + name)
   229  	}
   230  }
   231  
   232  // TestNodes tests nodes that are that are invalid CUE, but are accepted by
   233  // format.
   234  func TestNodes(t *testing.T) {
   235  	testCases := []struct {
   236  		name string
   237  		in   ast.Node
   238  		out  string
   239  	}{{
   240  		name: "old-style octal numbers",
   241  		in:   ast.NewLit(token.INT, "0123"),
   242  		out:  "0o123",
   243  	}, {
   244  		name: "labels with multi-line strings",
   245  		in: &ast.Field{
   246  			Label: ast.NewLit(token.STRING,
   247  				`"""
   248  					foo
   249  					bar
   250  					"""`,
   251  			),
   252  			Value: ast.NewIdent("goo"),
   253  		},
   254  		out: `"foo\nbar": goo`,
   255  	}, {
   256  		name: "foo",
   257  		in: func() ast.Node {
   258  			st := ast.NewStruct("version", ast.NewString("foo"))
   259  			st = ast.NewStruct("info", st)
   260  			ast.AddComment(st.Elts[0], internal.NewComment(true, "FOO"))
   261  			return st
   262  		}(),
   263  		out: `{
   264  	// FOO
   265  	info: {
   266  		version: "foo"
   267  	}
   268  }`,
   269  	}}
   270  	for _, tc := range testCases {
   271  		t.Run(tc.name, func(t *testing.T) {
   272  			b, err := Node(tc.in, Simplify())
   273  			if err != nil {
   274  				t.Fatal(err)
   275  			}
   276  			if got := string(b); got != tc.out {
   277  				t.Errorf("\ngot:  %v; want: %v", got, tc.out)
   278  			}
   279  		})
   280  	}
   281  
   282  }
   283  
   284  // Verify that the printer doesn't crash if the AST contains BadXXX nodes.
   285  func TestBadNodes(t *testing.T) {
   286  	const src = "package p\n("
   287  	const res = "package p\n\n(_|_)\n"
   288  	f, err := parser.ParseFile("", src, parser.ParseComments)
   289  	if err == nil {
   290  		t.Error("expected illegal program") // error in test
   291  	}
   292  	b, _ := Fprint(f)
   293  	if string(b) != res {
   294  		t.Errorf("got %q, expected %q", string(b), res)
   295  	}
   296  }
   297  func TestPackage(t *testing.T) {
   298  	f := &ast.File{
   299  		Decls: []ast.Decl{
   300  			&ast.Package{Name: ast.NewIdent("foo")},
   301  			&ast.EmbedDecl{
   302  				Expr: &ast.BasicLit{
   303  					Kind:     token.INT,
   304  					ValuePos: token.NoSpace.Pos(),
   305  					Value:    "1",
   306  				},
   307  			},
   308  		},
   309  	}
   310  	b, err := Node(f)
   311  	if err != nil {
   312  		t.Fatal(err)
   313  	}
   314  	const want = "package foo\n\n1\n"
   315  	if got := string(b); got != want {
   316  		t.Errorf("got %q, expected %q", got, want)
   317  	}
   318  }
   319  
   320  // idents is an iterator that returns all idents in f via the result channel.
   321  func idents(f *ast.File) <-chan *ast.Ident {
   322  	v := make(chan *ast.Ident)
   323  	go func() {
   324  		ast.Walk(f, func(n ast.Node) bool {
   325  			if ident, ok := n.(*ast.Ident); ok {
   326  				v <- ident
   327  			}
   328  			return true
   329  		}, nil)
   330  		close(v)
   331  	}()
   332  	return v
   333  }
   334  
   335  // identCount returns the number of identifiers found in f.
   336  func identCount(f *ast.File) int {
   337  	n := 0
   338  	for range idents(f) {
   339  		n++
   340  	}
   341  	return n
   342  }
   343  
   344  // Verify that the SourcePos mode emits correct //line comments
   345  // by testing that position information for matching identifiers
   346  // is maintained.
   347  func TestSourcePos(t *testing.T) {
   348  	const src = `package p
   349  
   350  import (
   351  	"go/printer"
   352  	"math"
   353  	"regexp"
   354  )
   355  
   356  let pi = 3.14
   357  let xx = 0
   358  t: {
   359  	x: int
   360  	y: int
   361  	z: int
   362  	u: number
   363  	v: number
   364  	w: number
   365  }
   366  e: a*t.x + b*t.y
   367  
   368  // two extra lines here // ...
   369  e2: c*t.z
   370  `
   371  
   372  	// parse original
   373  	f1, err := parser.ParseFile("src", src, parser.ParseComments)
   374  	if err != nil {
   375  		t.Fatal(err)
   376  	}
   377  
   378  	// pretty-print original
   379  	b, err := (&config{UseSpaces: true, Tabwidth: 8}).fprint(f1)
   380  	if err != nil {
   381  		t.Fatal(err)
   382  	}
   383  
   384  	// parse pretty printed original
   385  	// (//line comments must be interpreted even w/o syntax.ParseComments set)
   386  	f2, err := parser.ParseFile("", b, parser.AllErrors, parser.ParseComments)
   387  	if err != nil {
   388  		t.Fatalf("%s\n%s", err, b)
   389  	}
   390  
   391  	// At this point the position information of identifiers in f2 should
   392  	// match the position information of corresponding identifiers in f1.
   393  
   394  	// number of identifiers must be > 0 (test should run) and must match
   395  	n1 := identCount(f1)
   396  	n2 := identCount(f2)
   397  	if n1 == 0 {
   398  		t.Fatal("got no idents")
   399  	}
   400  	if n2 != n1 {
   401  		t.Errorf("got %d idents; want %d", n2, n1)
   402  	}
   403  
   404  	// verify that all identifiers have correct line information
   405  	i2range := idents(f2)
   406  	for i1 := range idents(f1) {
   407  		i2 := <-i2range
   408  
   409  		if i2 == nil || i1 == nil {
   410  			t.Fatal("non nil identifiers")
   411  		}
   412  		if i2.Name != i1.Name {
   413  			t.Errorf("got ident %s; want %s", i2.Name, i1.Name)
   414  		}
   415  
   416  		l1 := i1.Pos().Line()
   417  		l2 := i2.Pos().Line()
   418  		if l2 != l1 {
   419  			t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name)
   420  		}
   421  	}
   422  
   423  	if t.Failed() {
   424  		t.Logf("\n%s", b)
   425  	}
   426  }
   427  
   428  var decls = []string{
   429  	"package p\n\n" + `import "fmt"`,
   430  	"package p\n\n" + "let pi = 3.1415\nlet e = 2.71828\n\nlet x = pi",
   431  }
   432  
   433  func TestDeclLists(t *testing.T) {
   434  	for _, src := range decls {
   435  		file, err := parser.ParseFile("", src, parser.ParseComments)
   436  		if err != nil {
   437  			panic(err) // error in test
   438  		}
   439  
   440  		b, err := Fprint(file.Decls) // only print declarations
   441  		if err != nil {
   442  			panic(err) // error in test
   443  		}
   444  
   445  		out := string(b)
   446  
   447  		if out != src {
   448  			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
   449  		}
   450  	}
   451  }
   452  
   453  func TestIncorrectIdent(t *testing.T) {
   454  	testCases := []struct {
   455  		ident string
   456  		out   string
   457  	}{
   458  		{"foo", "foo"},
   459  		{"a.b.c", `"a.b.c"`},
   460  		{"for", "for"},
   461  	}
   462  	for _, tc := range testCases {
   463  		t.Run(tc.ident, func(t *testing.T) {
   464  			b, _ := Node(&ast.Field{Label: ast.NewIdent(tc.ident), Value: ast.NewIdent("A")})
   465  			if got, want := string(b), tc.out+`: A`; got != want {
   466  				t.Errorf("got %q; want %q", got, want)
   467  			}
   468  		})
   469  	}
   470  }
   471  
   472  // TextX is a skeleton test that can be filled in for debugging one-off cases.
   473  // Do not remove.
   474  func TestX(t *testing.T) {
   475  	t.Skip()
   476  	const src = `
   477  
   478  `
   479  	b, err := format([]byte(src), simplify)
   480  	if err != nil {
   481  		t.Error(err)
   482  	}
   483  	_ = b
   484  	t.Error("\n", string(b))
   485  }
   486  

View as plain text