From b6c892a08a29527211304b1558c2e584aa2e0cea Mon Sep 17 00:00:00 2001 From: Stefan Haller Date: Sat, 9 Sep 2023 15:00:58 +0200 Subject: [PATCH] Provide a simple way to debug an integration test --- .vscode/launch.json | 12 + cmd/integration_test/main.go | 6 +- go.mod | 1 + go.sum | 2 + pkg/integration/README.md | 9 + pkg/integration/clients/cli.go | 3 +- pkg/integration/clients/go_test.go | 1 + pkg/integration/clients/injector/main.go | 29 ++ pkg/integration/clients/tui.go | 28 +- pkg/integration/components/runner.go | 11 +- vendor/github.com/mitchellh/go-ps/.gitignore | 1 + vendor/github.com/mitchellh/go-ps/LICENSE.md | 21 ++ vendor/github.com/mitchellh/go-ps/README.md | 34 +++ vendor/github.com/mitchellh/go-ps/Vagrantfile | 43 +++ vendor/github.com/mitchellh/go-ps/process.go | 40 +++ .../mitchellh/go-ps/process_darwin.go | 138 ++++++++++ .../mitchellh/go-ps/process_freebsd.go | 260 ++++++++++++++++++ .../mitchellh/go-ps/process_linux.go | 35 +++ .../mitchellh/go-ps/process_solaris.go | 96 +++++++ .../mitchellh/go-ps/process_unix.go | 95 +++++++ .../mitchellh/go-ps/process_windows.go | 119 ++++++++ vendor/modules.txt | 3 + 22 files changed, 975 insertions(+), 12 deletions(-) create mode 100644 vendor/github.com/mitchellh/go-ps/.gitignore create mode 100644 vendor/github.com/mitchellh/go-ps/LICENSE.md create mode 100644 vendor/github.com/mitchellh/go-ps/README.md create mode 100644 vendor/github.com/mitchellh/go-ps/Vagrantfile create mode 100644 vendor/github.com/mitchellh/go-ps/process.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_darwin.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_freebsd.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_linux.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_solaris.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_unix.go create mode 100644 vendor/github.com/mitchellh/go-ps/process_windows.go diff --git a/.vscode/launch.json b/.vscode/launch.json index df6e0ce80..4e05edffb 100644 --- a/.vscode/launch.json +++ b/.vscode/launch.json @@ -41,6 +41,18 @@ "hideSystemGoroutines": true, "console": "integratedTerminal", }, + { + // To use this, first start an integration test with the "cli" runner and + // use the -debug option; e.g. + // $ make integration-test-cli -- -debug tag/reset.go + "name": "Attach to integration test runner", + "type": "go", + "request": "attach", + "mode": "local", + "processId": "test_lazygit", + "hideSystemGoroutines": true, + "console": "integratedTerminal", + }, ], "compounds": [ { diff --git a/cmd/integration_test/main.go b/cmd/integration_test/main.go index 32df7dd0a..7153691da 100644 --- a/cmd/integration_test/main.go +++ b/cmd/integration_test/main.go @@ -38,6 +38,7 @@ func main() { testNames := os.Args[2:] slow := false sandbox := false + waitForDebugger := false // get the next arg if it's --slow if len(os.Args) > 2 { if os.Args[2] == "--slow" || os.Args[2] == "-slow" { @@ -46,10 +47,13 @@ func main() { } else if os.Args[2] == "--sandbox" || os.Args[2] == "-sandbox" { testNames = os.Args[3:] sandbox = true + } else if os.Args[2] == "--debug" || os.Args[2] == "-debug" { + testNames = os.Args[3:] + waitForDebugger = true } } - clients.RunCLI(testNames, slow, sandbox) + clients.RunCLI(testNames, slow, sandbox, waitForDebugger) case "tui": clients.RunTUI() default: diff --git a/go.mod b/go.mod index a2c8c36de..2c609288d 100644 --- a/go.mod +++ b/go.mod @@ -25,6 +25,7 @@ require ( github.com/lucasb-eyer/go-colorful v1.2.0 github.com/mattn/go-runewidth v0.0.15 github.com/mgutz/str v1.2.0 + github.com/mitchellh/go-ps v1.0.0 github.com/pmezard/go-difflib v1.0.0 github.com/sahilm/fuzzy v0.1.0 github.com/samber/lo v1.31.0 diff --git a/go.sum b/go.sum index 0dcfeb428..5b4853b2c 100644 --- a/go.sum +++ b/go.sum @@ -230,6 +230,8 @@ github.com/mgutz/str v1.2.0 h1:4IzWSdIz9qPQWLfKZ0rJcV0jcUDpxvP4JVZ4GXQyvSw= github.com/mgutz/str v1.2.0/go.mod h1:w1v0ofgLaJdoD0HpQ3fycxKD1WtxpjSo151pK/31q6w= github.com/mitchellh/go-homedir v1.1.0 h1:lukF9ziXFxDFPkA1vsr5zpc1XuPDn/wFntq5mG+4E0Y= github.com/mitchellh/go-homedir v1.1.0/go.mod h1:SfyaCUpYCn1Vlf4IUYiD9fPX4A5wJrkLzIz1N1q0pr0= +github.com/mitchellh/go-ps v1.0.0 h1:i6ampVEEF4wQFF+bkYfwYgY+F/uYJDktmvLPf7qIgjc= +github.com/mitchellh/go-ps v1.0.0/go.mod h1:J4lOc8z8yJs6vUwklHw2XEIiT4z4C40KtWVN3nvg8Pg= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e h1:fD57ERR4JtEqsWbfPhv4DMiApHyliiK5xCTNVSPiaAs= github.com/niemeyer/pretty v0.0.0-20200227124842-a10e7caefd8e/go.mod h1:zD1mROLANZcx1PVRCS0qkT7pwLkGfwJo4zjcN/Tysno= github.com/onsi/ginkgo v1.6.0/go.mod h1:lLunBs/Ym6LB5Z9jYTR76FiuTmxDTDusOGeTQH+WWjE= diff --git a/pkg/integration/README.md b/pkg/integration/README.md index e0d32665a..1e0562509 100644 --- a/pkg/integration/README.md +++ b/pkg/integration/README.md @@ -61,6 +61,15 @@ If you've opened an integration test file in your editor you can run that file b The test will run in a VSCode terminal: ![image](https://user-images.githubusercontent.com/8456633/201500446-b87abf11-9653-438f-8a9a-e0bf8abdb7ee.png) +### Debugging tests + +Debugging an integration test is possible in two ways: + +1. Use the -debug option of the integration test runner's "cli" command, e.g. `go run cmd/integration_test/main.go cli -debug tag/reset.go` +2. Select a test in the "tui" runner and hit "d" to debug it. + +In both cases the test runner will print to the console that it is waiting for a debugger to attach, so now you need to tell your debugger to attach to a running process with the name "test_lazygit". If you are using Visual Studio Code, an easy way to do that is to use the "Attach to integration test runner" debug configuration. The test runner will resume automatically when it detects that a debugger was attached. Don't forget to set a breakpoint in the code that you want to step through, otherwise the test will just finish (i.e. it doesn't stop in the debugger automatically). + ### Sandbox mode Say you want to do a manual test of how lazygit handles merge-conflicts, but you can't be bothered actually finding a way to create merge conflicts in a repo. To make your life easier, you can simply run a merge-conflicts test in sandbox mode, meaning the setup step is run for you, and then instead of the test driving the lazygit session, you're allowed to drive it yourself. diff --git a/pkg/integration/clients/cli.go b/pkg/integration/clients/cli.go index 08cff3f00..571dea491 100644 --- a/pkg/integration/clients/cli.go +++ b/pkg/integration/clients/cli.go @@ -23,7 +23,7 @@ import ( // If invoked directly, you can specify tests to run by passing their names as positional arguments -func RunCLI(testNames []string, slow bool, sandbox bool) { +func RunCLI(testNames []string, slow bool, sandbox bool, waitForDebugger bool) { inputDelay := tryConvert(os.Getenv("INPUT_DELAY"), 0) if slow { inputDelay = SLOW_INPUT_DELAY @@ -35,6 +35,7 @@ func RunCLI(testNames []string, slow bool, sandbox bool) { runCmdInTerminal, runAndPrintFatalError, sandbox, + waitForDebugger, inputDelay, 1, ) diff --git a/pkg/integration/clients/go_test.go b/pkg/integration/clients/go_test.go index 18d8569bd..01f174211 100644 --- a/pkg/integration/clients/go_test.go +++ b/pkg/integration/clients/go_test.go @@ -52,6 +52,7 @@ func TestIntegration(t *testing.T) { }) }, false, + false, 0, // Allow two attempts at each test to get around flakiness 2, diff --git a/pkg/integration/clients/injector/main.go b/pkg/integration/clients/injector/main.go index 0754503e8..63c3e2a07 100644 --- a/pkg/integration/clients/injector/main.go +++ b/pkg/integration/clients/injector/main.go @@ -3,12 +3,14 @@ package main import ( "fmt" "os" + "time" "github.com/jesseduffield/lazygit/pkg/app" "github.com/jesseduffield/lazygit/pkg/app/daemon" "github.com/jesseduffield/lazygit/pkg/integration/components" "github.com/jesseduffield/lazygit/pkg/integration/tests" integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/mitchellh/go-ps" ) // The purpose of this program is to run lazygit with an integration test passed in. @@ -29,6 +31,15 @@ func main() { integrationTest := getIntegrationTest() + if os.Getenv("WAIT_FOR_DEBUGGER") != "" { + println("Waiting for debugger to attach...") + for !isDebuggerAttached() { + time.Sleep(time.Millisecond * 100) + } + + println("Debugger attached, continuing") + } + app.Start(dummyBuildInfo, integrationTest) } @@ -56,3 +67,21 @@ func getIntegrationTest() integrationTypes.IntegrationTest { panic("Could not find integration test with name: " + integrationTestName) } + +// Returns whether we are running under a debugger. It uses a heuristic to find +// out: when using dlv, it starts a debugserver executable (which is part of +// lldb), and the debuggee becomes a child process of that. So if the name of +// our parent process is "debugserver", we run under a debugger. This works even +// if the parent process used to be the shell and you then attach to the running +// executable. +// +// On Mac this works with VS Code, with the Jetbrains Goland IDE, and when using +// dlv attach in a terminal. I have not been able to verify that it works on +// other platforms, it may have to be adapted there. +func isDebuggerAttached() bool { + process, err := ps.FindProcess(os.Getppid()) + if err != nil { + return false + } + return process.Executable() == "debugserver" +} diff --git a/pkg/integration/clients/tui.go b/pkg/integration/clients/tui.go index 9b873caab..8036b118b 100644 --- a/pkg/integration/clients/tui.go +++ b/pkg/integration/clients/tui.go @@ -85,7 +85,7 @@ func RunTUI() { return nil } - suspendAndRunTest(currentTest, true, 0) + suspendAndRunTest(currentTest, true, false, 0) return nil }); err != nil { @@ -98,7 +98,7 @@ func RunTUI() { return nil } - suspendAndRunTest(currentTest, false, 0) + suspendAndRunTest(currentTest, false, false, 0) return nil }); err != nil { @@ -111,7 +111,20 @@ func RunTUI() { return nil } - suspendAndRunTest(currentTest, false, SLOW_INPUT_DELAY) + suspendAndRunTest(currentTest, false, false, SLOW_INPUT_DELAY) + + return nil + }); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding("list", 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error { + currentTest := app.getCurrentTest() + if currentTest == nil { + return nil + } + + suspendAndRunTest(currentTest, false, true, 0) return nil }); err != nil { @@ -271,12 +284,12 @@ func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod go } } -func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, inputDelay int) { +func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, inputDelay int) { if err := gocui.Screen.Suspend(); err != nil { panic(err) } - runTuiTest(test, sandbox, inputDelay) + runTuiTest(test, sandbox, waitForDebugger, inputDelay) fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return")) fmt.Scanln() // wait for enter press @@ -337,7 +350,7 @@ func (self *app) layout(g *gocui.Gui) error { keybindingsView.Title = "Keybindings" keybindingsView.Wrap = true keybindingsView.FgColor = gocui.ColorDefault - fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, o: open test file, shift+o: open test snapshot directory, forward-slash: filter") + fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, d: debug test, o: open test file, shift+o: open test snapshot directory, forward-slash: filter") } editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight) @@ -371,13 +384,14 @@ func quit(g *gocui.Gui, v *gocui.View) error { return gocui.ErrQuit } -func runTuiTest(test *components.IntegrationTest, sandbox bool, inputDelay int) { +func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, inputDelay int) { err := components.RunTests( []*components.IntegrationTest{test}, log.Printf, runCmdInTerminal, runAndPrintError, sandbox, + waitForDebugger, inputDelay, 1, ) diff --git a/pkg/integration/components/runner.go b/pkg/integration/components/runner.go index 9d380044a..339a7114e 100644 --- a/pkg/integration/components/runner.go +++ b/pkg/integration/components/runner.go @@ -29,6 +29,7 @@ func RunTests( runCmd func(cmd *exec.Cmd) error, testWrapper func(test *IntegrationTest, f func() error), sandbox bool, + waitForDebugger bool, inputDelay int, maxAttempts int, ) error { @@ -58,7 +59,7 @@ func RunTests( ) for i := 0; i < maxAttempts; i++ { - err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, inputDelay, gitVersion) + err := runTest(test, paths, projectRootDir, logf, runCmd, sandbox, waitForDebugger, inputDelay, gitVersion) if err != nil { if i == maxAttempts-1 { return err @@ -83,6 +84,7 @@ func runTest( logf func(format string, formatArgs ...interface{}), runCmd func(cmd *exec.Cmd) error, sandbox bool, + waitForDebugger bool, inputDelay int, gitVersion *git_commands.GitVersion, ) error { @@ -100,7 +102,7 @@ func runTest( return err } - cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, inputDelay) + cmd, err := getLazygitCommand(test, paths, projectRootDir, sandbox, waitForDebugger, inputDelay) if err != nil { return err } @@ -165,7 +167,7 @@ func getGitVersion() (*git_commands.GitVersion, error) { return git_commands.ParseGitVersion(versionStr) } -func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, inputDelay int) (*exec.Cmd, error) { +func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandbox bool, waitForDebugger bool, inputDelay int) (*exec.Cmd, error) { osCommand := oscommands.NewDummyOSCommand() err := os.RemoveAll(paths.Config()) @@ -197,6 +199,9 @@ func getLazygitCommand(test *IntegrationTest, paths Paths, rootDir string, sandb if sandbox { cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", SANDBOX_ENV_VAR, "true")) } + if waitForDebugger { + cmdObj.AddEnvVars("WAIT_FOR_DEBUGGER=true") + } if test.ExtraEnvVars() != nil { for key, value := range test.ExtraEnvVars() { cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", key, value)) diff --git a/vendor/github.com/mitchellh/go-ps/.gitignore b/vendor/github.com/mitchellh/go-ps/.gitignore new file mode 100644 index 000000000..a977916f6 --- /dev/null +++ b/vendor/github.com/mitchellh/go-ps/.gitignore @@ -0,0 +1 @@ +.vagrant/ diff --git a/vendor/github.com/mitchellh/go-ps/LICENSE.md b/vendor/github.com/mitchellh/go-ps/LICENSE.md new file mode 100644 index 000000000..229851590 --- /dev/null +++ b/vendor/github.com/mitchellh/go-ps/LICENSE.md @@ -0,0 +1,21 @@ +The MIT License (MIT) + +Copyright (c) 2014 Mitchell Hashimoto + +Permission is hereby granted, free of charge, to any person obtaining a copy +of this software and associated documentation files (the "Software"), to deal +in the Software without restriction, including without limitation the rights +to use, copy, modify, merge, publish, distribute, sublicense, and/or sell +copies of the Software, and to permit persons to whom the Software is +furnished to do so, subject to the following conditions: + +The above copyright notice and this permission notice shall be included in +all copies or substantial portions of the Software. + +THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR +IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY, +FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE +AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER +LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, +OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN +THE SOFTWARE. diff --git a/vendor/github.com/mitchellh/go-ps/README.md b/vendor/github.com/mitchellh/go-ps/README.md new file mode 100644 index 000000000..4e3d0e146 --- /dev/null +++ b/vendor/github.com/mitchellh/go-ps/README.md @@ -0,0 +1,34 @@ +# Process List Library for Go [![GoDoc](https://godoc.org/github.com/mitchellh/go-ps?status.png)](https://godoc.org/github.com/mitchellh/go-ps) + +go-ps is a library for Go that implements OS-specific APIs to list and +manipulate processes in a platform-safe way. The library can find and +list processes on Linux, Mac OS X, Solaris, and Windows. + +If you're new to Go, this library has a good amount of advanced Go educational +value as well. It uses some advanced features of Go: build tags, accessing +DLL methods for Windows, cgo for Darwin, etc. + +How it works: + + * **Darwin** uses the `sysctl` syscall to retrieve the process table. + * **Unix** uses the procfs at `/proc` to inspect the process tree. + * **Windows** uses the Windows API, and methods such as + `CreateToolhelp32Snapshot` to get a point-in-time snapshot of + the process table. + +## Installation + +Install using standard `go get`: + +``` +$ go get github.com/mitchellh/go-ps +... +``` + +## TODO + +Want to contribute? Here is a short TODO list of things that aren't +implemented for this library that would be nice: + + * FreeBSD support + * Plan9 support diff --git a/vendor/github.com/mitchellh/go-ps/Vagrantfile b/vendor/github.com/mitchellh/go-ps/Vagrantfile new file mode 100644 index 000000000..61662ab1e --- /dev/null +++ b/vendor/github.com/mitchellh/go-ps/Vagrantfile @@ -0,0 +1,43 @@ +# -*- mode: ruby -*- +# vi: set ft=ruby : + +# Vagrantfile API/syntax version. Don't touch unless you know what you're doing! +VAGRANTFILE_API_VERSION = "2" + +Vagrant.configure(VAGRANTFILE_API_VERSION) do |config| + config.vm.box = "chef/ubuntu-12.04" + + config.vm.provision "shell", inline: $script + + ["vmware_fusion", "vmware_workstation"].each do |p| + config.vm.provider "p" do |v| + v.vmx["memsize"] = "1024" + v.vmx["numvcpus"] = "2" + v.vmx["cpuid.coresPerSocket"] = "1" + end + end +end + +$script = <