blob: c171e753e3237add04cafcdbb909016164e8b571 [file] [log] [blame]
// Copyright 2024 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.
package overview
import (
"context"
"fmt"
"slices"
"testing"
"time"
"github.com/google/go-cmp/cmp"
"golang.org/x/oscar/internal/actions"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/github/wrap"
"golang.org/x/oscar/internal/llmapp"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/testutil"
)
var (
jan1_2024 = "2024-01-01T00:00:00Z"
jan1_2023 = "2023-01-01T00:00:00Z"
dec31_2023 = "2023-12-31T00:00:00Z"
dec30_2023 = "2023-12-30T00:00:00Z"
dec31_2022 = "2022-12-31T00:00:00Z"
)
func TestRun(t *testing.T) {
lg := testutil.Slogger(t)
project := "test/test"
check := testutil.Checker(t)
ctx := context.Background()
// "now" is Jan 1, 2024.
now := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
w := wrap.New("testbot", "test")
// Set default minimum comments to 1 for testing.
old := defaultMinComments
defaultMinComments = 1
t.Cleanup(func() { defaultMinComments = old })
// Add two issues created "now".
// Issue 1 has two comments.
// Issue 2 has 1 comment.
basicSetup := func(gh *github.Client) {
gh.Testing().AddIssue(project, &github.Issue{Number: 1, Body: "issue 1", CreatedAt: jan1_2024})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 1"})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 2"})
gh.Testing().AddIssue(project, &github.Issue{Number: 2, Body: "issue 2", CreatedAt: jan1_2024})
gh.Testing().AddIssueComment(project, 2, &github.IssueComment{Body: "issue 2 comment 1"})
}
const issueLink = "\n" + `<!-- Generated by Oscar. DO NOT EDIT. {"bot":"testbot","kind":"test"} -->` +
"\n" + sparkle + " @testbot's overview of this issue: test-url\n" + `<!-- oscar-end -->`
for _, tc := range []struct {
name string
setup func(gh *github.Client) // function to add test data
minComments *int
maxAge *time.Duration
autoApprove *bool
wantReport *actions.RunReport
wantEdits []*github.TestingEdit
}{
{
name: "basic",
setup: basicSetup,
wantReport: &actions.RunReport{
Completed: 2,
},
autoApprove: ptr(true),
wantEdits: []*github.TestingEdit{
{Project: project, Issue: 1, IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, `an overview of issue 1 with 2 comment(s)`, w),
}},
{Project: project, Issue: 1, IssueChanges: &github.IssueChanges{
Body: "issue 1" + issueLink,
}},
{Project: project, Issue: 2, IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, `an overview of issue 2 with 1 comment(s)`, w),
}},
{Project: project, Issue: 2, IssueChanges: &github.IssueChanges{
Body: "issue 2" + issueLink,
}},
},
},
{
name: "max age default",
setup: func(gh *github.Client) {
// Exactly meets cutoff (1 year)
gh.Testing().AddIssue(project, &github.Issue{Number: 1, Body: "issue 1", CreatedAt: jan1_2023})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 1"})
// Too old.
gh.Testing().AddIssue(project, &github.Issue{Number: 2, Body: "issue 2", CreatedAt: dec31_2022})
gh.Testing().AddIssueComment(project, 2, &github.IssueComment{Body: "issue 2 comment 1"})
},
autoApprove: ptr(true),
wantReport: &actions.RunReport{
Completed: 1,
},
wantEdits: []*github.TestingEdit{
{Project: project, Issue: 1, IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, "an overview of issue 1 with 1 comment(s)", w),
}},
{Project: project, Issue: 1, IssueChanges: &github.IssueChanges{
Body: "issue 1" + issueLink,
}},
},
},
{
name: "max age set",
setup: func(gh *github.Client) {
// Exactly meets cutoff.
gh.Testing().AddIssue(project, &github.Issue{Number: 1, Body: "issue 1", CreatedAt: dec31_2023})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 1"})
// Too old.
gh.Testing().AddIssue(project, &github.Issue{Number: 2, Body: "issue 2", CreatedAt: dec30_2023})
gh.Testing().AddIssueComment(project, 2, &github.IssueComment{Body: "issue 2 comment 1"})
},
autoApprove: ptr(true),
maxAge: ptr(time.Hour * 24),
wantReport: &actions.RunReport{
Completed: 1,
},
wantEdits: []*github.TestingEdit{
{Project: project, Issue: 1, IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, "an overview of issue 1 with 1 comment(s)", w),
}},
{Project: project, Issue: 1, IssueChanges: &github.IssueChanges{
Body: "issue 1" + issueLink,
}},
},
},
// TODO(tatianabradley): Additional unit test cases:
// - Other configuration (min comments, project, auto-approve)
// - Ignored events
// - Skipped issues
// - Posters with different names / bots do not conflict
} {
t.Run(tc.name, func(t *testing.T) {
db := storage.MemDB()
gh := github.New(lg, db, nil, nil)
tc.setup(gh)
p := newPoster(lg, db, gh, "test", "testbot")
p.EnableProject(project)
if tc.minComments != nil {
p.SetMinComments(*tc.minComments)
}
if tc.maxAge != nil {
p.SetMaxIssueAge(*tc.maxAge)
}
if tc.autoApprove != nil {
if *tc.autoApprove {
p.AutoApprove()
} else {
p.RequireApproval()
}
}
check(p.run(ctx, overviewFuncForTest(gh), now))
gotReport := actions.RunWithReport(ctx, lg, db)
if diff := cmp.Diff(tc.wantReport, gotReport); diff != "" {
t.Errorf("actions.RunWithReport mismatch (-want +got)\n:%s", diff)
}
if diff := cmp.Diff(tc.wantEdits, gh.Testing().Edits()); diff != "" {
t.Errorf("edits mismatch (-want +got)\n:%s", diff)
}
})
}
}
func ptr[T any](v T) *T {
return &v
}
func mustComment(t *testing.T, s string, w *wrap.Wrapper) string {
t.Helper()
c, err := comment(s, w)
if err != nil {
t.Fatal(err)
}
return c
}
func TestRunUpdate(t *testing.T) {
lg := testutil.Slogger(t)
db := storage.MemDB()
gh := github.New(lg, db, nil, nil)
project := "test/test"
check := testutil.Checker(t)
ctx := context.Background()
now := time.Date(2024, 1, 1, 0, 0, 0, 0, time.UTC)
gh.Testing().AddIssue(project, &github.Issue{Number: 1, Body: "issue 1", CreatedAt: jan1_2024})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 1"})
gh.Testing().AddIssueComment(project, 1, &github.IssueComment{Body: "issue 1 comment 2"})
gh.Testing().AddIssue(project, &github.Issue{Number: 2, Body: "issue 2", CreatedAt: jan1_2024})
gh.Testing().AddIssueComment(project, 2, &github.IssueComment{Body: "issue 2 comment 1"})
p := newPoster(lg, db, gh, "test", "testbot")
p.EnableProject(project)
p.SetMinComments(1)
p.AutoApprove()
// Use a test implementation to run the post action, which adds the expected
// values to the github testing client instead of diverting edits.
p.logAction = actions.Register(actionKind, &testPoster{p: p})
check(p.run(ctx, overviewFuncForTest(gh), now))
check(actions.Run(ctx, lg, db))
// Run again to pick up the new comments (added by the previous run).
// (Note that comments by the bot are ignored when generating the real
// overview).
check(p.run(ctx, overviewFuncForTest(gh), now))
check(actions.Run(ctx, lg, db))
wantEdits := []*github.TestingEdit{
{Project: project, Issue: 1, Comment: 10000000004,
IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, "an overview of issue 1 with 3 comment(s)", p.w),
}},
{Project: project, Issue: 2, Comment: 10000000005, IssueCommentChanges: &github.IssueCommentChanges{
Body: mustComment(t, "an overview of issue 2 with 2 comment(s)", p.w),
}},
}
if diff := cmp.Diff(wantEdits, gh.Testing().Edits()); diff != "" {
t.Errorf("run update: edits mismatch (-want +got)\n:%s", diff)
}
}
// testPoster is a test implementation of [actioner]
// that, for post actions, modifies the GitHub testing database (instead
// of diverting edits, which is what happens when we use
// the real [actioner] implementation).
// it uses the real implementation for update actions.
type testPoster struct {
p *poster
}
func (p *testPoster) Run(ctx context.Context, data []byte) ([]byte, error) {
return runFromActionLog(ctx, data, p.runTestAction)
}
func (*testPoster) ForDisplay([]byte) string { return "" }
func (tp *testPoster) runTestAction(ctx context.Context, a *action) (*result, error) {
// Use test implementation for post actions.
if a.isPost() {
n := tp.p.gh.Testing().AddIssueComment(a.Issue.Project(), a.Issue.Number, &github.IssueComment{
Body: a.Changes.Body,
})
url := fmt.Sprintf("%s#issuecomment-%d", a.Issue.HTMLURL, n)
body, err := appendCommentURL(a.Issue, tp.p.bot, url, tp.p.w)
if err != nil {
return nil, err
}
tp.p.gh.Testing().AddIssue(a.Issue.Project(),
&github.Issue{Number: a.Issue.Number, Body: body, CreatedAt: a.Issue.CreatedAt})
return &result{URL: url}, nil
}
return tp.p.runUpdateAction(ctx, a)
}
// overviewFuncForTest returns an overviewFunc that returns a phrase based on
// the issue number and its number of comments.
func overviewFuncForTest(gh *github.Client) overviewFunc {
return func(ctx context.Context, i *github.Issue) (*IssueResult, error) {
cs := slices.Collect(gh.Comments(i))
return &IssueResult{
TotalComments: len(cs),
LastComment: cs[len(cs)-1].CommentID(),
Overview: &llmapp.Result{
Response: fmt.Sprintf("an overview of issue %d with %d comment(s)", i.Number, len(cs)),
},
}, nil
}
}
func TestIsOverviewComment(t *testing.T) {
lg := testutil.Slogger(t)
db := storage.MemDB()
gh := github.New(lg, db, nil, nil)
p := newPoster(lg, db, gh, "test", "testbot")
c, err := comment("a comment", p.w)
if err != nil {
t.Fatal(err)
}
if !p.isOverviewComment(&github.IssueComment{Body: c}) {
t.Fatal("p.isOverviewComment = false, want true")
}
}