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 }