github.com/jwilner/pullquote@v1.0.0/pullquote_test.go (about) 1 package main 2 3 import ( 4 "bytes" 5 "context" 6 "fmt" 7 "io" 8 "io/ioutil" 9 "os" 10 "path" 11 "path/filepath" 12 "regexp" 13 "strconv" 14 "strings" 15 "testing" 16 ) 17 18 var reg = regexp.MustCompile 19 20 func Test_processFiles(t *testing.T) { 21 slCh := func(sl []string) <-chan string { 22 ch := make(chan string, len(sl)) 23 for _, s := range sl { 24 ch <- s 25 } 26 close(ch) 27 return ch 28 } 29 30 entries, err := ioutil.ReadDir("testdata/test_processFiles") 31 if err != nil { 32 t.Skipf("testdata/test_processFiles not usable: %v", err) 33 } 34 for _, e := range entries { 35 if !e.IsDir() { 36 continue 37 } 38 dataDir, err := filepath.Abs(filepath.Join("testdata/test_processFiles", e.Name())) 39 if err != nil { 40 t.Fatalf("abs: %v", err) 41 } 42 t.Run(e.Name(), func(t *testing.T) { 43 tDir := changeTmpDir(t) 44 defer tDir.Close() 45 46 var ( 47 inFiles, expectedFiles []string 48 golden bool 49 ) 50 if err := filepath.Walk(dataDir, func(path string, info os.FileInfo, err error) error { 51 switch { 52 case err != nil, info.IsDir(): 53 return err 54 case info.Name() == "GOLDEN": 55 golden = true 56 return nil 57 case strings.HasSuffix(path, ".expected.md"): 58 expectedFiles = append(expectedFiles, path) 59 inFiles = append(inFiles, strings.Replace(path[len(dataDir)+1:], ".expected", "", -1)) 60 return nil 61 default: 62 rel := path[len(dataDir)+1:] 63 return copyFile(path, rel) 64 } 65 }); err != nil { 66 t.Fatalf("unable to copy: %v", err) 67 } 68 69 checkEqual := func(t *testing.T) { 70 for i := 0; i < len(expectedFiles); i++ { 71 expected, err := ioutil.ReadFile(expectedFiles[i]) 72 if err != nil { 73 t.Fatal(err) 74 } 75 in, err := ioutil.ReadFile(inFiles[i]) 76 if err != nil { 77 t.Fatal(err) 78 } 79 if !bytes.Equal(expected, in) { 80 if golden { 81 if err := ioutil.WriteFile(expectedFiles[i], in, 0o644); err != nil { 82 t.Fatal(err) 83 } 84 return 85 } 86 t.Fatalf("outputs did not match\nwanted:\n\n%v\n\ngot:\n\n%v", string(expected), string(in)) 87 } 88 } 89 } 90 91 t.Run("first pass", func(t *testing.T) { 92 if err := processFiles(context.Background(), false, slCh(inFiles)); err != nil { 93 t.Fatal(err) 94 } 95 checkEqual(t) 96 }) 97 98 if t.Failed() { 99 t.SkipNow() 100 } 101 102 t.Run("idempotent", func(t *testing.T) { 103 if err := processFiles(context.Background(), false, slCh(inFiles)); err != nil { 104 t.Fatal(err) 105 } 106 checkEqual(t) 107 }) 108 }) 109 } 110 } 111 112 func Test_processFile(t *testing.T) { 113 for _, c := range []struct { 114 name string 115 files [][2]string 116 input, expected, err string 117 }{ 118 { 119 "inserts", 120 [][2]string{ 121 { 122 "my/path.go", 123 ` 124 hello 125 <!-- pullquote src=local.go start="func fooBar\\(\\) {" end="}" --> 126 <!-- /pullquote --> 127 bye 128 `, 129 }, 130 { 131 "my/local.go", 132 ` 133 func fooBar() { 134 // OK COOL 135 } 136 `, 137 }, 138 }, 139 "my/path.go", 140 ` 141 hello 142 <!-- pullquote src=local.go start="func fooBar\\(\\) {" end="}" --> 143 func fooBar() { 144 // OK COOL 145 } 146 <!-- /pullquote --> 147 bye 148 `, 149 "", 150 }, 151 { 152 "gopath", 153 [][2]string{ 154 { 155 "my/README.md", 156 ` 157 hello 158 <!-- goquote ./#fooBar --> 159 <!-- /goquote --> 160 bye 161 `, 162 }, 163 { 164 "my/local.go", 165 `package main 166 167 func fooBar() { 168 // OK COOL 169 } 170 `, 171 }, 172 }, 173 "my/README.md", 174 ` 175 hello 176 <!-- goquote ./#fooBar --> 177 ` + "```" + `go 178 func fooBar() { 179 // OK COOL 180 } 181 ` + "```" + ` 182 <!-- /goquote --> 183 bye 184 `, 185 "", 186 }, 187 } { 188 t.Run(c.name, func(t *testing.T) { 189 d := changeTmpDir(t) 190 defer d.Close() 191 192 for _, f := range c.files { 193 writeFile(t, f[0], f[1]) 194 } 195 196 s, err := processFile(context.Background(), d.tmpDir, c.input) 197 var errS string 198 if err != nil { 199 errS = err.Error() 200 } 201 if errS != c.err { 202 t.Fatalf("wanted %q but got %q", c.err, errS) 203 } else if err != nil { 204 return 205 } 206 207 b, err := ioutil.ReadFile(s) 208 if err != nil { 209 t.Fatal(err) 210 } 211 got := string(b) 212 if got != c.expected { 213 t.Fatalf("wanted:\n%q\ngot:\n%q", c.expected, got) 214 } 215 }) 216 } 217 } 218 219 func Test_parseLine(t *testing.T) { 220 for _, c := range []struct { 221 name, line string 222 pq *pullQuote 223 err string 224 }{ 225 { 226 "unquoted src", 227 "<!-- pullquote src=hi start=a end=b -->", 228 &pullQuote{originalTag: "pull", src: "hi", start: reg("a"), end: reg("b")}, 229 "", 230 }, 231 { 232 "quoted src", 233 `<!-- pullquote src="hi" start=a end=b -->`, 234 &pullQuote{originalTag: "pull", src: "hi", start: reg("a"), end: reg("b")}, 235 "", 236 }, 237 { 238 "escaped src", 239 `<!-- pullquote src="hi\\" start=a end=b -->`, 240 &pullQuote{originalTag: "pull", src: `hi\`, start: reg("a"), end: reg("b")}, 241 "", 242 }, 243 { 244 "escaped quote src", 245 `<!-- pullquote src="h \"" start=a end=b -->`, 246 &pullQuote{originalTag: "pull", src: `h "`, start: reg("a"), end: reg("b")}, 247 "", 248 }, 249 { 250 "escaped quote src middle", 251 `<!-- pullquote src="h\"here" start=a end=b -->`, 252 &pullQuote{originalTag: "pull", src: `h"here`, start: reg("a"), end: reg("b")}, 253 "", 254 }, 255 { 256 "escaped quote src middle multi backslash", 257 `<!-- pullquote src="h\\\"here" start=a end=b -->`, 258 &pullQuote{originalTag: "pull", src: `h\"here`, start: reg("a"), end: reg("b")}, 259 "", 260 }, 261 { 262 "start", 263 `<!-- pullquote src="here" start=hi end=b -->`, 264 &pullQuote{originalTag: "pull", src: `here`, start: reg("hi"), end: reg("b")}, 265 "", 266 }, 267 { 268 "here end", 269 `<!-- pullquote src="here.go" start="hi" end=bye -->`, 270 &pullQuote{originalTag: "pull", src: `here.go`, start: reg("hi"), end: reg("bye")}, 271 "", 272 }, 273 { 274 "no quotes", 275 `<!-- pullquote src=here.go start=hi end=bye fmt=codefence -->`, 276 &pullQuote{originalTag: "pull", src: `here.go`, start: reg("hi"), end: reg("bye"), fmt: "codefence"}, 277 "", 278 }, 279 { 280 "unclosed quotes", 281 `<!-- pullquote src="hi -->`, 282 nil, 283 fmt.Errorf("parsing pullquote at offset 0: %w", errTokUnterminated).Error(), 284 }, 285 { 286 "unclosed key", 287 `<!-- pullquote src -->`, 288 nil, 289 `parsing pullquote at offset 0: "src" requires value`, 290 }, 291 { 292 "unclosed escape", 293 `<!-- pullquote src="\ -->`, 294 nil, 295 fmt.Errorf("parsing pullquote at offset 0: %w", errTokUnterminated).Error(), 296 }, 297 { 298 "goquote", 299 `<!-- goquote .#Foo -->`, 300 &pullQuote{originalTag: "go", quoteType: "go", objPath: ".#Foo", fmt: "codefence", lang: "go"}, 301 "", 302 }, 303 { 304 "goquote quoted", 305 `<!-- goquote ".#Foo" -->`, 306 &pullQuote{originalTag: "go", quoteType: "go", objPath: ".#Foo", fmt: "codefence", lang: "go"}, 307 "", 308 }, 309 { 310 "goquote flag noreformat", 311 `<!-- goquote .#Foo noreformat -->`, 312 &pullQuote{ 313 originalTag: "go", 314 quoteType: "go", 315 objPath: ".#Foo", 316 fmt: "codefence", 317 lang: "go", 318 flags: noRealignTabs, 319 }, 320 "", 321 }, 322 { 323 "goquote example", 324 `<!-- goquote .#ExampleFooBar noreformat -->`, 325 &pullQuote{ 326 quoteType: "go", 327 originalTag: "go", 328 objPath: ".#ExampleFooBar", 329 fmt: "example", 330 lang: "go", 331 flags: noRealignTabs, 332 }, 333 "", 334 }, 335 { 336 "jsonquote example", 337 `<!-- jsonquote foo/bar#/biz/0/baz -->`, 338 &pullQuote{ 339 quoteType: "json", 340 originalTag: "json", 341 objPath: "foo/bar#/biz/0/baz", 342 fmt: "codefence", 343 lang: "json", 344 }, 345 "", 346 }, 347 } { 348 t.Run(c.name, func(t *testing.T) { 349 pq, err := readPullQuotes(context.Background(), strings.NewReader(c.line)) 350 351 var errS string 352 if err != nil { 353 errS = err.Error() 354 } 355 if errS != c.err { 356 t.Fatalf("wanted %q but got %q", c.err, err) 357 } else if err != nil { 358 return 359 } 360 361 comparePQ(t, "", c.line, c.pq, pq[0]) 362 }) 363 } 364 } 365 366 func Test_readPullQuotes(t *testing.T) { 367 type testCase struct { 368 name, contents string 369 pqs []*pullQuote 370 err string 371 } 372 cases := []testCase{ 373 { 374 "empty", 375 ``, 376 nil, 377 "", 378 }, 379 { 380 "valid single", 381 ` 382 <!-- pullquote src=here.go start=hi end=bye --> 383 <!-- /pullquote --> 384 `, 385 []*pullQuote{ 386 {originalTag: "pull", src: "here.go", start: reg("hi"), end: reg("bye")}, 387 }, 388 "", 389 }, 390 { 391 "valid multi", 392 ` 393 <!-- pullquote src=here.go start=hi end=bye --> 394 <!-- /pullquote --> 395 <!-- pullquote src=here1.go start=hi1 end=bye1 --> 396 <!-- /pullquote --> 397 `, 398 []*pullQuote{ 399 {originalTag: "pull", src: "here.go", start: reg("hi"), end: reg("bye")}, 400 {originalTag: "pull", src: "here1.go", start: reg("hi1"), end: reg("bye1")}, 401 }, 402 "", 403 }, 404 { 405 "skip codefence", 406 ` 407 ` + "```go" + ` 408 ~~~ 409 <!-- pullquote src=here.go start=hi end=bye --> 410 <!-- /pullquote --> 411 ~~~ 412 ` + "```" + ` 413 <!-- pullquote src=here1.go start=hi1 end=bye1 --><!-- /pullquote --> 414 `, 415 []*pullQuote{ 416 {originalTag: "pull", src: "here1.go", start: reg("hi1"), end: reg("bye1")}, 417 }, 418 "", 419 }, 420 { 421 "unfinished", 422 ` 423 <!-- pullquote src=here.go start=hi end=bye --> 424 `, 425 []*pullQuote{ 426 { 427 originalTag: "pull", 428 src: "here.go", 429 start: reg("hi"), 430 end: reg("bye"), 431 startIdx: 48, 432 endIdx: idxNoEnd, 433 }, 434 }, 435 "", 436 }, 437 { 438 "missing end", 439 ` 440 <!-- pullquote src=here.go start=hi --> 441 `, 442 nil, 443 "validating pullquote at offset 1: \"end\" cannot be unset", 444 }, 445 { 446 "missing start", 447 ` 448 <!-- pullquote src=here.go end=hi --> 449 `, 450 nil, 451 "validating pullquote at offset 1: \"start\" cannot be unset", 452 }, 453 { 454 "missing src", 455 ` 456 <!-- pullquote start=here.go end=hi --> 457 `, 458 nil, 459 "validating pullquote at offset 1: \"src\" cannot be unset", 460 }, 461 { 462 "markdown comment", 463 ` 464 <!-- pullquote src=README.md start=hello end=bye fmt=codefence lang=md --> 465 ` + "```" + `md 466 hello 467 <!-- goquote .#fooBar --> 468 bye 469 ` + "```" + ` 470 <!-- /pullquote --> 471 `, 472 []*pullQuote{ 473 { 474 src: "README.md", 475 start: reg("hello"), 476 end: reg("bye"), 477 fmt: "codefence", 478 lang: "md", 479 originalTag: "pull", 480 }, 481 }, 482 "", 483 }, 484 { 485 "from readme", 486 `hello 487 <!-- goquote .#ExampleFooBar --> 488 Code: 489 ` + "```" + `go 490 FooBar(i) 491 ` + "```" + ` 492 Output: 493 ` + "```" + ` 494 FooBarRan 0 495 ` + "```" + ` 496 <!-- /goquote --> 497 bye 498 `, 499 []*pullQuote{ 500 {quoteType: "go", originalTag: "go", objPath: ".#ExampleFooBar", fmt: fmtExample, lang: "go"}, 501 }, 502 "", 503 }, 504 } 505 506 if readMe := loadReadMe(t); readMe != "" { 507 cases = append(cases, testCase{ 508 name: "README.md", 509 contents: readMe, 510 pqs: []*pullQuote{ 511 { 512 quoteType: "go", 513 objPath: "testdata/test_processFiles/gopath#fooBar", 514 fmt: "codefence", 515 lang: "go", 516 originalTag: "go", 517 }, 518 { 519 src: "testdata/test_processFiles/gopath/README.md", 520 fmt: "codefence", 521 lang: "md", 522 originalTag: "pull", 523 start: reg("hello"), 524 end: reg("bye"), 525 }, 526 { 527 src: "testdata/test_processFiles/gopath/README.expected.md", 528 fmt: "codefence", 529 lang: "md", 530 originalTag: "pull", 531 start: reg("hello"), 532 end: reg("bye"), 533 }, 534 { 535 src: "testdata/test_processFiles/jsonpath/README.expected.md", 536 fmt: "codefence", 537 lang: "md", 538 originalTag: "pull", 539 start: reg("hello"), 540 end: reg("bye"), 541 }, 542 { 543 quoteType: "go", 544 objPath: ".#keySrc", 545 fmt: "codefence", 546 lang: "go", 547 originalTag: "go", 548 flags: includeGroup, 549 }, 550 { 551 quoteType: "go", 552 objPath: ".#keysCommonOptional", 553 fmt: "codefence", 554 lang: "go", 555 originalTag: "go", 556 flags: includeGroup, 557 }, 558 }, 559 }) 560 } 561 562 for _, c := range cases { 563 t.Run(c.name, func(t *testing.T) { 564 pqs, err := readPullQuotes(context.Background(), strings.NewReader(c.contents)) 565 var errS string 566 if err != nil { 567 errS = err.Error() 568 } 569 if c.err != errS { 570 t.Fatalf("wanted %q but got %q", c.err, errS) 571 } else if err != nil { 572 return 573 } 574 575 if len(pqs) != len(c.pqs) { 576 t.Fatalf("expected %d pqs but got %d", len(c.pqs), len(pqs)) 577 } 578 579 for i := 0; i < len(pqs); i++ { 580 comparePQ(t, strconv.Itoa(i), c.contents, c.pqs[i], pqs[i]) 581 } 582 }) 583 } 584 } 585 586 func loadReadMe(t *testing.T) string { 587 f, err := os.Open("README.md") 588 if os.IsNotExist(err) { 589 t.Logf("README.md does not exist; not running test") 590 return "" 591 } 592 if err != nil { 593 t.Fatalf("unable to load README.md: %v", err) 594 } 595 defer func() { 596 _ = f.Close() 597 }() 598 599 b, err := ioutil.ReadAll(f) 600 if err != nil { 601 t.Fatalf("Unable to read all: %v", err) 602 } 603 return string(b) 604 } 605 606 func Test_expandPullQuotes(t *testing.T) { 607 608 for _, c := range []struct { 609 name, fn string 610 files [][2]string 611 pqs []*pullQuote 612 expected []string 613 err string 614 }{ 615 { 616 "single", 617 "my/path.go", 618 [][2]string{{"my/local.go", 619 ` 620 func fooBar() { 621 // OK COOL 622 } 623 `}}, 624 []*pullQuote{ 625 {src: "local.go", start: reg(`func fooBar\(\) {`), end: reg(`}`)}, 626 }, 627 []string{"func fooBar() {\n\t// OK COOL\n}"}, 628 "", 629 }, 630 { 631 "endCount", 632 "my/path.go", 633 [][2]string{{"my/local.go", 634 ` 635 func fooBar() { 636 // OK COOL 637 } 638 639 func fooBaz() { 640 // ok 641 } 642 `}}, 643 []*pullQuote{ 644 {src: "local.go", start: reg(`func fooBar\(\) {`), end: reg(`}`), endCount: 2}, 645 }, 646 []string{"func fooBar() {\n\t// OK COOL\n}\n\nfunc fooBaz() {\n\t// ok\n}"}, 647 "", 648 }, 649 { 650 "two serially", 651 "my/path.go", 652 [][2]string{{"my/local.go", 653 ` 654 func fooBar() { 655 // OK COOL 656 } 657 658 func baz() { 659 // also good 660 } 661 `}}, 662 []*pullQuote{ 663 {src: "local.go", start: reg(`func baz\(\) {`), end: reg(`}`)}, 664 {src: "local.go", start: reg(`func fooBar\(\) {`), end: reg(`}`)}, 665 }, 666 []string{ 667 "func baz() {\n\t// also good\n}", 668 "func fooBar() {\n\t// OK COOL\n}", 669 }, 670 "", 671 }, 672 { 673 "overlap", 674 "my/path.go", 675 [][2]string{{"my/local.go", 676 ` 677 func fooBar() { 678 // OK COOL 679 } 680 `}}, 681 []*pullQuote{ 682 {src: "local.go", start: reg(`OK`), end: reg(`COOL`)}, 683 {src: "local.go", start: reg(`func fooBar\(\) {`), end: reg(`}`)}, 684 }, 685 []string{ 686 "\t// OK COOL", 687 "func fooBar() {\n\t// OK COOL\n}", 688 }, 689 "", 690 }, 691 { 692 "multipath", 693 "my/path.go", 694 [][2]string{ 695 { 696 "my/local.go", 697 ` 698 func fooBar() { 699 // OK COOL 700 } 701 `, 702 }, 703 { 704 "my/other.go", 705 ` 706 func fooBaz() { 707 // OK COOL 708 } 709 `, 710 }, 711 }, 712 []*pullQuote{ 713 {src: "local.go", start: reg(`func fooBar\(\) {`), end: reg(`}`)}, 714 {src: "other.go", start: reg(`func fooBaz\(\) {`), end: reg(`}`)}, 715 {src: "other.go", start: reg(`OK COOL`), end: reg(`OK COOL`)}, 716 }, 717 []string{ 718 "func fooBar() {\n\t// OK COOL\n}", 719 "func fooBaz() {\n\t// OK COOL\n}", 720 "\t// OK COOL", 721 }, 722 "", 723 }, 724 { 725 "func with doc comment", 726 "my/path.go", 727 [][2]string{{"my/local.go", 728 `package main 729 730 // doc comment 731 func fooBar() { 732 // OK COOL 733 fmt.Println("nice") 734 } 735 `}}, 736 []*pullQuote{ 737 {quoteType: "go", objPath: "local.go#fooBar"}, 738 }, 739 []string{"// doc comment\nfunc fooBar() {\n\t// OK COOL\n\tfmt.Println(\"nice\")\n}"}, 740 "", 741 }, 742 { 743 "type decl", 744 "my/path.go", 745 [][2]string{{"my/local.go", 746 `package main 747 748 type ( 749 // Foo does some stuff 750 // and other stuff 751 Foo struct { 752 // floating inline 753 A int // trailing inline 754 // Also this 755 } 756 // Bar does some other stuff 757 Bar struct { 758 B int 759 } 760 ) 761 `}}, 762 []*pullQuote{ 763 {quoteType: "go", objPath: "local.go#Foo"}, 764 }, 765 766 []string{ 767 `// Foo does some stuff 768 // and other stuff 769 Foo struct { 770 // floating inline 771 A int // trailing inline 772 // Also this 773 }`, 774 }, 775 "", 776 }, 777 { 778 "const decl", 779 "my/path.go", 780 [][2]string{{"my/local.go", 781 `package main 782 783 const ( 784 // Foo does some important things 785 Foo = iota 786 Bar 787 ) 788 789 `}}, 790 []*pullQuote{ 791 {quoteType: "go", objPath: "local.go#Foo"}, 792 }, 793 []string{ 794 `// Foo does some important things 795 Foo = iota`, 796 }, 797 "", 798 }, 799 { 800 "const decl include groups", 801 "my/path.go", 802 [][2]string{{"my/local.go", 803 `package main 804 805 // a bunch of great stuff 806 const ( 807 // Foo does some important things 808 Foo = iota 809 Bar 810 ) 811 812 `}}, 813 []*pullQuote{ 814 {quoteType: "go", objPath: "local.go#Foo", flags: includeGroup}, 815 }, 816 []string{ 817 `// a bunch of great stuff 818 const ( 819 // Foo does some important things 820 Foo = iota 821 Bar 822 )`, 823 }, 824 "", 825 }, 826 { 827 "third party", 828 "my/path.go", 829 [][2]string{}, 830 []*pullQuote{ 831 {quoteType: "go", objPath: "errors#New"}, 832 }, 833 []string{"// New returns an error that formats as the given text.\n// Each call to New returns a distinct error value even if the text is identical.\nfunc New(text string) error {\n\treturn &errorString{text}\n}"}, 834 "", 835 }, 836 { 837 "random var", 838 "my/path.go", 839 [][2]string{{"my/local.go", 840 `package main 841 842 // doc comment 843 func fooBar() { 844 // OK COOL 845 a := 23 846 fmt.Println(a) 847 } 848 `}}, 849 []*pullQuote{ 850 {quoteType: "go", objPath: "local.go#a"}, 851 }, 852 []string{"a := 23"}, 853 "", 854 }, 855 { 856 "zero var", 857 "my/path.go", 858 [][2]string{{"my/local.go", 859 `package main 860 861 // blah means nothing 862 var blah int 863 `}}, 864 []*pullQuote{ 865 {quoteType: "go", objPath: "local.go#blah"}, 866 }, 867 []string{ 868 `// blah means nothing 869 var blah int`, 870 }, 871 "", 872 }, 873 { 874 "inline const", 875 "my/path.go", 876 [][2]string{{"my/local.go", 877 `package main 878 879 func fooBar() { 880 // const blah 881 const a int = 23 882 } 883 `}}, 884 []*pullQuote{ 885 {quoteType: "go", objPath: "./#a"}, 886 }, 887 []string{ 888 `// const blah 889 const a int = 23`, 890 }, 891 "", 892 }, 893 } { 894 t.Run(c.name, func(t *testing.T) { 895 d := changeTmpDir(t) 896 defer d.Close() 897 898 for _, f := range c.files { 899 writeFile(t, f[0], f[1]) 900 } 901 for _, pq := range c.pqs { 902 if pq.src != "" { 903 pq.src = filepath.Join(filepath.Dir(c.fn), pq.src) 904 } 905 if pq.objPath != "" && strings.HasPrefix(pq.objPath, "./") || strings.Contains(pq.objPath, ".go") { 906 pq.objPath = filepath.Join(filepath.Dir(c.fn), pq.objPath) 907 } 908 } 909 res, err := expandPullQuotes(context.Background(), c.pqs) 910 var errS string 911 if err != nil { 912 errS = err.Error() 913 } 914 if errS != c.err { 915 t.Fatalf("expected %q but got %q", c.err, errS) 916 } else if err != nil { 917 return 918 } 919 920 if len(res) != len(c.expected) { 921 t.Fatalf("wanted %v matches but got %v", len(c.expected), len(res)) 922 } 923 for i := range res { 924 if res[i].String != c.expected[i] { 925 t.Errorf("wanted at %d:\n%q\ngot:\n%q", i, c.expected[i], res[i].String) 926 } 927 } 928 }) 929 } 930 } 931 932 func writeFile(t *testing.T, fileLoc, val string) { 933 if err := os.MkdirAll(path.Dir(fileLoc), 0o755); err != nil { 934 t.Fatal(err) 935 } 936 func() { 937 f, err := os.Create(fileLoc) 938 if err != nil { 939 t.Fatal(err) 940 } 941 defer func() { 942 _ = f.Close() 943 }() 944 if _, err := io.WriteString(f, val); err != nil { 945 t.Fatal(err) 946 } 947 }() 948 } 949 950 type testDir struct { 951 tmpDir, origWd string 952 } 953 954 func (t *testDir) Close() { 955 _ = os.Chdir(t.origWd) 956 _ = os.RemoveAll(t.tmpDir) 957 } 958 959 func changeTmpDir(t *testing.T) *testDir { 960 tmpDir, err := ioutil.TempDir("", strings.Replace(t.Name(), "/", "_", -1)) 961 if err != nil { 962 t.Fatal(err) 963 } 964 965 wd, err := os.Getwd() 966 if err != nil { 967 _ = os.RemoveAll(tmpDir) 968 t.Fatal(err) 969 } 970 971 if err = os.Chdir(tmpDir); err != nil { 972 _ = os.RemoveAll(tmpDir) 973 t.Fatal(err) 974 } 975 976 return &testDir{tmpDir, wd} 977 } 978 979 func comparePQ(t *testing.T, label string, src string, expected, got *pullQuote) { 980 type check struct { 981 name string 982 l, r interface{} 983 } 984 checks := []check{ 985 {"quoteType", expected.quoteType, got.quoteType}, 986 {"originalTag", expected.originalTag, got.originalTag}, 987 988 {"objPath", expected.objPath, got.objPath}, 989 {"src", expected.src, got.src}, 990 {"fmt", expected.fmt, got.fmt}, 991 {"lang", expected.lang, got.lang}, 992 993 {"endCount", expected.endCount, got.endCount}, 994 {"start", expected.start, got.start}, 995 {"end", expected.end, got.end}, 996 997 {"flags", int(expected.flags), int(got.flags)}, 998 } 999 1000 if expected.startIdx != 0 || expected.endIdx != 0 { 1001 checks = append(checks, []check{ 1002 {"startIdx", expected.startIdx, got.startIdx}, 1003 {"endIdx", expected.endIdx, got.endIdx}, 1004 }...) 1005 } 1006 1007 for _, comp := range checks { 1008 switch v := comp.l.(type) { 1009 case string: 1010 if v != comp.r.(string) { 1011 t.Errorf("%v.%v: wanted %q but got %q", label, comp.name, comp.l, comp.r) 1012 } 1013 case int: 1014 if v != comp.r.(int) { 1015 t.Errorf("%v.%v: wanted %v but got %v", label, comp.name, comp.l, comp.r) 1016 } 1017 case *regexp.Regexp: 1018 var expS, gotS string 1019 if v != nil { 1020 expS = v.String() 1021 } 1022 if r := comp.r.(*regexp.Regexp); r != nil { 1023 gotS = r.String() 1024 } 1025 if expS != gotS { 1026 t.Errorf("%v.%v: wanted %q but got %q", label, comp.name, expS, gotS) 1027 } 1028 default: 1029 panic("unknown type") 1030 } 1031 } 1032 1033 if src != "" && !(expected.startIdx != 0 || expected.endIdx != 0) { 1034 src = src[:got.startIdx] 1035 src = src[strings.LastIndex(src, "<!--"):] 1036 1037 pqs, err := readPullQuotes(context.Background(), strings.NewReader(src)) 1038 if err != nil { 1039 t.Errorf("unexpected error while loading pqs for comparison: %v", err) 1040 return 1041 } 1042 if len(pqs) == 0 { 1043 t.Errorf("expected at least one pullquote at provided offset") 1044 return 1045 } 1046 pqs[0].startIdx, pqs[0].endIdx = 0, 0 1047 comparePQ(t, label+".reloaded", "", pqs[0], got) 1048 } 1049 } 1050 1051 func copyFile(src, dst string) error { 1052 if err := os.MkdirAll(filepath.Dir(dst), 0o755); err != nil { 1053 return err 1054 } 1055 1056 f, err := os.Open(src) 1057 if err != nil { 1058 return err 1059 } 1060 defer func() { 1061 _ = f.Close() 1062 }() 1063 1064 g, err := os.Create(dst) 1065 if err != nil { 1066 return err 1067 } 1068 defer func() { 1069 _ = g.Close() 1070 }() 1071 1072 _, err = io.Copy(g, f) 1073 return err 1074 } 1075 1076 type pos struct { 1077 str string 1078 // if start end are provided, check they match offsets returned; otherwise, check str matches offsets returned. 1079 start, end int 1080 } 1081 1082 func Test_tokenizingScanner(t *testing.T) { 1083 for _, tt := range []struct { 1084 name string 1085 val string 1086 res []pos 1087 }{ 1088 { 1089 "whitespace stripped", 1090 " abc ", 1091 []pos{{"abc", 2, 5}}, 1092 }, 1093 { 1094 "quoted", 1095 ` "abc =" `, 1096 []pos{{"abc =", 2, 9}}, 1097 }, 1098 { 1099 "escaped quote", 1100 ` "abc \"" `, 1101 []pos{{`abc "`, 2, 10}}, 1102 }, 1103 { 1104 "equals in the middle", 1105 ` "abc \""=23 `, 1106 []pos{{`abc "`, 2, 10}, {`=`, 10, 11}, {`23`, 11, 13}}, 1107 }, 1108 { 1109 "double escape", 1110 ` "abc \\"=23 `, 1111 []pos{{`abc \`, 2, 10}, {`=`, 10, 11}, {`23`, 11, 13}}, 1112 }, 1113 } { 1114 t.Run(tt.name, func(t *testing.T) { 1115 runScannerTest(t, tokenizingScanner(strings.NewReader(tt.val)), tt.val, tt.res) 1116 }) 1117 } 1118 } 1119 1120 func Test_htmlCommentScanner(t *testing.T) { 1121 type testCase struct { 1122 name string 1123 val string 1124 res []pos 1125 } 1126 cases := []testCase{ 1127 { 1128 "finds", 1129 `abcdef<!--1234567890-->ghj`, 1130 []pos{{str: "<!--1234567890-->"}}, 1131 }, 1132 { 1133 "finds", 1134 `abcdef<!--1234567890-->ghj<!--ok-->`, 1135 []pos{{str: "<!--1234567890-->"}, {str: "<!--ok-->"}}, 1136 }, 1137 { 1138 "nothing between", 1139 `a<!---->b`, 1140 []pos{{str: "<!---->"}}, 1141 }, 1142 { 1143 "unfinished start", 1144 `abcdef<!--`, 1145 nil, 1146 }, 1147 { 1148 "unfinished end", 1149 `abcdef<!----`, 1150 nil, 1151 }, 1152 { 1153 "markdown comment", 1154 ` 1155 <!-- pullquote src=README.md start=hello end=bye fmt=codefence lang=md --> 1156 ` + "```" + `md 1157 hello 1158 <!-- goquote .#fooBar --> 1159 bye 1160 ` + "```" + ` 1161 <!-- /pullquote --> 1162 `, 1163 []pos{ 1164 {str: "<!-- pullquote src=README.md start=hello end=bye fmt=codefence lang=md -->"}, 1165 {str: "<!-- /pullquote -->"}, 1166 }, 1167 }, 1168 { 1169 "example", 1170 `hello 1171 <!-- goquote .#ExampleFooBar --> 1172 Code: 1173 ` + "```" + `go 1174 FooBar(i) 1175 ` + "```" + ` 1176 Output: 1177 ` + "```" + ` 1178 FooBarRan 0 1179 ` + "```" + ` 1180 <!-- /goquote --> 1181 bye 1182 `, 1183 []pos{ 1184 {str: "<!-- goquote .#ExampleFooBar -->"}, 1185 {str: "<!-- /goquote -->"}, 1186 }, 1187 }, 1188 } 1189 if readMe := loadReadMe(t); readMe != "" { 1190 cases = append(cases, testCase{ 1191 "README.md", 1192 readMe, 1193 []pos{ 1194 {str: "<!-- goquote testdata/test_processFiles/gopath#fooBar -->"}, 1195 {str: "<!-- /goquote -->"}, 1196 {str: "<!-- pullquote src=testdata/test_processFiles/gopath/README.md start=hello end=bye fmt=codefence lang=md -->"}, 1197 {str: "<!-- /pullquote -->"}, 1198 {str: "<!-- pullquote src=testdata/test_processFiles/gopath/README.expected.md start=hello end=bye fmt=codefence lang=md -->"}, 1199 {str: "<!-- /pullquote -->"}, 1200 {str: "<!-- pullquote src=testdata/test_processFiles/jsonpath/README.expected.md start=hello end=bye fmt=codefence lang=md -->"}, 1201 {str: "<!-- /pullquote -->"}, 1202 {str: "<!-- goquote .#keySrc includegroup -->"}, 1203 {str: "<!-- /goquote -->"}, 1204 {str: "<!-- goquote .#keysCommonOptional includegroup -->"}, 1205 {str: "<!-- /goquote -->"}, 1206 }, 1207 }) 1208 } 1209 1210 for _, tt := range cases { 1211 t.Run(tt.name, func(t *testing.T) { 1212 runScannerTest(t, htmlCommentScanner(strings.NewReader(tt.val)), tt.val, tt.res) 1213 }) 1214 } 1215 } 1216 1217 func runScannerTest(t *testing.T, sc *trackingScanner, val string, expected []pos) { 1218 var res []pos 1219 for sc.Scan() { 1220 res = append(res, pos{sc.Text(), sc.start, sc.end}) 1221 } 1222 if err := sc.Err(); err != nil { 1223 t.Fatalf("unexpected error: %v", err) 1224 } 1225 for i, r := range expected { 1226 if i >= len(res) { 1227 t.Errorf("token %d: wanted %v but missing", i, r) 1228 continue 1229 } 1230 if r.str != res[i].str { 1231 t.Errorf("token %d: wanted %v but got %v", i, r.str, res[i].str) 1232 } 1233 if r.start != 0 || r.end != 0 { 1234 if r.start != res[i].start { 1235 t.Errorf("token start %d: wanted %v but got %v", i, r.start, res[i].start) 1236 } 1237 if r.end != res[i].end { 1238 t.Errorf("token end %d: wanted %v but got %v", i, r.end, res[i].end) 1239 } 1240 continue 1241 } 1242 if val[res[i].start:res[i].end] != res[i].str { 1243 t.Errorf("expected returned indices [%d, %d) to match output but got %v", res[i].start, res[i].end, 1244 res[i].str) 1245 } 1246 } 1247 for i, r := range res { 1248 if i >= len(expected) { 1249 t.Errorf("token %d: got unexpected %v", i, r) 1250 continue 1251 } 1252 } 1253 } 1254 1255 func Test_filesChanged(t *testing.T) { 1256 td := changeTmpDir(t) 1257 defer td.Close() 1258 1259 var fs []*os.File 1260 defer func() { 1261 for _, f := range fs { 1262 _ = f.Close() 1263 } 1264 }() 1265 writeTmp := func(data string) *os.File { 1266 fA, err := ioutil.TempFile(td.tmpDir, "") 1267 if err != nil { 1268 t.Fatal(err) 1269 } 1270 fs = append(fs, fA) 1271 1272 if _, err = fA.WriteString(data); err != nil { 1273 t.Fatal(err) 1274 } 1275 1276 return fA 1277 } 1278 1279 a := writeTmp("abcdefghijklmonp") 1280 b := writeTmp("abcdefghijklmonp") 1281 c := writeTmp("abcdefghijklmon") // different 1282 1283 t.Run("identity", func(t *testing.T) { 1284 changed, err := filesChanged(a, a) 1285 if err != nil { 1286 t.Fatalf("unexpected failure: %v", err) 1287 } 1288 if changed { 1289 t.Fatal("Expected same file to be equal to itself") 1290 } 1291 }) 1292 1293 t.Run("same contents", func(t *testing.T) { 1294 changed, err := filesChanged(a, b) 1295 if err != nil { 1296 t.Fatalf("unexpected failure: %v", err) 1297 } 1298 if changed { 1299 t.Fatal("Expected same contexts to be equal") 1300 } 1301 }) 1302 1303 t.Run("different contents", func(t *testing.T) { 1304 changed, err := filesChanged(a, c) 1305 if err != nil { 1306 t.Fatalf("unexpected failure: %v", err) 1307 } 1308 if !changed { 1309 t.Fatal("Expected different contents to be unequal") 1310 } 1311 }) 1312 }