From f76196937a6d4e5dfe86736ab06f521180a0c7ce Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Sun, 4 Oct 2020 18:41:33 +1100 Subject: [PATCH] support integration testing WIP --- .gitignore | 3 +- pkg/gui/gui_test.go | 224 ++++++++++++++++++++++++ pkg/gui/recording.go | 17 +- test/integration/commit/recording.json | 226 +++++++++++++++++++++++++ test/integration/commit/snapshot.txt | 13 ++ test/integration/squash/recording.json | 1 + test/integration/squash/snapshot.txt | 42 +++++ 7 files changed, 521 insertions(+), 5 deletions(-) create mode 100644 pkg/gui/gui_test.go create mode 100644 test/integration/commit/recording.json create mode 100644 test/integration/commit/snapshot.txt create mode 100644 test/integration/squash/recording.json create mode 100644 test/integration/squash/snapshot.txt diff --git a/.gitignore b/.gitignore index 42ef840fe..d6b15ef10 100644 --- a/.gitignore +++ b/.gitignore @@ -25,4 +25,5 @@ lazygit !.circleci/ !.github/ -test/git_server/data \ No newline at end of file +test/git_server/data +test/integration_test/ diff --git a/pkg/gui/gui_test.go b/pkg/gui/gui_test.go new file mode 100644 index 000000000..d0a22710a --- /dev/null +++ b/pkg/gui/gui_test.go @@ -0,0 +1,224 @@ +package gui + +import ( + "fmt" + "io/ioutil" + "os" + "os/exec" + "path/filepath" + "testing" + + "github.com/go-errors/errors" + "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/stretchr/testify/assert" +) + +// To run an integration test, e.g. for test 'commit', go: +// go test pkg/gui/gui_test.go -run /commit +// +// To record keypresses for an integration test, pass RECORD_EVENTS=true like so: +// RECORD_EVENTS=true go test pkg/gui/gui_test.go -run /commit +// +// To update a snapshot for an integration test, pass UPDATE_SNAPSHOT=true +// UPDATE_SNAPSHOT=true go test pkg/gui/gui_test.go -run /commit +// +// When RECORD_EVENTS is true, updates will be updated automatically +// +// integration tests are run in test/integration_test and the final test does +// not clean up that directory so you can cd into it to see for yourself what +// happened when a test failed. +// +// TODO: support passing an env var for playback speed, given it's currently pretty fast + +type integrationTest struct { + name string + prepare func() error +} + +func generateSnapshot(t *testing.T) string { + osCommand := oscommands.NewDummyOSCommand() + cmd := `sh -c "git status; cat ./*; git log --pretty=%B -p"` + + snapshot, err := osCommand.RunCommandWithOutput(cmd) + assert.NoError(t, err) + + return snapshot +} + +func findOrCreateDir(path string) { + _, err := os.Stat(path) + if err != nil { + if os.IsNotExist(err) { + err = os.MkdirAll(path, 0777) + if err != nil { + panic(err) + } + } else { + panic(err) + } + } +} + +func Test(t *testing.T) { + tests := []integrationTest{ + { + name: "commit", + prepare: createFixture1, + }, + { + name: "squash", + prepare: createFixture2, + }, + } + + gotoRootDirectory() + + rootDir, err := os.Getwd() + if err != nil { + panic(err) + } + + for _, test := range tests { + test := test + t.Run(test.name, func(t *testing.T) { + testPath := filepath.Join(rootDir, "test", "integration", test.name) + findOrCreateDir(testPath) + + replayPath := filepath.Join(testPath, "recording.json") + snapshotPath := filepath.Join(testPath, "snapshot.txt") + + err := os.Chdir(rootDir) + assert.NoError(t, err) + + prepareIntegrationTestDir() + + err = test.prepare() + assert.NoError(t, err) + + record := os.Getenv("RECORD_EVENTS") != "" + runLazygit(t, replayPath, record) + + updateSnapshot := os.Getenv("UPDATE_SNAPSHOT") != "" + + actual := generateSnapshot(t) + + if updateSnapshot { + err := ioutil.WriteFile(snapshotPath, []byte(actual), 0600) + assert.NoError(t, err) + } + + expectedBytes, err := ioutil.ReadFile(snapshotPath) + assert.NoError(t, err) + expected := string(expectedBytes) + + assert.Equal(t, expected, actual, fmt.Sprintf("expected:\n%s\nactual:\n%s\n", expected, actual)) + }) + } +} + +func createFixture1() error { + cmds := []string{ + "git init", + `sh -c "echo test > myfile"`, + } + + return runCommands(cmds) +} + +func createFixture2() error { + cmds := []string{ + "git init", + `sh -c "echo test1 > myfile1"`, + `git add .`, + `git commit -am "myfile1"`, + `sh -c "echo test2 > myfile2"`, + `git add .`, + `git commit -am "myfile2"`, + `sh -c "echo test3 > myfile3"`, + `git add .`, + `git commit -am "myfile3"`, + `sh -c "echo test4 > myfile4"`, + `git add .`, + `git commit -am "myfile4"`, + `sh -c "echo test5 > myfile5"`, + `git add .`, + `git commit -am "myfile5"`, + } + + return runCommands(cmds) +} + +func runCommands(cmds []string) error { + osCommand := oscommands.NewDummyOSCommand() + + for _, cmd := range cmds { + if err := osCommand.RunCommand(cmd); err != nil { + return errors.New(fmt.Sprintf("error running command `%s`: %v", cmd, err)) + } + } + + return nil +} + +func gotoRootDirectory() { + for { + _, err := os.Stat(".git") + + if err == nil { + return + } + + if !os.IsNotExist(err) { + panic(err) + } + + if err = os.Chdir(".."); err != nil { + panic(err) + } + } +} + +func runLazygit(t *testing.T, replayPath string, record bool) { + osCommand := oscommands.NewDummyOSCommand() + + var cmd *exec.Cmd + if record { + cmd = osCommand.ExecutableFromString("lazygit") + cmd.Env = append( + cmd.Env, + fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath), + ) + } else { + cmd = osCommand.ExecutableFromString("lazygit") + cmd.Env = append( + cmd.Env, + fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath), + ) + } + err := osCommand.RunExecutable(cmd) + assert.NoError(t, err) +} + +func prepareIntegrationTestDir() { + path := filepath.Join("test", "integration_test") + + // remove contents of integration test directory + dir, err := ioutil.ReadDir(path) + if err != nil { + if os.IsNotExist(err) { + err = os.Mkdir(path, 0777) + if err != nil { + panic(err) + } + } else { + panic(err) + } + } + for _, d := range dir { + os.RemoveAll(filepath.Join(path, d.Name())) + } + + if err := os.Chdir(path); err != nil { + panic(err) + } +} diff --git a/pkg/gui/recording.go b/pkg/gui/recording.go index 5e0914303..57bf27948 100644 --- a/pkg/gui/recording.go +++ b/pkg/gui/recording.go @@ -9,7 +9,11 @@ import ( ) func recordingEvents() bool { - return os.Getenv("RECORD_EVENTS") == "true" + return recordEventsTo() != "" +} + +func recordEventsTo() string { + return os.Getenv("RECORD_EVENTS_TO") } func (gui *Gui) timeSinceStart() int64 { @@ -29,11 +33,14 @@ func (gui *Gui) replayRecordedEvents() { ticker := time.NewTicker(time.Millisecond) defer ticker.Stop() - var leeway int64 = 1000 + // might need to add leeway if this ends up flakey + var leeway int64 = 0 + // humans are slow so this speeds things up. + var speed int64 = 5 for _, event := range events { for range ticker.C { - now := gui.timeSinceStart() - leeway + now := gui.timeSinceStart()*speed - leeway if gui.g != nil && now >= event.Timestamp { gui.g.ReplayedEvents <- *event.Event break @@ -70,7 +77,9 @@ func (gui *Gui) saveRecordedEvents() error { return err } - return ioutil.WriteFile("recorded_events.json", jsonEvents, 0600) + path := recordEventsTo() + + return ioutil.WriteFile(path, jsonEvents, 0600) } func (gui *Gui) recordEvents() { diff --git a/test/integration/commit/recording.json b/test/integration/commit/recording.json new file mode 100644 index 000000000..d362b0271 --- /dev/null +++ b/test/integration/commit/recording.json @@ -0,0 +1,226 @@ +[ + { + "Timestamp": 41, + "Event": { + "Type": 1, + "Mod": 0, + "Key": 0, + "Ch": 0, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 0, + "Bytes": null + } + }, + { + "Timestamp": 1042, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 32, + "Ch": 0, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "IA==" + } + }, + { + "Timestamp": 1602, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 99, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "Yw==" + } + }, + { + "Timestamp": 2010, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 109, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "bQ==" + } + }, + { + "Timestamp": 2170, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 121, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "eQ==" + } + }, + { + "Timestamp": 2234, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 32, + "Ch": 0, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "IA==" + } + }, + { + "Timestamp": 2354, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 99, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "Yw==" + } + }, + { + "Timestamp": 2410, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 111, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "bw==" + } + }, + { + "Timestamp": 2578, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 109, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "bQ==" + } + }, + { + "Timestamp": 2690, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 109, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "bQ==" + } + }, + { + "Timestamp": 2730, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 105, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "aQ==" + } + }, + { + "Timestamp": 2850, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 116, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "dA==" + } + }, + { + "Timestamp": 2954, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 13, + "Ch": 0, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "DQ==" + } + }, + { + "Timestamp": 3625, + "Event": { + "Type": 0, + "Mod": 0, + "Key": 0, + "Ch": 113, + "Width": 0, + "Height": 0, + "Err": null, + "MouseX": 0, + "MouseY": 0, + "N": 1, + "Bytes": "cQ==" + } + } +] diff --git a/test/integration/commit/snapshot.txt b/test/integration/commit/snapshot.txt new file mode 100644 index 000000000..0c50ae828 --- /dev/null +++ b/test/integration/commit/snapshot.txt @@ -0,0 +1,13 @@ +On branch master +nothing to commit, working tree clean +test +my commit + + +diff --git a/myfile b/myfile +new file mode 100644 +index 0000000..9daeafb +--- /dev/null ++++ b/myfile +@@ -0,0 +1 @@ ++test diff --git a/test/integration/squash/recording.json b/test/integration/squash/recording.json new file mode 100644 index 000000000..5bb4996b7 --- /dev/null +++ b/test/integration/squash/recording.json @@ -0,0 +1 @@ +[{"Timestamp":14,"Event":{"Type":1,"Mod":0,"Key":0,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":0,"Bytes":null}},{"Timestamp":482,"Event":{"Type":0,"Mod":0,"Key":65514,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09D"}},{"Timestamp":626,"Event":{"Type":0,"Mod":0,"Key":65514,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09D"}},{"Timestamp":810,"Event":{"Type":0,"Mod":0,"Key":65516,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09C"}},{"Timestamp":937,"Event":{"Type":0,"Mod":0,"Key":65516,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09C"}},{"Timestamp":1065,"Event":{"Type":0,"Mod":0,"Key":65516,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09C"}},{"Timestamp":1591,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":101,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"ZQ=="}},{"Timestamp":2034,"Event":{"Type":0,"Mod":0,"Key":65517,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09B"}},{"Timestamp":2243,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":115,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"cw=="}},{"Timestamp":2554,"Event":{"Type":0,"Mod":0,"Key":65517,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09B"}},{"Timestamp":2803,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":100,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"ZA=="}},{"Timestamp":3209,"Event":{"Type":0,"Mod":0,"Key":65517,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":3,"Bytes":"G09B"}},{"Timestamp":3522,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":102,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"Zg=="}},{"Timestamp":4066,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":109,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"bQ=="}},{"Timestamp":5091,"Event":{"Type":0,"Mod":0,"Key":13,"Ch":0,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"DQ=="}},{"Timestamp":5834,"Event":{"Type":0,"Mod":0,"Key":0,"Ch":113,"Width":0,"Height":0,"Err":null,"MouseX":0,"MouseY":0,"N":1,"Bytes":"cQ=="}}] \ No newline at end of file diff --git a/test/integration/squash/snapshot.txt b/test/integration/squash/snapshot.txt new file mode 100644 index 000000000..445bf0f96 --- /dev/null +++ b/test/integration/squash/snapshot.txt @@ -0,0 +1,42 @@ +On branch master +nothing to commit, working tree clean +test1 +test2 +test3 +test5 +myfile2 + +myfile3 + + +diff --git a/myfile2 b/myfile2 +new file mode 100644 +index 0000000..180cf83 +--- /dev/null ++++ b/myfile2 +@@ -0,0 +1 @@ ++test2 +diff --git a/myfile3 b/myfile3 +new file mode 100644 +index 0000000..df6b0d2 +--- /dev/null ++++ b/myfile3 +@@ -0,0 +1 @@ ++test3 +diff --git a/myfile5 b/myfile5 +new file mode 100644 +index 0000000..4f346f1 +--- /dev/null ++++ b/myfile5 +@@ -0,0 +1 @@ ++test5 +myfile1 + + +diff --git a/myfile1 b/myfile1 +new file mode 100644 +index 0000000..a5bce3f +--- /dev/null ++++ b/myfile1 +@@ -0,0 +1 @@ ++test1