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
### 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 /<test name>
# 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 /<test name>
```
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
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 /<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:
```
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 /<test name>
```
## Feedback
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.
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))

View file

@ -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))
}

View file

@ -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)

View file

@ -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))