cuelang.org/go@v0.10.1/cue/format/format_test.go (about)

     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  	"io/fs"
    21  	"os"
    22  	"path"
    23  	"path/filepath"
    24  	"strings"
    25  	"testing"
    26  
    27  	"github.com/go-quicktest/qt"
    28  	"golang.org/x/tools/txtar"
    29  
    30  	"cuelang.org/go/cue/ast"
    31  	"cuelang.org/go/cue/parser"
    32  	"cuelang.org/go/cue/token"
    33  	"cuelang.org/go/internal"
    34  	"cuelang.org/go/internal/cuetest"
    35  )
    36  
    37  var (
    38  	defaultConfig = newConfig([]Option{})
    39  	Fprint        = defaultConfig.fprint
    40  )
    41  
    42  func TestFiles(t *testing.T) {
    43  	txtarFiles, err := filepath.Glob("testdata/*.txtar")
    44  	qt.Assert(t, qt.IsNil(err))
    45  	for _, txtarFile := range txtarFiles {
    46  		ar, err := txtar.ParseFile(txtarFile)
    47  		qt.Assert(t, qt.IsNil(err))
    48  
    49  		opts := []Option{TabIndent(true)}
    50  		for _, word := range strings.Fields(string(ar.Comment)) {
    51  			switch word {
    52  			case "simplify":
    53  				opts = append(opts, Simplify())
    54  			case "sort-imports":
    55  				opts = append(opts, sortImportsOption())
    56  			}
    57  		}
    58  
    59  		tfs, err := txtar.FS(ar)
    60  		qt.Assert(t, qt.IsNil(err))
    61  		inputFiles, err := fs.Glob(tfs, "*.input")
    62  		qt.Assert(t, qt.IsNil(err))
    63  
    64  		for _, inputFile := range inputFiles {
    65  			goldenFile := strings.TrimSuffix(inputFile, ".input") + ".golden"
    66  			t.Run(path.Join(txtarFile, inputFile), func(t *testing.T) {
    67  				src, err := fs.ReadFile(tfs, inputFile)
    68  				qt.Assert(t, qt.IsNil(err))
    69  
    70  				res, err := Source(src, opts...)
    71  				qt.Assert(t, qt.IsNil(err))
    72  
    73  				// make sure formatted output is syntactically correct
    74  				_, err = parser.ParseFile("", res, parser.AllErrors)
    75  				qt.Assert(t, qt.IsNil(err))
    76  
    77  				// update golden files if necessary
    78  				// TODO(mvdan): deduplicate this code with UpdateGoldenFiles on txtar files?
    79  				if cuetest.UpdateGoldenFiles {
    80  					for i := range ar.Files {
    81  						file := &ar.Files[i]
    82  						if file.Name == goldenFile {
    83  							file.Data = res
    84  							return
    85  						}
    86  					}
    87  					ar.Files = append(ar.Files, txtar.File{
    88  						Name: goldenFile,
    89  						Data: res,
    90  					})
    91  					return
    92  				}
    93  
    94  				// get golden
    95  				gld, err := fs.ReadFile(tfs, goldenFile)
    96  				qt.Assert(t, qt.IsNil(err))
    97  
    98  				// formatted source and golden must be the same
    99  				qt.Assert(t, qt.Equals(string(res), string(gld)))
   100  
   101  				// TODO(mvdan): check that all files format in an idempotent way,
   102  				// i.e. that formatting a golden file results in no changes.
   103  			})
   104  		}
   105  		if cuetest.UpdateGoldenFiles {
   106  			err = os.WriteFile(txtarFile, txtar.Format(ar), 0o666)
   107  			qt.Assert(t, qt.IsNil(err))
   108  		}
   109  	}
   110  }
   111  
   112  // Verify that the printer can be invoked during initialization.
   113  func init() {
   114  	const name = "foobar"
   115  	b, err := Fprint(&ast.Ident{Name: name})
   116  	if err != nil {
   117  		panic(err) // error in test
   118  	}
   119  	// in debug mode, the result contains additional information;
   120  	// ignore it
   121  	if s := string(b); !debug && s != name {
   122  		panic("got " + s + ", want " + name)
   123  	}
   124  }
   125  
   126  // TestNodes tests nodes that are invalid CUE, but are accepted by
   127  // format.
   128  func TestNodes(t *testing.T) {
   129  	testCases := []struct {
   130  		name string
   131  		in   ast.Node
   132  		out  string
   133  	}{{
   134  		name: "old-style octal numbers",
   135  		in:   ast.NewLit(token.INT, "0123"),
   136  		out:  "0o123",
   137  	}, {
   138  		name: "labels with multi-line strings",
   139  		in: &ast.Field{
   140  			Label: ast.NewLit(token.STRING,
   141  				`"""
   142  					foo
   143  					bar
   144  					"""`,
   145  			),
   146  			Value: ast.NewIdent("goo"),
   147  		},
   148  		out: `"foo\nbar": goo`,
   149  	}, {
   150  		name: "foo",
   151  		in: func() ast.Node {
   152  			st := ast.NewStruct("version", ast.NewString("foo"))
   153  			st = ast.NewStruct("info", st)
   154  			ast.AddComment(st.Elts[0], internal.NewComment(true, "FOO"))
   155  			return st
   156  		}(),
   157  		out: `{
   158  	// FOO
   159  	info: {
   160  		version: "foo"
   161  	}
   162  }`,
   163  	}}
   164  	for _, tc := range testCases {
   165  		t.Run(tc.name, func(t *testing.T) {
   166  			b, err := Node(tc.in, Simplify())
   167  			if err != nil {
   168  				t.Fatal(err)
   169  			}
   170  			if got := string(b); got != tc.out {
   171  				t.Errorf("\ngot:  %v; want: %v", got, tc.out)
   172  			}
   173  		})
   174  	}
   175  
   176  }
   177  
   178  // Verify that the printer doesn't crash if the AST contains Bad... nodes.
   179  func TestBadNodes(t *testing.T) {
   180  	const src = "package p\n("
   181  	const res = "package p\n\n(_|_)\n"
   182  	f, err := parser.ParseFile("", src, parser.ParseComments)
   183  	if err == nil {
   184  		t.Error("expected illegal program") // error in test
   185  	}
   186  	b, _ := Fprint(f)
   187  	if string(b) != res {
   188  		t.Errorf("got %q, expected %q", string(b), res)
   189  	}
   190  }
   191  func TestPackage(t *testing.T) {
   192  	f := &ast.File{
   193  		Decls: []ast.Decl{
   194  			&ast.Package{Name: ast.NewIdent("foo")},
   195  			&ast.EmbedDecl{
   196  				Expr: &ast.BasicLit{
   197  					Kind:     token.INT,
   198  					ValuePos: token.NoSpace.Pos(),
   199  					Value:    "1",
   200  				},
   201  			},
   202  		},
   203  	}
   204  	b, err := Node(f)
   205  	if err != nil {
   206  		t.Fatal(err)
   207  	}
   208  	const want = "package foo\n\n1\n"
   209  	if got := string(b); got != want {
   210  		t.Errorf("got %q, expected %q", got, want)
   211  	}
   212  }
   213  
   214  // idents is an iterator that returns all idents in f via the result channel.
   215  func idents(f *ast.File) <-chan *ast.Ident {
   216  	v := make(chan *ast.Ident)
   217  	go func() {
   218  		ast.Walk(f, func(n ast.Node) bool {
   219  			if ident, ok := n.(*ast.Ident); ok {
   220  				v <- ident
   221  			}
   222  			return true
   223  		}, nil)
   224  		close(v)
   225  	}()
   226  	return v
   227  }
   228  
   229  // identCount returns the number of identifiers found in f.
   230  func identCount(f *ast.File) int {
   231  	n := 0
   232  	for range idents(f) {
   233  		n++
   234  	}
   235  	return n
   236  }
   237  
   238  // Verify that the SourcePos mode emits correct //line comments
   239  // by testing that position information for matching identifiers
   240  // is maintained.
   241  func TestSourcePos(t *testing.T) {
   242  	const src = `package p
   243  
   244  import (
   245  	"go/printer"
   246  	"math"
   247  	"regexp"
   248  )
   249  
   250  let pi = 3.14
   251  let xx = 0
   252  t: {
   253  	x: int
   254  	y: int
   255  	z: int
   256  	u: number
   257  	v: number
   258  	w: number
   259  }
   260  e: a*t.x + b*t.y
   261  
   262  // two extra lines here // ...
   263  e2: c*t.z
   264  `
   265  
   266  	// parse original
   267  	f1, err := parser.ParseFile("src", src, parser.ParseComments)
   268  	if err != nil {
   269  		t.Fatal(err)
   270  	}
   271  
   272  	// pretty-print original
   273  	b, err := (&config{UseSpaces: true, Tabwidth: 8}).fprint(f1)
   274  	if err != nil {
   275  		t.Fatal(err)
   276  	}
   277  
   278  	// parse pretty printed original
   279  	// (//line comments must be interpreted even w/o syntax.ParseComments set)
   280  	f2, err := parser.ParseFile("", b, parser.AllErrors, parser.ParseComments)
   281  	if err != nil {
   282  		t.Fatalf("%s\n%s", err, b)
   283  	}
   284  
   285  	// At this point the position information of identifiers in f2 should
   286  	// match the position information of corresponding identifiers in f1.
   287  
   288  	// number of identifiers must be > 0 (test should run) and must match
   289  	n1 := identCount(f1)
   290  	n2 := identCount(f2)
   291  	if n1 == 0 {
   292  		t.Fatal("got no idents")
   293  	}
   294  	if n2 != n1 {
   295  		t.Errorf("got %d idents; want %d", n2, n1)
   296  	}
   297  
   298  	// verify that all identifiers have correct line information
   299  	i2range := idents(f2)
   300  	for i1 := range idents(f1) {
   301  		i2 := <-i2range
   302  
   303  		if i2 == nil || i1 == nil {
   304  			t.Fatal("non nil identifiers")
   305  		}
   306  		if i2.Name != i1.Name {
   307  			t.Errorf("got ident %s; want %s", i2.Name, i1.Name)
   308  		}
   309  
   310  		l1 := i1.Pos().Line()
   311  		l2 := i2.Pos().Line()
   312  		if l2 != l1 {
   313  			t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name)
   314  		}
   315  	}
   316  
   317  	if t.Failed() {
   318  		t.Logf("\n%s", b)
   319  	}
   320  }
   321  
   322  var decls = []string{
   323  	"package p\n\n" + `import "fmt"`,
   324  	"package p\n\n" + "let pi = 3.1415\nlet e = 2.71828\n\nlet x = pi",
   325  }
   326  
   327  func TestDeclLists(t *testing.T) {
   328  	for _, src := range decls {
   329  		file, err := parser.ParseFile("", src, parser.ParseComments)
   330  		if err != nil {
   331  			panic(err) // error in test
   332  		}
   333  
   334  		b, err := Fprint(file.Decls) // only print declarations
   335  		if err != nil {
   336  			panic(err) // error in test
   337  		}
   338  
   339  		out := string(b)
   340  
   341  		if out != src {
   342  			t.Errorf("\ngot : %q\nwant: %q\n", out, src)
   343  		}
   344  	}
   345  }
   346  
   347  func TestIncorrectIdent(t *testing.T) {
   348  	testCases := []struct {
   349  		ident string
   350  		out   string
   351  	}{
   352  		{"foo", "foo"},
   353  		{"a.b.c", `"a.b.c"`},
   354  		{"for", "for"},
   355  	}
   356  	for _, tc := range testCases {
   357  		t.Run(tc.ident, func(t *testing.T) {
   358  			b, _ := Node(&ast.Field{Label: ast.NewIdent(tc.ident), Value: ast.NewIdent("A")})
   359  			if got, want := string(b), tc.out+`: A`; got != want {
   360  				t.Errorf("got %q; want %q", got, want)
   361  			}
   362  		})
   363  	}
   364  }
   365  
   366  // TextX is a skeleton test that can be filled in for debugging one-off cases.
   367  // Do not remove.
   368  func TestX(t *testing.T) {
   369  	t.Skip()
   370  	const src = `
   371  
   372  `
   373  	b, err := Source([]byte(src), Simplify())
   374  	if err != nil {
   375  		t.Error(err)
   376  	}
   377  	_ = b
   378  	t.Error("\n", string(b))
   379  }