blob: 3153090902e21f7a280ac0ce0c3cbd6364df4044 [file] [log] [blame] [edit]
// 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 repro
import (
"context"
"fmt"
"log/slog"
"os"
"os/exec"
"path/filepath"
"strings"
"golang.org/x/oscar/internal/github"
)
// LocalTester is an implementation of [CaseTester] that runs
// commands on the local system. It assumes git and Go.
// This is only for testing and should not be used in production.
// It is exported so that it can be used by tests of other packages.
type LocalTester struct {
lg *slog.Logger
goTester *GoTester
}
// Verify that [*LocalTester] implements [CaseTester].
var _ CaseTester = &LocalTester{}
// NewLocalTester returns a new [LocalTester].
func NewLocalTester(ctx context.Context, lg *slog.Logger) (*LocalTester, error) {
gt, err := NewGoTester(ctx, lg, localExecutor{})
if err != nil {
return nil, err
}
lt := &LocalTester{
lg: lg,
goTester: gt,
}
return lt, nil
}
// Destroy destroys a [LocalTester], removing the temporary directory.
func (lt *LocalTester) Destroy() {
lt.goTester.Destroy()
}
// Clean adds a package declaration and imports to try to
// make a test case runnable. This implements [CaseTester.Clean].
func (lt *LocalTester) Clean(ctx context.Context, bodyStr string) (string, error) {
return lt.goTester.Clean(ctx, bodyStr)
}
// CleanVersions tries to make the versions guessed by the LLM valid.
// It implements [CaseTester.CleanVersions].
func (lt *LocalTester) CleanVersions(ctx context.Context, pass, fail string) (string, string) {
return lt.goTester.CleanVersions(ctx, pass, fail)
}
// Try runs a test case at the suggested version.
// It implements [CaseTester.Try].
func (lt *LocalTester) Try(ctx context.Context, body, version string) (bool, error) {
return lt.goTester.Try(ctx, body, version)
}
// Bisect runs a bisection of a test case.
// This implements [CaseTester.Bisect].
func (lt *LocalTester) Bisect(ctx context.Context, issue *github.Issue, body, pass, fail string) (string, error) {
runtester := lt.goTester.runTester()
bodyFile := filepath.Join(lt.goTester.testDir(), "body.go")
if err := os.WriteFile(bodyFile, []byte(body), 0o666); err != nil {
return "", err
}
repoDir := lt.goTester.repoDir()
var le localExecutor
_, err := le.Execute(ctx, lt.lg, repoDir, "git", "bisect", "start", fail, pass)
if err != nil {
return "", err
}
_, err = le.Execute(ctx, lt.lg, repoDir, "git", "bisect", "run", runtester, bodyFile)
if err != nil {
return "", err
}
out, err := le.Execute(ctx, lt.lg, repoDir, "git", "rev-parse", "HEAD")
if err != nil {
return "", err
}
outStr := strings.TrimSpace(string(out))
// TODO: Somehow mark this as the failing commit.
lt.lg.Debug("found failing commit", "rev", outStr)
return outStr, nil
}
// localExecutor implements [Executor] by running programs locally.
type localExecutor struct{}
// Execute implements [Executor.Execute].
func (localExecutor) Execute(ctx context.Context, lg *slog.Logger, dir string, command string, args ...string) ([]byte, error) {
cmd := exec.CommandContext(ctx, command, args...)
cmd.Dir = dir
out, err := cmd.Output()
if err != nil {
if ee, ok := err.(*exec.ExitError); ok {
lg.Error("command failed", "cmd", cmd.String(), "err", err, "stdout", out, "stderr", ee.Stderr)
} else {
lg.Error("command failed", "cmd", cmd.String(), "err", err, "stdout", out)
}
return nil, fmt.Errorf("%s failed: %v", cmd, err)
}
lg.Info("command succeeded", "cmd", cmd.String(), "stdout", out)
return out, nil
}
// LookPath implements [Executor.LookPath].
func (localExecutor) LookPath(file string) (string, error) {
return exec.LookPath(file)
}