| // Copyright 2023 The Go Authors. All rights reserved. | |
| // Use of this source code is governed by a BSD-style | |
| // license that can be found in the LICENSE file. | |
| package relnote | |
| import ( | |
| "fmt" | |
| "io/fs" | |
| "os" | |
| "path/filepath" | |
| "reflect" | |
| "runtime" | |
| "slices" | |
| "strings" | |
| "testing" | |
| "testing/fstest" | |
| "github.com/google/go-cmp/cmp" | |
| "golang.org/x/tools/txtar" | |
| md "rsc.io/markdown" | |
| ) | |
| func TestCheckFragment(t *testing.T) { | |
| for _, test := range []struct { | |
| in string | |
| // part of err.Error(), or empty if success | |
| want string | |
| }{ | |
| { | |
| // has a TODO | |
| "# heading\nTODO(jba)", | |
| "", | |
| }, | |
| { | |
| // has a sentence | |
| "# heading\nSomething.", | |
| "", | |
| }, | |
| { | |
| // sentence is inside some formatting | |
| "# heading\n- _Some_*thing.*", | |
| "", | |
| }, | |
| { | |
| // questions and exclamations are OK | |
| "# H1\n Are questions ok? \n# H2\n Must write this note!", | |
| "", | |
| }, | |
| { | |
| "", | |
| "must contain a complete sentence", | |
| }, | |
| { | |
| "# heading", | |
| "must contain a complete sentence", | |
| }, | |
| } { | |
| got := CheckFragment(test.in) | |
| if test.want == "" { | |
| if got != nil { | |
| t.Errorf("%q: got %q, want nil", test.in, got) | |
| } | |
| } else if got == nil || !strings.Contains(got.Error(), test.want) { | |
| t.Errorf("%q: got %q, want error containing %q", test.in, got, test.want) | |
| } | |
| } | |
| } | |
| func TestMerge(t *testing.T) { | |
| testFiles, err := filepath.Glob(filepath.Join("testdata", "merge", "*.txt")) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(testFiles) == 0 { | |
| t.Fatal("no tests") | |
| } | |
| for _, f := range testFiles { | |
| t.Run(strings.TrimSuffix(filepath.Base(f), ".txt"), func(t *testing.T) { | |
| fsys, want, err := parseTestFile(f) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| gotDoc, err := Merge(fsys) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| got := md.ToMarkdown(gotDoc) | |
| if diff := cmp.Diff(want, got); diff != "" { | |
| t.Errorf("mismatch (-want, +got)\n%s", diff) | |
| } | |
| }) | |
| } | |
| } | |
| func TestStdlibPackage(t *testing.T) { | |
| for _, test := range []struct { | |
| in string | |
| want string | |
| }{ | |
| {"", ""}, | |
| {"net/a.md", ""}, | |
| {"stdlib/net/a.md", ""}, | |
| {"stdlib/minor/net/a.md", "net"}, | |
| {"stdlib/minor/heading.md", ""}, | |
| {"stdlib/minor/net/http/a.md", "net/http"}, | |
| } { | |
| got := stdlibPackage(test.in) | |
| if w := test.want; got != w { | |
| t.Errorf("%q: got %q, want %q", test.in, got, w) | |
| } | |
| } | |
| } | |
| func TestStdlibPackageHeading(t *testing.T) { | |
| h := stdlibPackageHeading("net/http", 1) | |
| got := md.ToMarkdown(h) | |
| want := "#### [`net/http`](/pkg/net/http/)\n" | |
| if got != want { | |
| t.Errorf("\ngot %q\nwant %q", got, want) | |
| } | |
| } | |
| // parseTestFile translates a txtar archive into an fs.FS, except for the | |
| // file "want", whose contents are returned separately. | |
| func parseTestFile(filename string) (fsys fs.FS, want string, err error) { | |
| ar, err := txtar.ParseFile(filename) | |
| if err != nil { | |
| return nil, "", err | |
| } | |
| mfs := make(fstest.MapFS) | |
| for _, f := range ar.Files { | |
| if f.Name == "want" { | |
| want = string(f.Data) | |
| } else { | |
| mfs[f.Name] = &fstest.MapFile{Data: f.Data} | |
| } | |
| } | |
| if want == "" { | |
| return nil, "", fmt.Errorf("%s: missing 'want'", filename) | |
| } | |
| return mfs, want, nil | |
| } | |
| func TestSortedMarkdownFilenames(t *testing.T) { | |
| want := []string{ | |
| "a.md", | |
| "b.md", | |
| "b/a.md", | |
| "b/c.md", | |
| "ba/a.md", | |
| } | |
| mfs := make(fstest.MapFS) | |
| for _, fn := range want { | |
| mfs[fn] = &fstest.MapFile{} | |
| } | |
| mfs["README"] = &fstest.MapFile{} | |
| mfs["b/other.txt"] = &fstest.MapFile{} | |
| got, err := sortedMarkdownFilenames(mfs) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if !slices.Equal(got, want) { | |
| t.Errorf("\ngot %v\nwant %v", got, want) | |
| } | |
| } | |
| func TestRemoveEmptySections(t *testing.T) { | |
| doc := NewParser().Parse(` | |
| # h1 | |
| not empty | |
| # h2 | |
| ## h3 | |
| ### h4 | |
| #### h5 | |
| ### h6 | |
| ### h7 | |
| ## h8 | |
| something | |
| ## h9 | |
| # h10 | |
| `) | |
| bs := removeEmptySections(doc.Blocks) | |
| got := md.ToMarkdown(&md.Document{Blocks: bs}) | |
| want := md.ToMarkdown(NewParser().Parse(` | |
| # h1 | |
| not empty | |
| # h2 | |
| ## h8 | |
| something | |
| `)) | |
| if got != want { | |
| t.Errorf("\ngot:\n%s\nwant:\n%s", got, want) | |
| } | |
| } | |
| func TestParseAPIFile(t *testing.T) { | |
| fsys := fstest.MapFS{ | |
| "123.next": &fstest.MapFile{Data: []byte(` | |
| pkg p1, type T struct | |
| pkg p2, func F(int, bool) #123 | |
| pkg syscall (windows-386), const WSAENOPROTOOPT = 10042 #62254 | |
| `)}, | |
| } | |
| got, err := parseAPIFile(fsys, "123.next") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| want := []APIFeature{ | |
| {"p1", "", "type T struct", 0}, | |
| {"p2", "", "func F(int, bool)", 123}, | |
| {"syscall", "(windows-386)", "const WSAENOPROTOOPT = 10042", 62254}, | |
| } | |
| if !reflect.DeepEqual(got, want) { | |
| t.Errorf("\ngot %#v\nwant %#v", got, want) | |
| } | |
| } | |
| func TestCheckAPIFile(t *testing.T) { | |
| testFiles, err := filepath.Glob(filepath.Join("testdata", "checkAPIFile", "*.txt")) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| if len(testFiles) == 0 { | |
| t.Fatal("no tests") | |
| } | |
| for _, f := range testFiles { | |
| t.Run(strings.TrimSuffix(filepath.Base(f), ".txt"), func(t *testing.T) { | |
| fsys, want, err := parseTestFile(f) | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| var got string | |
| gotErr := CheckAPIFile(fsys, "api.txt", fsys, "doc/next") | |
| if gotErr != nil { | |
| got = gotErr.Error() | |
| } | |
| want = strings.TrimSpace(want) | |
| if got != want { | |
| t.Errorf("\ngot %s\nwant %s", got, want) | |
| } | |
| }) | |
| } | |
| } | |
| func TestAllAPIFilesForErrors(t *testing.T) { | |
| if testing.Short() { | |
| t.Skip("skipping in short mode") | |
| } | |
| fsys := os.DirFS(filepath.Join(runtime.GOROOT(), "api")) | |
| apiFiles, err := fs.Glob(fsys, "*.txt") | |
| if err != nil { | |
| t.Fatal(err) | |
| } | |
| for _, f := range apiFiles { | |
| if _, err := parseAPIFile(fsys, f); err != nil { | |
| t.Errorf("parseTestFile(%q) failed with %v", f, err) | |
| } | |
| } | |
| } | |
| func TestSymbolLinks(t *testing.T) { | |
| for _, test := range []struct { | |
| in string | |
| want string | |
| }{ | |
| {"a b", "a b"}, | |
| {"a [b", "a [b"}, | |
| {"a [b[", "a [b["}, | |
| {"a b[X]", "a b[X]"}, | |
| {"a [Buffer] b", "a [`Buffer`](/pkg/bytes#Buffer) b"}, | |
| {"a [Buffer]\nb", "a [`Buffer`](/pkg/bytes#Buffer)\nb"}, | |
| {"a [bytes.Buffer], b", "a [`bytes.Buffer`](/pkg/bytes#Buffer), b"}, | |
| {"[bytes.Buffer.String]", "[`bytes.Buffer.String`](/pkg/bytes#Buffer.String)"}, | |
| {"a--[encoding/json.Marshal].", "a--[`encoding/json.Marshal`](/pkg/encoding/json#Marshal)."}, | |
| {"a [math] and s[math] and [NewBuffer].", "a [`math`](/pkg/math) and s[math] and [`NewBuffer`](/pkg/bytes#NewBuffer)."}, | |
| {"A [*log/slog.Logger]", "A [`*log/slog.Logger`](/pkg/log/slog#Logger)"}, | |
| {"Not in code `[math]`.", "Not in code `[math]`."}, | |
| // Link text that already has backticks. | |
| {"a [`Buffer`] b", "a [`Buffer`](/pkg/bytes#Buffer) b"}, | |
| {"[`bytes.Buffer.String`]", "[`bytes.Buffer.String`](/pkg/bytes#Buffer.String)"}, | |
| // Links inside inline elements with nested content. | |
| {"**must use [Buffer]**", "**must use [`Buffer`](/pkg/bytes#Buffer)**"}, | |
| {"*must use [Buffer] value*", "*must use [`Buffer`](/pkg/bytes#Buffer) value*"}, | |
| {"_**[Buffer]**_", "_**[`Buffer`](/pkg/bytes#Buffer)**_"}, | |
| } { | |
| doc := NewParser().Parse(test.in) | |
| addSymbolLinks(doc, "bytes") | |
| got := strings.TrimSpace(md.ToMarkdown(doc)) | |
| if got != test.want { | |
| t.Errorf("\nin: %s\ngot: %s\nwant: %s", test.in, got, test.want) | |
| } | |
| } | |
| } |