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  }