blob: 75fbf9e76299b3e5f57b37ddec335ed13305548c [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 commentfix
import (
"bytes"
"context"
"io"
"path/filepath"
"strings"
"sync"
"testing"
"text/template"
"time"
"golang.org/x/oscar/internal/diff"
"golang.org/x/oscar/internal/github"
"golang.org/x/oscar/internal/storage"
"golang.org/x/oscar/internal/testutil"
"golang.org/x/tools/txtar"
)
var ctx = context.Background()
func TestTestdata(t *testing.T) {
files, err := filepath.Glob("testdata/*.txt")
testutil.Check(t, err)
for _, file := range files {
t.Run(filepath.Base(file), func(t *testing.T) {
a, err := txtar.ParseFile(file)
testutil.Check(t, err)
var f Fixer
tmpl, err := new(template.Template).Parse(string(a.Comment))
testutil.Check(t, err)
testutil.Check(t, tmpl.Execute(io.Discard, &f))
for i := 0; i+2 <= len(a.Files); {
in := a.Files[i]
out := a.Files[i+1]
i += 2
name := strings.TrimSuffix(in.Name, ".in")
if name != strings.TrimSuffix(out.Name, ".out") {
t.Fatalf("mismatched file pair: %s and %s", in.Name, out.Name)
}
t.Run(name, func(t *testing.T) {
newBody, fixed := f.Fix(string(in.Data))
if fixed != (newBody != "") {
t.Fatalf("Fix() = %q, %v (len(newBody)=%d but fixed=%v)", newBody, fixed, len(newBody), fixed)
}
if newBody != string(out.Data) {
t.Fatalf("Fix: incorrect output:\n%s", string(diff.Diff("want", []byte(out.Data), "have", []byte(newBody))))
}
})
}
})
}
}
func TestPanics(t *testing.T) {
testutil.StopPanic(func() {
var f Fixer
f.EnableEdits()
t.Errorf("EnableEdits on zero Fixer did not panic")
})
testutil.StopPanic(func() {
var f Fixer
f.EnableProject("abc/xyz")
t.Errorf("EnableProject on zero Fixer did not panic")
})
var f Fixer
if err := f.Run(ctx); err == nil {
t.Errorf("Run on zero Fixer did not err")
}
}
func TestErrors(t *testing.T) {
var f Fixer
if err := f.AutoLink(`\`, ""); err == nil {
t.Fatalf("AutoLink succeeded on bad regexp")
}
if err := f.ReplaceText(`\`, ""); err == nil {
t.Fatalf("ReplaceText succeeded on bad regexp")
}
if err := f.ReplaceURL(`\`, ""); err == nil {
t.Fatalf("ReplaceText succeeded on bad regexp")
}
}
func TestGitHub(t *testing.T) {
// Check for comment with too-new cutoff and edits disabled.
// Finds nothing but also no-op.
gh := testGitHub(t)
db := storage.MemDB()
lg, buf := testutil.SlogBuffer()
f := New(lg, gh, db, "fixer1")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.SetTimeLimit(time.Date(2222, 1, 1, 1, 1, 1, 1, time.UTC))
f.ReplaceText("cancelled", "canceled")
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs mention rewrite of old comment:\n%s", buf.Bytes())
}
// Check again with old enough cutoff.
// Finds comment but does not edit, does not advance cursor.
f = New(lg, gh, db, "fixer1")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.SetTimeLimit(time.Time{})
f.ReplaceText("cancelled", "canceled")
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if !bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs do not mention rewrite of comment:\n%s", buf.Bytes())
}
if bytes.Contains(buf.Bytes(), []byte("editing github")) {
t.Fatalf("logs incorrectly mention editing github:\n%s", buf.Bytes())
}
// Run with too-new cutoff and edits enabled, should make issue not seen again.
buf.Truncate(0)
f.SetTimeLimit(time.Date(2222, 1, 1, 1, 1, 1, 1, time.UTC))
f.EnableEdits()
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
f.SetTimeLimit(time.Time{})
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
// Write comment (now using fixer2 to avoid 'marked as old' in fixer1).
lg, buf = testutil.SlogBuffer()
f = New(lg, gh, db, "fixer2")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.ReplaceText("cancelled", "canceled")
f.SetTimeLimit(time.Time{})
f.EnableEdits()
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if !bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs do not mention rewrite of comment:\n%s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte("editing github")) {
t.Fatalf("logs do not mention editing github:\n%s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`editing github" url=https://api.github.com/repos/rsc/tmp/issues/18`)) {
t.Fatalf("logs do not mention editing issue body:\n%s", buf.Bytes())
}
if bytes.Contains(buf.Bytes(), []byte(`editing github" url=https://api.github.com/repos/rsc/tmp/issues/19`)) {
t.Fatalf("logs incorrectly mention editing pull request body:\n%s", buf.Bytes())
}
if !bytes.Contains(buf.Bytes(), []byte(`editing github" url=https://api.github.com/repos/rsc/tmp/issues/comments/10000000001`)) {
t.Fatalf("logs do not mention editing issue comment:\n%s", buf.Bytes())
}
if bytes.Contains(buf.Bytes(), []byte("ERROR")) {
t.Fatalf("editing failed:\n%s", buf.Bytes())
}
// Try again; comment should now be marked old in watcher.
lg, buf = testutil.SlogBuffer()
f = New(lg, gh, db, "fixer2")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.ReplaceText("cancelled", "canceled")
f.EnableEdits()
f.SetTimeLimit(time.Time{})
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
// Check that not enabling the project doesn't edit comments.
lg, buf = testutil.SlogBuffer()
f = New(lg, gh, db, "fixer3")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("xyz/tmp")
f.ReplaceText("cancelled", "canceled")
f.EnableEdits()
f.SetTimeLimit(time.Time{})
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
// Check that when there's nothing to do, we still mark things old.
lg, buf = testutil.SlogBuffer()
f = New(lg, gh, db, "fixer4")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.ReplaceText("zyzzyva", "ZYZZYVA")
f.EnableEdits()
f.SetTimeLimit(time.Time{})
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
// Reverse the replacement and run again with same name; should not consider any comments.
lg, buf = testutil.SlogBuffer()
f = New(lg, gh, db, "fixer4")
f.SetStderr(testutil.LogWriter(t))
f.EnableProject("rsc/tmp")
f.ReplaceText("c", "C")
f.EnableEdits()
f.SetTimeLimit(time.Time{})
f.Run(ctx)
// t.Logf("output:\n%s", buf)
if bytes.Contains(buf.Bytes(), []byte("commentfix rewrite")) {
t.Fatalf("logs incorrectly mention rewrite of comment:\n%s", buf.Bytes())
}
}
func TestFixGitHubIssue(t *testing.T) {
ctx := context.Background()
t.Run("basic", func(t *testing.T) {
f, project, buf, check := newFixer(t)
check(f.FixGitHubIssue(ctx, project, 18))
testutil.ExpectLog(t, buf, "commentfix rewrite", 2) // fix body and comment
})
t.Run("twice", func(t *testing.T) {
f, project, buf, check := newFixer(t)
check(f.FixGitHubIssue(ctx, project, 18))
testutil.ExpectLog(t, buf, "commentfix rewrite", 2) // fix body and comment
// Running Fix again doesn't change anything because fixes were
// already applied.
check(f.FixGitHubIssue(ctx, project, 18))
testutil.ExpectLog(t, buf, "commentfix rewrite", 2) // nothing new
testutil.ExpectLog(t, buf, "commentfix already applied", 2)
})
t.Run("fix-run", func(t *testing.T) {
f, project, buf, check := newFixer(t)
check(f.FixGitHubIssue(ctx, project, 20))
testutil.ExpectLog(t, buf, "commentfix rewrite", 1) // fix body of issue 20
// Run still fixes issue 18 because FixGitHubIssue
// doesn't modify Run's watcher.
check(f.Run(ctx))
testutil.ExpectLog(t, buf, "commentfix rewrite", 3) // fix body and comment of issue 18
testutil.ExpectLog(t, buf, "commentfix already applied", 1)
})
t.Run("fix-run-watcher", func(t *testing.T) {
f, project, buf, check := newFixer(t)
check(f.FixGitHubIssue(ctx, project, 18))
check(f.FixGitHubIssue(ctx, project, 20))
testutil.ExpectLog(t, buf, "commentfix rewrite", 3) // fix body of issue 20
// Run sees that fixes have already been applied and advances
// watcher.
check(f.Run(ctx))
testutil.ExpectLog(t, buf, "commentfix rewrite", 3) // no change
testutil.ExpectLog(t, buf, "commentfix already applied", 3)
// Run doesn't do anything because its watcher has been advanced.
check(f.Run(ctx))
testutil.ExpectLog(t, buf, "commentfix rewrite", 3) // no change
testutil.ExpectLog(t, buf, "commentfix already applied", 3) // no change
})
t.Run("fix-run-concurrent", func(t *testing.T) {
f, project, buf, check := newFixer(t)
var wg sync.WaitGroup
wg.Add(1)
go func() {
check(f.FixGitHubIssue(ctx, project, 20))
wg.Done()
}()
wg.Add(1)
go func() {
check(f.Run(ctx))
wg.Done()
}()
wg.Add(1)
go func() {
check(f.FixGitHubIssue(ctx, project, 18))
wg.Done()
}()
wg.Wait()
// Each action is attempted twice.
testutil.ExpectLog(t, buf, "commentfix rewrite", 3)
testutil.ExpectLog(t, buf, "commentfix already applied", 3)
})
t.Run("fix-concurrent", func(t *testing.T) {
f, project, buf, check := newFixer(t)
var wg sync.WaitGroup
n := 5
wg.Add(n)
for range n {
go func() {
check(f.FixGitHubIssue(ctx, project, 20))
wg.Done()
}()
}
wg.Wait()
testutil.ExpectLog(t, buf, "commentfix rewrite", 1)
testutil.ExpectLog(t, buf, "commentfix already applied", n-1)
})
}
func newFixer(t *testing.T) (_ *Fixer, project string, _ *bytes.Buffer, check func(error)) {
gh := testGitHub(t)
db := storage.MemDB()
lg, buf := testutil.SlogBuffer()
f := New(lg, gh, db, t.Name())
f.SetStderr(testutil.LogWriter(t))
project = "rsc/tmp"
f.EnableProject(project)
f.ReplaceText("cancelled", "canceled")
f.SetTimeLimit(time.Time{})
f.EnableEdits()
return f, project, buf, testutil.Checker(t)
}
func testGitHub(t *testing.T) *github.Client {
db := storage.MemDB()
gh := github.New(testutil.Slogger(t), db, nil, nil)
gh.Testing().AddIssue("rsc/tmp", &github.Issue{
Number: 18,
Title: "spellchecking",
Body: "Contexts are cancelled.",
CreatedAt: "2024-06-17T20:16:49-04:00",
UpdatedAt: "2024-06-17T20:16:49-04:00",
})
// Ignored (pull request).
gh.Testing().AddIssue("rsc/tmp", &github.Issue{
Number: 19,
Title: "spellchecking",
Body: "Contexts are cancelled.",
CreatedAt: "2024-06-17T20:16:49-04:00",
UpdatedAt: "2024-06-17T20:16:49-04:00",
PullRequest: new(struct{}),
})
gh.Testing().AddIssueComment("rsc/tmp", 18, &github.IssueComment{
Body: "No really, contexts are cancelled.",
CreatedAt: "2024-06-17T20:16:49-04:00",
UpdatedAt: "2024-06-17T20:16:49-04:00",
})
gh.Testing().AddIssueComment("rsc/tmp", 18, &github.IssueComment{
Body: "Completely unrelated.",
CreatedAt: "2024-06-17T20:16:49-04:00",
UpdatedAt: "2024-06-17T20:16:49-04:00",
})
gh.Testing().AddIssue("rsc/tmp", &github.Issue{
Number: 20,
Title: "spellchecking 2",
Body: "Contexts are cancelled.",
CreatedAt: "2024-06-17T20:16:49-04:00",
UpdatedAt: "2024-06-17T20:16:49-04:00",
})
return gh
}