allow sandbox mode with integration tests

This commit is contained in:
Jesse Duffield 2022-01-15 20:24:19 +11:00
parent 8ca71eeb36
commit 2691477aff
5 changed files with 137 additions and 66 deletions

View file

@ -33,17 +33,32 @@ git commit -am "myfile1"
## Running tests ## 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: To run all tests - assuming you're at the project root:
``` ```
go test ./pkg/gui/ go test ./pkg/gui/
``` ```
To run them in parallel To run them in parallel
``` ```
PARALLEL=true go test ./pkg/gui PARALLEL=true go test ./pkg/gui
``` ```
To run a single test To run a single test
``` ```
go test ./pkg/gui -run /<test name> go test ./pkg/gui -run /<test name>
# For example, to run the `tags` test: # 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 To run a test at a certain speed
``` ```
SPEED=2 go test ./pkg/gui -run /<test name> SPEED=2 go test ./pkg/gui -run /<test name>
``` ```
To update a snapshot To update a snapshot
``` ```
UPDATE_SNAPSHOTS=true go test ./pkg/gui -run /<test name> MODE=updateSnapshot go test ./pkg/gui -run /<test name>
``` ```
## Creating a new test ## Creating a new test
To create 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 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.
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. 2. Update the `setup.sh` any way you like
4) From the lazygit root directory, run: 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 /<test name> MODE=record go test ./pkg/gui -run /<test name>
``` ```
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: The resulting directory will look like:
``` ```
actual/ (the resulting repo after running the test, ignored by git) actual/ (the resulting repo after running the test, ignored by git)
expected/ (the 'snapshot' repo) 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. 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 /<test name>
```
## Feedback ## Feedback
If you think this process can be improved, let me know! It shouldn't be too hard to change things. If you think this process can be improved, let me know! It shouldn't be too hard to change things.

View file

@ -39,8 +39,7 @@ import (
// original playback speed. Speed may be a decimal. // original playback speed. Speed may be a decimal.
func Test(t *testing.T) { func Test(t *testing.T) {
record := false mode := integration.GetModeFromEnv()
updateSnapshots := os.Getenv("UPDATE_SNAPSHOTS") != ""
speedEnv := os.Getenv("SPEED") speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != "" includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
@ -53,8 +52,7 @@ func Test(t *testing.T) {
assert.NoError(t, err) assert.NoError(t, err)
}) })
}, },
updateSnapshots, mode,
record,
speedEnv, speedEnv,
func(t *testing.T, expected string, actual string, prefix string) { 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)) assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))

View file

@ -24,6 +24,36 @@ type Test struct {
Skip bool `json:"skip"` 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 // 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 // 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. // 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{}), logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) error, runCmd func(cmd *exec.Cmd) error,
fnWrapper func(test *Test, f func(*testing.T) error), fnWrapper func(test *Test, f func(*testing.T) error),
updateSnapshots bool, mode Mode,
record bool,
speedEnv string, speedEnv string,
onFail func(t *testing.T, expected string, actual string, prefix string), onFail func(t *testing.T, expected string, actual string, prefix string),
includeSkipped bool, includeSkipped bool,
@ -65,7 +94,7 @@ func RunTests(
} }
fnWrapper(test, func(t *testing.T) error { 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) testPath := filepath.Join(testDir, test.Name)
actualRepoDir := filepath.Join(testPath, "actual") actualRepoDir := filepath.Join(testPath, "actual")
expectedRepoDir := filepath.Join(testPath, "expected") expectedRepoDir := filepath.Join(testPath, "expected")
@ -73,10 +102,10 @@ func RunTests(
expectedRemoteDir := filepath.Join(testPath, "expected_remote") expectedRemoteDir := filepath.Join(testPath, "expected_remote")
logf("path: %s", testPath) logf("path: %s", testPath)
// three retries at normal speed for the sake of flakey tests
speeds = append(speeds, 1)
for i, speed := range speeds { 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) findOrCreateDir(testPath)
prepareIntegrationTestDir(actualRepoDir) prepareIntegrationTestDir(actualRepoDir)
@ -88,7 +117,7 @@ func RunTests(
configDir := filepath.Join(testPath, "used_config") 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 { if err != nil {
return err return err
} }
@ -98,7 +127,7 @@ func RunTests(
return err return err
} }
if updateSnapshots { if mode == UPDATE_SNAPSHOT || mode == RECORD {
err = oscommands.CopyDir(actualRepoDir, expectedRepoDir) err = oscommands.CopyDir(actualRepoDir, expectedRepoDir)
if err != nil { if err != nil {
return err return err
@ -122,39 +151,41 @@ func RunTests(
} }
} }
actualRepo, expectedRepo, err := generateSnapshots(actualRepoDir, expectedRepoDir) if mode != SANDBOX {
if err != nil { actualRepo, expectedRepo, err := generateSnapshots(actualRepoDir, expectedRepoDir)
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 { if err != nil {
return err return err
} }
} else if folderExists(actualRemoteDir) {
actualRemote = "remote folder exists"
}
if expectedRepo == actualRepo && expectedRemote == actualRemote { actualRemote := "remote folder does not exist"
logf("%s: success at speed %f\n", test.Name, speed) expectedRemote := "remote folder does not exist"
break if folderExists(expectedRemoteDir) {
} actualRemote, expectedRemote, err = generateSnapshotsForRemote(actualRemoteDir, expectedRemoteDir)
if err != nil {
// if the snapshots and we haven't tried all playback speeds different we'll retry at a slower speed return err
if i == len(speeds)-1 { }
// get the log file and print that } else if folderExists(actualRemoteDir) {
bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log")) actualRemote = "remote folder exists"
if err != nil {
return err
} }
logf("%s", string(bytes))
if expectedRepo != actualRepo { if expectedRepo == actualRepo && expectedRemote == actualRemote {
onFail(t, expectedRepo, actualRepo, "repo") logf("%s: success at speed %f\n", test.Name, speed)
} else { break
onFail(t, expectedRemote, actualRemote, "remote") }
// 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") return filepath.Join("/tmp", "lazygit", "test_lazygit")
} }
func getTestSpeeds(testStartSpeed float64, updateSnapshots bool, speedStr string) []float64 { func getTestSpeeds(testStartSpeed float64, mode Mode, speedStr string) []float64 {
if updateSnapshots { if mode != TEST {
// have to go at original speed if updating snapshots in case we go to fast and create a junk snapshot // have to go at original speed if updating snapshots in case we go to fast and create a junk snapshot
return []float64{1.0} return []float64{1.0}
} }
@ -254,7 +285,7 @@ func getTestSpeeds(testStartSpeed float64, updateSnapshots bool, speedStr string
if startSpeed > 5 { if startSpeed > 5 {
speeds = append(speeds, 5) speeds = append(speeds, 5)
} }
speeds = append(speeds, 1) speeds = append(speeds, 1, 1)
return speeds return speeds
} }
@ -400,7 +431,7 @@ func generateSnapshotsForRemote(actualDir string, expectedDir string) (string, s
return actual, expected, nil 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() osCommand := oscommands.NewDummyOSCommand()
replayPath := filepath.Join(testPath, "recording.json") 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 := osCommand.Cmd.New(cmdStr)
cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed)) cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed))
if record { switch mode {
case RECORD:
cmdObj.AddEnvVars(fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath)) cmdObj.AddEnvVars(fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath))
} else { case TEST, UPDATE_SNAPSHOT:
cmdObj.AddEnvVars(fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath)) cmdObj.AddEnvVars(fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath))
} }

View file

@ -106,7 +106,21 @@ func main() {
return nil 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) app.runSubprocess(cmd)
return nil return nil
@ -128,13 +142,13 @@ func main() {
log.Panicln(err) 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() currentTest := app.getCurrentTest()
if currentTest == nil { if currentTest == nil {
return 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) app.runSubprocess(cmd)
return nil return nil
@ -347,7 +361,7 @@ func (app *App) layout(g *gocui.Gui) error {
keybindingsView.Title = "Keybindings" keybindingsView.Title = "Keybindings"
keybindingsView.Wrap = true keybindingsView.Wrap = true
keybindingsView.FgColor = gocui.ColorDefault 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) editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)

View file

@ -11,19 +11,18 @@ import (
"github.com/stretchr/testify/assert" "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 // 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 // test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests.
// tests.
// //
// If invoked directly, you can specify a test by passing it as the first argument. // 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. // as an env var.
func main() { func main() {
record := os.Getenv("RECORD_EVENTS") != "" mode := integration.GetModeFromEnv()
updateSnapshots := record || os.Getenv("UPDATE_SNAPSHOTS") != ""
speedEnv := os.Getenv("SPEED") speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != "" includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true"
selectedTestName := os.Args[1] selectedTestName := os.Args[1]
err := integration.RunTests( err := integration.RunTests(
@ -37,8 +36,7 @@ func main() {
log.Print(err.Error()) log.Print(err.Error())
} }
}, },
updateSnapshots, mode,
record,
speedEnv, speedEnv,
func(_t *testing.T, expected string, actual string, prefix string) { 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)) assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))