golang.org/x/build@v0.0.0-20240506185731-218518f32b70/relnote/relnote_test.go (about)

     1  // Copyright 2023 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 relnote
     6  
     7  import (
     8  	"fmt"
     9  	"io/fs"
    10  	"os"
    11  	"path/filepath"
    12  	"reflect"
    13  	"runtime"
    14  	"slices"
    15  	"strings"
    16  	"testing"
    17  	"testing/fstest"
    18  
    19  	"github.com/google/go-cmp/cmp"
    20  	"golang.org/x/tools/txtar"
    21  	md "rsc.io/markdown"
    22  )
    23  
    24  func TestCheckFragment(t *testing.T) {
    25  	for _, test := range []struct {
    26  		in string
    27  		// part of err.Error(), or empty if success
    28  		want string
    29  	}{
    30  		{
    31  			// has a TODO
    32  			"# heading\nTODO(jba)",
    33  			"",
    34  		},
    35  		{
    36  			// has a sentence
    37  			"# heading\nSomething.",
    38  			"",
    39  		},
    40  		{
    41  			// sentence is inside some formatting
    42  			"# heading\n- _Some_*thing.*",
    43  			"",
    44  		},
    45  		{
    46  			// questions and exclamations are OK
    47  			"# H1\n Are questions ok? \n# H2\n Must write this note!",
    48  			"",
    49  		},
    50  		{
    51  			"",
    52  			"must contain a complete sentence",
    53  		},
    54  		{
    55  			"# heading",
    56  			"must contain a complete sentence",
    57  		},
    58  	} {
    59  		got := CheckFragment(test.in)
    60  		if test.want == "" {
    61  			if got != nil {
    62  				t.Errorf("%q: got %q, want nil", test.in, got)
    63  			}
    64  		} else if got == nil || !strings.Contains(got.Error(), test.want) {
    65  			t.Errorf("%q: got %q, want error containing %q", test.in, got, test.want)
    66  		}
    67  	}
    68  }
    69  
    70  func TestMerge(t *testing.T) {
    71  	testFiles, err := filepath.Glob(filepath.Join("testdata", "merge", "*.txt"))
    72  	if err != nil {
    73  		t.Fatal(err)
    74  	}
    75  	if len(testFiles) == 0 {
    76  		t.Fatal("no tests")
    77  	}
    78  	for _, f := range testFiles {
    79  		t.Run(strings.TrimSuffix(filepath.Base(f), ".txt"), func(t *testing.T) {
    80  			fsys, want, err := parseTestFile(f)
    81  			if err != nil {
    82  				t.Fatal(err)
    83  			}
    84  			gotDoc, err := Merge(fsys)
    85  			if err != nil {
    86  				t.Fatal(err)
    87  			}
    88  			got := md.ToMarkdown(gotDoc)
    89  			if diff := cmp.Diff(want, got); diff != "" {
    90  				t.Errorf("mismatch (-want, +got)\n%s", diff)
    91  			}
    92  		})
    93  	}
    94  }
    95  
    96  func TestStdlibPackage(t *testing.T) {
    97  	for _, test := range []struct {
    98  		in   string
    99  		want string
   100  	}{
   101  		{"", ""},
   102  		{"net/a.md", ""},
   103  		{"stdlib/net/a.md", ""},
   104  		{"stdlib/minor/net/a.md", "net"},
   105  		{"stdlib/minor/heading.md", ""},
   106  		{"stdlib/minor/net/http/a.md", "net/http"},
   107  	} {
   108  		got := stdlibPackage(test.in)
   109  		if w := test.want; got != w {
   110  			t.Errorf("%q: got %q, want %q", test.in, got, w)
   111  		}
   112  	}
   113  }
   114  
   115  func TestStdlibPackageHeading(t *testing.T) {
   116  	h := stdlibPackageHeading("net/http", 1)
   117  	got := md.ToMarkdown(h)
   118  	want := "#### [`net/http`](/pkg/net/http/)\n"
   119  	if got != want {
   120  		t.Errorf("\ngot  %q\nwant %q", got, want)
   121  	}
   122  }
   123  
   124  // parseTestFile translates a txtar archive into an fs.FS, except for the
   125  // file "want", whose contents are returned separately.
   126  func parseTestFile(filename string) (fsys fs.FS, want string, err error) {
   127  	ar, err := txtar.ParseFile(filename)
   128  	if err != nil {
   129  		return nil, "", err
   130  	}
   131  	mfs := make(fstest.MapFS)
   132  	for _, f := range ar.Files {
   133  		if f.Name == "want" {
   134  			want = string(f.Data)
   135  		} else {
   136  			mfs[f.Name] = &fstest.MapFile{Data: f.Data}
   137  		}
   138  	}
   139  	if want == "" {
   140  		return nil, "", fmt.Errorf("%s: missing 'want'", filename)
   141  	}
   142  	return mfs, want, nil
   143  }
   144  
   145  func TestSortedMarkdownFilenames(t *testing.T) {
   146  	want := []string{
   147  		"a.md",
   148  		"b.md",
   149  		"b/a.md",
   150  		"b/c.md",
   151  		"ba/a.md",
   152  	}
   153  	mfs := make(fstest.MapFS)
   154  	for _, fn := range want {
   155  		mfs[fn] = &fstest.MapFile{}
   156  	}
   157  	mfs["README"] = &fstest.MapFile{}
   158  	mfs["b/other.txt"] = &fstest.MapFile{}
   159  	got, err := sortedMarkdownFilenames(mfs)
   160  	if err != nil {
   161  		t.Fatal(err)
   162  	}
   163  	if !slices.Equal(got, want) {
   164  		t.Errorf("\ngot  %v\nwant %v", got, want)
   165  	}
   166  }
   167  
   168  func TestRemoveEmptySections(t *testing.T) {
   169  	doc := NewParser().Parse(`
   170  # h1
   171  not empty
   172  
   173  # h2
   174  
   175  ## h3
   176  
   177  ### h4
   178  
   179  #### h5
   180  
   181  ### h6
   182  
   183  ### h7
   184  
   185  ## h8
   186  something
   187  
   188  ## h9
   189  
   190  # h10
   191  `)
   192  	bs := removeEmptySections(doc.Blocks)
   193  	got := md.ToMarkdown(&md.Document{Blocks: bs})
   194  	want := md.ToMarkdown(NewParser().Parse(`
   195  # h1
   196  not empty
   197  
   198  # h2
   199  
   200  ## h8
   201  something
   202  `))
   203  	if got != want {
   204  		t.Errorf("\ngot:\n%s\nwant:\n%s", got, want)
   205  	}
   206  }
   207  
   208  func TestParseAPIFile(t *testing.T) {
   209  	fsys := fstest.MapFS{
   210  		"123.next": &fstest.MapFile{Data: []byte(`
   211  pkg p1, type T struct
   212  pkg p2, func F(int, bool) #123
   213  pkg syscall (windows-386), const WSAENOPROTOOPT = 10042 #62254
   214  	`)},
   215  	}
   216  	got, err := parseAPIFile(fsys, "123.next")
   217  	if err != nil {
   218  		t.Fatal(err)
   219  	}
   220  	want := []APIFeature{
   221  		{"p1", "", "type T struct", 0},
   222  		{"p2", "", "func F(int, bool)", 123},
   223  		{"syscall", "(windows-386)", "const WSAENOPROTOOPT = 10042", 62254},
   224  	}
   225  	if !reflect.DeepEqual(got, want) {
   226  		t.Errorf("\ngot  %#v\nwant %#v", got, want)
   227  	}
   228  }
   229  
   230  func TestCheckAPIFile(t *testing.T) {
   231  	testFiles, err := filepath.Glob(filepath.Join("testdata", "checkAPIFile", "*.txt"))
   232  	if err != nil {
   233  		t.Fatal(err)
   234  	}
   235  	if len(testFiles) == 0 {
   236  		t.Fatal("no tests")
   237  	}
   238  	for _, f := range testFiles {
   239  		t.Run(strings.TrimSuffix(filepath.Base(f), ".txt"), func(t *testing.T) {
   240  			fsys, want, err := parseTestFile(f)
   241  			if err != nil {
   242  				t.Fatal(err)
   243  			}
   244  			var got string
   245  			gotErr := CheckAPIFile(fsys, "api.txt", fsys, "doc/next")
   246  			if gotErr != nil {
   247  				got = gotErr.Error()
   248  			}
   249  			want = strings.TrimSpace(want)
   250  			if got != want {
   251  				t.Errorf("\ngot  %s\nwant %s", got, want)
   252  			}
   253  		})
   254  	}
   255  }
   256  
   257  func TestAllAPIFilesForErrors(t *testing.T) {
   258  	if testing.Short() {
   259  		t.Skip("skipping in short mode")
   260  	}
   261  	fsys := os.DirFS(filepath.Join(runtime.GOROOT(), "api"))
   262  	apiFiles, err := fs.Glob(fsys, "*.txt")
   263  	if err != nil {
   264  		t.Fatal(err)
   265  	}
   266  	for _, f := range apiFiles {
   267  		if _, err := parseAPIFile(fsys, f); err != nil {
   268  			t.Errorf("parseTestFile(%q) failed with %v", f, err)
   269  		}
   270  	}
   271  }
   272  
   273  func TestSymbolLinks(t *testing.T) {
   274  	for _, test := range []struct {
   275  		in   string
   276  		want string
   277  	}{
   278  		{"a b", "a b"},
   279  		{"a [b", "a [b"},
   280  		{"a [b[", "a [b["},
   281  		{"a b[X]", "a b[X]"},
   282  		{"a [Buffer] b", "a [`Buffer`](/pkg/bytes#Buffer) b"},
   283  		{"a [Buffer]\nb", "a [`Buffer`](/pkg/bytes#Buffer)\nb"},
   284  		{"a [bytes.Buffer], b", "a [`bytes.Buffer`](/pkg/bytes#Buffer), b"},
   285  		{"[bytes.Buffer.String]", "[`bytes.Buffer.String`](/pkg/bytes#Buffer.String)"},
   286  		{"a--[encoding/json.Marshal].", "a--[`encoding/json.Marshal`](/pkg/encoding/json#Marshal)."},
   287  		{"a [math] and s[math] and [NewBuffer].", "a [`math`](/pkg/math) and s[math] and [`NewBuffer`](/pkg/bytes#NewBuffer)."},
   288  		{"A [*log/slog.Logger]", "A [`*log/slog.Logger`](/pkg/log/slog#Logger)"},
   289  		{"Not in code `[math]`.", "Not in code `[math]`."},
   290  		// Link text that already has backticks.
   291  		{"a [`Buffer`] b", "a [`Buffer`](/pkg/bytes#Buffer) b"},
   292  		{"[`bytes.Buffer.String`]", "[`bytes.Buffer.String`](/pkg/bytes#Buffer.String)"},
   293  		// Links inside inline elements with nested content.
   294  		{"**must use [Buffer]**", "**must use [`Buffer`](/pkg/bytes#Buffer)**"},
   295  		{"*must use [Buffer] value*", "*must use [`Buffer`](/pkg/bytes#Buffer) value*"},
   296  		{"_**[Buffer]**_", "_**[`Buffer`](/pkg/bytes#Buffer)**_"},
   297  	} {
   298  		doc := NewParser().Parse(test.in)
   299  		addSymbolLinks(doc, "bytes")
   300  		got := strings.TrimSpace(md.ToMarkdown(doc))
   301  		if got != test.want {
   302  			t.Errorf("\nin:   %s\ngot:  %s\nwant: %s", test.in, got, test.want)
   303  		}
   304  	}
   305  
   306  }