1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2024-11-24 08:52:21 +02:00

remove legacy integration tests

This commit is contained in:
Jesse Duffield 2023-02-26 11:23:36 +11:00
parent f82f4f6dbc
commit 8b5d59c238
20 changed files with 7 additions and 1523 deletions

View File

@ -46,36 +46,6 @@ jobs:
# we're passing -short so that we skip the integration tests, which will be run in parallel below
run: |
go test ./... -short
integration-tests-old:
runs-on: ubuntu-latest
strategy:
fail-fast: false
matrix:
parallelism: [5]
index: [0,1,2,3,4]
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
env:
GOFLAGS: -mod=vendor
steps:
- name: Checkout code
uses: actions/checkout@v2
- name: Setup Go
uses: actions/setup-go@v1
with:
go-version: 1.18.x
- name: Cache build
uses: actions/cache@v1
with:
path: |
~/.cache/go-build
~/go/pkg/mod
key: ${{runner.os}}-go-${{hashFiles('**/go.sum')}}-test
restore-keys: |
${{runner.os}}-go-
- name: Test code
# for file.allow thing see https://vielmetti.typepad.com/logbook/2022/10/git-security-fixes-lead-to-fatal-transport-file-not-allowed-error-in-ci-systems-cve-2022-39253.html
run: |
git config --global protocol.file.allow always && PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
integration-tests:
runs-on: ubuntu-latest
name: "Integration Tests"

9
.gitignore vendored
View File

