relnote: account for build in API file
Most lines in api/next/NNN.txt files look like
pkg PACKAGE, FEATURE #ISSUE
but there can also be build information, like
pkg PACKAGE (windows-386), FEATURE #ISSUE
Fix the parser to account for that.
For golang/go#64169.
Change-Id: I7b82084f1a9589d162aa7f4fc8abbe5b0199b4d4
Reviewed-on: https://go-review.googlesource.com/c/build/+/564396
Reviewed-by: Dmitri Shuralyov <dmitshur@golang.org>
LUCI-TryBot-Result: Go LUCI <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
Reviewed-by: Dmitri Shuralyov <dmitshur@google.com>
diff --git a/relnote/relnote.go b/relnote/relnote.go
index 8b926bf..0012df2 100644
--- a/relnote/relnote.go
+++ b/relnote/relnote.go
@@ -327,16 +327,20 @@
// like the ones in the main go repo in the api directory.
type APIFeature struct {
Package string // package that the feature is in
+ Build string // build that the symbol is relevant for (e.g. GOOS, GOARCH)
Feature string // everything about the feature other than the package
Issue int // the issue that introduced the feature, or 0 if none
}
-var apiFileLineRegexp = regexp.MustCompile(`^pkg ([^,]+), ([^#]*)(#\d+)?$`)
+// This regexp has four capturing groups: package, build, feature and issue.
+var apiFileLineRegexp = regexp.MustCompile(`^pkg ([^ \t]+)[ \t]*(\([^)]+\))?, ([^#]*)(#\d+)?$`)
// parseAPIFile parses a file in the api format and returns a list of the file's features.
// A feature is represented by a single line that looks like
//
-// PKG WORDS #ISSUE
+// pkg PKG (BUILD) FEATURE #ISSUE
+//
+// where the BUILD and ISSUE may be absent.
func parseAPIFile(fsys fs.FS, filename string) ([]APIFeature, error) {
f, err := fsys.Open(filename)
if err != nil {
@@ -347,20 +351,24 @@
scan := bufio.NewScanner(f)
for scan.Scan() {
line := strings.TrimSpace(scan.Text())
- if line == "" {
+ if line == "" || line[0] == '#' {
continue
}
matches := apiFileLineRegexp.FindStringSubmatch(line)
if len(matches) == 0 {
return nil, fmt.Errorf("%s: malformed line %q", filename, line)
}
+ if len(matches) != 5 {
+ return nil, fmt.Errorf("wrong number of matches for line %q", line)
+ }
f := APIFeature{
Package: matches[1],
- Feature: strings.TrimSpace(matches[2]),
+ Build: matches[2],
+ Feature: strings.TrimSpace(matches[3]),
}
- if len(matches) > 3 && len(matches[3]) > 0 {
+ if issue := matches[4]; issue != "" {
var err error
- f.Issue, err = strconv.Atoi(matches[3][1:]) // skip leading '#'
+ f.Issue, err = strconv.Atoi(issue[1:]) // skip leading '#'
if err != nil {
return nil, err
}
diff --git a/relnote/relnote_test.go b/relnote/relnote_test.go
index c6000be..34112f7 100644
--- a/relnote/relnote_test.go
+++ b/relnote/relnote_test.go
@@ -7,8 +7,10 @@
import (
"fmt"
"io/fs"
+ "os"
"path/filepath"
"reflect"
+ "runtime"
"slices"
"strings"
"testing"
@@ -208,6 +210,7 @@
"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")
@@ -215,11 +218,12 @@
t.Fatal(err)
}
want := []APIFeature{
- {"p1", "type T struct", 0},
- {"p2", "func F(int, bool)", 123},
+ {"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)
+ t.Errorf("\ngot %#v\nwant %#v", got, want)
}
}
@@ -250,6 +254,22 @@
}
}
+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