cmd/stacks: match predicates against individual configurations claimStacks currently concatenates all configurations associated with a stack before evaluating a predicate. As a result, different terms of a conjunction may match different configurations. For example, a stack observed on linux/amd64 and darwin/arm64 incorrectly matches a predicate requiring linux/arm64. Evaluate the predicate separately for each stack/configuration pair, and claim the stack if any individual configuration matches. Add coverage for cross-configuration combinations. Fixes golang/go#80660 Change-Id: Ie5200bab1d238dc85ef11c778eb682ce63b348e3 Reviewed-on: https://go-review.googlesource.com/c/telemetry/+/808383 Reviewed-by: race quite <quiterace@gmail.com> Reviewed-by: Hongxiang Jiang <hxjiang@golang.org> Reviewed-by: Alan Donovan <adonovan@google.com> Auto-Submit: Hongxiang Jiang <hxjiang@golang.org> LUCI-TryBot-Result: golang-scoped@luci-project-accounts.iam.gserviceaccount.com <golang-scoped@luci-project-accounts.iam.gserviceaccount.com>
diff --git a/cmd/stacks/stacks.go b/cmd/stacks/stacks.go index 453a215..7141b5c 100644 --- a/cmd/stacks/stacks.go +++ b/cmd/stacks/stacks.go
@@ -41,9 +41,9 @@ // > | expr && expr // > | expr || expr // -// Each string literal must match complete words in the text, -// which consists of stack followed by one or more configuration -// lines of the form +// Each string literal must match complete words in the text. +// The predicate is evaluated separately for each configuration, +// against text consisting of the stack followed by one line of the form // // GOTOOLCHAIN=go1.23.0 GOOS=darwin GOARCH=arm64 golang.org/x/tools/gopls@v0.22.0 // @@ -588,33 +588,24 @@ // that appear in the body by chance. // // 2. if the issue body contains a ```#!stacks``` predicate that -// matches the text, consisting of stack and configuration lines. +// matches the stack together with at least one of its configurations. // // We log an error if two different issues attempt to claim // the same stack. func claimStacks(issues []*Issue, stacks map[string]map[Info]int64) map[string]*Issue { log.Println("Processing claims...") - // This is O(new stacks x existing issues). + // This is O(new stacks x existing issues x stack configurations). claimedBy := make(map[string]*Issue) claimType := make(map[string]bool) // records whether claim was due to predicate (true) or ID (false) in issue body for stack, counts := range stacks { id := stackID(stack) - // Construct the stack + configuration text to be matched. - var buf strings.Builder - buf.WriteString(stack) - for info := range counts { - fmt.Fprintf(&buf, "\nGOTOOLCHAIN=%s GOOS=%s GOARCH=%s %s@%s", - info.GoVersion, info.GOOS, info.GOARCH, info.Program, info.ProgramVersion) - } - text := buf.String() - for _, issue := range issues { byPredicate := false if strings.Contains(issue.Body, id) { // nop - } else if issue.matches != nil && issue.matches(text) { + } else if issue.matches != nil && matchesAnyConfig(issue.matches, stack, counts) { byPredicate = true if false { log.Printf("predicate %s matches stack %s", issue.predicate, id) @@ -649,6 +640,19 @@ return claimedBy } +// matchesAnyConfig reports whether match accepts the stack in at least one of +// the configurations in counts. +func matchesAnyConfig(match func(string) bool, stack string, counts map[Info]int64) bool { + for info := range counts { + text := fmt.Sprintf("%s\nGOTOOLCHAIN=%s GOOS=%s GOARCH=%s %s@%s", + stack, info.GoVersion, info.GOOS, info.GOARCH, info.Program, info.ProgramVersion) + if match(text) { + return true + } + } + return false +} + // updateIssues updates existing issues that claimed new stacks by predicate. func updateIssues(cli *githubClient, repo string, issues []*Issue, stacks map[string]map[Info]int64, stackToURL map[string]string) { log.Println("Updating issues...")
diff --git a/cmd/stacks/stacks_test.go b/cmd/stacks/stacks_test.go index 2af7b49..c191d38 100644 --- a/cmd/stacks/stacks_test.go +++ b/cmd/stacks/stacks_test.go
@@ -387,3 +387,36 @@ t.Errorf("issue #2 newStacks = %v, want [%q]", issues[1].newStacks, stack2) } } + +func TestClaimStacksMultipleConfigs(t *testing.T) { + const stack = "runtime.main" + stacks := map[string]map[Info]int64{ + stack: { + {GOOS: "linux", GOARCH: "amd64"}: 1, + {GOOS: "darwin", GOARCH: "arm64"}: 1, + }, + } + + for _, test := range []struct { + name string + predicate string + want bool + }{ + {"linux/amd64", `"GOOS=linux" && "GOARCH=amd64"`, true}, + {"darwin/arm64", `"GOOS=darwin" && "GOARCH=arm64"`, true}, + {"linux/arm64", `"GOOS=linux" && "GOARCH=arm64"`, false}, + {"darwin/amd64", `"GOOS=darwin" && "GOARCH=amd64"`, false}, + } { + t.Run(test.name, func(t *testing.T) { + match, err := parsePredicate(test.predicate) + if err != nil { + t.Fatal(err) + } + issue := &Issue{Number: 1, predicate: test.predicate, matches: match} + claimed := claimStacks([]*Issue{issue}, stacks) + if got := claimed[stackID(stack)] == issue; got != test.want { + t.Errorf("claimStacks with predicate %q claimed stack: %t, want %t", test.predicate, got, test.want) + } + }) + } +}