github.com/joomcode/cue@v0.4.4-0.20221111115225-539fe3512047/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 "bytes" 21 "fmt" 22 "io/ioutil" 23 "path/filepath" 24 "testing" 25 "time" 26 27 "github.com/joomcode/cue/cue/ast" 28 "github.com/joomcode/cue/cue/errors" 29 "github.com/joomcode/cue/cue/parser" 30 "github.com/joomcode/cue/cue/token" 31 "github.com/joomcode/cue/internal" 32 "github.com/joomcode/cue/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 := ioutil.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 := ioutil.WriteFile(golden, res, 0644); err != nil { 139 t.Error(err) 140 } 141 return 142 } 143 144 // get golden 145 gld, err := ioutil.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 }