1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-04-23 12:18:51 +02:00

Capture test code coverage stats (#3135)

This commit is contained in:
Jesse Duffield 2023-11-30 19:21:22 +11:00 committed by GitHub
commit f9b4bcde38
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
7 changed files with 152 additions and 78 deletions

View File

@ -36,7 +36,14 @@ jobs:
- name: Test code - name: Test code
# we're passing -short so that we skip the integration tests, which will be run in parallel below # we're passing -short so that we skip the integration tests, which will be run in parallel below
run: | run: |
go test ./... -short mkdir -p /tmp/code_coverage
go test ./... -short -cover -args "-test.gocoverdir=/tmp/code_coverage"
- name: Upload code coverage artifacts
uses: actions/upload-artifact@v3
with:
name: coverage-unit-${{ matrix.os }}-${{ github.run_id }}
path: /tmp/code_coverage
integration-tests: integration-tests:
strategy: strategy:
fail-fast: false fail-fast: false
@ -86,8 +93,17 @@ jobs:
- name: Print git version - name: Print git version
run: git --version run: git --version
- name: Test code - name: Test code
env:
# See https://go.dev/blog/integration-test-coverage
LAZYGIT_GOCOVERDIR: /tmp/code_coverage
run: | run: |
mkdir -p /tmp/code_coverage
./scripts/run_integration_tests.sh ./scripts/run_integration_tests.sh
- name: Upload code coverage artifacts
uses: actions/upload-artifact@v3
with:
name: coverage-integration-${{ matrix.git-version }}-${{ github.run_id }}
path: /tmp/code_coverage
build: build:
runs-on: ubuntu-latest runs-on: ubuntu-latest
env: env:
@ -169,3 +185,31 @@ jobs:
mode: exactly mode: exactly
count: 1 count: 1
labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n" labels: "ignore-for-release, feature, enhancement, bug, maintenance, docs, i18n"
upload-coverage:
# List all jobs that produce coverage files
needs: [unit-tests, integration-tests]
runs-on: ubuntu-latest
steps:
- name: Checkout code
uses: actions/checkout@v3
- name: Download all coverage artifacts
uses: actions/download-artifact@v3
with:
path: /tmp/code_coverage
- name: Combine coverage files
run: |
# Find all directories in /tmp/code_coverage and create a comma-separated list
COVERAGE_DIRS=$(find /tmp/code_coverage -mindepth 1 -maxdepth 1 -type d -printf '/tmp/code_coverage/%f,' | sed 's/,$//')
echo "Coverage directories: $COVERAGE_DIRS"
# Run the combine command with the generated list
go tool covdata textfmt -i=$COVERAGE_DIRS -o coverage.out
echo "Combined coverage:"
go tool cover -func coverage.out | tail -1 | awk '{print $3}'
- name: Upload to Codacy
run: |
CODACY_PROJECT_TOKEN=${{ secrets.CODACY_PROJECT_TOKEN }} \
bash <(curl -Ls https://coverage.codacy.com/get.sh) report \
--force-coverage-parser go -r coverage.out

2
.gitignore vendored
View File

@ -42,3 +42,5 @@ __debug_bin
.worktrees .worktrees
demo/output/* demo/output/*
coverage.out

View File

@ -29,17 +29,18 @@ func RunCLI(testNames []string, slow bool, sandbox bool, waitForDebugger bool, r
inputDelay = SLOW_INPUT_DELAY inputDelay = SLOW_INPUT_DELAY
} }
err := components.RunTests( err := components.RunTests(components.RunTestArgs{
getTestsToRun(testNames), Tests: getTestsToRun(testNames),
log.Printf, Logf: log.Printf,
runCmdInTerminal, RunCmd: runCmdInTerminal,
runAndPrintFatalError, TestWrapper: runAndPrintFatalError,
sandbox, Sandbox: sandbox,
waitForDebugger, WaitForDebugger: waitForDebugger,
raceDetector, RaceDetector: raceDetector,
inputDelay, CodeCoverageDir: "",
1, InputDelay: inputDelay,
) MaxAttempts: 1,
})
if err != nil { if err != nil {
log.Print(err.Error()) log.Print(err.Error())
} }

View File

@ -28,13 +28,17 @@ func TestIntegration(t *testing.T) {
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1) parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0) parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
raceDetector := os.Getenv("LAZYGIT_RACE_DETECTOR") != "" raceDetector := os.Getenv("LAZYGIT_RACE_DETECTOR") != ""
// LAZYGIT_GOCOVERDIR is the directory where we write coverage files to. If this directory
// is defined, go binaries built with the -cover flag will write coverage files to
// to it.
codeCoverageDir := os.Getenv("LAZYGIT_GOCOVERDIR")
testNumber := 0 testNumber := 0
err := components.RunTests( err := components.RunTests(components.RunTestArgs{
tests.GetTests(), Tests: tests.GetTests(),
t.Logf, Logf: t.Logf,
runCmdHeadless, RunCmd: runCmdHeadless,
func(test *components.IntegrationTest, f func() error) { TestWrapper: func(test *components.IntegrationTest, f func() error) {
defer func() { testNumber += 1 }() defer func() { testNumber += 1 }()
if testNumber%parallelTotal != parallelIndex { if testNumber%parallelTotal != parallelIndex {
return return
@ -52,13 +56,14 @@ func TestIntegration(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
}) })
}, },
false, Sandbox: false,
false, WaitForDebugger: false,
raceDetector, RaceDetector: raceDetector,
0, CodeCoverageDir: codeCoverageDir,
InputDelay: 0,
// Allow two attempts at each test to get around flakiness // Allow two attempts at each test to get around flakiness
2, MaxAttempts: 2,
) })
assert.NoError(t, err) assert.NoError(t, err)
} }

View File

@ -385,17 +385,18 @@ func quit(g *gocui.Gui, v *gocui.View) error {
} }
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) { func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
err := components.RunTests( err := components.RunTests(components.RunTestArgs{
[]*components.IntegrationTest{test}, Tests: []*components.IntegrationTest{test},
log.Printf, Logf: log.Printf,
runCmdInTerminal, RunCmd: runCmdInTerminal,
runAndPrintError, TestWrapper: runAndPrintError,
sandbox, Sandbox: sandbox,
waitForDebugger, WaitForDebugger: waitForDebugger,
raceDetector, RaceDetector: raceDetector,
inputDelay, CodeCoverageDir: "",
1, InputDelay: inputDelay,
) MaxAttempts: 1,
})
if err != nil { if err != nil {
log.Println(err.Error()) log.Println(err.Error())
} }

View File

@ -20,21 +20,24 @@ const (
GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL" GIT_CONFIG_GLOBAL_ENV_VAR = "GIT_CONFIG_GLOBAL"
) )
type RunTestArgs struct {
Tests []*IntegrationTest
Logf func(format string, formatArgs ...interface{})
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
}
// This function lets you run tests either from within `go test` or from a regular binary. // 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 // 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 // showing what's actually happening during the test, but it's still good at running
// tests in telling you about their results. // tests in telling you about their results.
func RunTests( func RunTests(args RunTestArgs) error {
tests []*IntegrationTest,
logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) (int, error),
testWrapper func(test *IntegrationTest, f func() error),
sandbox bool,
waitForDebugger bool,
raceDetector bool,
inputDelay int,
maxAttempts int,
) error {
projectRootDir := lazycoreUtils.GetLazyRootDirectory() projectRootDir := lazycoreUtils.GetLazyRootDirectory()
err := os.Chdir(projectRootDir) err := os.Chdir(projectRootDir)
if err != nil { if err != nil {
@ -42,8 +45,7 @@ func RunTests(
} }
testDir := filepath.Join(projectRootDir, "test", "_results") testDir := filepath.Join(projectRootDir, "test", "_results")
if err := buildLazygit(args); err != nil {
if err := buildLazygit(waitForDebugger, raceDetector); err != nil {
return err return err
} }
@ -52,21 +54,21 @@ func RunTests(
return err return err
} }
for _, test := range tests { for _, test := range args.Tests {
test := test test := test
testWrapper(test, func() error { //nolint: thelper args.TestWrapper(test, func() error { //nolint: thelper
paths := NewPaths( paths := NewPaths(
filepath.Join(testDir, test.Name()), filepath.Join(testDir, test.Name()),
) )
for i := 0; i < maxAttempts; i++ { for i := 0; i < args.MaxAttempts; i++ {
err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, waitForDebugger, raceDetector, inputDelay, gitVersion) err := runTest(test, args, paths, projectRootDir, gitVersion)
if err != nil { if err != nil {
if i == maxAttempts-1 { if i == args.MaxAttempts-1 {
return err return err
} }
logf("retrying test %s", test.Name()) args.Logf("retrying test %s", test.Name())
} else { } else {
break break
} }
@ -81,23 +83,18 @@ func RunTests(
func runTest( func runTest(
test *IntegrationTest, test *IntegrationTest,
args RunTestArgs,
paths Paths, paths Paths,
projectRootDir string, projectRootDir string,
logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) (int, error),
sandbox bool,
waitForDebugger bool,
raceDetector bool,
inputDelay int,
gitVersion *git_commands.GitVersion, gitVersion *git_commands.GitVersion,
) error { ) error {
if test.Skip() { if test.Skip() {
logf("Skipping test %s", test.Name()) args.Logf("Skipping test %s", test.Name())
return nil return nil
} }
if !test.ShouldRunForGitVersion(gitVersion) { if !test.ShouldRunForGitVersion(gitVersion) {
logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch) args.Logf("Skipping test %s for git version %d.%d.%d", test.Name(), gitVersion.Major, gitVersion.Minor, gitVersion.Patch)
return nil return nil
} }
@ -105,18 +102,18 @@ func runTest(
return err return err
} }
cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, waitForDebugger, inputDelay) cmd, err := getLazygitCommand(test, args, paths, projectRootDir)
if err != nil { if err != nil {
return err return err
} }
pid, err := runCmd(cmd) pid, err := args.RunCmd(cmd)
// Print race detector log regardless of the command's exit status // Print race detector log regardless of the command's exit status
if raceDetector { if args.RaceDetector {
logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid) logPath := fmt.Sprintf("%s.%d", raceDetectorLogsPath(), pid)
if bytes, err := os.ReadFile(logPath); err == nil { if bytes, err := os.ReadFile(logPath); err == nil {
logf("Race detector log:\n" + string(bytes)) args.Logf("Race detector log:\n" + string(bytes))
} }
} }
@ -139,20 +136,19 @@ func prepareTestDir(
return createFixture(test, paths, rootDir) return createFixture(test, paths, rootDir)
} }
func buildLazygit(debug bool, raceDetector bool) error { func buildLazygit(testArgs RunTestArgs) error {
// // TODO: remove this line!
// // skipping this because I'm not making changes to the app code atm.
// return nil
args := []string{"go", "build"} args := []string{"go", "build"}
if debug { if testArgs.WaitForDebugger {
// Disable compiler optimizations (-N) and inlining (-l) because this // Disable compiler optimizations (-N) and inlining (-l) because this
// makes debugging work better // makes debugging work better
args = append(args, "-gcflags=all=-N -l") args = append(args, "-gcflags=all=-N -l")
} }
if raceDetector { if testArgs.RaceDetector {
args = append(args, "-race") args = append(args, "-race")
} }
if testArgs.CodeCoverageDir != "" {
args = append(args, "-cover")
}
args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go")) args = append(args, "-o", tempLazygitPath(), filepath.FromSlash("pkg/integration/clients/injector/main.go"))
osCommand := oscommands.NewDummyOSCommand() osCommand := oscommands.NewDummyOSCommand()
return osCommand.Cmd.New(args).Run() return osCommand.Cmd.New(args).Run()
@ -183,7 +179,7 @@ func getGitVersion() (*git_commands.GitVersion, error) {
return git_commands.ParseGitVersion(versionStr) return git_commands.ParseGitVersion(versionStr)
} }
func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, waitForDebugger bool, inputDelay int) (*exec.Cmd, error) { func getLazygitCommand(test *IntegrationTest, args RunTestArgs, paths Paths, rootDir string) (*exec.Cmd, error) {
osCommand := oscommands.NewDummyOSCommand() osCommand := oscommands.NewDummyOSCommand()
err := os.RemoveAll(paths.Config()) err := os.RemoveAll(paths.Config())
@ -211,11 +207,18 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
cmdObj := osCommand.Cmd.New(cmdArgs) cmdObj := osCommand.Cmd.New(cmdArgs)
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())) cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", TEST_NAME_ENV_VAR, test.Name()))
if sandbox { if args.Sandbox {
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true")) cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true"))
} }
if waitForDebugger { if args.WaitForDebugger {
cmdObj.AddEnvVars(fmt.Sprintf("%s=true", WAIT_FOR_DEBUGGER_ENV_VAR)) 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 // Set a race detector log path only to avoid spamming the terminal with the
@ -227,8 +230,8 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb
} }
} }
if inputDelay > 0 { if args.InputDelay > 0 {
cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", inputDelay)) cmdObj.AddEnvVars(fmt.Sprintf("INPUT_DELAY=%d", args.InputDelay))
} }
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir))) cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", GIT_CONFIG_GLOBAL_ENV_VAR, globalGitConfigPath(rootDir)))

View File

@ -10,7 +10,25 @@ fi
cp test/global_git_config ~/.gitconfig cp test/global_git_config ~/.gitconfig
go test pkg/integration/clients/*.go # if the LAZYGIT_GOCOVERDIR env var is set, we'll capture code coverage data
if [ -n "$LAZYGIT_GOCOVERDIR" ]; then
# Go expects us to either be running the test binary directly or running `go test`, but because
# we're doing both and because we want to combine coverage data for both, we need to be a little
# hacky. To capture the coverage data for the test runner we pass the test.gocoverdir positional
# arg, but if we do that then the GOCOVERDIR env var (which you typically pass to the test binary) will be overwritten by the test runner. So we're passing LAZYGIT_COCOVERDIR instead
# and then internally passing that to the test binary as GOCOVERDIR.
go test -cover -coverpkg=github.com/jesseduffield/lazygit/pkg/... pkg/integration/clients/*.go -args -test.gocoverdir="/tmp/code_coverage"
# We're merging the coverage data for the sake of having fewer artefacts to upload.
# We can't merge inline so we're merging to a tmp dir then moving back to the original.
mkdir -p /tmp/code_coverage_merged
go tool covdata merge -i=/tmp/code_coverage -o=/tmp/code_coverage_merged
rm -rf /tmp/code_coverage
mv /tmp/code_coverage_merged /tmp/code_coverage
else
go test pkg/integration/clients/*.go
fi
EXITCODE=$? EXITCODE=$?
if test -f ~/.gitconfig.lazygit.bak; then if test -f ~/.gitconfig.lazygit.bak; then