github.com/shijuvar/go@v0.0.0-20141209052335-e8f13700b70c/src/go/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/ioutil" 16 "path/filepath" 17 "testing" 18 "time" 19 ) 20 21 const ( 22 dataDir = "testdata" 23 tabwidth = 8 24 ) 25 26 var update = flag.Bool("update", false, "update golden files") 27 28 var fset = token.NewFileSet() 29 30 type checkMode uint 31 32 const ( 33 export checkMode = 1 << iota 34 rawFormat 35 idempotent 36 ) 37 38 // format parses src, prints the corresponding AST, verifies the resulting 39 // src is syntactically correct, and returns the resulting src or an error 40 // if any. 41 func format(src []byte, mode checkMode) ([]byte, error) { 42 // parse src 43 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 44 if err != nil { 45 return nil, fmt.Errorf("parse: %s\n%s", err, src) 46 } 47 48 // filter exports if necessary 49 if mode&export != 0 { 50 ast.FileExports(f) // ignore result 51 f.Comments = nil // don't print comments that are not in AST 52 } 53 54 // determine printer configuration 55 cfg := Config{Tabwidth: tabwidth} 56 if mode&rawFormat != 0 { 57 cfg.Mode |= RawFormat 58 } 59 60 // print AST 61 var buf bytes.Buffer 62 if err := cfg.Fprint(&buf, fset, f); err != nil { 63 return nil, fmt.Errorf("print: %s", err) 64 } 65 66 // make sure formatted output is syntactically correct 67 res := buf.Bytes() 68 if _, err := parser.ParseFile(fset, "", res, 0); err != nil { 69 return nil, fmt.Errorf("re-parse: %s\n%s", err, buf.Bytes()) 70 } 71 72 return res, nil 73 } 74 75 // lineAt returns the line in text starting at offset offs. 76 func lineAt(text []byte, offs int) []byte { 77 i := offs 78 for i < len(text) && text[i] != '\n' { 79 i++ 80 } 81 return text[offs:i] 82 } 83 84 // diff compares a and b. 85 func diff(aname, bname string, a, b []byte) error { 86 var buf bytes.Buffer // holding long error message 87 88 // compare lengths 89 if len(a) != len(b) { 90 fmt.Fprintf(&buf, "\nlength changed: len(%s) = %d, len(%s) = %d", aname, len(a), bname, len(b)) 91 } 92 93 // compare contents 94 line := 1 95 offs := 1 96 for i := 0; i < len(a) && i < len(b); i++ { 97 ch := a[i] 98 if ch != b[i] { 99 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", aname, line, i-offs+1, lineAt(a, offs)) 100 fmt.Fprintf(&buf, "\n%s:%d:%d: %s", bname, line, i-offs+1, lineAt(b, offs)) 101 fmt.Fprintf(&buf, "\n\n") 102 break 103 } 104 if ch == '\n' { 105 line++ 106 offs = i + 1 107 } 108 } 109 110 if buf.Len() > 0 { 111 return errors.New(buf.String()) 112 } 113 return nil 114 } 115 116 func runcheck(t *testing.T, source, golden string, mode checkMode) { 117 src, err := ioutil.ReadFile(source) 118 if err != nil { 119 t.Error(err) 120 return 121 } 122 123 res, err := format(src, mode) 124 if err != nil { 125 t.Error(err) 126 return 127 } 128 129 // update golden files if necessary 130 if *update { 131 if err := ioutil.WriteFile(golden, res, 0644); err != nil { 132 t.Error(err) 133 } 134 return 135 } 136 137 // get golden 138 gld, err := ioutil.ReadFile(golden) 139 if err != nil { 140 t.Error(err) 141 return 142 } 143 144 // formatted source and golden must be the same 145 if err := diff(source, golden, res, gld); err != nil { 146 t.Error(err) 147 return 148 } 149 150 if mode&idempotent != 0 { 151 // formatting golden must be idempotent 152 // (This is very difficult to achieve in general and for now 153 // it is only checked for files explicitly marked as such.) 154 res, err = format(gld, mode) 155 if err := diff(golden, fmt.Sprintf("format(%s)", golden), gld, res); err != nil { 156 t.Errorf("golden is not idempotent: %s", err) 157 } 158 } 159 } 160 161 func check(t *testing.T, source, golden string, mode checkMode) { 162 // run the test 163 cc := make(chan int) 164 go func() { 165 runcheck(t, source, golden, mode) 166 cc <- 0 167 }() 168 169 // wait with timeout 170 select { 171 case <-time.After(10 * time.Second): // plenty of a safety margin, even for very slow machines 172 // test running past time out 173 t.Errorf("%s: running too slowly", source) 174 case <-cc: 175 // test finished within allotted time margin 176 } 177 } 178 179 type entry struct { 180 source, golden string 181 mode checkMode 182 } 183 184 // Use go test -update to create/update the respective golden files. 185 var data = []entry{ 186 {"empty.input", "empty.golden", idempotent}, 187 {"comments.input", "comments.golden", 0}, 188 {"comments.input", "comments.x", export}, 189 {"comments2.input", "comments2.golden", idempotent}, 190 {"linebreaks.input", "linebreaks.golden", idempotent}, 191 {"expressions.input", "expressions.golden", idempotent}, 192 {"expressions.input", "expressions.raw", rawFormat | idempotent}, 193 {"declarations.input", "declarations.golden", 0}, 194 {"statements.input", "statements.golden", 0}, 195 {"slow.input", "slow.golden", idempotent}, 196 } 197 198 func TestFiles(t *testing.T) { 199 for _, e := range data { 200 source := filepath.Join(dataDir, e.source) 201 golden := filepath.Join(dataDir, e.golden) 202 check(t, source, golden, e.mode) 203 // TODO(gri) check that golden is idempotent 204 //check(t, golden, golden, e.mode) 205 } 206 } 207 208 // TestLineComments, using a simple test case, checks that consecutive line 209 // comments are properly terminated with a newline even if the AST position 210 // information is incorrect. 211 // 212 func TestLineComments(t *testing.T) { 213 const src = `// comment 1 214 // comment 2 215 // comment 3 216 package main 217 ` 218 219 fset := token.NewFileSet() 220 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 221 if err != nil { 222 panic(err) // error in test 223 } 224 225 var buf bytes.Buffer 226 fset = token.NewFileSet() // use the wrong file set 227 Fprint(&buf, fset, f) 228 229 nlines := 0 230 for _, ch := range buf.Bytes() { 231 if ch == '\n' { 232 nlines++ 233 } 234 } 235 236 const expected = 3 237 if nlines < expected { 238 t.Errorf("got %d, expected %d\n", nlines, expected) 239 t.Errorf("result:\n%s", buf.Bytes()) 240 } 241 } 242 243 // Verify that the printer can be invoked during initialization. 244 func init() { 245 const name = "foobar" 246 var buf bytes.Buffer 247 if err := Fprint(&buf, fset, &ast.Ident{Name: name}); err != nil { 248 panic(err) // error in test 249 } 250 // in debug mode, the result contains additional information; 251 // ignore it 252 if s := buf.String(); !debug && s != name { 253 panic("got " + s + ", want " + name) 254 } 255 } 256 257 // Verify that the printer doesn't crash if the AST contains BadXXX nodes. 258 func TestBadNodes(t *testing.T) { 259 const src = "package p\n(" 260 const res = "package p\nBadDecl\n" 261 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 262 if err == nil { 263 t.Error("expected illegal program") // error in test 264 } 265 var buf bytes.Buffer 266 Fprint(&buf, fset, f) 267 if buf.String() != res { 268 t.Errorf("got %q, expected %q", buf.String(), res) 269 } 270 } 271 272 // testComment verifies that f can be parsed again after printing it 273 // with its first comment set to comment at any possible source offset. 274 func testComment(t *testing.T, f *ast.File, srclen int, comment *ast.Comment) { 275 f.Comments[0].List[0] = comment 276 var buf bytes.Buffer 277 for offs := 0; offs <= srclen; offs++ { 278 buf.Reset() 279 // Printing f should result in a correct program no 280 // matter what the (incorrect) comment position is. 281 if err := Fprint(&buf, fset, f); err != nil { 282 t.Error(err) 283 } 284 if _, err := parser.ParseFile(fset, "", buf.Bytes(), 0); err != nil { 285 t.Fatalf("incorrect program for pos = %d:\n%s", comment.Slash, buf.String()) 286 } 287 // Position information is just an offset. 288 // Move comment one byte down in the source. 289 comment.Slash++ 290 } 291 } 292 293 // Verify that the printer produces a correct program 294 // even if the position information of comments introducing newlines 295 // is incorrect. 296 func TestBadComments(t *testing.T) { 297 const src = ` 298 // first comment - text and position changed by test 299 package p 300 import "fmt" 301 const pi = 3.14 // rough circle 302 var ( 303 x, y, z int = 1, 2, 3 304 u, v float64 305 ) 306 func fibo(n int) { 307 if n < 2 { 308 return n /* seed values */ 309 } 310 return fibo(n-1) + fibo(n-2) 311 } 312 ` 313 314 f, err := parser.ParseFile(fset, "", src, parser.ParseComments) 315 if err != nil { 316 t.Error(err) // error in test 317 } 318 319 comment := f.Comments[0].List[0] 320 pos := comment.Pos() 321 if fset.Position(pos).Offset != 1 { 322 t.Error("expected offset 1") // error in test 323 } 324 325 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "//-style comment"}) 326 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment */"}) 327 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style \n comment */"}) 328 testComment(t, f, len(src), &ast.Comment{Slash: pos, Text: "/*-style comment \n\n\n */"}) 329 } 330 331 type visitor chan *ast.Ident 332 333 func (v visitor) Visit(n ast.Node) (w ast.Visitor) { 334 if ident, ok := n.(*ast.Ident); ok { 335 v <- ident 336 } 337 return v 338 } 339 340 // idents is an iterator that returns all idents in f via the result channel. 341 func idents(f *ast.File) <-chan *ast.Ident { 342 v := make(visitor) 343 go func() { 344 ast.Walk(v, f) 345 close(v) 346 }() 347 return v 348 } 349 350 // identCount returns the number of identifiers found in f. 351 func identCount(f *ast.File) int { 352 n := 0 353 for range idents(f) { 354 n++ 355 } 356 return n 357 } 358 359 // Verify that the SourcePos mode emits correct //line comments 360 // by testing that position information for matching identifiers 361 // is maintained. 362 func TestSourcePos(t *testing.T) { 363 const src = ` 364 package p 365 import ( "go/printer"; "math" ) 366 const pi = 3.14; var x = 0 367 type t struct{ x, y, z int; u, v, w float32 } 368 func (t *t) foo(a, b, c int) int { 369 return a*t.x + b*t.y + 370 // two extra lines here 371 // ... 372 c*t.z 373 } 374 ` 375 376 // parse original 377 f1, err := parser.ParseFile(fset, "src", src, parser.ParseComments) 378 if err != nil { 379 t.Fatal(err) 380 } 381 382 // pretty-print original 383 var buf bytes.Buffer 384 err = (&Config{Mode: UseSpaces | SourcePos, Tabwidth: 8}).Fprint(&buf, fset, f1) 385 if err != nil { 386 t.Fatal(err) 387 } 388 389 // parse pretty printed original 390 // (//line comments must be interpreted even w/o parser.ParseComments set) 391 f2, err := parser.ParseFile(fset, "", buf.Bytes(), 0) 392 if err != nil { 393 t.Fatalf("%s\n%s", err, buf.Bytes()) 394 } 395 396 // At this point the position information of identifiers in f2 should 397 // match the position information of corresponding identifiers in f1. 398 399 // number of identifiers must be > 0 (test should run) and must match 400 n1 := identCount(f1) 401 n2 := identCount(f2) 402 if n1 == 0 { 403 t.Fatal("got no idents") 404 } 405 if n2 != n1 { 406 t.Errorf("got %d idents; want %d", n2, n1) 407 } 408 409 // verify that all identifiers have correct line information 410 i2range := idents(f2) 411 for i1 := range idents(f1) { 412 i2 := <-i2range 413 414 if i2.Name != i1.Name { 415 t.Errorf("got ident %s; want %s", i2.Name, i1.Name) 416 } 417 418 l1 := fset.Position(i1.Pos()).Line 419 l2 := fset.Position(i2.Pos()).Line 420 if l2 != l1 { 421 t.Errorf("got line %d; want %d for %s", l2, l1, i1.Name) 422 } 423 } 424 425 if t.Failed() { 426 t.Logf("\n%s", buf.Bytes()) 427 } 428 } 429 430 var decls = []string{ 431 `import "fmt"`, 432 "const pi = 3.1415\nconst e = 2.71828\n\nvar x = pi", 433 "func sum(x, y int) int\t{ return x + y }", 434 } 435 436 func TestDeclLists(t *testing.T) { 437 for _, src := range decls { 438 file, err := parser.ParseFile(fset, "", "package p;"+src, parser.ParseComments) 439 if err != nil { 440 panic(err) // error in test 441 } 442 443 var buf bytes.Buffer 444 err = Fprint(&buf, fset, file.Decls) // only print declarations 445 if err != nil { 446 panic(err) // error in test 447 } 448 449 out := buf.String() 450 if out != src { 451 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 452 } 453 } 454 } 455 456 var stmts = []string{ 457 "i := 0", 458 "select {}\nvar a, b = 1, 2\nreturn a + b", 459 "go f()\ndefer func() {}()", 460 } 461 462 func TestStmtLists(t *testing.T) { 463 for _, src := range stmts { 464 file, err := parser.ParseFile(fset, "", "package p; func _() {"+src+"}", parser.ParseComments) 465 if err != nil { 466 panic(err) // error in test 467 } 468 469 var buf bytes.Buffer 470 err = Fprint(&buf, fset, file.Decls[0].(*ast.FuncDecl).Body.List) // only print statements 471 if err != nil { 472 panic(err) // error in test 473 } 474 475 out := buf.String() 476 if out != src { 477 t.Errorf("\ngot : %q\nwant: %q\n", out, src) 478 } 479 } 480 } 481 482 func TestBaseIndent(t *testing.T) { 483 // The testfile must not contain multi-line raw strings since those 484 // are not indented (because their values must not change) and make 485 // this test fail. 486 const filename = "printer.go" 487 src, err := ioutil.ReadFile(filename) 488 if err != nil { 489 panic(err) // error in test 490 } 491 492 file, err := parser.ParseFile(fset, filename, src, 0) 493 if err != nil { 494 panic(err) // error in test 495 } 496 497 var buf bytes.Buffer 498 for indent := 0; indent < 4; indent++ { 499 buf.Reset() 500 (&Config{Tabwidth: tabwidth, Indent: indent}).Fprint(&buf, fset, file) 501 // all code must be indented by at least 'indent' tabs 502 lines := bytes.Split(buf.Bytes(), []byte{'\n'}) 503 for i, line := range lines { 504 if len(line) == 0 { 505 continue // empty lines don't have indentation 506 } 507 n := 0 508 for j, b := range line { 509 if b != '\t' { 510 // end of indentation 511 n = j 512 break 513 } 514 } 515 if n < indent { 516 t.Errorf("line %d: got only %d tabs; want at least %d: %q", i, n, indent, line) 517 } 518 } 519 } 520 } 521 522 // TestFuncType tests that an ast.FuncType with a nil Params field 523 // can be printed (per go/ast specification). Test case for issue 3870. 524 func TestFuncType(t *testing.T) { 525 src := &ast.File{ 526 Name: &ast.Ident{Name: "p"}, 527 Decls: []ast.Decl{ 528 &ast.FuncDecl{ 529 Name: &ast.Ident{Name: "f"}, 530 Type: &ast.FuncType{}, 531 }, 532 }, 533 } 534 535 var buf bytes.Buffer 536 if err := Fprint(&buf, fset, src); err != nil { 537 t.Fatal(err) 538 } 539 got := buf.String() 540 541 const want = `package p 542 543 func f() 544 ` 545 546 if got != want { 547 t.Fatalf("got:\n%s\nwant:\n%s\n", got, want) 548 } 549 } 550 551 // TextX is a skeleton test that can be filled in for debugging one-off cases. 552 // Do not remove. 553 func TestX(t *testing.T) { 554 const src = ` 555 package p 556 func _() {} 557 ` 558 _, err := format([]byte(src), 0) 559 if err != nil { 560 t.Error(err) 561 } 562 }