mirror of
https://github.com/jesseduffield/lazygit.git
synced 2026-08-28 10:15:32 -05:00
358 lines
9.6 KiB
Go
358 lines
9.6 KiB
Go
package components
|
|
|
|
import (
|
|
"errors"
|
|
"fmt"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"sync"
|
|
"time"
|
|
|
|
lazycoreUtils "github.com/jesseduffield/lazycore/pkg/utils"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
|
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
|
"github.com/jesseduffield/lazygit/pkg/utils"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
type RunTestArgs struct {
|
|
Tests []*IntegrationTest
|
|
Logf func(format string, formatArgs ...any)
|
|
RunCmd func(cmd *exec.Cmd) (int, error)
|
|
TestWrapper func(test *IntegrationTest, f func() error)
|
|
Sandbox bool
|
|
WaitForDebugger bool
|
|
RaceDetector bool
|
|
CodeCoverageDir string
|
|
InputDelay int
|
|
MaxAttempts int
|
|
// If set, each test's run duration is appended to this file (as
|
|
// "<seconds> <test name>"). run_integration_tests.sh prints the slowest at
|
|
// the end, so slow or anomalous tests can be spotted across CI runs. We
|
|
// write to a file rather than stdout/stderr because `go test` captures
|
|
// those and only shows them with -v. Empty disables it.
|
|
LogTimingsPath string
|
|
}
|
|
|
|
// This function lets you run tests either from within `go test` or from a regular binary.
|
|
// The reason for having two separate ways of testing is that `go test` isn't great at
|
|
// showing what's actually happening during the test, but it's still good at running
|
|
// tests in telling you about their results.
|
|
func RunTests(args RunTestArgs) error {
|
|
projectRootDir := lazycoreUtils.GetLazyRootDirectory()
|
|
err := os.Chdir(projectRootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
testDir := filepath.Join(projectRootDir, "test", "_results")
|
|
if err := buildLazygit(args); err != nil {
|
|
return err
|
|
}
|
|
|
|
gitVersion, err := getGitVersion()
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Start each run with a fresh timings file (see RunTestArgs.LogTimingsPath).
|
|
if args.LogTimingsPath != "" {
|
|
_ = os.Remove(args.LogTimingsPath)
|
|
}
|
|
|
|
for _, test := range args.Tests {
|
|
args.TestWrapper(test, func() error {
|
|
paths := NewPaths(
|
|
filepath.Join(testDir, test.Name()),
|
|
)
|
|
|
|
for i := range args.MaxAttempts {
|
|
err := runTest(test, args, paths, projectRootDir, gitVersion)
|
|
if err != nil {
|
|
if i == args.MaxAttempts-1 {
|
|
return err
|
|
}
|
|
args.Logf("retrying test %s", test.Name())
|
|
} else {
|
|
break
|
|
}
|
|
}
|
|
|
|
return nil
|
|
})
|
|
}
|
|
|
|
return nil
|
|
}
|
|
|
|
func runTest(
|
|
test *IntegrationTest,
|
|
args RunTestArgs,
|
|
paths Paths,
|
|
projectRootDir string,
|
|
gitVersion *git_commands.GitVersion,
|
|
) error {
|
|
if test.Skip() {
|
|
args.Logf("Skipping test %s", test.Name())
|
|
return nil
|
|
}
|
|
|
|
if !test.ShouldRunForGitVersion(gitVersion) {
|
|
args.Logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
|
|
return nil
|
|
}
|
|
|
|
workingDir, err := prepareTestDir(test, paths, projectRootDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
cmd, err := getLazygitCommand(test, args, paths, projectRootDir, workingDir)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
start := time.Now()
|
|
pid, err := args.RunCmd(cmd)
|
|
if args.LogTimingsPath != "" {
|
|
logTestTiming(args.LogTimingsPath, test.Name(), time.Since(start))
|
|
}
|
|
|
|
// Print race detector log regardless of the command's exit status
|
|
if args.RaceDetector {
|
|
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
|
|
if bytes, err := os.ReadFile(logPath); err == nil {
|
|
args.Logf("Race detector log:\n" + string(bytes))
|
|
}
|
|
}
|
|
|
|
return err
|
|
}
|
|
|
|
// timingsMutex serializes appends to the timings file, since tests run in
|
|
// parallel.
|
|
var timingsMutex sync.Mutex
|
|
|
|
func logTestTiming(path, name string, duration time.Duration) {
|
|
timingsMutex.Lock()
|
|
defer timingsMutex.Unlock()
|
|
|
|
f, err := os.OpenFile(path, os.O_APPEND|os.O_CREATE|os.O_WRONLY, 0o644)
|
|
if err != nil {
|
|
return
|
|
}
|
|
defer f.Close()
|
|
|
|
fmt.Fprintf(f, "%.2f %s\n", duration.Seconds(), name)
|
|
}
|
|
|
|
func prepareTestDir(
|
|
test *IntegrationTest,
|
|
paths Paths,
|
|
rootDir string,
|
|
) (string, error) {
|
|
findOrCreateDir(paths.Root())
|
|
deleteAndRecreateEmptyDir(paths.Actual())
|
|
|
|
err := os.Mkdir(paths.ActualRepo(), 0o777)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
|
|
return createFixture(test, paths, rootDir)
|
|
}
|
|
|
|
func buildLazygit(testArgs RunTestArgs) error {
|
|
args := []string{"go", "build"}
|
|
if testArgs.WaitForDebugger {
|
|
// Disable compiler optimizations (-N) and inlining (-l) because this
|
|
// makes debugging work better
|
|
args = append(args, "-gcflags=all=-N -l")
|
|
}
|
|
if testArgs.RaceDetector {
|
|
args = append(args, "-race")
|
|
}
|
|
if testArgs.CodeCoverageDir != "" {
|
|
args = append(args, "-cover")
|
|
}
|
|
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
|
|
osCommand := oscommands.NewDummyOSCommand()
|
|
return osCommand.Cmd.New(args).Run()
|
|
}
|
|
|
|
// A failing setup step panics with this so that the remaining steps, which
|
|
// would only produce follow-on failures, are skipped.
|
|
type fixtureFailure string
|
|
|
|
// Sets up the fixture for test and returns the working directory to invoke
|
|
// lazygit in.
|
|
func createFixture(test *IntegrationTest, paths Paths, rootDir string) (workingDir string, err error) {
|
|
// Tests run as parallel subtests, and a panic escaping one of them takes
|
|
// down the whole test binary, discarding every other test's result along
|
|
// with it. Report a broken fixture as this test's error instead.
|
|
defer func() {
|
|
panicValue := recover()
|
|
if panicValue == nil {
|
|
return
|
|
}
|
|
failure, ok := panicValue.(fixtureFailure)
|
|
if !ok {
|
|
panic(panicValue)
|
|
}
|
|
err = errors.New(string(failure))
|
|
}()
|
|
|
|
env := NewTestEnvironment(rootDir)
|
|
|
|
env = append(env, fmt.Sprintf("%s=%s", PWD, paths.ActualRepo()))
|
|
shell := NewShell(
|
|
paths.ActualRepo(),
|
|
env,
|
|
func(errorMsg string) { panic(fixtureFailure(errorMsg)) },
|
|
)
|
|
shell.Init()
|
|
|
|
test.SetupRepo(shell)
|
|
|
|
return shell.dir, nil
|
|
}
|
|
|
|
func testPath(rootdir string) string {
|
|
return filepath.Join(rootdir, "test")
|
|
}
|
|
|
|
func globalGitConfigPath(rootDir string) string {
|
|
return filepath.Join(testPath(rootDir), "global_git_config")
|
|
}
|
|
|
|
func getGitVersion() (*git_commands.GitVersion, error) {
|
|
osCommand := oscommands.NewDummyOSCommand()
|
|
cmdObj := osCommand.Cmd.New([]string{"git", "--version"})
|
|
versionStr, err := cmdObj.RunWithOutput()
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
return git_commands.ParseGitVersion(versionStr)
|
|
}
|
|
|
|
func getLazygitCommand(
|
|
test *IntegrationTest,
|
|
args RunTestArgs,
|
|
paths Paths,
|
|
rootDir string,
|
|
workingDir string,
|
|
) (*exec.Cmd, error) {
|
|
osCommand := oscommands.NewDummyOSCommand()
|
|
|
|
err := os.RemoveAll(paths.Config())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
|
|
err = oscommands.CopyDir(templateConfigDir, paths.Config())
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
|
|
resolvedExtraArgs := lo.Map(test.ExtraCmdArgs(), func(arg string, _ int) string {
|
|
return utils.ResolvePlaceholderString(arg, map[string]string{
|
|
"actualPath": paths.Actual(),
|
|
"actualRepoPath": paths.ActualRepo(),
|
|
})
|
|
})
|
|
|
|
cmdArgs := make([]string, 0, 3+len(resolvedExtraArgs))
|
|
cmdArgs = append(cmdArgs, tempLazygitPath(), "-debug", "--use-config-dir="+paths.Config())
|
|
cmdArgs = append(cmdArgs, resolvedExtraArgs...)
|
|
|
|
// Use a limited environment for test isolation, including pass through
|
|
// of just allowed host environment variables
|
|
cmdObj := osCommand.Cmd.NewWithEnviron(cmdArgs, NewTestEnvironment(rootDir))
|
|
|
|
// Integration tests related to symlink behavior need a PWD that
|
|
// preserves symlinks. By default, SetWd will set a symlink-resolved
|
|
// value for PWD. Here, we override that with the path (that may)
|
|
// contain a symlink to simulate behavior in a user's shell correctly.
|
|
cmdObj.SetWd(workingDir)
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", PWD, workingDir))
|
|
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", LAZYGIT_ROOT_DIR, rootDir))
|
|
|
|
if args.CodeCoverageDir != "" {
|
|
// We set this explicitly here rather than inherit it from the test runner's
|
|
// environment because the test runner has its own coverage directory that
|
|
// it writes to and so if we pass GOCOVERDIR to that, it will be overwritten.
|
|
cmdObj.AddEnvVars("GOCOVERDIR=" + args.CodeCoverageDir)
|
|
}
|
|
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
|
|
if args.Sandbox {
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
|
|
}
|
|
if args.WaitForDebugger {
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=true", WAIT_FOR_DEBUGGER_ENV_VAR))
|
|
}
|
|
// Set a race detector log path only to avoid spamming the terminal with the
|
|
// logs. We are not showing this anywhere yet.
|
|
cmdObj.AddEnvVars(fmt.Sprintf("GORACE=log_path=%s", raceDetectorLogsPath()))
|
|
if test.ExtraEnvVars() != nil {
|
|
for key, value := range test.ExtraEnvVars() {
|
|
resolvedValue := utils.ResolvePlaceholderString(value, map[string]string{
|
|
"actualPath": paths.Actual(),
|
|
"actualRepoPath": paths.ActualRepo(),
|
|
})
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", key, resolvedValue))
|
|
}
|
|
}
|
|
|
|
if args.InputDelay > 0 {
|
|
cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", args.InputDelay))
|
|
}
|
|
|
|
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))
|
|
|
|
return cmdObj.GetCmd(), nil
|
|
}
|
|
|
|
func tempLazygitPath() string {
|
|
return filepath.Join("/tmp", "lazygit", "test_lazygit")
|
|
}
|
|
|
|
func raceDetectorLogsPath() string {
|
|
return filepath.Join("/tmp", "lazygit", "race_log")
|
|
}
|
|
|
|
func findOrCreateDir(path string) {
|
|
_, err := os.Stat(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = os.MkdirAll(path, 0o777)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
}
|
|
|
|
func deleteAndRecreateEmptyDir(path string) {
|
|
// remove contents of integration test directory
|
|
dir, err := os.ReadDir(path)
|
|
if err != nil {
|
|
if os.IsNotExist(err) {
|
|
err = os.Mkdir(path, 0o777)
|
|
if err != nil {
|
|
panic(err)
|
|
}
|
|
} else {
|
|
panic(err)
|
|
}
|
|
}
|
|
for _, d := range dir {
|
|
os.RemoveAll(filepath.Join(path, d.Name()))
|
|
}
|
|
}
|