blob: 452113a15818d6b26adc0e0751a12af01406f2c9 [file] [log] [blame]
// Copyright 2025 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.
//go:build linux || darwin
package main
import (
"encoding/json"
"strings"
"testing"
)
func TestReadPCLineTable(t *testing.T) {
if testing.Short() {
// TODO(prattmic): It would be nice to have a unit test that
// didn't require downloading.
t.Skip("downloads source from the internet, skipping in -short")
}
type testCase struct {
name string
info Info
wantSymbol string
wantFileLine FileLine
}
tests := []testCase{
{
name: "gopls",
info: Info{
Program: "golang.org/x/tools/gopls",
ProgramVersion: "v0.16.1",
GoVersion: "go1.23.4",
GOOS: "linux",
GOARCH: "amd64",
},
wantSymbol: "golang.org/x/tools/gopls/internal/cmd.(*Application).Run",
wantFileLine: FileLine{
file: "golang.org/x/tools/gopls/internal/cmd/cmd.go",
line: 230,
},
},
{
name: "compile",
info: Info{
Program: "cmd/compile",
ProgramVersion: "go1.23.4",
GoVersion: "go1.23.4",
GOOS: "linux",
GOARCH: "amd64",
},
wantSymbol: "runtime.main",
wantFileLine: FileLine{
file: "runtime/proc.go",
line: 147,
},
},
}
for _, tc := range tests {
t.Run(tc.name, func(t *testing.T) {
stacksDir := t.TempDir()
pcln, err := readPCLineTable(tc.info, stacksDir)
if err != nil {
t.Fatalf("readPCLineTable got err %v want nil", err)
}
got, ok := pcln[tc.wantSymbol]
if !ok {
t.Fatalf("PCLineTable want entry %s got !ok from pcln %+v", tc.wantSymbol, pcln)
}
if got != tc.wantFileLine {
t.Fatalf("symbol %s got FileLine %+v want %+v", tc.wantSymbol, got, tc.wantFileLine)
}
})
}
}
func TestParsePredicate(t *testing.T) {
for _, tc := range []struct {
expr string
arg string
want bool
}{
{`"x"`, `"x"`, true},
{`"x"`, `"axe"`, false}, // literals match whole words
{`"x"`, "val:x+5", true},
{`"fu+12"`, "x:fu+12,", true},
{`"fu+12"`, "snafu+12,", false},
{`"fu+12"`, "x:fu+123,", false},
{`"a.*b"`, "a.*b", true}, // regexp metachars are escaped
{`"a.*b"`, "axxb", false}, // ditto
{`"x"`, `"y"`, false},
{`!"x"`, "x", false},
{`!"x"`, "y", true},
{`"x" && "y"`, "xy", false},
{`"x" && "y"`, "x y", true},
{`"x" && "y"`, "x", false},
{`"x" && "y"`, "y", false},
{`"xz" && "zy"`, "xzy", false},
{`"xz" && "zy"`, "zy,xz", true},
{`"x" || "y"`, "x\ny", true},
{`"x" || "y"`, "x", true},
{`"x" || "y"`, "y", true},
{`"x" || "y"`, "z", false},
} {
eval, err := parsePredicate(tc.expr)
if err != nil {
t.Fatal(err)
}
got := eval(tc.arg)
if got != tc.want {
t.Errorf("%s applied to %q: got %t, want %t", tc.expr, tc.arg, got, tc.want)
}
}
}
func TestParsePredicateError(t *testing.T) {
// Validate that bad predicates return errors.
for _, expr := range []string{
``,
`1`,
`foo`, // an identifier, not a literal
`"x" + "y"`,
`"x" &&`,
`~"x"`,
`f(1)`,
} {
if _, err := parsePredicate(expr); err == nil {
t.Errorf("%s: got nil, want error", expr)
}
}
}
// which takes the bulk of the time.
func TestUpdateIssues(t *testing.T) {
if testing.Short() {
t.Skip("downloads source from the internet, skipping in -short")
}
c := &githubClient{divertChanges: true}
const stack1 = "stack1"
id1 := stackID(stack1)
stacksToURL := map[string]string{stack1: "URL1"}
// checkIssueComment asserts that the change adds an issue of the specified
// number, with a body that contains various strings.
checkIssueComment := func(t *testing.T, change any, number int, version string) {
t.Helper()
cic, ok := change.(addIssueComment)
if !ok {
t.Fatalf("got %T, want addIssueComment", change)
}
if cic.number != number {
t.Errorf("issue number: got %d, want %d", cic.number, number)
}
for _, want := range []string{"URL1", stack1, id1, "golang.org/x/tools/gopls@" + version} {
if !strings.Contains(cic.comment, want) {
t.Errorf("missing %q in comment:\n%s", want, cic.comment)
}
}
}
t.Run("open issue", func(t *testing.T) {
issues := []*Issue{{
Number: 1,
State: "open",
newStacks: []string{stack1},
}}
info := Info{
Program: "golang.org/x/tools/gopls",
ProgramVersion: "v0.16.1",
}
stacks := map[string]map[Info]int64{stack1: map[Info]int64{info: 3}}
updateIssues(c, issues, stacks, stacksToURL)
changes := c.takeChanges()
if g, w := len(changes), 2; g != w {
t.Fatalf("got %d changes, want %d", g, w)
}
// The first change creates an issue comment.
checkIssueComment(t, changes[0], 1, "v0.16.1")
// The second change updates the issue body, and only the body.
ui, ok := changes[1].(updateIssue)
if !ok {
t.Fatalf("got %T, want updateIssue", changes[1])
}
if ui.number != 1 {
t.Errorf("issue number: got %d, want 1", ui.number)
}
if ui.Body == "" || ui.State != "" || ui.StateReason != "" {
t.Errorf("updating other than just the body:\n%+v", ui)
}
want := "Dups: " + id1
if !strings.Contains(ui.Body, want) {
t.Errorf("missing %q in body %q", want, ui.Body)
}
})
t.Run("should be reopened", func(t *testing.T) {
issues := []*Issue{{
// Issue purportedly fixed in v0.16.0
Number: 2,
State: "closed",
StateReason: "completed",
Milestone: &Milestone{Title: "gopls/v0.16.0"},
newStacks: []string{stack1},
}}
// New stack in a later version.
info := Info{
Program: "golang.org/x/tools/gopls",
ProgramVersion: "v0.17.0",
}
stacks := map[string]map[Info]int64{stack1: map[Info]int64{info: 3}}
updateIssues(c, issues, stacks, stacksToURL)
changes := c.takeChanges()
if g, w := len(changes), 2; g != w {
t.Fatalf("got %d changes, want %d", g, w)
}
// The first change creates an issue comment.
checkIssueComment(t, changes[0], 2, "v0.17.0")
// The second change updates the issue body, state, and state reason.
ui, ok := changes[1].(updateIssue)
if !ok {
t.Fatalf("got %T, want updateIssue", changes[1])
}
if ui.number != 2 {
t.Errorf("issue number: got %d, want 2", ui.number)
}
if ui.Body == "" || ui.State != "open" || ui.StateReason != "reopened" {
t.Errorf(`update fields should be non-empty body, state "open", state reason "reopened":\n%+v`, ui)
}
want := "Dups: " + id1
if !strings.Contains(ui.Body, want) {
t.Errorf("missing %q in body %q", want, ui.Body)
}
})
}
func TestMarshalUpdateIssueFields(t *testing.T) {
// Verify that only the non-empty fields of updateIssueFields are marshalled.
for _, tc := range []struct {
fields updateIssue
want string
}{
{updateIssue{Body: "b"}, `{"body":"b"}`},
{updateIssue{State: "open"}, `{"state":"open"}`},
{updateIssue{State: "open", StateReason: "reopened"}, `{"state":"open","state_reason":"reopened"}`},
} {
bytes, err := json.Marshal(tc.fields)
if err != nil {
t.Fatal(err)
}
got := string(bytes)
if got != tc.want {
t.Errorf("%+v: got %s, want %s", tc.fields, got, tc.want)
}
}
}
func TestShouldReopen(t *testing.T) {
const stack = "stack"
const gopls = "golang.org/x/tools/gopls"
goplsMilestone := &Milestone{Title: "gopls/v0.2.0"}
goMilestone := &Milestone{Title: "Go1.23"}
for _, tc := range []struct {
name string
issue Issue
info Info
want bool
}{
{
"issue open",
Issue{State: "open", Milestone: goplsMilestone},
Info{Program: gopls, ProgramVersion: "v0.2.0"},
false,
},
{
"issue closed but not fixed",
Issue{State: "closed", StateReason: "not_planned", Milestone: goplsMilestone},
Info{Program: gopls, ProgramVersion: "v0.2.0"},
false,
},
{
"different program",
Issue{State: "closed", StateReason: "completed", Milestone: goplsMilestone},
Info{Program: "other", ProgramVersion: "v0.2.0"},
false,
},
{
"later version",
Issue{State: "closed", StateReason: "completed", Milestone: goplsMilestone},
Info{Program: gopls, ProgramVersion: "v0.3.0"},
true,
},
{
"earlier version",
Issue{State: "closed", StateReason: "completed", Milestone: goplsMilestone},
Info{Program: gopls, ProgramVersion: "v0.1.0"},
false,
},
{
"same version",
Issue{State: "closed", StateReason: "completed", Milestone: goplsMilestone},
Info{Program: gopls, ProgramVersion: "v0.2.0"},
true,
},
{
"compiler later version",
Issue{State: "closed", StateReason: "completed", Milestone: goMilestone},
Info{Program: "cmd/compile", ProgramVersion: "go1.24"},
true,
},
{
"compiler earlier version",
Issue{State: "closed", StateReason: "completed", Milestone: goMilestone},
Info{Program: "cmd/compile", ProgramVersion: "go1.22"},
false,
},
{
"compiler same version",
Issue{State: "closed", StateReason: "completed", Milestone: goMilestone},
Info{Program: "cmd/compile", ProgramVersion: "go1.23"},
true,
},
} {
t.Run(tc.name, func(t *testing.T) {
tc.issue.Number = 1
tc.issue.newStacks = []string{stack}
got := shouldReopen(&tc.issue, map[string]map[Info]int64{stack: map[Info]int64{tc.info: 1}})
if got != tc.want {
t.Errorf("got %t, want %t", got, tc.want)
}
})
}
}