lazygit/pkg/integration
Stefan Haller 8b99a3c949 Drop update-ref commands at the top of the rebase-todo file
The rebase.updateRefs feature of git is very useful to rebase a stack of
branches and keep everything nicely stacked; however, it is usually in the way
when you make a copy of a branch and want to rebase it "away" from the original
branch in some way or other. For example, the original branch might sit on main,
and you want to rebase the copy onto devel to see if things still compile there.
Or you want to do some heavy history rewriting experiments on the copy, but keep
the original branch in case the experiments fail. Or you want to split a branch
in two because it contains two unrelated sets of changes; so you make a copy,
and drop half of the commits from the copy, then check out the original branch
and drop the other half of the commits from it.

In all these cases, git's updateRefs feature insists on moving the original
branch along with the copy in the first rebase that you make on the copy. I
think this is a bug in git, it should create update-ref todos only for branches
that point into the middle of your branch (because only then do they form a
stack), not when they point at the head (because then it's a copy). I had a long
discussion about this on the git mailing list [1], but people either don't agree
or don't care enough.

So we fix this on our side: whenever we start a rebase for whatever reason, be
it interactive, non-interactive, or behind-the-scenes, we drop any update-ref
todos that are at the very top of the todo list, which fixes all the
above-mentioned scenarios nicely.

I will admit that there's one scenario where git's behavior is the desired one,
and the fix in this PR makes it worse: when you create a new branch off of an
existing one, with the intention of creating a stack of branches, but before you
make the first commit on the new branch you realize some problem with the first
branch (e.g. a commit that needs to be reworded or dropped). It this case you do
want both branches to be affected by the change. In my experience this scenario
is much rarer than the other ones that I described above, and it's also much
easier to recover from: just check out the other branch again and hard-reset it
to the rebased one.

[1]
https://public-inbox.org/git/354f9fed-567f-42c8-9da9-148a5e223022@haller-berlin.de/
2024-04-22 20:59:15 +02:00
..
clients Set working directory in lazygit test command 2024-01-12 19:59:31 +11:00
components pkg: fix some typos 2024-04-20 13:47:39 +02:00
tests Drop update-ref commands at the top of the rebase-todo file 2024-04-22 20:59:15 +02:00
types Make it mandatory to acknowledge toasts in tests 2024-01-14 17:42:03 +01:00
README.md Set working directory in lazygit test command 2024-01-12 19:59:31 +11:00

Integration Tests

The pkg/integration package is for integration testing: that is, actually running a real lazygit session and having a robot pretend to be a human user and then making assertions that everything works as expected.

TL;DR: integration tests live in pkg/integration/tests. Run integration tests with:

go run cmd/integration_test/main.go tui

or

go run cmd/integration_test/main.go cli [--slow or --sandbox] [testname or testpath...]

Writing tests

The tests live in pkg/integration/tests. Each test is registered in pkg/integration/tests/test_list.go which is an auto-generated file. You can re-generate that file by running go generate ./... at the root of the Lazygit repo.

Each test has two important steps: the setup step and the run step.

Setup step

In the setup step, we prepare a repo with shell commands, for example, creating a merge conflict that will need to be resolved upon opening lazygit. This is all done via the shell argument.

When the test runs, lazygit will open in the same working directory that the shell ends up in (so if you want to start lazygit somewhere other than the default location, you can use shell.Chdir() at the end of the setup step to set that working directory.

Run step

The run step has two arguments passed in:

  1. t (the test driver)
  2. keys

t is for driving the gui by pressing certain keys, selecting list items, etc. keys is for use when getting the test to press a particular key e.g. t.Views().Commits().Focus().PressKey(keys.Universal.Confirm)

Running tests

There are three ways to invoke a test:

  1. go run cmd/integration_test/main.go cli [--slow or --sandbox] [testname or testpath...]
  2. go run cmd/integration_test/main.go tui
  3. go test pkg/integration/clients/*.go

The first, the test runner, is for directly running a test from the command line. If you pass no arguments, it runs all tests. The second, the TUI, is for running tests from a terminal UI where it's easier to find a test and run it without having to copy it's name and paste it into the terminal. This is the easiest approach by far. The third, the go-test command, intended only for use in CI, to be run along with the other go test tests. This runs the tests in headless mode so there's no visual output.

The name of a test is based on its path, so the name of the test at pkg/integration/tests/commit/new_branch.go is commit/new_branch. So to run it with our test runner you would run go run cmd/integration_test/main.go cli commit/new_branch.

You can pass the INPUT_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses or mouse clicks, which helps for watching a test at a realistic speed to understand what it's doing. Or you can pass the '--slow' flag which sets a pre-set 'slow' key delay. In the tui you can press 't' to run the test in slow mode.

The resultant repo will be stored in test/_results, so if you're not sure what went wrong you can go there and inspect the repo.

Running tests in VSCode

If you've opened an integration test file in your editor you can run that file by bringing up the command panel with cmd+shift+p and typing 'run task', then selecting the test task you want to run

image image The test will run in a VSCode terminal: image

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.

To run a test in sandbox mode you can press 's' on a test in the test TUI or in the test runner pass the --sandbox argument.

Tips for writing tests

Handle most setup in the shell part of the test

Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer.

Create helper functions for (very) frequently used test logic

If within a test directory you find several tests need to share some logic, you can create a file called shared.go in that directory to hold shared helper functions (see pkg/integration/tests/filter_by_path/shared.go for an example).

If you need to share test logic across test directories you can put helper functions in the tests/shared package. If you find yourself frequently doing the same thing from within a test across test directories, for example, responding a particular popup, consider adding a helper method to pkg/integration/components/common.go. If you look around the code in the components directory you may find another place that's sensible to put your helper function.

Don't do too much in one test

If you're testing different pieces of functionality, it's better to test them in isolation using multiple short tests, compared to one larger longer test. Sometimes it's appropriate to have a longer test which tests how various different pieces interact, but err on the side of keeping things short.

Testing against old git versions

Our CI tests against multiple git versions. If your test fails on an old version, then to troubleshoot you'll need to install the failing git version. One option is to use rtx (see installation steps in the readme) with the git plugin like so:

rtx plugin add git
rtx install git 2.20.0
rtx local git 2.20.0