@ -35,14 +35,7 @@ lazygit.exe
test/git_server/data
# we'll scrap these lines once we've fully moved over to the new integration test approach
test/integration/*/actual/
test/integration/*/used_config/
# these sample hooks waste too much space
test/integration/*/expected/**/hooks/
test/integration/*/expected_remote/**/hooks/
test/integration_new/**
test/integration/**
oryxBuildBinary
__debug_bin

View File

@ -17,11 +17,6 @@ Usage:
If you pass no test names, it runs all tests
Accepted environment variables:
KEY_PRESS_DELAY (e.g. 200): the number of milliseconds to wait between keypresses
MODE:
* ask (default): if a snapshot test fails, asks if you want to update the snapshot
* check: if a snapshot test fails, exits with an error
* update: if a snapshot test fails, updates the snapshot
* sandbox: uses the test's setup step to run the test in a sandbox where you can do whatever you want
TUI mode:
> go run cmd/integration_test/main.go tui

View File

@ -52,7 +52,7 @@ 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_test.go
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.
@ -62,7 +62,7 @@ The name of a test is based on its path, so the name of the test at `pkg/integra
You can pass the KEY_PRESS_DELAY env var to the test runner in order to set a delay in milliseconds between keypresses, 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/integration_new`, so if you're not sure what went wrong you can go there and inspect the repo.
The resultant repo will be stored in `test/integration`, so if you're not sure what went wrong you can go there and inspect the repo.
### Running tests in VSCode
@ -78,21 +78,3 @@ The test will run in a VSCode terminal:
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.
## Migration process
You can watch how to migrate tests in this youtube [video](https://youtu.be/cJtOJu6-HcA).
At the time of writing, most tests are created under an old approach, where you would record yourself in a lazygit session and then the test would replay the keybindings with the same timestamps. This old approach is great for writing tests quickly, but is much harder to maintain. It has to rely on snapshots to determining if a test passes or fails, and can't do assertions along the way. It's also harder to grok what's the intention behind certain actions that take place within the test (e.g. was the recorder intentionally switching to another panel or was that just a misclick?).
At the moment, all the deprecated test code lives in pkg/integration/deprecated. Hopefully in the very near future we migrate everything across so that we don't need to maintain two systems.
We should never write any new tests under the old method, and if a given test breaks because of new functionality, it's best to simply rewrite it under the new approach. If you want to run a test for the sake of watching what it does so that you can transcribe it into the new approach, you can run:
```
go run pkg/integration/deprecated/cmd/tui/main.go
```
The tests in the old format live in test/integration. In the old format, test definitions are co-located with snapshots. The setup step is done in a `setup.sh` shell script and the `recording.json` file contains the recorded keypresses to be replayed during the test.
If you have rewritten an integration test under the new pattern, be sure to delete the old integration test directory.

View File

@ -3,8 +3,8 @@
package clients
// this is the new way of running tests. See pkg/integration/integration_tests/commit.go
// for an example
// This file allows you to use `go test` to run integration tests.
// See See pkg/integration/README.md for more info.
import (
"bytes"

View File

@ -137,7 +137,7 @@ func RunTUI() {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code test/integration_new/%s", currentTest.Name()))
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code test/integration/%s", currentTest.Name()))
if err := cmd.Run(); err != nil {
return err
}

View File

@ -3,7 +3,7 @@ package components
import "path/filepath"
// convenience struct for easily getting directories within our test directory.
// We have one test directory for each test, found in test/integration_new.
// We have one test directory for each test, found in test/integration.
type Paths struct {
// e.g. test/integration/test_name
root string

View File

@ -1,65 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"testing"
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
"github.com/stretchr/testify/assert"
)
// Deprecated: This file is part of the old way of doing things.
// see https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.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.
//
// 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 MODE=record
// as an env var.
func main() {
mode := deprecated.GetModeFromEnv()
speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true"
selectedTestName := os.Args[1]
err := deprecated.RunTests(
log.Printf,
runCmdInTerminal,
func(test *deprecated.IntegrationTest, f func(*testing.T) error) {
if selectedTestName != "" && test.Name != selectedTestName {
return
}
if err := f(nil); err != nil {
log.Print(err.Error())
}
},
mode,
speedEnv,
func(_t *testing.T, expected string, actual string, prefix string) { //nolint:thelper
assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
},
includeSkipped,
)
if err != nil {
log.Print(err.Error())
}
}
type MockTestingT struct{}
func (t MockTestingT) Errorf(format string, args ...interface{}) {
fmt.Printf(format, args...)
}
func runCmdInTerminal(cmd *exec.Cmd) error {
cmd.Stdout = os.Stdout
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
return cmd.Run()
}

View File

@ -1,422 +0,0 @@
package main
import (
"fmt"
"log"
"os"
"os/exec"
"path/filepath"
"github.com/jesseduffield/gocui"
"github.com/jesseduffield/lazygit/pkg/gui"
"github.com/jesseduffield/lazygit/pkg/gui/style"
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// Deprecated. See lazy_integration for the new approach.
// this program lets you manage integration tests in a TUI. See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md for more info.
type App struct {
tests []*deprecated.IntegrationTest
itemIdx int
testDir string
editing bool
g *gocui.Gui
}
func (app *App) getCurrentTest() *deprecated.IntegrationTest {
if len(app.tests) > 0 {
return app.tests[app.itemIdx]
}
return nil
}
func (app *App) refreshTests() {
app.loadTests()
app.g.Update(func(*gocui.Gui) error {
listView, err := app.g.View("list")
if err != nil {
return err
}
listView.Clear()
for _, test := range app.tests {
fmt.Fprintln(listView, test.Name)
}
return nil
})
}
func (app *App) loadTests() {
tests, err := deprecated.LoadTests(app.testDir)
if err != nil {
log.Panicln(err)
}
app.tests = tests
if app.itemIdx > len(app.tests)-1 {
app.itemIdx = len(app.tests) - 1
}
}
func main() {
rootDir := deprecated.GetRootDirectory()
testDir := filepath.Join(rootDir, "test", "integration")
app := &App{testDir: testDir}
app.loadTests()
g, err := gocui.NewGui(gocui.OutputTrue, false, gocui.NORMAL, false, gui.RuneReplacements)
if err != nil {
log.Panicln(err)
}
g.Cursor = false
app.g = g
g.SetManagerFunc(app.layout)
if err := g.SetKeybinding("list", gocui.KeyArrowUp, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
if app.itemIdx > 0 {
app.itemIdx--
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.KeyCtrlC, gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'q', gocui.ModNone, quit); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'r', 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=record go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", '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 pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", gocui.KeyEnter, 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 go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", '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 MODE=updateSnapshot go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 't', 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 SPEED=1 go run pkg/integration/deprecated/cmd/runner/main.go %s", currentTest.Name))
app.runSubprocess(cmd)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'o', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("code -r %s/%s/test.json", app.testDir, currentTest.Name))
if err := cmd.Run(); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'n', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
// need to duplicate that folder and then re-fetch our tests.
dir := app.testDir + "/" + app.getCurrentTest().Name
newDir := dir + "_Copy"
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("cp -r %s %s", dir, newDir))
if err := cmd.Run(); err != nil {
return err
}
app.loadTests()
app.refreshTests()
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'm', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
app.editing = true
if _, err := g.SetCurrentView("editor"); err != nil {
return err
}
editorView, err := g.View("editor")
if err != nil {
return err
}
editorView.Clear()
fmt.Fprint(editorView, currentTest.Name)
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("list", 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
dir := app.testDir + "/" + app.getCurrentTest().Name
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("rm -rf %s", dir))
if err := cmd.Run(); err != nil {
return err
}
app.refreshTests()
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("editor", gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
app.editing = false
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
editorView, err := g.View("editor")
if err != nil {
return err
}
dir := app.testDir + "/" + app.getCurrentTest().Name
newDir := app.testDir + "/" + editorView.Buffer()
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("mv %s %s", dir, newDir))
if err := cmd.Run(); err != nil {
return err
}
editorView.Clear()
app.refreshTests()
return nil
}); err != nil {
log.Panicln(err)
}
if err := g.SetKeybinding("editor", gocui.KeyEsc, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
app.editing = false
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
return nil
}); err != nil {
log.Panicln(err)
}
err = g.MainLoop()
g.Close()
switch err {
case gocui.ErrQuit:
return
default:
log.Panicln(err)
}
}
func (app *App) runSubprocess(cmd *exec.Cmd) {
if err := gocui.Screen.Suspend(); err != nil {
panic(err)
}
cmd.Stdin = os.Stdin
cmd.Stderr = os.Stderr
cmd.Stdout = os.Stdout
if err := cmd.Run(); err != nil {
log.Println(err.Error())
}
cmd.Stdin = nil
cmd.Stderr = nil
cmd.Stdout = nil
fmt.Fprintf(os.Stdout, "\n%s", style.FgGreen.Sprint("press enter to return"))
fmt.Scanln() // wait for enter press
if err := gocui.Screen.Resume(); err != nil {
panic(err)
}
}
func (app *App) layout(g *gocui.Gui) error {
maxX, maxY := g.Size()
descriptionViewHeight := 7
keybindingsViewHeight := 3
editorViewHeight := 3
if !app.editing {
editorViewHeight = 0
} else {
descriptionViewHeight = 0
keybindingsViewHeight = 0
}
g.Cursor = app.editing
g.FgColor = gocui.ColorGreen
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
if err != nil {
if !gocui.IsUnknownView(err) {
return err
}
listView.Highlight = true
listView.SelBgColor = gocui.ColorBlue
listView.Clear()
for _, test := range app.tests {
fmt.Fprintln(listView, test.Name)
}
listView.Title = "Tests"
listView.FgColor = gocui.ColorDefault
if _, err := g.SetCurrentView("list"); err != nil {
return err
}
}
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
if err != nil {
if !gocui.IsUnknownView(err) {
return err
}
descriptionView.Title = "Test description"
descriptionView.Wrap = true
descriptionView.FgColor = gocui.ColorDefault
}
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
if err != nil {
if !gocui.IsUnknownView(err) {
return err
}
keybindingsView.Title = "Keybindings"
keybindingsView.Wrap = true
keybindingsView.FgColor = gocui.ColorDefault
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, t: run test at original speed")
}
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
if err != nil {
if !gocui.IsUnknownView(err) {
return err
}
editorView.Title = "Enter Name"
editorView.FgColor = gocui.ColorDefault
editorView.Editable = true
}
currentTest := app.getCurrentTest()
if currentTest == nil {
return nil
}
descriptionView.Clear()
fmt.Fprintf(descriptionView, "Speed: %f. %s", currentTest.Speed, currentTest.Description)
if err := g.SetKeybinding("list", gocui.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
if app.itemIdx < len(app.tests)-1 {
app.itemIdx++
}
listView, err := g.View("list")
if err != nil {
return err
}
listView.FocusPoint(0, app.itemIdx)
return nil
}); err != nil {
log.Panicln(err)
}
return nil
}
func quit(g *gocui.Gui, v *gocui.View) error {
return gocui.ErrQuit
}

View File

@ -1,122 +0,0 @@
//go:build !windows
// +build !windows
package deprecated
import (
"bytes"
"errors"
"fmt"
"io"
"io/ioutil"
"os"
"os/exec"
"strconv"
"testing"
"github.com/creack/pty"
"github.com/stretchr/testify/assert"
)
// Deprecated.
// This file is quite similar to integration/main.go. The main difference is that this file is
// run via `go test` whereas the other is run via `test/lazyintegration/main.go` which provides
// a convenient gui wrapper around our integration tests. The `go test` approach is better
// for CI and for running locally in the background to ensure you haven't broken
// anything while making changes. If you want to visually see what's happening when a test is run,
// you'll need to take the other approach
//
// As for this file, to run an integration test, e.g. for test 'commit', go:
// go test pkg/gui/old_gui_test.go -run /commit
//
// To update a snapshot for an integration test, pass UPDATE_SNAPSHOTS=true
// UPDATE_SNAPSHOTS=true go test pkg/gui/old_gui_test.go -run /commit
//
// integration tests are run in test/integration/<test_name>/actual 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 fails.
//
// To override speed, pass e.g. `SPEED=1` as an env var. Otherwise we start each test
// at a high speed and then drop down to lower speeds upon each failure until finally
// trying at the original playback speed (speed 1). A speed of 2 represents twice the
// original playback speed. Speed may be a decimal.
func Test(t *testing.T) {
if testing.Short() {
t.Skip("Skipping integration tests in short mode")
}
mode := GetModeFromEnv()
speedEnv := os.Getenv("SPEED")
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
parallelTotal := tryConvert(os.Getenv("PARALLEL_TOTAL"), 1)
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
testNumber := 0
err := RunTests(
t.Logf,
runCmdHeadless,
func(test *IntegrationTest, f func(*testing.T) error) {
defer func() { testNumber += 1 }()
if testNumber%parallelTotal != parallelIndex {
return
}
t.Run(test.Name, func(t *testing.T) {
err := f(t)
assert.NoError(t, err)
})
},
mode,
speedEnv,
func(t *testing.T, expected string, actual string, prefix string) {
t.Helper()
assert.Equal(t, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
},
includeSkipped,
)
assert.NoError(t, err)
}
func tryConvert(numStr string, defaultVal int) int {
num, err := strconv.Atoi(numStr)
if err != nil {
return defaultVal
}
return num
}
func runCmdHeadless(cmd *exec.Cmd) error {
cmd.Env = append(
cmd.Env,
"HEADLESS=true",
"TERM=xterm",
)
// not writing stderr to the pty because we want to capture a panic if
// there is one. But some commands will not be in tty mode if stderr is
// not a terminal. We'll need to keep an eye out for that.
stderr := new(bytes.Buffer)
cmd.Stderr = stderr
// these rows and columns are ignored because internally we use tcell's
// simulation screen. However we still need the pty for the sake of
// running other commands in a pty.
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
if err != nil {
return err
}
_, _ = io.Copy(ioutil.Discard, f)
if cmd.Wait() != nil {
// return an error with the stderr output
return errors.New(stderr.String())
}
return f.Close()
}

View File

@ -1,564 +0,0 @@
package deprecated
import (
"encoding/json"
"errors"
"fmt"
"io/ioutil"
"log"
"os"
"os/exec"
"path/filepath"
"strconv"
"strings"
"testing"
"github.com/jesseduffield/generics/slices"
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
"github.com/jesseduffield/lazygit/pkg/secureexec"
)
// Deprecated: This file is part of the old way of doing things. See pkg/integration/integration.go for the new way
// This package is for running our integration test suite. See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md for more info.
type IntegrationTest struct {
Name string `json:"name"`
Speed float64 `json:"speed"`
Description string `json:"description"`
ExtraCmdArgs string `json:"extraCmdArgs"`
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, updateSnapshot, 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.
func RunTests(
logf func(format string, formatArgs ...interface{}),
runCmd func(cmd *exec.Cmd) error,
fnWrapper func(test *IntegrationTest, f func(*testing.T) error),
mode Mode,
speedEnv string,
onFail func(t *testing.T, expected string, actual string, prefix string),
includeSkipped bool,
) error {
rootDir := GetRootDirectory()
err := os.Chdir(rootDir)
if err != nil {
return err
}
testDir := filepath.Join(rootDir, "test", "integration")
osCommand := oscommands.NewDummyOSCommand()
err = osCommand.Cmd.New("go build -o " + tempLazygitPath()).Run()
if err != nil {
return err
}
tests, err := LoadTests(testDir)
if err != nil {
return err
}
for _, test := range tests {
test := test
fnWrapper(test, func(t *testing.T) error { //nolint: thelper
if test.Skip && !includeSkipped {
logf("skipping test: %s", test.Name)
return nil
}
speeds := getTestSpeeds(test.Speed, mode, speedEnv)
testPath := filepath.Join(testDir, test.Name)
actualDir := filepath.Join(testPath, "actual")
expectedDir := filepath.Join(testPath, "expected")
actualRepoDir := filepath.Join(actualDir, "repo")
logf("path: %s", testPath)
for i, speed := range speeds {
if mode != SANDBOX && mode != RECORD {
logf("%s: attempting test at speed %f\n", test.Name, speed)
}
findOrCreateDir(testPath)
prepareIntegrationTestDir(actualDir)
findOrCreateDir(actualRepoDir)
err := createFixture(testPath, actualRepoDir)
if err != nil {
return err
}
configDir := filepath.Join(testPath, "used_config")
cmd, err := getLazygitCommand(testPath, rootDir, mode, speed, test.ExtraCmdArgs)
if err != nil {
return err
}
err = runCmd(cmd)
if err != nil {
return err
}
if mode == UPDATE_SNAPSHOT || mode == RECORD {
// create/update snapshot
err = oscommands.CopyDir(actualDir, expectedDir)
if err != nil {
return err
}
if err := renameSpecialPaths(expectedDir); err != nil {
return err
}
logf("%s", "updated snapshot")
} else {
if err := validateSameRepos(expectedDir, actualDir); err != nil {
return err
}
// iterate through each repo in the expected dir and comparet to the corresponding repo in the actual dir
expectedFiles, err := ioutil.ReadDir(expectedDir)
if err != nil {
return err
}
success := true
for _, f := range expectedFiles {
if !f.IsDir() {
return errors.New("unexpected file (as opposed to directory) in integration test 'expected' directory")
}
// get corresponding file name from actual dir
actualRepoPath := filepath.Join(actualDir, f.Name())
expectedRepoPath := filepath.Join(expectedDir, f.Name())
actualRepo, expectedRepo, err := generateSnapshots(actualRepoPath, expectedRepoPath)
if err != nil {
return err
}
if expectedRepo != actualRepo {
success = false
// if the snapshot doesn't match and we haven't tried all playback speeds different we'll retry at a slower speed
if i < len(speeds)-1 {
break
}
// get the log file and print it
bytes, err := os.ReadFile(filepath.Join(configDir, "development.log"))
if err != nil {
return err
}
logf("%s", string(bytes))
onFail(t, expectedRepo, actualRepo, f.Name())
}
}
if success {
logf("%s: success at speed %f\n", test.Name, speed)
break
}
}
}
return nil
})
}
return nil
}
// validates that the actual and expected dirs have the same repo names (doesn't actually check the contents of the repos)
func validateSameRepos(expectedDir string, actualDir string) error {
// iterate through each repo in the expected dir and compare to the corresponding repo in the actual dir
expectedFiles, err := ioutil.ReadDir(expectedDir)
if err != nil {
return err
}
var actualFiles []os.FileInfo
actualFiles, err = ioutil.ReadDir(actualDir)
if err != nil {
return err
}
expectedFileNames := slices.Map(expectedFiles, getFileName)
actualFileNames := slices.Map(actualFiles, getFileName)
if !slices.Equal(expectedFileNames, actualFileNames) {
return fmt.Errorf("expected and actual repo dirs do not match: expected: %s, actual: %s", expectedFileNames, actualFileNames)
}
return nil
}
func getFileName(f os.FileInfo) string {
return f.Name()
}
func prepareIntegrationTestDir(actualDir string) {
// remove contents of integration test directory
dir, err := ioutil.ReadDir(actualDir)
if err != nil {
if os.IsNotExist(err) {
err = os.Mkdir(actualDir, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
for _, d := range dir {
os.RemoveAll(filepath.Join(actualDir, d.Name()))
}
}
func GetRootDirectory() string {
path, err := os.Getwd()
if err != nil {
panic(err)
}
for {
_, err := os.Stat(filepath.Join(path, ".git"))
if err == nil {
return path
}
if !os.IsNotExist(err) {
panic(err)
}
path = filepath.Dir(path)
if path == "/" {
log.Fatal("must run in lazygit folder or child folder")
}
}
}
func createFixture(testPath, actualDir string) error {
bashScriptPath := filepath.Join(testPath, "setup.sh")
cmd := secureexec.Command("bash", bashScriptPath, actualDir)
if output, err := cmd.CombinedOutput(); err != nil {
return errors.New(string(output))
}
return nil
}
func tempLazygitPath() string {
return filepath.Join("/tmp", "lazygit", "test_lazygit")
}
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}
}
if speedStr != "" {
speed, err := strconv.ParseFloat(speedStr, 64)
if err != nil {
panic(err)
}
return []float64{speed}
}
// default is 10, 5, 1
startSpeed := 10.0
if testStartSpeed != 0 {
startSpeed = testStartSpeed
}
speeds := []float64{startSpeed}
if startSpeed > 5 {
speeds = append(speeds, 5)
}
speeds = append(speeds, 1, 0.5, 0.5)
return speeds
}
func LoadTests(testDir string) ([]*IntegrationTest, error) {
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
if err != nil {
return nil, err
}
tests := make([]*IntegrationTest, len(paths))
for i, path := range paths {
data, err := os.ReadFile(path)
if err != nil {
return nil, err
}
test := &IntegrationTest{}
err = json.Unmarshal(data, test)
if err != nil {
return nil, err
}
test.Name = strings.TrimPrefix(filepath.Dir(path), testDir+"/")
tests[i] = test
}
return tests, nil
}
func findOrCreateDir(path string) {
_, err := os.Stat(path)
if err != nil {
if os.IsNotExist(err) {
err = os.MkdirAll(path, 0o777)
if err != nil {
panic(err)
}
} else {
panic(err)
}
}
}
// note that we don't actually store this snapshot in the lazygit repo.
// Instead we store the whole expected git repo of our test, so that
// we can easily change what we want to compare without needing to regenerate
// snapshots for each test.
func generateSnapshot(dir string) (string, error) {
osCommand := oscommands.NewDummyOSCommand()
_, err := os.Stat(filepath.Join(dir, ".git"))
if err != nil {
return "git directory not found", nil
}
snapshot := ""
cmdStrs := []string{
`remote show -n origin`, // remote branches
// TODO: find a way to bring this back without breaking tests
// `ls-remote origin`,
`status`, // file tree
`log --pretty=%B|%an|%ae -p -1`, // log
`tag -n`, // tags
`stash list`, // stash
`submodule foreach 'git status'`, // submodule status
`submodule foreach 'git log --pretty=%B -p -1'`, // submodule log
`submodule foreach 'git tag -n'`, // submodule tags
`submodule foreach 'git stash list'`, // submodule stash
}
for _, cmdStr := range cmdStrs {
// ignoring error for now. If there's an error it could be that there are no results
output, _ := osCommand.Cmd.New(fmt.Sprintf("git -C %s %s", dir, cmdStr)).RunWithOutput()
snapshot += fmt.Sprintf("git %s:\n%s\n", cmdStr, output)
}
snapshot += "files in repo:\n"
err = filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.IsDir() {
if f.Name() == ".git" {
return filepath.SkipDir
}
return nil
}
bytes, err := os.ReadFile(path)
if err != nil {
return err
}
relativePath, err := filepath.Rel(dir, path)
if err != nil {
return err
}
snapshot += fmt.Sprintf("path: %s\ncontent:\n%s\n", relativePath, string(bytes))
return nil
})
if err != nil {
return "", err
}
return snapshot, nil
}
func generateSnapshots(actualDir string, expectedDir string) (string, string, error) {
actual, err := generateSnapshot(actualDir)
if err != nil {
return "", "", err
}
// there are a couple of reasons we're not generating the snapshot in expectedDir directly:
// Firstly we don't want to have to revert our .git file back to .git_keep.
// Secondly, the act of calling git commands like 'git status' actually changes the index
// for some reason, and we don't want to leave your lazygit working tree dirty as a result.
expectedDirCopyDir := filepath.Join(filepath.Dir(expectedDir), "expected_dir_test")
err = oscommands.CopyDir(expectedDir, expectedDirCopyDir)
if err != nil {
return "", "", err
}
defer func() {
err := os.RemoveAll(expectedDirCopyDir)
if err != nil {
panic(err)
}
}()
if err := restoreSpecialPaths(expectedDirCopyDir); err != nil {
return "", "", err
}
expected, err := generateSnapshot(expectedDirCopyDir)
if err != nil {
return "", "", err
}
return actual, expected, nil
}
func getPathsToRename(dir string, needle string, contains string) []string {
pathsToRename := []string{}
err := filepath.Walk(dir, func(path string, f os.FileInfo, err error) error {
if err != nil {
return err
}
if f.Name() == needle && (contains == "" || strings.Contains(path, contains)) {
pathsToRename = append(pathsToRename, path)
}
return nil
})
if err != nil {
panic(err)
}
return pathsToRename
}
var specialPathMappings = []struct{ original, new, contains string }{
// git refuses to track .git or .gitmodules in subdirectories so we need to rename them
{".git", ".git_keep", ""},
{".gitmodules", ".gitmodules_keep", ""},
// we also need git to ignore the contents of our test gitignore files so that
// we actually commit files that are ignored within the test.
{".gitignore", "lg_ignore_file", ""},
// this is the .git/info/exclude file. We're being a little more specific here
// so that we don't accidentally mess with some other file named 'exclude' in the test.
{"exclude", "lg_exclude_file", ".git/info/exclude"},
}
func renameSpecialPaths(dir string) error {
for _, specialPath := range specialPathMappings {
for _, path := range getPathsToRename(dir, specialPath.original, specialPath.contains) {
err := os.Rename(path, filepath.Join(filepath.Dir(path), specialPath.new))
if err != nil {
return err
}
}
}
return nil
}
func restoreSpecialPaths(dir string) error {
for _, specialPath := range specialPathMappings {
for _, path := range getPathsToRename(dir, specialPath.new, specialPath.contains) {
err := os.Rename(path, filepath.Join(filepath.Dir(path), specialPath.original))
if err != nil {
return err
}
}
}
return nil
}
func getLazygitCommand(testPath string, rootDir string, mode Mode, speed float64, extraCmdArgs string) (*exec.Cmd, error) {
osCommand := oscommands.NewDummyOSCommand()
replayPath := filepath.Join(testPath, "recording.json")
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
actualRepoDir := filepath.Join(testPath, "actual", "repo")
exists, err := osCommand.FileExists(filepath.Join(testPath, "config"))
if err != nil {
return nil, err
}
if exists {
templateConfigDir = filepath.Join(testPath, "config")
}
configDir := filepath.Join(testPath, "used_config")
err = os.RemoveAll(configDir)
if err != nil {
return nil, err
}
err = oscommands.CopyDir(templateConfigDir, configDir)
if err != nil {
return nil, err
}
cmdStr := fmt.Sprintf("%s -debug --use-config-dir=%s --path=%s %s", tempLazygitPath(), configDir, actualRepoDir, extraCmdArgs)
cmdObj := osCommand.Cmd.New(cmdStr)
cmdObj.AddEnvVars(fmt.Sprintf("SPEED=%f", speed))
switch mode {
case RECORD:
cmdObj.AddEnvVars(fmt.Sprintf("RECORD_EVENTS_TO=%s", replayPath))
case TEST, UPDATE_SNAPSHOT:
cmdObj.AddEnvVars(fmt.Sprintf("REPLAY_EVENTS_FROM=%s", replayPath))
}
return cmdObj.GetCmd(), nil
}

View File

@ -1,23 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
cat <<EOT >> windowslf.txt
asdf
asdf
EOT
cat <<EOT >> linuxlf.txt
asdf
asdf
EOT
cat <<EOT >> bomtest.txt
A,B,C,D,E
F,G,H,I,J
K,L,M,N,O
P,Q,R,S,T
U,V,W,X,Y
Z,1,2,3,4
EOT

View File

@ -1,19 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
touch foo
git add foo
git commit -m "init"
git branch -a
git branch test
git branch TEST
git checkout TEST
git checkout TeST
git checkout TesT
git checkout TEsT
git branch -a

View File

@ -1,7 +0,0 @@
#!/bin/bash
echo "test1"
sleep 1
echo "test2"
sleep 1
echo "test3"

View File

@ -1,19 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
git config gpg.program $(which gpg)
git config user.signingkey E304229F # test key
git config commit.gpgsign true
git config credential.helper store
git config credential.helper cache 1
touch foo
git add foo
touch bar
git add bar

View File

@ -1,19 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
i=2
end=100
while [ $i -le $end ]; do
echo "file${i}" > file${i}
git add file${i}
git commit -m file${i}
i=$(($i+1))
done
echo "unstaged change" > file100

View File

@ -1,155 +0,0 @@
#!/bin/sh
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
function add_spacing {
for i in {1..60}
do
echo "..." >> $1
done
}
mkdir directory
echo "test1" > directory/file
echo "test1" > directory/file2
echo "Here is a story that has been told throuhg the ages" >> file1
git add file1
git add directory
git commit -m "first commit"
git checkout -b feature/cherry-picking
echo "this is file number 1 that I'm going to cherry-pick" > cherrypicking1
echo "this is file number 2 that I'm going to cherry-pick" > cherrypicking2
git add .
git commit -am "first commit freshman year"
echo "this is file number 3 that I'm going to cherry-pick" > cherrypicking3
git add .
git commit -am "second commit subway eat fresh"
echo "this is file number 4 that I'm going to cherry-pick" > cherrypicking4
git add .
git commit -am "third commit fresh"
echo "this is file number 5 that I'm going to cherry-pick" > cherrypicking5
git add .
git commit -am "fourth commit cool"
echo "this is file number 6 that I'm going to cherry-pick" > cherrypicking6
git add .
git commit -am "fifth commit nice"
echo "this is file number 7 that I'm going to cherry-pick" > cherrypicking7
git add .
git commit -am "sixth commit haha"
echo "this is file number 8 that I'm going to cherry-pick" > cherrypicking8
git add .
git commit -am "seventh commit yeah"
echo "this is file number 9 that I'm going to cherry-pick" > cherrypicking9
git add .
git commit -am "eighth commit woo"
git checkout -b develop
echo "once upon a time there was a dog" >> file1
add_spacing file1
echo "once upon a time there was another dog" >> file1
git add file1
echo "test2" > directory/file
echo "test2" > directory/file2
git add directory
git commit -m "first commit on develop"
git checkout master
echo "once upon a time there was a cat" >> file1
add_spacing file1
echo "once upon a time there was another cat" >> file1
git add file1
echo "test3" > directory/file
echo "test3" > directory/file2
git add directory
git commit -m "first commit on master"
git checkout develop
echo "once upon a time there was a mouse" >> file3
git add file3
git commit -m "second commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file3
git add file3
git commit -m "second commit on master"
git checkout develop
echo "once upon a time there was a mouse" >> file4
git add file4
git commit -m "third commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file4
git add file4
git commit -m "third commit on master"
git checkout develop
echo "once upon a time there was a mouse" >> file5
git add file5
git commit -m "fourth commit on develop"
git checkout master
echo "once upon a time there was a horse" >> file5
git add file5
git commit -m "fourth commit on master"
# this is for the autostash feature
git checkout -b base_branch
echo "original1\noriginal2\noriginal3" > file
git add file
git commit -m "file"
git checkout -b other_branch
git checkout base_branch
echo "new1\noriginal2\noriginal3" > file
git add file
git commit -m "file changed"
git checkout other_branch
echo "new2\noriginal2\noriginal3" > file

View File

@ -1,12 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
cp ../extras/pre-commit .git/hooks/pre-commit
chmod +x .git/hooks/pre-commit
echo "file" > file
git add file

View File

@ -1,22 +0,0 @@
#!/bin/bash
set -ex; rm -rf repo; mkdir repo; cd repo
git init
git config user.email "test@example.com"
git config user.name "Lazygit Tester"
# Add some ansi, unicode, zero width joiner characters
cat <<EOT >> charstest.txt
ANSI Œ (U+0152 &OElig; Latin capital ligature OE Latin Extended-A)
¥ (0xA5 U+00A5 &yen; yes sign)
ƒ (0x83 U+0192 &fnof; Latin small letter f with hook)
ZWJ https://en.wikipedia.org/wiki/Zero-width_joiner / https://unicode.org/Public/emoji/4.0/emoji-zwj-sequences.txt 👶(👨‍👦)
UNICODE ☆ 🤓 え 术
EOT
git add charstest.txt
git commit -m "Test chars Œ¥ƒ👶👨‍👦☆ 🤓 え 术👩‍💻👩🏻‍💻👩🏽‍💻👩🏼‍💻👩🏾‍💻👩🏿‍💻👨‍💻👨🏻‍💻👨🏼‍💻👨🏽‍💻👨🏾‍💻👨🏿‍💻 commit"
echo "我喜歡編碼" >> charstest.txt
echo "நான் குறியீடு விரும்புகிறேன்" >> charstest.txt
git add charstest.txt
git commit -m "Test chars 我喜歡編碼 நான் குறியீடு விரும்புகிறேன் commit"

View File

@ -1,7 +0,0 @@
#!/bin/bash
# For testing subprocesses that require input
# Ask the user for login details
read -p 'Username: ' user
read -sp 'Password: ' pass
echo
echo Hello $user