diff --git a/docs/Integration_Tests.md b/docs/Integration_Tests.md index 814e593ca..1bb797b3a 100644 --- a/docs/Integration_Tests.md +++ b/docs/Integration_Tests.md @@ -33,17 +33,32 @@ git commit -am "myfile1" ## Running tests +### From a TUI + +You can run/record/sandbox tests via a TUI with the following command: + +``` +go run test/lazyintegration/main.go +``` + +This TUI makes much of the following documentation redundant, but feel free to read through anyway! + +### From command line + To run all tests - assuming you're at the project root: + ``` go test ./pkg/gui/ ``` To run them in parallel + ``` PARALLEL=true go test ./pkg/gui ``` To run a single test + ``` go test ./pkg/gui -run / # For example, to run the `tags` test: @@ -51,29 +66,35 @@ go test ./pkg/gui -run /tags ``` To run a test at a certain speed + ``` SPEED=2 go test ./pkg/gui -run / ``` To update a snapshot + ``` -UPDATE_SNAPSHOTS=true go test ./pkg/gui -run / +MODE=updateSnapshot go test ./pkg/gui -run / ``` ## Creating a new test To create a new test: -1) Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test. -2) Update the `setup.sh` any way you like -3) If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used. -4) From the lazygit root directory, run: + +1. Copy and paste an existing test directory and rename the new directory to whatever you want the test name to be. Update the test.json file's description to describe your test. +2. Update the `setup.sh` any way you like +3. If you want to have a config folder for just that test, create a `config` directory to contain a `config.yml` and optionally a `state.yml` file. Otherwise, the `test/default_test_config` directory will be used. +4. From the lazygit root directory, run: + ``` -RECORD_EVENTS=true go test ./pkg/gui -run / +MODE=record go test ./pkg/gui -run / ``` -5) Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better! -6) Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created. + +5. Feel free to re-attempt recording as many times as you like. In the absence of a proper testing framework, the more deliberate your keypresses, the better! +6. Once satisfied with the recording, stage all the newly created files: `test.json`, `setup.sh`, `recording.json` and the `expected` directory that contains a copy of the repo you created. The resulting directory will look like: + ``` actual/ (the resulting repo after running the test, ignored by git) expected/ (the 'snapshot' repo) @@ -85,6 +106,14 @@ recording.json Feel free to create a hierarchy of directories in the `test/integration` directory to group tests by feature. +## Sandboxing + +The integration tests serve a secondary purpose of providing a setup for easy sandboxing. If you want to run a test in sandbox mode (meaning the session won't be recorded and we won't create/update snapshots), go: + +``` +MODE=sandbox go test ./pkg/gui -run / +``` + ## Feedback If you think this process can be improved, let me know! It shouldn't be too hard to change things. diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go index 6e7a26b79..e35ab1896 100644 --- a/pkg/gui/gui_test.go +++ b/pkg/gui/gui_test.go @@ -39,8 +39,7 @@ import ( // original playback speed. Speed may be a decimal. func Test(t *testing.T) { - record := false - updateSnapshots := os.Getenv("UPDATE_SNAPSHOTS") != "" + mode := integration.GetModeFromEnv() speedEnv := os.Getenv("SPEED") includeSkipped := os.Getenv("INCLUDE_SKIPPED") != "" @@ -53,8 +52,7 @@ func Test(t *testing.T) { assert.NoError(t, err) }) }, - updateSnapshots, - record, + mode, speedEnv, func(t *testing.T, expected string, actual string, prefix string) { assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual)) diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index 9c498e849..3ac6f0c8c 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -24,6 +24,36 @@ type Test struct { Skip bool `json:"skip"` } +type Mode int + +const ( + // default: for when we're just running a test and comparing to the snapshot + TEST = iota + // for when we want to record a test and set the snapshot based on the result + RECORD + // when we just want to use the setup of the test for our own sandboxing purposes. + // This does not record the session and does not create/update snapshots + SANDBOX + // running a test but updating the snapshot + UPDATE_SNAPSHOT +) + +func GetModeFromEnv() Mode { + switch os.Getenv("MODE") { + case "record": + return RECORD + case "", "test": + return TEST + case "updateSnapshot": + return UPDATE_SNAPSHOT + case "sandbox": + return SANDBOX + default: + log.Fatalf("unknown test mode: %s, must be one of [test, record, update, sandbox]", os.Getenv("MODE")) + panic("unreachable") + } +} + // this function is used by both `go test` and from our lazyintegration gui, but // errors need to be handled differently in each (for example go test is always // working with *testing.T) so we pass in any differences as args here. @@ -31,8 +61,7 @@ func RunTests( logf func(format string, formatArgs ...interface{}), runCmd func(cmd *exec.Cmd) error, fnWrapper func(test *Test, f func(*testing.T) error), - updateSnapshots bool, - record bool, + mode Mode, speedEnv string, onFail func(t *testing.T, expected string, actual string, prefix string), includeSkipped bool, @@ -65,7 +94,7 @@ func RunTests( } fnWrapper(test, func(t *testing.T) error { - speeds := getTestSpeeds(test.Speed, updateSnapshots, speedEnv) + speeds := getTestSpeeds(test.Speed, mode, speedEnv) testPath := filepath.Join(testDir, test.Name) actualRepoDir := filepath.Join(testPath, "actual") expectedRepoDir := filepath.Join(testPath, "expected") @@ -73,10 +102,10 @@ func RunTests( expectedRemoteDir := filepath.Join(testPath, "expected_remote") logf("path: %s", testPath) - // three retries at normal speed for the sake of flakey tests - speeds = append(speeds, 1) for i, speed := range speeds { - logf("%s: attempting test at speed %f\n", test.Name, speed) + if mode != SANDBOX && mode != RECORD { + logf("%s: attempting test at speed %f\n", test.Name, speed) + } findOrCreateDir(testPath) prepareIntegrationTestDir(actualRepoDir) @@ -88,7 +117,7 @@ func RunTests( configDir := filepath.Join(testPath, "used_config") - cmd, err := getLazygitCommand(testPath, rootDir, record, speed, test.ExtraCmdArgs) + cmd, err := getLazygitCommand(testPath, rootDir, mode, speed, test.ExtraCmdArgs) if err != nil { return err } @@ -98,7 +127,7 @@ func RunTests( return err } - if updateSnapshots { + if mode == UPDATE_SNAPSHOT || mode == RECORD { err = oscommands.CopyDir(actualRepoDir, expectedRepoDir) if err != nil { return err @@ -122,39 +151,41 @@ func RunTests( } } - actualRepo, expectedRepo, err := generateSnapshots(actualRepoDir, expectedRepoDir) - if err != nil { - return err - } - - actualRemote := "remote folder does not exist" - expectedRemote := "remote folder does not exist" - if folderExists(expectedRemoteDir) { - actualRemote, expectedRemote, err = generateSnapshotsForRemote(actualRemoteDir, expectedRemoteDir) + if mode != SANDBOX { + actualRepo, expectedRepo, err := generateSnapshots(actualRepoDir, expectedRepoDir) if err != nil { return err } - } else if folderExists(actualRemoteDir) { - actualRemote = "remote folder exists" - } - if expectedRepo == actualRepo && expectedRemote == actualRemote { - logf("%s: success at speed %f\n", test.Name, speed) - break - } - - // if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed - if i == len(speeds)-1 { - // get the log file and print that - bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log")) - if err != nil { - return err + actualRemote := "remote folder does not exist" + expectedRemote := "remote folder does not exist" + if folderExists(expectedRemoteDir) { + actualRemote, expectedRemote, err = generateSnapshotsForRemote(actualRemoteDir, expectedRemoteDir) + if err != nil { + return err + } + } else if folderExists(actualRemoteDir) { + actualRemote = "remote folder exists" } - logf("%s", string(bytes)) - if expectedRepo != actualRepo { - onFail(t, expectedRepo, actualRepo, "repo") - } else { - onFail(t, expectedRemote, actualRemote, "remote") + + if expectedRepo == actualRepo && expectedRemote == actualRemote { + logf("%s: success at speed %f\n", test.Name, speed) + break + } + + // if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed + if i == len(speeds)-1 { + // get the log file and print that + bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log")) + if err != nil { + return err + } + logf("%s", string(bytes)) + if expectedRepo != actualRepo { + onFail(t, expectedRepo, actualRepo, "repo") + } else { + onFail(t, expectedRemote, actualRemote, "remote") + } } } } @@ -231,8 +262,8 @@ func tempLazygitPath() string { return filepath.Join("/tmp", "lazygit", "test_lazygit") } -func getTestSpeeds(testStartSpeed float64, updateSnapshots bool, speedStr string) []float64 { - if updateSnapshots { +func getTestSpeeds(testStartSpeed float64, mode Mode, speedStr string) []float64 { + if mode != TEST { // have to go at original speed if updating snapshots in case we go to fast and create a junk snapshot return []float64{1.0} } @@ -254,7 +285,7 @@ func getTestSpeeds(testStartSpeed float64, updateSnapshots bool, speedStr string if startSpeed > 5 { speeds = append(speeds, 5) } - speeds = append(speeds, 1) + speeds = append(speeds, 1, 1) return speeds } @@ -400,7 +431,7 @@ func generateSnapshotsForRemote(actualDir string, expectedDir string) (string, s return actual, expected, nil } -func getLazygitCommand(testPath string, rootDir string, record bool, speed float64, extraCmdArgs string) (*exec.Cmd, error) { +func getLazygitCommand(testPath string, rootDir string, mode Mode, speed float64, extraCmdArgs string) (*exec.Cmd, error) { osCommand := oscommands.NewDummyOSCommand() replayPath := filepath.Join(testPath, "recording.json") @@ -432,9 +463,10 @@ func getLazygitCommand(testPath string, rootDir string, record bool, speed float cmdObj := osCommand.Cmd.New(cmdStr) cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed)) - if record { + switch mode { + case RECORD: cmdObj.AddEnvVars(fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath)) - } else { + case TEST, UPDATE_SNAPSHOT: cmdObj.AddEnvVars(fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath)) } diff --git a/test/lazyintegration/main.go b/test/lazyintegration/main.go index 3bd299abf..6940333ed 100644 --- a/test/lazyintegration/main.go +++ b/test/lazyintegration/main.go @@ -106,7 +106,21 @@ func main() { return nil } - cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true RECORD_EVENTS=true go run test/runner/main.go %s", currentTest.Name)) + cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=record go run test/runner/main.go %s", currentTest.Name)) + app.runSubprocess(cmd) + + return nil + }); err != nil { + log.Panicln(err) + } + + if err := g.SetKeybinding("list", nil, 's', gocui.ModNone, func(*gocui.Gui, *gocui.View) error { + currentTest := app.getCurrentTest() + if currentTest == nil { + return nil + } + + cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=sandbox go run test/runner/main.go %s", currentTest.Name)) app.runSubprocess(cmd) return nil @@ -128,13 +142,13 @@ func main() { log.Panicln(err) } - if err := g.SetKeybinding("list", nil, 's', gocui.ModNone, func(*gocui.Gui, *gocui.View) error { + if err := g.SetKeybinding("list", nil, 'u', gocui.ModNone, func(*gocui.Gui, *gocui.View) error { currentTest := app.getCurrentTest() if currentTest == nil { return nil } - cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true UPDATE_SNAPSHOTS=true go run test/runner/main.go %s", currentTest.Name)) + cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=updateSnapshot go run test/runner/main.go %s", currentTest.Name)) app.runSubprocess(cmd) return nil @@ -347,7 +361,7 @@ func (app *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, s: run test and update snapshots, r: record test, o: open test config, n: duplicate test, m: rename test, d: delete test") + fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, u: run test and update snapshots, r: record test, s: sandbox, o: open test config, n: duplicate test, m: rename test, d: delete test") } editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight) diff --git a/test/runner/main.go b/test/runner/main.go index b122568fe..af6195cbc 100644 --- a/test/runner/main.go +++ b/test/runner/main.go @@ -11,19 +11,18 @@ import ( "github.com/stretchr/testify/assert" ) +// see https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md // This file can be invoked directly, but you might find it easier to go through -// test/lazyintegration/main.go, which provides a convenient gui wrapper to integration -// tests. +// test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests. // // If invoked directly, you can specify a test by passing it as the first argument. -// You can also specify that you want to record a test by passing RECORD_EVENTS=true +// You can also specify that you want to record a test by passing MODE=record // as an env var. func main() { - record := os.Getenv("RECORD_EVENTS") != "" - updateSnapshots := record || os.Getenv("UPDATE_SNAPSHOTS") != "" + mode := integration.GetModeFromEnv() speedEnv := os.Getenv("SPEED") - includeSkipped := os.Getenv("INCLUDE_SKIPPED") != "" + includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true" selectedTestName := os.Args[1] err := integration.RunTests( @@ -37,8 +36,7 @@ func main() { log.Print(err.Error()) } }, - updateSnapshots, - record, + mode, speedEnv, func(_t *testing.T, expected string, actual string, prefix string) { assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))