github.com/remobjects/goldbaselibrary@v0.0.0-20230924164425-d458680a936b/Source/Gold/printer/printer_test.go (about) 1 // Copyright 2009 The Go Authors. All rights reserved. 2 // Use of this source code is governed by a BSD-style 3 // license that can be found in the LICENSE file. 4 5 package printer 6 7 import ( 8 "bytes" 9 "errors" 10 "flag" 11 "fmt" 12 "go/ast" 13 "go/parser" 14 "go/token" 15 "io" 16 "io/ioutil" 17 "path/filepath" 18 "testing" 19 "time" 20 ) 21 22 const ( 23 dataDir = "testdata" 24 tabwidth = 8 25 ) 26 27 var update = flag.Bool("update", false, "update golden files") 28 29 var fset = token.NewFileSet() 30 31 type checkMode uint 32 33 const ( 34 export checkMode = 1 << iota 35 rawFormat 36 idempotent 37 ) 38 39 // format parses src, prints the corresponding AST, verifies the resulting 40 // src is syntactically correct, and returns the resulting src or an error 41 // if any. 42 func format(src []byte, mode checkMode) ([]byte, error) { 43 // parse src 44 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 45 if err != nil { 46 return nil, fmt.Errorf("parse: %s\n%s", err, src) 47 } 48 49 // filter exports if necessary 50 if mode&export != 0 { 51 ast.FileExports(f) // ignore result 52 f.Comments = nil // don't print comments that are not in AST 53 } 54 55 // determine printer configuration 56 cfg := Config{Tabwidth: tabwidth} 57 if mode&rawFormat != 0 { 58 cfg.Mode |= RawFormat 59 } 60 61 // print AST 62 var buf bytes.Buffer 63 if err := cfg.Fprint(&buf, fset, f); err != nil { 64 return nil, fmt.Errorf("print: %s", err) 65 } 66 67 // make sure formatted output is syntactically correct 68 res := buf.Bytes() 69 if _, err := parser.ParseFile(fset, "", res, 0); err != nil { 70 return nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes()) 71 } 72 73 return res, nil 74 } 75 76 // lineAt returns the line in text starting at offset offs. 77 func lineAt(text []byte, offs int) []byte { 78 i := offs 79 for i < len(text) && text[i] != '\n' { 80 i++ 81 } 82 return text[offs:i] 83 } 84 85 // diff compares a and b. 86 func diff(aname, bname string, a, b []byte) error { 87 var buf bytes.Buffer // holding long error message 88 89 // compare lengths 90 if len(a) != len(b) { 91 fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b)) 92 } 93 94 // compare contents 95 line := 1 96 offs := 1 97 for i := 0; i < len(a) && i < len(b); i++ { 98 ch := a[i] 99 if ch != b[i] { 100 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs)) 101 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs)) 102 fmt.Fprintf(&buf, "\n\n") 103 break 104 } 105 if ch == '\n' { 106 line++ 107 offs = i + 1 108 } 109 } 110 111 if buf.Len() > 0 { 112 return errors.New(buf.String()) 113 } 114 return nil 115 } 116 117 func runcheck(t *testing.T, source, golden string, mode checkMode) { 118 src, err := ioutil.ReadFile(source) 119 if err != nil { 120 t.Error(err) 121 return 122 } 123 124 res, err := format(src, mode) 125 if err != nil { 126 t.Error(err) 127 return 128 } 129 130 // update golden files if necessary 131 if *update { 132 if err := ioutil.WriteFile(golden, res, 0644); err != nil { 133 t.Error(err) 134 } 135 return 136 } 137 138 // get golden 139 gld, err := ioutil.ReadFile(golden) 140 if err != nil { 141 t.Error(err) 142 return 143 } 144 145 // formatted source and golden must be the same 146 if err := diff(source, golden, res, gld); err != nil { 147 t.Error(err) 148 return 149 } 150 151 if mode&idempotent != 0 { 152 // formatting golden must be idempotent 153 // (This is very difficult to achieve in general and for now 154 // it is only checked for files explicitly marked as such.) 155 res, err = format(gld, mode) 156 if err != nil { 157 t.Error(err) 158 return 159 } 160 if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil { 161 t.Errorf("golden is not idempotent: %s", err) 162 } 163 } 164 } 165 166 func check(t *testing.T, source, golden string, mode checkMode) { 167 // run the test 168 cc := make(chan int) 169 go func() { 170 runcheck(t, source, golden, mode) 171 cc <- 0 172 }() 173 174 // wait with timeout 175 select { 176 case <-time.After(10 * time.Second): // plenty of a safety margin, even for very slow machines 177 // test running past time out 178 t.Errorf("%s: running too slowly", source) 179 case <-cc: 180 // test finished within allotted time margin 181 } 182 } 183 184 type entry struct { 185 source, golden string 186 mode checkMode 187 } 188 189 // Use go test -update to create/update the respective golden files. 190 var data = []entry{ 191 {"empty.input", "empty.golden", idempotent}, 192 {"comments.input", "comments.golden", 0}, 193 {"comments.input", "comments.x", export}, 194 {"comments2.input", "comments2.golden", idempotent}, 195 {"alignment.input", "alignment.golden", idempotent}, 196 {"linebreaks.input", "linebreaks.golden", idempotent}, 197 {"expressions.input", "expressions.golden", idempotent}, 198 {"expressions.input", "expressions.raw", rawFormat | idempotent}, 199 {"declarations.input", "declarations.golden", 0}, 200 {"statements.input", "statements.golden", 0}, 201 {"slow.input", "slow.golden", idempotent}, 202 {"complit.input", "complit.x", export}, 203 } 204 205 func TestFiles(t *testing.T) { 206 t.Parallel() 207 for _, e := range data { 208 source := filepath.Join(dataDir, e.source) 209 golden := filepath.Join(dataDir, e.golden) 210 mode := e.mode 211 t.Run(e.source, func(t *testing.T) { 212 t.Parallel() 213 check(t, source, golden, mode) 214 // TODO(gri) check that golden is idempotent 215 //check(t, golden, golden, e.mode) 216 }) 217 } 218 } 219 220 // TestLineComments, using a simple test case, checks that consecutive line 221 // comments are properly terminated with a newline even if the AST position 222 // information is incorrect. 223 // 224 func TestLineComments(t *testing.T) { 225 const src = `// comment 1 226 // comment 2 227 // comment 3 228 package main 229 ` 230 231 fset := token.NewFileSet() 232 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 233 if err != nil { 234 panic(err) // error in test 235 } 236 237 var buf bytes.Buffer 238 fset = token.NewFileSet() // use the wrong file set 239 Fprint(&buf, fset, f) 240 241 nlines := 0 242 for _, ch := range buf.Bytes() { 243 if ch == '\n' { 244 nlines++ 245 } 246 } 247 248 const expected = 3 249 if nlines < expected { 250 t.Errorf("got %d, expected %d\n", nlines, expected) 251 t.Errorf("result:\n%s", buf.Bytes()) 252 } 253 } 254 255 // Verify that the printer can be invoked during initialization. 256 func init() { 257 const name = "foobar" 258 var buf bytes.Buffer 259 if err := Fprint(&buf, fset, &ast.Ident{Name: name}); err != nil { 260 panic(err) // error in test 261 } 262 // in debug mode, the result contains additional information; 263 // ignore it 264 if s := buf.String(); !debug && s != name { 265 panic("got " + s + ", want " + name) 266 } 267 } 268 269 // Verify that the printer doesn't crash if the AST contains BadXXX nodes. 270 func TestBadNodes(t *testing.T) { 271 const src = "package p\n(" 272 const res = "package p\nBadDecl\n" 273 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 274 if err == nil { 275 t.Error("expected illegal program") // error in test 276 } 277 var buf bytes.Buffer 278 Fprint(&buf, fset, f) 279 if buf.String() != res { 280 t.Errorf("got %q, expected %q", buf.String(), res) 281 } 282 } 283 284 // testComment verifies that f can be parsed again after printing it 285 // with its first comment set to comment at any possible source offset. 286 func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) { 287 f.Comments[0].List[0] = comment 288 var buf bytes.Buffer 289 for offs := 0; offs <= srclen; offs++ { 290 buf.Reset() 291 // Printing f should result in a correct program no 292 // matter what the (incorrect) comment position is. 293 if err := Fprint(&buf, fset, f); err != nil { 294 t.Error(err) 295 } 296 if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil { 297 t.Fatalf("incorrect program for pos = %d:\n%s", comment.Slash, buf.String()) 298 } 299 // Position information is just an offset. 300 // Move comment one byte down in the source. 301 comment.Slash++ 302 } 303 } 304 305 // Verify that the printer produces a correct program 306 // even if the position information of comments introducing newlines 307 // is incorrect. 308 func TestBadComments(t *testing.T) { 309 t.Parallel() 310 const src = ` 311 // first comment - text and position changed by test 312 package p 313 import "fmt" 314 const pi = 3.14 // rough circle 315 var ( 316 x, y, z int = 1, 2, 3 317 u, v float64 318 ) 319 func fibo(n int) { 320 if n < 2 { 321 return n /* seed values */ 322 } 323 return fibo(n-1) + fibo(n-2) 324 } 325 ` 326 327 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 328 if err != nil { 329 t.Error(err) // error in test 330 } 331 332 comment := f.Comments[0].List[0] 333 pos := comment.Pos() 334 if fset.PositionFor(pos, false /* absolute position */).Offset != 1 { 335 t.Error("expected offset 1") // error in test 336 } 337 338 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "//-style comment"}) 339 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment */"}) 340 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style \n comment */"}) 341 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment \n\n\n */"}) 342 } 343 344 type visitor chan *ast.Ident 345 346 func (v visitor) Visit(n ast.Node) (w ast.Visitor) { 347 if ident, ok := n.(*ast.Ident); ok { 348 v <- ident 349 } 350 return v 351 } 352 353 // idents is an iterator that returns all idents in f via the result channel. 354 func idents(f *ast.File) <-chan *ast.Ident { 355 v := make(visitor) 356 go func() { 357 ast.Walk(v, f) 358 close(v) 359 }() 360 return v 361 } 362 363 // identCount returns the number of identifiers found in f. 364 func identCount(f *ast.File) int { 365 n := 0 366 for range idents(f) { 367 n++ 368 } 369 return n 370 } 371 372 // Verify that the SourcePos mode emits correct //line directives 373 // by testing that position information for matching identifiers 374 // is maintained. 375 func TestSourcePos(t *testing.T) { 376 const src = ` 377 package p 378 import ( "go/printer"; "math" ) 379 const pi = 3.14; var x = 0 380 type t struct{ x, y, z int; u, v, w float32 } 381 func (t *t) foo(a, b, c int) int { 382 return a*t.x + b*t.y + 383 // two extra lines here 384 // ... 385 c*t.z 386 } 387 ` 388 389 // parse original 390 f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments) 391 if err != nil { 392 t.Fatal(err) 393 } 394 395 // pretty-print original 396 var buf bytes.Buffer 397 err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) 398 if err != nil { 399 t.Fatal(err) 400 } 401 402 // parse pretty printed original 403 // (//line directives must be interpreted even w/o parser.ParseComments set) 404 f2, err := parser.ParseFile(fset, "", buf.Bytes(), 0) 405 if err != nil { 406 t.Fatalf("%s\n%s", err, buf.Bytes()) 407 } 408 409 // At this point the position information of identifiers in f2 should 410 // match the position information of corresponding identifiers in f1. 411 412 // number of identifiers must be > 0 (test should run) and must match 413 n1 := identCount(f1) 414 n2 := identCount(f2) 415 if n1 == 0 { 416 t.Fatal("got no idents") 417 } 418 if n2 != n1 { 419 t.Errorf("got %d idents; want %d", n2, n1) 420 } 421 422 // verify that all identifiers have correct line information 423 i2range := idents(f2) 424 for i1 := range idents(f1) { 425 i2 := <-i2range 426 427 if i2.Name != i1.Name { 428 t.Errorf("got ident %s; want %s", i2.Name, i1.Name) 429 } 430 431 // here we care about the relative (line-directive adjusted) positions 432 l1 := fset.Position(i1.Pos()).Line 433 l2 := fset.Position(i2.Pos()).Line 434 if l2 != l1 { 435 t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name) 436 } 437 } 438 439 if t.Failed() { 440 t.Logf("\n%s", buf.Bytes()) 441 } 442 } 443 444 // Verify that the SourcePos mode doesn't emit unnecessary //line directives 445 // before empty lines. 446 func TestIssue5945(t *testing.T) { 447 const orig = ` 448 package p // line 2 449 func f() {} // line 3 450 451 var x, y, z int 452 453 454 func g() { // line 8 455 } 456 ` 457 458 const want = `//line src.go:2 459 package p 460 461 //line src.go:3 462 func f() {} 463 464 var x, y, z int 465 466 //line src.go:8 467 func g() { 468 } 469 ` 470 471 // parse original 472 f1, err := parser.ParseFile(fset, "src.go", orig, 0) 473 if err != nil { 474 t.Fatal(err) 475 } 476 477 // pretty-print original 478 var buf bytes.Buffer 479 err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) 480 if err != nil { 481 t.Fatal(err) 482 } 483 got := buf.String() 484 485 // compare original with desired output 486 if got != want { 487 t.Errorf("got:\n%s\nwant:\n%s\n", got, want) 488 } 489 } 490 491 var decls = []string{ 492 `import "fmt"`, 493 "const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi", 494 "func sum(x, y int) int\t{ return x + y }", 495 } 496 497 func TestDeclLists(t *testing.T) { 498 for _, src := range decls { 499 file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments) 500 if err != nil { 501 panic(err) // error in test 502 } 503 504 var buf bytes.Buffer 505 err = Fprint(&buf, fset, file.Decls) // only print declarations 506 if err != nil { 507 panic(err) // error in test 508 } 509 510 out := buf.String() 511 if out != src { 512 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 513 } 514 } 515 } 516 517 var stmts = []string{ 518 "i := 0", 519 "select {}\nvar a, b = 1, 2\nreturn a + b", 520 "go f()\ndefer func() {}()", 521 } 522 523 func TestStmtLists(t *testing.T) { 524 for _, src := range stmts { 525 file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments) 526 if err != nil { 527 panic(err) // error in test 528 } 529 530 var buf bytes.Buffer 531 err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements 532 if err != nil { 533 panic(err) // error in test 534 } 535 536 out := buf.String() 537 if out != src { 538 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 539 } 540 } 541 } 542 543 func TestBaseIndent(t *testing.T) { 544 t.Parallel() 545 // The testfile must not contain multi-line raw strings since those 546 // are not indented (because their values must not change) and make 547 // this test fail. 548 const filename = "printer.go" 549 src, err := ioutil.ReadFile(filename) 550 if err != nil { 551 panic(err) // error in test 552 } 553 554 file, err := parser.ParseFile(fset, filename, src, 0) 555 if err != nil { 556 panic(err) // error in test 557 } 558 559 for indent := 0; indent < 4; indent++ { 560 indent := indent 561 t.Run(fmt.Sprint(indent), func(t *testing.T) { 562 t.Parallel() 563 var buf bytes.Buffer 564 (&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file) 565 // all code must be indented by at least 'indent' tabs 566 lines := bytes.Split(buf.Bytes(), []byte{'\n'}) 567 for i, line := range lines { 568 if len(line) == 0 { 569 continue // empty lines don't have indentation 570 } 571 n := 0 572 for j, b := range line { 573 if b != '\t' { 574 // end of indentation 575 n = j 576 break 577 } 578 } 579 if n < indent { 580 t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line) 581 } 582 } 583 }) 584 } 585 } 586 587 // TestFuncType tests that an ast.FuncType with a nil Params field 588 // can be printed (per go/ast specification). Test case for issue 3870. 589 func TestFuncType(t *testing.T) { 590 src := &ast.File{ 591 Name: &ast.Ident{Name: "p"}, 592 Decls: []ast.Decl{ 593 &ast.FuncDecl{ 594 Name: &ast.Ident{Name: "f"}, 595 Type: &ast.FuncType{}, 596 }, 597 }, 598 } 599 600 var buf bytes.Buffer 601 if err := Fprint(&buf, fset, src); err != nil { 602 t.Fatal(err) 603 } 604 got := buf.String() 605 606 const want = `package p 607 608 func f() 609 ` 610 611 if got != want { 612 t.Fatalf("got:\n%s\nwant:\n%s\n", got, want) 613 } 614 } 615 616 type limitWriter struct { 617 remaining int 618 errCount int 619 } 620 621 func (l *limitWriter) Write(buf []byte) (n int, err error) { 622 n = len(buf) 623 if n >= l.remaining { 624 n = l.remaining 625 err = io.EOF 626 l.errCount++ 627 } 628 l.remaining -= n 629 return n, err 630 } 631 632 // Test whether the printer stops writing after the first error 633 func TestWriteErrors(t *testing.T) { 634 t.Parallel() 635 const filename = "printer.go" 636 src, err := ioutil.ReadFile(filename) 637 if err != nil { 638 panic(err) // error in test 639 } 640 file, err := parser.ParseFile(fset, filename, src, 0) 641 if err != nil { 642 panic(err) // error in test 643 } 644 for i := 0; i < 20; i++ { 645 lw := &limitWriter{remaining: i} 646 err := (&Config{Mode: RawFormat}).Fprint(lw, fset, file) 647 if lw.errCount > 1 { 648 t.Fatal("Writes continued after first error returned") 649 } 650 // We expect errCount be 1 iff err is set 651 if (lw.errCount != 0) != (err != nil) { 652 t.Fatal("Expected err when errCount != 0") 653 } 654 } 655 } 656 657 // TextX is a skeleton test that can be filled in for debugging one-off cases. 658 // Do not remove. 659 func TestX(t *testing.T) { 660 const src = ` 661 package p 662 func _() {} 663 ` 664 _, err := format([]byte(src), 0) 665 if err != nil { 666 t.Error(err) 667 } 668 } 669 670 func TestCommentedNode(t *testing.T) { 671 const ( 672 input = `package main 673 674 func foo() { 675 // comment inside func 676 } 677 678 // leading comment 679 type bar int // comment2 680 681 ` 682 683 foo = `func foo() { 684 // comment inside func 685 }` 686 687 bar = `// leading comment 688 type bar int // comment2 689 ` 690 ) 691 692 fset := token.NewFileSet() 693 f, err := parser.ParseFile(fset, "input.go", input, parser.ParseComments) 694 if err != nil { 695 t.Fatal(err) 696 } 697 698 var buf bytes.Buffer 699 700 err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[0], Comments: f.Comments}) 701 if err != nil { 702 t.Fatal(err) 703 } 704 705 if buf.String() != foo { 706 t.Errorf("got %q, want %q", buf.String(), foo) 707 } 708 709 buf.Reset() 710 711 err = Fprint(&buf, fset, &CommentedNode{Node: f.Decls[1], Comments: f.Comments}) 712 if err != nil { 713 t.Fatal(err) 714 } 715 716 if buf.String() != bar { 717 t.Errorf("got %q, want %q", buf.String(), bar) 718 } 719 } 720 721 func TestIssue11151(t *testing.T) { 722 const src = "package p\t/*\r/1\r*\r/2*\r\r\r\r/3*\r\r+\r\r/4*/\n" 723 fset := token.NewFileSet() 724 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 725 if err != nil { 726 t.Fatal(err) 727 } 728 729 var buf bytes.Buffer 730 Fprint(&buf, fset, f) 731 got := buf.String() 732 const want = "package p\t/*/1*\r/2*\r/3*+/4*/\n" // \r following opening /* should be stripped 733 if got != want { 734 t.Errorf("\ngot : %q\nwant: %q", got, want) 735 } 736 737 // the resulting program must be valid 738 _, err = parser.ParseFile(fset, "", got, 0) 739 if err != nil { 740 t.Errorf("%v\norig: %q\ngot : %q", err, src, got) 741 } 742 } 743 744 // If a declaration has multiple specifications, a parenthesized 745 // declaration must be printed even if Lparen is token.NoPos. 746 func TestParenthesizedDecl(t *testing.T) { 747 // a package with multiple specs in a single declaration 748 const src = "package p; var ( a float64; b int )" 749 fset := token.NewFileSet() 750 f, err := parser.ParseFile(fset, "", src, 0) 751 if err != nil { 752 t.Fatal(err) 753 } 754 755 // print the original package 756 var buf bytes.Buffer 757 err = Fprint(&buf, fset, f) 758 if err != nil { 759 t.Fatal(err) 760 } 761 original := buf.String() 762 763 // now remove parentheses from the declaration 764 for i := 0; i != len(f.Decls); i++ { 765 f.Decls[i].(*ast.GenDecl).Lparen = token.NoPos 766 } 767 buf.Reset() 768 err = Fprint(&buf, fset, f) 769 if err != nil { 770 t.Fatal(err) 771 } 772 noparen := buf.String() 773 774 if noparen != original { 775 t.Errorf("got %q, want %q", noparen, original) 776 } 777 }