mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-03-17 21:18:31 +02:00
Merge pull request #2094 from jesseduffield/better-integration-tests
This commit is contained in:
commit
ba7d639940
28
.github/workflows/automerge.yml
vendored
28
.github/workflows/automerge.yml
vendored
@ -1,28 +0,0 @@
|
||||
name: automerge
|
||||
on:
|
||||
pull_request:
|
||||
types:
|
||||
- labeled
|
||||
- unlabeled
|
||||
- synchronize
|
||||
- opened
|
||||
- edited
|
||||
- ready_for_review
|
||||
- reopened
|
||||
- unlocked
|
||||
pull_request_review:
|
||||
types:
|
||||
- submitted
|
||||
check_suite:
|
||||
types:
|
||||
- completed
|
||||
status: {}
|
||||
jobs:
|
||||
automerge:
|
||||
runs-on: ubuntu-latest
|
||||
steps:
|
||||
- name: automerge
|
||||
uses: "pascalgn/automerge-action@135f0bdb927d9807b5446f7ca9ecc2c51de03c4a"
|
||||
env:
|
||||
GITHUB_TOKEN: "${{ secrets.GITHUB_TOKEN }}"
|
||||
MERGE_METHOD: rebase
|
30
.github/workflows/ci.yml
vendored
30
.github/workflows/ci.yml
vendored
@ -46,14 +46,14 @@ 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:
|
||||
integration-tests-old:
|
||||
runs-on: ubuntu-latest
|
||||
strategy:
|
||||
fail-fast: false
|
||||
matrix:
|
||||
parallelism: [5]
|
||||
index: [0,1,2,3,4]
|
||||
name: "Integration Tests (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
name: "Integration Tests (Old pattern) (${{ matrix.index }}/${{ matrix.parallelism }})"
|
||||
env:
|
||||
GOFLAGS: -mod=vendor
|
||||
steps:
|
||||
@ -74,7 +74,31 @@ jobs:
|
||||
${{runner.os}}-go-
|
||||
- name: Test code
|
||||
run: |
|
||||
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/gui/gui_test.go
|
||||
PARALLEL_TOTAL=${{ matrix.parallelism }} PARALLEL_INDEX=${{ matrix.index }} go test pkg/integration/deprecated/*.go
|
||||
integration-tests:
|
||||
runs-on: ubuntu-latest
|
||||
name: "Integration Tests"
|
||||
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
|
||||
run: |
|
||||
go test pkg/integration/*.go
|
||||
build:
|
||||
runs-on: ubuntu-latest
|
||||
env:
|
||||
|
10
.gitignore
vendored
10
.gitignore
vendored
@ -33,11 +33,19 @@ lazygit.exe
|
||||
!.gitmodules_keep
|
||||
|
||||
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/**/actual/
|
||||
test/integration_new/**/used_config/
|
||||
# these sample hooks waste too much space
|
||||
test/integration_new/**/expected/**/hooks/
|
||||
test/integration_new/**/expected_remote/**/hooks/
|
||||
|
||||
oryxBuildBinary
|
||||
__debug_bin
|
||||
__debug_bin
|
||||
|
@ -63,9 +63,9 @@ by setting [`formatting.gofumpt`](https://github.com/golang/tools/blob/master/go
|
||||
```jsonc
|
||||
// .vscode/settings.json
|
||||
{
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
"gopls": {
|
||||
"formatting.gofumpt": true
|
||||
}
|
||||
}
|
||||
```
|
||||
|
||||
@ -82,6 +82,7 @@ From most places in the codebase you have access to a logger e.g. `gui.Log.Warn(
|
||||
If you find that the existing logs are too noisy, you can set the log level with e.g. `LOG_LEVEL=warn go run main.go -debug` and then only use `Warn` logs yourself.
|
||||
|
||||
If you need to log from code in the vendor directory (e.g. the `gocui` package), you won't have access to the logger, but you can easily add logging support by adding the following:
|
||||
|
||||
```go
|
||||
func newLogger() *logrus.Entry {
|
||||
// REPLACE THE BELOW PATH WITH YOUR ACTUAL LOG PATH (YOU'LL SEE THIS PRINTED WHEN YOU RUN `lazygit --logs`
|
||||
@ -118,9 +119,7 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
"request": "launch",
|
||||
"mode": "auto",
|
||||
"program": "main.go",
|
||||
"args": [
|
||||
"--debug"
|
||||
],
|
||||
"args": ["--debug"],
|
||||
"console": "externalTerminal" // <-- you need this to actually see the lazygit UI in a window while debugging
|
||||
}
|
||||
]
|
||||
@ -129,7 +128,7 @@ If you want to trigger a debug session from VSCode, you can use the following sn
|
||||
|
||||
## Testing
|
||||
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. Lazygit has its own integration test system where you can build a sandbox repo with a shell script, record yourself doing something, and commit the resulting repo snapshot. It's pretty damn cool! To learn more see [here](https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md)
|
||||
Lazygit has two kinds of tests: unit tests and integration tests. Unit tests go in files that end in `_test.go`, and are written in Go. For integration tests, see [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
||||
## Updating Gocui
|
||||
|
||||
|
@ -1,122 +1 @@
|
||||
# How To Make And Run Integration Tests For lazygit
|
||||
|
||||
Integration tests are located in `test/integration`. Each test will run a bash script to prepare a test repo, then replay a recorded lazygit session from within that repo, and then the resultant repo will be compared to an expected repo that was created upon the initial recording. Each integration test lives in its own directory, and the name of the directory becomes the name of the test. Within the directory must be the following files:
|
||||
|
||||
### `test.json`
|
||||
|
||||
An example of a `test.json` is:
|
||||
|
||||
```
|
||||
{ "description": "Open a confirmation, then open a menu over that, then close the menu. Verify that the confirmation popup also closes automatically", "speed": 20 }
|
||||
```
|
||||
|
||||
The `speed` key refers to the playback speed as a multiple of the original recording speed. So 20 means the test will run 20 times faster than the original recording speed. If a test fails for a given speed, it will drop the speed and re-test, until finally attempting the test at the original speed. If you omit the speed, it will default to 10.
|
||||
|
||||
### `setup.sh`
|
||||
|
||||
This is a bash script containing the instructions for creating the test repo from scratch. For example:
|
||||
|
||||
```
|
||||
#!/bin/sh
|
||||
|
||||
cd $1
|
||||
|
||||
git init
|
||||
|
||||
git config user.email "CI@example.com"
|
||||
git config user.name "CI"
|
||||
|
||||
echo test1 > myfile1
|
||||
git add .
|
||||
git commit -am "myfile1"
|
||||
```
|
||||
|
||||
Be sure to:
|
||||
|
||||
- ensure that by the end of the test you've got at least one commit in the repo, as we've had issues in the past when that wasn't the case.
|
||||
- set the git user email and name as above so that your own user details aren't included in the snapshot.
|
||||
|
||||
## 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:
|
||||
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
|
||||
|
||||
```
|
||||
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:
|
||||
|
||||
```
|
||||
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.
|
||||
|
||||
The resulting directory will look like:
|
||||
|
||||
```
|
||||
actual/ (the resulting repo(s) after running the test, ignored by git)
|
||||
expected/ (the 'snapshot' repo(s))
|
||||
config/ (need not be present)
|
||||
test.json
|
||||
setup.sh
|
||||
recording.json
|
||||
```
|
||||
|
||||
## 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.
|
||||
see new docs [here](https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md)
|
||||
|
214
main.go
214
main.go
@ -1,222 +1,24 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
yaml "github.com/jesseduffield/yaml"
|
||||
"github.com/samber/lo"
|
||||
)
|
||||
|
||||
const DEFAULT_VERSION = "unversioned"
|
||||
|
||||
// These values may be set by the build script via the LDFLAGS argument
|
||||
var (
|
||||
commit string
|
||||
version = DEFAULT_VERSION
|
||||
date string
|
||||
version string
|
||||
buildSource = "unknown"
|
||||
)
|
||||
|
||||
func main() {
|
||||
updateBuildInfo()
|
||||
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := ""
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
versionFlag := false
|
||||
flaggy.Bool(&versionFlag, "v", "version", "Print the current version")
|
||||
|
||||
debuggingFlag := false
|
||||
flaggy.Bool(&debuggingFlag, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
||||
|
||||
logFlag := false
|
||||
flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
configFlag := false
|
||||
flaggy.Bool(&configFlag, "c", "config", "Print the default config")
|
||||
|
||||
configDirFlag := false
|
||||
flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory")
|
||||
|
||||
useConfigDir := ""
|
||||
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
||||
|
||||
workTree := ""
|
||||
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
||||
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfig := ""
|
||||
flaggy.String(&customConfig, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debuggingFlag = true
|
||||
ldFlagsBuildInfo := &app.BuildInfo{
|
||||
Commit: commit,
|
||||
Date: date,
|
||||
Version: version,
|
||||
BuildSource: buildSource,
|
||||
}
|
||||
|
||||
if repoPath != "" {
|
||||
if workTree != "" || gitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(repoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
workTree = absRepoPath
|
||||
gitDir = filepath.Join(absRepoPath, ".git")
|
||||
}
|
||||
|
||||
if customConfig != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", customConfig)
|
||||
}
|
||||
|
||||
if useConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", useConfigDir)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
env.SetGitWorkTreeEnv(workTree)
|
||||
}
|
||||
|
||||
if gitDir != "" {
|
||||
env.SetGitDirEnv(gitDir)
|
||||
}
|
||||
|
||||
if versionFlag {
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", commit, date, buildSource, version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configFlag {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n", buf.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if configDirFlag {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if logFlag {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if workTree != "" {
|
||||
if err := os.Chdir(workTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", version, commit, date, buildSource, debuggingFlag, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
common, err := app.NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(gitArg)
|
||||
|
||||
app.Run(appConfig, common, types.NewStartArgs(filterPath, parsedGitArg))
|
||||
}
|
||||
|
||||
func parseGitArg(gitArg string) types.GitArg {
|
||||
typedArg := types.GitArg(gitArg)
|
||||
|
||||
// using switch so that linter catches when a new git arg value is defined but not handled here
|
||||
switch typedArg {
|
||||
case types.GitArgNone, types.GitArgStatus, types.GitArgBranch, types.GitArgLog, types.GitArgStash:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
string(types.GitArgStatus),
|
||||
string(types.GitArgBranch),
|
||||
string(types.GitArgLog),
|
||||
string(types.GitArgStash),
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
func updateBuildInfo() {
|
||||
// if the version has already been set by build flags then we'll honour that.
|
||||
// chances are it's something like v0.31.0 which is more informative than a
|
||||
// commit hash.
|
||||
if version != DEFAULT_VERSION {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
revision, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.revision"
|
||||
})
|
||||
if ok {
|
||||
commit = revision.Value
|
||||
// if lazygit was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
version = utils.ShortSha(revision.Value)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
if ok {
|
||||
date = time.Value
|
||||
}
|
||||
app.Start(ldFlagsBuildInfo, nil)
|
||||
}
|
||||
|
@ -14,6 +14,7 @@ import (
|
||||
"github.com/go-errors/errors"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/oscommands"
|
||||
@ -22,7 +23,6 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/constants"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/i18n"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
)
|
||||
@ -38,7 +38,11 @@ type App struct {
|
||||
Updater *updates.Updater // may only need this on the Gui
|
||||
}
|
||||
|
||||
func Run(config config.AppConfigurer, common *common.Common, startArgs types.StartArgs) {
|
||||
func Run(
|
||||
config config.AppConfigurer,
|
||||
common *common.Common,
|
||||
startArgs appTypes.StartArgs,
|
||||
) {
|
||||
app, err := NewApp(config, common)
|
||||
|
||||
if err == nil {
|
||||
@ -213,7 +217,7 @@ func (app *App) setupRepo() (bool, error) {
|
||||
return false, nil
|
||||
}
|
||||
|
||||
func (app *App) Run(startArgs types.StartArgs) error {
|
||||
func (app *App) Run(startArgs appTypes.StartArgs) error {
|
||||
err := app.Gui.RunAndHandleError(startArgs)
|
||||
return err
|
||||
}
|
||||
|
265
pkg/app/entry_point.go
Normal file
265
pkg/app/entry_point.go
Normal file
@ -0,0 +1,265 @@
|
||||
package app
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"fmt"
|
||||
"log"
|
||||
"os"
|
||||
"path/filepath"
|
||||
"runtime"
|
||||
"runtime/debug"
|
||||
"strings"
|
||||
|
||||
"github.com/integrii/flaggy"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/logs"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
"github.com/samber/lo"
|
||||
"gopkg.in/yaml.v3"
|
||||
)
|
||||
|
||||
type cliArgs struct {
|
||||
RepoPath string
|
||||
FilterPath string
|
||||
GitArg string
|
||||
PrintVersionInfo bool
|
||||
Debug bool
|
||||
TailLogs bool
|
||||
PrintDefaultConfig bool
|
||||
PrintConfigDir bool
|
||||
UseConfigDir string
|
||||
WorkTree string
|
||||
GitDir string
|
||||
CustomConfigFile string
|
||||
}
|
||||
|
||||
type BuildInfo struct {
|
||||
Commit string
|
||||
Date string
|
||||
Version string
|
||||
BuildSource string
|
||||
}
|
||||
|
||||
func Start(buildInfo *BuildInfo, integrationTest integrationTypes.IntegrationTest) {
|
||||
cliArgs := parseCliArgsAndEnvVars()
|
||||
mergeBuildInfo(buildInfo)
|
||||
|
||||
if cliArgs.RepoPath != "" {
|
||||
if cliArgs.WorkTree != "" || cliArgs.GitDir != "" {
|
||||
log.Fatal("--path option is incompatible with the --work-tree and --git-dir options")
|
||||
}
|
||||
|
||||
absRepoPath, err := filepath.Abs(cliArgs.RepoPath)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
cliArgs.WorkTree = absRepoPath
|
||||
cliArgs.GitDir = filepath.Join(absRepoPath, ".git")
|
||||
}
|
||||
|
||||
if cliArgs.CustomConfigFile != "" {
|
||||
os.Setenv("LG_CONFIG_FILE", cliArgs.CustomConfigFile)
|
||||
}
|
||||
|
||||
if cliArgs.UseConfigDir != "" {
|
||||
os.Setenv("CONFIG_DIR", cliArgs.UseConfigDir)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
env.SetGitWorkTreeEnv(cliArgs.WorkTree)
|
||||
}
|
||||
|
||||
if cliArgs.GitDir != "" {
|
||||
env.SetGitDirEnv(cliArgs.GitDir)
|
||||
}
|
||||
|
||||
if cliArgs.PrintVersionInfo {
|
||||
fmt.Printf("commit=%s, build date=%s, build source=%s, version=%s, os=%s, arch=%s\n", buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, buildInfo.Version, runtime.GOOS, runtime.GOARCH)
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.PrintDefaultConfig {
|
||||
var buf bytes.Buffer
|
||||
encoder := yaml.NewEncoder(&buf)
|
||||
err := encoder.Encode(config.GetDefaultConfig())
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
fmt.Printf("%s\n", buf.String())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.PrintConfigDir {
|
||||
fmt.Printf("%s\n", config.ConfigDir())
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.TailLogs {
|
||||
logs.TailLogs()
|
||||
os.Exit(0)
|
||||
}
|
||||
|
||||
if cliArgs.WorkTree != "" {
|
||||
if err := os.Chdir(cliArgs.WorkTree); err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
tempDir, err := os.MkdirTemp("", "lazygit-*")
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
defer os.RemoveAll(tempDir)
|
||||
|
||||
appConfig, err := config.NewAppConfig("lazygit", buildInfo.Version, buildInfo.Commit, buildInfo.Date, buildInfo.BuildSource, cliArgs.Debug, tempDir)
|
||||
if err != nil {
|
||||
log.Fatal(err.Error())
|
||||
}
|
||||
|
||||
if integrationTest != nil {
|
||||
integrationTest.SetupConfig(appConfig)
|
||||
}
|
||||
|
||||
common, err := NewCommon(appConfig)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
|
||||
if daemon.InDaemonMode() {
|
||||
daemon.Handle(common)
|
||||
return
|
||||
}
|
||||
|
||||
parsedGitArg := parseGitArg(cliArgs.GitArg)
|
||||
|
||||
Run(appConfig, common, appTypes.NewStartArgs(cliArgs.FilterPath, parsedGitArg, integrationTest))
|
||||
}
|
||||
|
||||
func parseCliArgsAndEnvVars() *cliArgs {
|
||||
flaggy.DefaultParser.ShowVersionWithVersionFlag = false
|
||||
|
||||
repoPath := ""
|
||||
flaggy.String(&repoPath, "p", "path", "Path of git repo. (equivalent to --work-tree=<path> --git-dir=<path>/.git/)")
|
||||
|
||||
filterPath := ""
|
||||
flaggy.String(&filterPath, "f", "filter", "Path to filter on in `git log -- <path>`. When in filter mode, the commits, reflog, and stash are filtered based on the given path, and some operations are restricted")
|
||||
|
||||
gitArg := ""
|
||||
flaggy.AddPositionalValue(&gitArg, "git-arg", 1, false, "Panel to focus upon opening lazygit. Accepted values (based on git terminology): status, branch, log, stash. Ignored if --filter arg is passed.")
|
||||
|
||||
printVersionInfo := false
|
||||
flaggy.Bool(&printVersionInfo, "v", "version", "Print the current version")
|
||||
|
||||
debug := false
|
||||
flaggy.Bool(&debug, "d", "debug", "Run in debug mode with logging (see --logs flag below). Use the LOG_LEVEL env var to set the log level (debug/info/warn/error)")
|
||||
|
||||
tailLogs := false
|
||||
flaggy.Bool(&tailLogs, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)")
|
||||
|
||||
printDefaultConfig := false
|
||||
flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config")
|
||||
|
||||
printConfigDir := false
|
||||
flaggy.Bool(&printConfigDir, "cd", "print-config-dir", "Print the config directory")
|
||||
|
||||
useConfigDir := ""
|
||||
flaggy.String(&useConfigDir, "ucd", "use-config-dir", "override default config directory with provided directory")
|
||||
|
||||
workTree := ""
|
||||
flaggy.String(&workTree, "w", "work-tree", "equivalent of the --work-tree git argument")
|
||||
|
||||
gitDir := ""
|
||||
flaggy.String(&gitDir, "g", "git-dir", "equivalent of the --git-dir git argument")
|
||||
|
||||
customConfigFile := ""
|
||||
flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)")
|
||||
|
||||
flaggy.Parse()
|
||||
|
||||
if os.Getenv("DEBUG") == "TRUE" {
|
||||
debug = true
|
||||
}
|
||||
|
||||
return &cliArgs{
|
||||
RepoPath: repoPath,
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
PrintVersionInfo: printVersionInfo,
|
||||
Debug: debug,
|
||||
TailLogs: tailLogs,
|
||||
PrintDefaultConfig: printDefaultConfig,
|
||||
PrintConfigDir: printConfigDir,
|
||||
UseConfigDir: useConfigDir,
|
||||
WorkTree: workTree,
|
||||
GitDir: gitDir,
|
||||
CustomConfigFile: customConfigFile,
|
||||
}
|
||||
}
|
||||
|
||||
func parseGitArg(gitArg string) appTypes.GitArg {
|
||||
typedArg := appTypes.GitArg(gitArg)
|
||||
|
||||
// using switch so that linter catches when a new git arg value is defined but not handled here
|
||||
switch typedArg {
|
||||
case appTypes.GitArgNone, appTypes.GitArgStatus, appTypes.GitArgBranch, appTypes.GitArgLog, appTypes.GitArgStash:
|
||||
return typedArg
|
||||
}
|
||||
|
||||
permittedValues := []string{
|
||||
string(appTypes.GitArgStatus),
|
||||
string(appTypes.GitArgBranch),
|
||||
string(appTypes.GitArgLog),
|
||||
string(appTypes.GitArgStash),
|
||||
}
|
||||
|
||||
log.Fatalf("Invalid git arg value: '%s'. Must be one of the following values: %s. e.g. 'lazygit status'. See 'lazygit --help'.",
|
||||
gitArg,
|
||||
strings.Join(permittedValues, ", "),
|
||||
)
|
||||
|
||||
panic("unreachable")
|
||||
}
|
||||
|
||||
// the buildInfo struct we get passed in is based on what's baked into the lazygit
|
||||
// binary via the LDFLAGS argument. Some lazygit distributions will make use of these
|
||||
// arguments and some will not. Go recently started baking in build info
|
||||
// into the binary by default e.g. the git commit hash. So in this function
|
||||
// we merge the two together, giving priority to the stuff set by LDFLAGS.
|
||||
// Note: this mutates the argument passed in
|
||||
func mergeBuildInfo(buildInfo *BuildInfo) {
|
||||
// if the version has already been set by build flags then we'll honour that.
|
||||
// chances are it's something like v0.31.0 which is more informative than a
|
||||
// commit hash.
|
||||
if buildInfo.Version != "" {
|
||||
return
|
||||
}
|
||||
|
||||
buildInfo.Version = "unversioned"
|
||||
|
||||
goBuildInfo, ok := debug.ReadBuildInfo()
|
||||
if !ok {
|
||||
return
|
||||
}
|
||||
|
||||
revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.revision"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Commit = revision.Value
|
||||
// if lazygit was built from source we'll show the version as the
|
||||
// abbreviated commit hash
|
||||
buildInfo.Version = utils.ShortSha(revision.Value)
|
||||
}
|
||||
|
||||
// if version hasn't been set we assume that neither has the date
|
||||
time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool {
|
||||
return setting.Key == "vcs.time"
|
||||
})
|
||||
if ok {
|
||||
buildInfo.Date = time.Value
|
||||
}
|
||||
}
|
@ -1,4 +1,8 @@
|
||||
package types
|
||||
package app
|
||||
|
||||
import (
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// StartArgs is the struct that represents some things we want to do on program start
|
||||
type StartArgs struct {
|
||||
@ -6,6 +10,8 @@ type StartArgs struct {
|
||||
FilterPath string
|
||||
// GitArg determines what context we open in
|
||||
GitArg GitArg
|
||||
// integration test (only relevant when invoking lazygit in the context of an integration test)
|
||||
IntegrationTest integrationTypes.IntegrationTest
|
||||
}
|
||||
|
||||
type GitArg string
|
||||
@ -18,9 +24,10 @@ const (
|
||||
GitArgStash GitArg = "stash"
|
||||
)
|
||||
|
||||
func NewStartArgs(filterPath string, gitArg GitArg) StartArgs {
|
||||
func NewStartArgs(filterPath string, gitArg GitArg, test integrationTypes.IntegrationTest) StartArgs {
|
||||
return StartArgs{
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
FilterPath: filterPath,
|
||||
GitArg: gitArg,
|
||||
IntegrationTest: test,
|
||||
}
|
||||
}
|
@ -27,8 +27,6 @@ type AppConfig struct {
|
||||
IsNewRepo bool
|
||||
}
|
||||
|
||||
// AppConfigurer interface allows individual app config structs to inherit Fields
|
||||
// from AppConfig and still be used by lazygit.
|
||||
type AppConfigurer interface {
|
||||
GetDebug() bool
|
||||
|
||||
|
@ -10,6 +10,7 @@ import (
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/git_config"
|
||||
@ -31,6 +32,7 @@ import (
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/services/custom_commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/tasks"
|
||||
"github.com/jesseduffield/lazygit/pkg/theme"
|
||||
"github.com/jesseduffield/lazygit/pkg/updates"
|
||||
@ -213,7 +215,7 @@ const (
|
||||
COMPLETE
|
||||
)
|
||||
|
||||
func (gui *Gui) onNewRepo(startArgs types.StartArgs, reuseState bool) error {
|
||||
func (gui *Gui) onNewRepo(startArgs appTypes.StartArgs, reuseState bool) error {
|
||||
var err error
|
||||
gui.git, err = commands.NewGitCommand(
|
||||
gui.Common,
|
||||
@ -245,7 +247,7 @@ func (gui *Gui) onNewRepo(startArgs types.StartArgs, reuseState bool) error {
|
||||
// it gets a bit confusing to land back in the status panel when visiting a repo
|
||||
// you've already switched from. There's no doubt some easy way to make the UX
|
||||
// optimal for all cases but I'm too lazy to think about what that is right now
|
||||
func (gui *Gui) resetState(startArgs types.StartArgs, reuseState bool) {
|
||||
func (gui *Gui) resetState(startArgs appTypes.StartArgs, reuseState bool) {
|
||||
currentDir, err := os.Getwd()
|
||||
|
||||
if reuseState {
|
||||
@ -300,28 +302,28 @@ func (gui *Gui) resetState(startArgs types.StartArgs, reuseState bool) {
|
||||
gui.RepoStateMap[Repo(currentDir)] = gui.State
|
||||
}
|
||||
|
||||
func initialScreenMode(startArgs types.StartArgs) WindowMaximisation {
|
||||
if startArgs.FilterPath != "" || startArgs.GitArg != types.GitArgNone {
|
||||
func initialScreenMode(startArgs appTypes.StartArgs) WindowMaximisation {
|
||||
if startArgs.FilterPath != "" || startArgs.GitArg != appTypes.GitArgNone {
|
||||
return SCREEN_HALF
|
||||
} else {
|
||||
return SCREEN_NORMAL
|
||||
}
|
||||
}
|
||||
|
||||
func initialContext(contextTree *context.ContextTree, startArgs types.StartArgs) types.IListContext {
|
||||
func initialContext(contextTree *context.ContextTree, startArgs appTypes.StartArgs) types.IListContext {
|
||||
var initialContext types.IListContext = contextTree.Files
|
||||
|
||||
if startArgs.FilterPath != "" {
|
||||
initialContext = contextTree.LocalCommits
|
||||
} else if startArgs.GitArg != types.GitArgNone {
|
||||
} else if startArgs.GitArg != appTypes.GitArgNone {
|
||||
switch startArgs.GitArg {
|
||||
case types.GitArgStatus:
|
||||
case appTypes.GitArgStatus:
|
||||
initialContext = contextTree.Files
|
||||
case types.GitArgBranch:
|
||||
case appTypes.GitArgBranch:
|
||||
initialContext = contextTree.Branches
|
||||
case types.GitArgLog:
|
||||
case appTypes.GitArgLog:
|
||||
initialContext = contextTree.LocalCommits
|
||||
case types.GitArgStash:
|
||||
case appTypes.GitArgStash:
|
||||
initialContext = contextTree.Stash
|
||||
default:
|
||||
panic("unhandled git arg")
|
||||
@ -417,13 +419,15 @@ var RuneReplacements = map[rune]string{
|
||||
graph.CommitSymbol: "o",
|
||||
}
|
||||
|
||||
func (gui *Gui) initGocui(headless bool) (*gocui.Gui, error) {
|
||||
recordEvents := recordingEvents()
|
||||
func (gui *Gui) initGocui(headless bool, test integrationTypes.IntegrationTest) (*gocui.Gui, error) {
|
||||
recordEvents := RecordingEvents()
|
||||
playMode := gocui.NORMAL
|
||||
if recordEvents {
|
||||
playMode = gocui.RECORDING
|
||||
} else if replaying() {
|
||||
} else if Replaying() {
|
||||
playMode = gocui.REPLAYING
|
||||
} else if test != nil {
|
||||
playMode = gocui.REPLAYING_NEW
|
||||
}
|
||||
|
||||
g, err := gocui.NewGui(gocui.OutputTrue, OverlappingEdges, playMode, headless, RuneReplacements)
|
||||
@ -474,8 +478,8 @@ func (gui *Gui) viewTabMap() map[string][]context.TabView {
|
||||
}
|
||||
|
||||
// Run: setup the gui with keybindings and start the mainloop
|
||||
func (gui *Gui) Run(startArgs types.StartArgs) error {
|
||||
g, err := gui.initGocui(headless())
|
||||
func (gui *Gui) Run(startArgs appTypes.StartArgs) error {
|
||||
g, err := gui.initGocui(Headless(), startArgs.IntegrationTest)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
@ -490,23 +494,7 @@ func (gui *Gui) Run(startArgs types.StartArgs) error {
|
||||
})
|
||||
deadlock.Opts.Disable = !gui.Debug
|
||||
|
||||
if replaying() {
|
||||
gui.g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: getRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
}
|
||||
|
||||
var err error
|
||||
gui.g.Recording, err = gui.loadRecording()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
gui.handleTestMode(startArgs.IntegrationTest)
|
||||
|
||||
gui.g.OnSearchEscape = gui.onSearchEscape
|
||||
if err := gui.Config.ReloadUserConfig(); err != nil {
|
||||
@ -567,7 +555,7 @@ func (gui *Gui) Run(startArgs types.StartArgs) error {
|
||||
return gui.g.MainLoop()
|
||||
}
|
||||
|
||||
func (gui *Gui) RunAndHandleError(startArgs types.StartArgs) error {
|
||||
func (gui *Gui) RunAndHandleError(startArgs appTypes.StartArgs) error {
|
||||
gui.stopChan = make(chan struct{})
|
||||
return utils.SafeWithError(func() error {
|
||||
if err := gui.Run(startArgs); err != nil {
|
||||
@ -593,7 +581,7 @@ func (gui *Gui) RunAndHandleError(startArgs types.StartArgs) error {
|
||||
}
|
||||
}
|
||||
|
||||
if err := gui.saveRecording(gui.g.Recording); err != nil {
|
||||
if err := SaveRecording(gui.g.Recording); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
@ -627,7 +615,7 @@ func (gui *Gui) runSubprocessWithSuspense(subprocess oscommands.ICmdObj) (bool,
|
||||
gui.Mutexes.SubprocessMutex.Lock()
|
||||
defer gui.Mutexes.SubprocessMutex.Unlock()
|
||||
|
||||
if replaying() {
|
||||
if Replaying() {
|
||||
// we do not yet support running subprocesses within integration tests. So if
|
||||
// we're replaying an integration test and we're inside this method, something
|
||||
// has gone wrong, so we should fail
|
||||
|
73
pkg/gui/gui_driver.go
Normal file
73
pkg/gui/gui_driver.go
Normal file
@ -0,0 +1,73 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"time"
|
||||
|
||||
"github.com/gdamore/tcell/v2"
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/keybindings"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// this gives our integration test a way of interacting with the gui for sending keypresses
|
||||
// and reading state.
|
||||
type GuiDriver struct {
|
||||
gui *Gui
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &GuiDriver{}
|
||||
|
||||
func (self *GuiDriver) PressKey(keyStr string) {
|
||||
key := keybindings.GetKey(keyStr)
|
||||
|
||||
var r rune
|
||||
var tcellKey tcell.Key
|
||||
switch v := key.(type) {
|
||||
case rune:
|
||||
r = v
|
||||
tcellKey = tcell.KeyRune
|
||||
case gocui.Key:
|
||||
tcellKey = tcell.Key(v)
|
||||
}
|
||||
|
||||
self.gui.g.ReplayedEvents.Keys <- gocui.NewTcellKeyEventWrapper(
|
||||
tcell.NewEventKey(tcellKey, r, tcell.ModNone),
|
||||
0,
|
||||
)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Keys() config.KeybindingConfig {
|
||||
return self.gui.Config.GetUserConfig().Keybinding
|
||||
}
|
||||
|
||||
func (self *GuiDriver) CurrentContext() types.Context {
|
||||
return self.gui.c.CurrentContext()
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Model() *types.Model {
|
||||
return self.gui.State.Model
|
||||
}
|
||||
|
||||
func (self *GuiDriver) Fail(message string) {
|
||||
self.gui.g.Close()
|
||||
// need to give the gui time to close
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
panic(message)
|
||||
}
|
||||
|
||||
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
|
||||
func (self *GuiDriver) Log(message string) {
|
||||
self.gui.c.Log.Warn(message)
|
||||
}
|
||||
|
||||
// logs in the actual UI (in the commands panel)
|
||||
func (self *GuiDriver) LogUI(message string) {
|
||||
self.gui.c.LogAction(message)
|
||||
}
|
||||
|
||||
func (self *GuiDriver) CheckedOutRef() *models.Branch {
|
||||
return self.gui.helpers.Refs.GetCheckedOutRef()
|
||||
}
|
@ -9,6 +9,7 @@ import (
|
||||
"sync"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
appTypes "github.com/jesseduffield/lazygit/pkg/app/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/commands"
|
||||
"github.com/jesseduffield/lazygit/pkg/env"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/presentation/icons"
|
||||
@ -152,7 +153,7 @@ func (gui *Gui) dispatchSwitchToRepo(path string, reuse bool) error {
|
||||
gui.Mutexes.RefreshingFilesMutex.Lock()
|
||||
defer gui.Mutexes.RefreshingFilesMutex.Unlock()
|
||||
|
||||
return gui.onNewRepo(types.StartArgs{}, reuse)
|
||||
return gui.onNewRepo(appTypes.StartArgs{}, reuse)
|
||||
}
|
||||
|
||||
// updateRecentRepoList registers the fact that we opened lazygit in this repo,
|
||||
|
@ -1,74 +0,0 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
)
|
||||
|
||||
func recordingEvents() bool {
|
||||
return recordEventsTo() != ""
|
||||
}
|
||||
|
||||
func recordEventsTo() string {
|
||||
return os.Getenv("RECORD_EVENTS_TO")
|
||||
}
|
||||
|
||||
func replaying() bool {
|
||||
return os.Getenv("REPLAY_EVENTS_FROM") != ""
|
||||
}
|
||||
|
||||
func headless() bool {
|
||||
return os.Getenv("HEADLESS") != ""
|
||||
}
|
||||
|
||||
func getRecordingSpeed() float64 {
|
||||
// humans are slow so this speeds things up.
|
||||
speed := 1.0
|
||||
envReplaySpeed := os.Getenv("SPEED")
|
||||
if envReplaySpeed != "" {
|
||||
var err error
|
||||
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return speed
|
||||
}
|
||||
|
||||
func (gui *Gui) loadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recording := &gocui.Recording{}
|
||||
|
||||
err = json.Unmarshal(data, &recording)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
|
||||
func (gui *Gui) saveRecording(recording *gocui.Recording) error {
|
||||
if !recordingEvents() {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonEvents, err := json.Marshal(recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := recordEventsTo()
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0o600)
|
||||
}
|
@ -1,6 +1,9 @@
|
||||
package custom_commands
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"text/template"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/common"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
)
|
||||
@ -101,3 +104,23 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption,
|
||||
Value: value,
|
||||
}, nil
|
||||
}
|
||||
|
||||
type CustomCommandObject struct {
|
||||
// deprecated. Use Responses instead
|
||||
PromptResponses []string
|
||||
Form map[string]string
|
||||
}
|
||||
|
||||
func ResolveTemplate(templateStr string, object interface{}) (string, error) {
|
||||
tmpl, err := template.New("template").Parse(templateStr)
|
||||
if err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
var buf bytes.Buffer
|
||||
if err := tmpl.Execute(&buf, object); err != nil {
|
||||
return "", err
|
||||
}
|
||||
|
||||
return buf.String(), nil
|
||||
}
|
||||
|
124
pkg/gui/test_mode.go
Normal file
124
pkg/gui/test_mode.go
Normal file
@ -0,0 +1,124 @@
|
||||
package gui
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"io/ioutil"
|
||||
"log"
|
||||
"os"
|
||||
"strconv"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/gocui"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
type IntegrationTest interface {
|
||||
Run(guiAdapter *GuiDriver)
|
||||
}
|
||||
|
||||
func (gui *Gui) handleTestMode(test integrationTypes.IntegrationTest) {
|
||||
if test != nil {
|
||||
go func() {
|
||||
time.Sleep(time.Millisecond * 100)
|
||||
|
||||
test.Run(&GuiDriver{gui: gui})
|
||||
|
||||
gui.g.Update(func(*gocui.Gui) error {
|
||||
return gocui.ErrQuit
|
||||
})
|
||||
|
||||
time.Sleep(time.Second * 1)
|
||||
|
||||
log.Fatal("gocui should have already exited")
|
||||
}()
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
|
||||
if Replaying() {
|
||||
gui.g.RecordingConfig = gocui.RecordingConfig{
|
||||
Speed: GetRecordingSpeed(),
|
||||
Leeway: 100,
|
||||
}
|
||||
|
||||
var err error
|
||||
gui.g.Recording, err = LoadRecording()
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
go utils.Safe(func() {
|
||||
time.Sleep(time.Second * 40)
|
||||
log.Fatal("40 seconds is up, lazygit recording took too long to complete")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func Headless() bool {
|
||||
return os.Getenv("HEADLESS") != ""
|
||||
}
|
||||
|
||||
// OLD integration test format stuff
|
||||
|
||||
func Replaying() bool {
|
||||
return os.Getenv("REPLAY_EVENTS_FROM") != ""
|
||||
}
|
||||
|
||||
func RecordingEvents() bool {
|
||||
return recordEventsTo() != ""
|
||||
}
|
||||
|
||||
func recordEventsTo() string {
|
||||
return os.Getenv("RECORD_EVENTS_TO")
|
||||
}
|
||||
|
||||
func GetRecordingSpeed() float64 {
|
||||
// humans are slow so this speeds things up.
|
||||
speed := 1.0
|
||||
envReplaySpeed := os.Getenv("SPEED")
|
||||
if envReplaySpeed != "" {
|
||||
var err error
|
||||
speed, err = strconv.ParseFloat(envReplaySpeed, 64)
|
||||
if err != nil {
|
||||
log.Fatal(err)
|
||||
}
|
||||
}
|
||||
return speed
|
||||
}
|
||||
|
||||
func LoadRecording() (*gocui.Recording, error) {
|
||||
path := os.Getenv("REPLAY_EVENTS_FROM")
|
||||
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
recording := &gocui.Recording{}
|
||||
|
||||
err = json.Unmarshal(data, &recording)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
return recording, nil
|
||||
}
|
||||
|
||||
func SaveRecording(recording *gocui.Recording) error {
|
||||
if !RecordingEvents() {
|
||||
return nil
|
||||
}
|
||||
|
||||
jsonEvents, err := json.Marshal(recording)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
path := recordEventsTo()
|
||||
|
||||
return ioutil.WriteFile(path, jsonEvents, 0o600)
|
||||
}
|
74
pkg/integration/README.md
Normal file
74
pkg/integration/README.md
Normal file
@ -0,0 +1,74 @@
|
||||
# Integration Tests
|
||||
|
||||
The pkg/integration pacakge is for integration testing: that is, actually running a real lazygit session and having a robot pretend to be a human user and then making assertions that everything works as expected.
|
||||
|
||||
## Writing tests
|
||||
|
||||
The tests live in pkg/integration/tests. Each test has two important steps: the setup step and the run step.
|
||||
|
||||
### Setup step
|
||||
|
||||
In the setup step, we prepare a repo with shell commands, for example, creating a merge conflict that will need to be resolved upon opening lazygit. This is all done via the `shell` argument.
|
||||
|
||||
### Run step
|
||||
|
||||
The run step has four arguments passed in:
|
||||
|
||||
1. `shell`
|
||||
2. `input`
|
||||
3. `assert`
|
||||
4. `keys`
|
||||
|
||||
`shell` we've already seen in the setup step. The reason it's passed into the run step is that we may want to emulate background events. For example, the user modifying a file outside of lazygit.
|
||||
|
||||
`input` is for driving the gui by pressing certain keys, selecting list items, etc.
|
||||
|
||||
`assert` is for asserting on the state of the lazygit session. When you call a method on `assert`, the assert struct will wait for the assertion to hold true and then continue (failing the test after a timeout). For this reason, assertions have two purposes: one is to ensure the test fails as soon as something unexpected happens, but another is to allow lazygit to process a keypress before you follow up with more keypresses. If you input a bunch of keypresses too quickly lazygit might get confused.
|
||||
|
||||
### Tips
|
||||
|
||||
Try to do as much setup work as possible in your setup step. For example, if all you're testing is that the user is able to resolve merge conflicts, create the merge conflicts in the setup step. On the other hand, if you're testing to see that lazygit can warn the user about merge conflicts after an attempted merge, it's fine to wait until the run step to actually create the conflicts. If the run step is focused on the thing you're trying to test, the test will run faster and its intent will be clearer.
|
||||
|
||||
Use assertions to ensure that lazygit has processed all your keybindings so far. For example, if you press 'n' on a branch to create a new branch, assert that the confirmation view is now focused.
|
||||
|
||||
If you find yourself doing something frequently in a test, consider making it a method in one of the helper arguments. For example, instead of calling `input.PressKey(keys.Universal.Confirm)` in 100 places, it's better to have a method `input.Confirm()`. This is not to say that everything should be made into a method on the input struct: just things that are particularly common in tests.
|
||||
|
||||
## Running tests
|
||||
|
||||
There are three ways to invoke a test:
|
||||
|
||||
1. go run pkg/integration/cmd/runner/main.go [<testname>...]
|
||||
2. go run pkg/integration/cmd/tui/main.go
|
||||
3. go test pkg/integration/go_test.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.
|
||||
The third, the go-test command, intended only for use in CI, to be run along with the other `go test` tests. This runs the tests in headless mode so there's no visual output.
|
||||
|
||||
The name of a test is based on its path, so the name of the test at `pkg/integration/tests/commit/new_branch.go` is commit/new_branch. So to run it with our test runner you would run `go run pkg/integration/cmd/runner/main.go commit/new_branch`.
|
||||
|
||||
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 in the tui you can press 't' to run the test with a pre-set delay.
|
||||
|
||||
### Snapshots
|
||||
|
||||
At the moment (this is subject to change) each test has a snapshot repo created after running for the first time. These snapshots live in `test/integration_new`, in folders named 'expected' (alongside the 'actual' folders which contain the resulting repo from the last test run). Whenever you run a test, the resultant repo will be compared against the snapshot repo and if they're different, you'll be asked whether you want to update the snapshot. If you want to update a snapshot without being prompted you can pass MODE=updateSnapshot to the test runner or the go test command. This is useful when you've made a change to
|
||||
|
||||
### Sandbox mode
|
||||
|
||||
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 pass the env var MODE=sandbox to the test runner.
|
||||
|
||||
## Migration process
|
||||
|
||||
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 entirely 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 the 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.
|
56
pkg/integration/cmd/injector/main.go
Normal file
56
pkg/integration/cmd/injector/main.go
Normal file
@ -0,0 +1,56 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/app"
|
||||
"github.com/jesseduffield/lazygit/pkg/app/daemon"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// The purpose of this program is to run lazygit with an integration test passed in.
|
||||
// We could have done the check on LAZYGIT_TEST_NAME in the root main.go but
|
||||
// that would mean lazygit would be depending on integration test code which
|
||||
// would bloat the binary.
|
||||
|
||||
// You should not invoke this program directly. Instead you should go through
|
||||
// pkg/integration/cmd/runner/main.go or pkg/integration/cmd/tui/main.go
|
||||
|
||||
func main() {
|
||||
dummyBuildInfo := &app.BuildInfo{
|
||||
Commit: "",
|
||||
Date: "",
|
||||
Version: "",
|
||||
BuildSource: "integration test",
|
||||
}
|
||||
|
||||
integrationTest := getIntegrationTest()
|
||||
|
||||
app.Start(dummyBuildInfo, integrationTest)
|
||||
}
|
||||
|
||||
func getIntegrationTest() integrationTypes.IntegrationTest {
|
||||
if daemon.InDaemonMode() {
|
||||
// if we've invoked lazygit as a daemon from within lazygit,
|
||||
// we don't want to pass a test to the rest of the code.
|
||||
return nil
|
||||
}
|
||||
|
||||
integrationTestName := os.Getenv(integration.LAZYGIT_TEST_NAME_ENV_VAR)
|
||||
if integrationTestName == "" {
|
||||
panic(fmt.Sprintf(
|
||||
"expected %s environment variable to be set, given that we're running an integration test",
|
||||
integration.LAZYGIT_TEST_NAME_ENV_VAR,
|
||||
))
|
||||
}
|
||||
|
||||
for _, candidateTest := range integration.Tests {
|
||||
if candidateTest.Name() == integrationTestName {
|
||||
return candidateTest
|
||||
}
|
||||
}
|
||||
|
||||
panic("Could not find integration test with name: " + integrationTestName)
|
||||
}
|
73
pkg/integration/cmd/runner/main.go
Normal file
73
pkg/integration/cmd/runner/main.go
Normal file
@ -0,0 +1,73 @@
|
||||
package main
|
||||
|
||||
import (
|
||||
"log"
|
||||
"os"
|
||||
"os/exec"
|
||||
|
||||
"github.com/jesseduffield/generics/slices"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
// see pkg/integration/README.md
|
||||
|
||||
// The purpose of this program is to run integration tests. It does this by
|
||||
// building our injector program (in the sibling injector directory) and then for
|
||||
// each test we're running, invoke the injector program with the test's name as
|
||||
// an environment variable. Then the injector finds the test and passes it to
|
||||
// the lazygit startup code.
|
||||
|
||||
// If invoked directly, you can specify tests to run by passing their names as positional arguments
|
||||
|
||||
func main() {
|
||||
mode := integration.GetModeFromEnv()
|
||||
includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true"
|
||||
var testsToRun []*components.IntegrationTest
|
||||
|
||||
if len(os.Args) > 1 {
|
||||
outer:
|
||||
for _, testName := range os.Args[1:] {
|
||||
// check if our given test name actually exists
|
||||
for _, test := range integration.Tests {
|
||||
if test.Name() == testName {
|
||||
testsToRun = append(testsToRun, test)
|
||||
continue outer
|
||||
}
|
||||
}
|
||||
log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/tests.go`?", testName)
|
||||
}
|
||||
} else {
|
||||
testsToRun = integration.Tests
|
||||
}
|
||||
|
||||
testNames := slices.Map(testsToRun, func(test *components.IntegrationTest) string {
|
||||
return test.Name()
|
||||
})
|
||||
|
||||
err := integration.RunTests(
|
||||
log.Printf,
|
||||
runCmdInTerminal,
|
||||
func(test *components.IntegrationTest, f func() error) {
|
||||
if !slices.Contains(testNames, test.Name()) {
|
||||
return
|
||||
}
|
||||
if err := f(); err != nil {
|
||||
log.Print(err.Error())
|
||||
}
|
||||
},
|
||||
mode,
|
||||
includeSkipped,
|
||||
)
|
||||
if err != nil {
|
||||
log.Print(err.Error())
|
||||
}
|
||||
}
|
||||
|
||||
func runCmdInTerminal(cmd *exec.Cmd) error {
|
||||
cmd.Stdout = os.Stdout
|
||||
cmd.Stdin = os.Stdin
|
||||
cmd.Stderr = os.Stderr
|
||||
|
||||
return cmd.Run()
|
||||
}
|
306
pkg/integration/cmd/tui/main.go
Normal file
306
pkg/integration/cmd/tui/main.go
Normal file
@ -0,0 +1,306 @@
|
||||
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"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
)
|
||||
|
||||
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
|
||||
|
||||
type App struct {
|
||||
tests []*components.IntegrationTest
|
||||
itemIdx int
|
||||
testDir string
|
||||
filtering bool
|
||||
g *gocui.Gui
|
||||
}
|
||||
|
||||
func (app *App) getCurrentTest() *components.IntegrationTest {
|
||||
if len(app.tests) > 0 {
|
||||
return app.tests[app.itemIdx]
|
||||
}
|
||||
return nil
|
||||
}
|
||||
|
||||
func (app *App) loadTests() {
|
||||
app.tests = integration.Tests
|
||||
if app.itemIdx > len(app.tests)-1 {
|
||||
app.itemIdx = len(app.tests) - 1
|
||||
}
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootDir := integration.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", '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/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/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 KEY_PRESS_DELAY=200 go run pkg/integration/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 pkg/integration/tests/%s", currentTest.Name()))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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 test/integration_new/%s", currentTest.Name()))
|
||||
if err := cmd.Run(); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
if err := g.SetKeybinding("list", '/', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
||||
app.filtering = true
|
||||
if _, err := g.SetCurrentView("editor"); err != nil {
|
||||
return err
|
||||
}
|
||||
editorView, err := g.View("editor")
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
editorView.Clear()
|
||||
|
||||
return nil
|
||||
}); err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
|
||||
// not using the editor yet, but will use it to help filter the list
|
||||
if err := g.SetKeybinding("editor", gocui.KeyEsc, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
||||
app.filtering = 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.filtering {
|
||||
editorViewHeight = 0
|
||||
} else {
|
||||
descriptionViewHeight = 0
|
||||
keybindingsViewHeight = 0
|
||||
}
|
||||
g.Cursor = app.filtering
|
||||
g.FgColor = gocui.ColorGreen
|
||||
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
listView.Highlight = true
|
||||
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 err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
descriptionView.Title = "Test description"
|
||||
descriptionView.Wrap = true
|
||||
descriptionView.FgColor = gocui.ColorDefault
|
||||
}
|
||||
|
||||
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
keybindingsView.Title = "Keybindings"
|
||||
keybindingsView.Wrap = true
|
||||
keybindingsView.FgColor = gocui.ColorDefault
|
||||
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
|
||||
}
|
||||
|
||||
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
|
||||
if err != nil {
|
||||
if err.Error() != "unknown view" {
|
||||
return err
|
||||
}
|
||||
editorView.Title = "Filter"
|
||||
editorView.FgColor = gocui.ColorDefault
|
||||
editorView.Editable = true
|
||||
}
|
||||
|
||||
currentTest := app.getCurrentTest()
|
||||
if currentTest == nil {
|
||||
return nil
|
||||
}
|
||||
|
||||
descriptionView.Clear()
|
||||
fmt.Fprint(descriptionView, 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
|
||||
}
|
111
pkg/integration/components/assert.go
Normal file
111
pkg/integration/components/assert.go
Normal file
@ -0,0 +1,111 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
// through this struct we assert on the state of the lazygit gui
|
||||
|
||||
type Assert struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
}
|
||||
|
||||
func NewAssert(gui integrationTypes.GuiDriver) *Assert {
|
||||
return &Assert{gui: gui}
|
||||
}
|
||||
|
||||
func (self *Assert) WorkingTreeFileCount(expectedCount int) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Files)
|
||||
|
||||
return actualCount == expectedCount, fmt.Sprintf(
|
||||
"Expected %d changed working tree files, but got %d",
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) CommitCount(expectedCount int) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actualCount := len(self.gui.Model().Commits)
|
||||
|
||||
return actualCount == expectedCount, fmt.Sprintf(
|
||||
"Expected %d commits present, but got %d",
|
||||
expectedCount, actualCount,
|
||||
)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) HeadCommitMessage(expectedMessage string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
if len(self.gui.Model().Commits) == 0 {
|
||||
return false, "Expected at least one commit to be present"
|
||||
}
|
||||
|
||||
headCommit := self.gui.Model().Commits[0]
|
||||
if headCommit.Name != expectedMessage {
|
||||
return false, fmt.Sprintf(
|
||||
"Expected commit message to be '%s', but got '%s'",
|
||||
expectedMessage, headCommit.Name,
|
||||
)
|
||||
}
|
||||
|
||||
return true, ""
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) CurrentViewName(expectedViewName string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actual := self.gui.CurrentContext().GetView().Name()
|
||||
return actual == expectedViewName, fmt.Sprintf("Expected current view name to be '%s', but got '%s'", expectedViewName, actual)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) CurrentBranchName(expectedViewName string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
actual := self.gui.CheckedOutRef().Name
|
||||
return actual == expectedViewName, fmt.Sprintf("Expected current branch name to be '%s', but got '%s'", expectedViewName, actual)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) InListContext() {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
currentContext := self.gui.CurrentContext()
|
||||
_, ok := currentContext.(types.IListContext)
|
||||
return ok, fmt.Sprintf("Expected current context to be a list context, but got %s", currentContext.GetKey())
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) SelectedLineContains(text string) {
|
||||
self.assertWithRetries(func() (bool, string) {
|
||||
line := self.gui.CurrentContext().GetView().SelectedLine()
|
||||
return strings.Contains(line, text), fmt.Sprintf("Expected selected line to contain '%s', but got '%s'", text, line)
|
||||
})
|
||||
}
|
||||
|
||||
func (self *Assert) assertWithRetries(test func() (bool, string)) {
|
||||
waitTimes := []int{0, 1, 5, 10, 200, 500, 1000}
|
||||
|
||||
var message string
|
||||
for _, waitTime := range waitTimes {
|
||||
time.Sleep(time.Duration(waitTime) * time.Millisecond)
|
||||
|
||||
var ok bool
|
||||
ok, message = test()
|
||||
if ok {
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.Fail(message)
|
||||
}
|
||||
|
||||
// for when you just want to fail the test yourself
|
||||
func (self *Assert) Fail(message string) {
|
||||
self.gui.Fail(message)
|
||||
}
|
166
pkg/integration/components/input.go
Normal file
166
pkg/integration/components/input.go
Normal file
@ -0,0 +1,166 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
)
|
||||
|
||||
type Input struct {
|
||||
gui integrationTypes.GuiDriver
|
||||
keys config.KeybindingConfig
|
||||
assert *Assert
|
||||
pushKeyDelay int
|
||||
}
|
||||
|
||||
func NewInput(gui integrationTypes.GuiDriver, keys config.KeybindingConfig, assert *Assert, pushKeyDelay int) *Input {
|
||||
return &Input{
|
||||
gui: gui,
|
||||
keys: keys,
|
||||
assert: assert,
|
||||
pushKeyDelay: pushKeyDelay,
|
||||
}
|
||||
}
|
||||
|
||||
// key is something like 'w' or '<space>'. It's best not to pass a direct value,
|
||||
// but instead to go through the default user config to get a more meaningful key name
|
||||
func (self *Input) PressKeys(keyStrs ...string) {
|
||||
for _, keyStr := range keyStrs {
|
||||
self.pressKey(keyStr)
|
||||
}
|
||||
}
|
||||
|
||||
func (self *Input) pressKey(keyStr string) {
|
||||
self.Wait(self.pushKeyDelay)
|
||||
|
||||
self.gui.PressKey(keyStr)
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToStatusWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[0])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToFilesWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[1])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToBranchesWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[2])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToCommitsWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[3])
|
||||
}
|
||||
|
||||
func (self *Input) SwitchToStashWindow() {
|
||||
self.pressKey(self.keys.Universal.JumpToBlock[4])
|
||||
}
|
||||
|
||||
func (self *Input) Type(content string) {
|
||||
for _, char := range content {
|
||||
self.pressKey(string(char))
|
||||
}
|
||||
}
|
||||
|
||||
// i.e. pressing enter
|
||||
func (self *Input) Confirm() {
|
||||
self.pressKey(self.keys.Universal.Confirm)
|
||||
}
|
||||
|
||||
// i.e. pressing escape
|
||||
func (self *Input) Cancel() {
|
||||
self.pressKey(self.keys.Universal.Return)
|
||||
}
|
||||
|
||||
// i.e. pressing space
|
||||
func (self *Input) Select() {
|
||||
self.pressKey(self.keys.Universal.Select)
|
||||
}
|
||||
|
||||
// i.e. pressing down arrow
|
||||
func (self *Input) NextItem() {
|
||||
self.pressKey(self.keys.Universal.NextItem)
|
||||
}
|
||||
|
||||
// i.e. pressing up arrow
|
||||
func (self *Input) PreviousItem() {
|
||||
self.pressKey(self.keys.Universal.PrevItem)
|
||||
}
|
||||
|
||||
func (self *Input) ContinueMerge() {
|
||||
self.PressKeys(self.keys.Universal.CreateRebaseOptionsMenu)
|
||||
self.assert.SelectedLineContains("continue")
|
||||
self.Confirm()
|
||||
}
|
||||
|
||||
func (self *Input) ContinueRebase() {
|
||||
self.ContinueMerge()
|
||||
}
|
||||
|
||||
// for when you want to allow lazygit to process something before continuing
|
||||
func (self *Input) Wait(milliseconds int) {
|
||||
time.Sleep(time.Duration(milliseconds) * time.Millisecond)
|
||||
}
|
||||
|
||||
func (self *Input) LogUI(message string) {
|
||||
self.gui.LogUI(message)
|
||||
}
|
||||
|
||||
func (self *Input) Log(message string) {
|
||||
self.gui.LogUI(message)
|
||||
}
|
||||
|
||||
// this will look for a list item in the current panel and if it finds it, it will
|
||||
// enter the keypresses required to navigate to it.
|
||||
// The test will fail if:
|
||||
// - the user is not in a list item
|
||||
// - no list item is found containing the given text
|
||||
// - multiple list items are found containing the given text in the initial page of items
|
||||
//
|
||||
// NOTE: this currently assumes that ViewBufferLines returns all the lines that can be accessed.
|
||||
// If this changes in future, we'll need to update this code to first attempt to find the item
|
||||
// in the current page and failing that, jump to the top of the view and iterate through all of it,
|
||||
// looking for the item.
|
||||
func (self *Input) NavigateToListItemContainingText(text string) {
|
||||
self.assert.InListContext()
|
||||
|
||||
currentContext := self.gui.CurrentContext().(types.IListContext)
|
||||
|
||||
view := currentContext.GetView()
|
||||
|
||||
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
|
||||
matchCount := 0
|
||||
matchIndex := -1
|
||||
for i, line := range view.ViewBufferLines() {
|
||||
if strings.Contains(line, text) {
|
||||
matchCount++
|
||||
matchIndex = i
|
||||
}
|
||||
}
|
||||
if matchCount > 1 {
|
||||
self.assert.Fail(fmt.Sprintf("Found %d matches for %s, expected only a single match", matchCount, text))
|
||||
}
|
||||
if matchCount == 1 {
|
||||
selectedLineIdx := view.SelectedLineIdx()
|
||||
if selectedLineIdx == matchIndex {
|
||||
return
|
||||
}
|
||||
if selectedLineIdx < matchIndex {
|
||||
for i := selectedLineIdx; i < matchIndex; i++ {
|
||||
self.NextItem()
|
||||
}
|
||||
return
|
||||
} else {
|
||||
for i := selectedLineIdx; i > matchIndex; i-- {
|
||||
self.PreviousItem()
|
||||
}
|
||||
return
|
||||
}
|
||||
}
|
||||
|
||||
self.assert.Fail(fmt.Sprintf("Could not find item containing text: %s", text))
|
||||
}
|
83
pkg/integration/components/shell.go
Normal file
83
pkg/integration/components/shell.go
Normal file
@ -0,0 +1,83 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
"github.com/mgutz/str"
|
||||
)
|
||||
|
||||
// this is for running shell commands, mostly for the sake of setting up the repo
|
||||
// but you can also run the commands from within lazygit to emulate things happening
|
||||
// in the background.
|
||||
type Shell struct{}
|
||||
|
||||
func NewShell() *Shell {
|
||||
return &Shell{}
|
||||
}
|
||||
|
||||
func (s *Shell) RunCommand(cmdStr string) *Shell {
|
||||
args := str.ToArgv(cmdStr)
|
||||
cmd := secureexec.Command(args[0], args[1:]...)
|
||||
cmd.Env = os.Environ()
|
||||
|
||||
output, err := cmd.CombinedOutput()
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error running command: %s\n%s", cmdStr, string(output)))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Shell) CreateFile(path string, content string) *Shell {
|
||||
err := ioutil.WriteFile(path, []byte(content), 0o644)
|
||||
if err != nil {
|
||||
panic(fmt.Sprintf("error creating file: %s\n%s", path, err))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
||||
|
||||
func (s *Shell) NewBranch(name string) *Shell {
|
||||
return s.RunCommand("git checkout -b " + name)
|
||||
}
|
||||
|
||||
func (s *Shell) GitAdd(path string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git add \"%s\"", path))
|
||||
}
|
||||
|
||||
func (s *Shell) GitAddAll() *Shell {
|
||||
return s.RunCommand("git add -A")
|
||||
}
|
||||
|
||||
func (s *Shell) Commit(message string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git commit -m \"%s\"", message))
|
||||
}
|
||||
|
||||
func (s *Shell) EmptyCommit(message string) *Shell {
|
||||
return s.RunCommand(fmt.Sprintf("git commit --allow-empty -m \"%s\"", message))
|
||||
}
|
||||
|
||||
// convenience method for creating a file and adding it
|
||||
func (s *Shell) CreateFileAndAdd(fileName string, fileContents string) *Shell {
|
||||
return s.
|
||||
CreateFile(fileName, fileContents).
|
||||
GitAdd(fileName)
|
||||
}
|
||||
|
||||
// creates commits 01, 02, 03, ..., n with a new file in each
|
||||
// The reason for padding with zeroes is so that it's easier to do string
|
||||
// matches on the commit messages when there are many of them
|
||||
func (s *Shell) CreateNCommits(n int) *Shell {
|
||||
for i := 1; i <= n; i++ {
|
||||
s.CreateFileAndAdd(
|
||||
fmt.Sprintf("file%02d.txt", i),
|
||||
fmt.Sprintf("file%02d content", i),
|
||||
).
|
||||
Commit(fmt.Sprintf("commit %02d", i))
|
||||
}
|
||||
|
||||
return s
|
||||
}
|
129
pkg/integration/components/test.go
Normal file
129
pkg/integration/components/test.go
Normal file
@ -0,0 +1,129 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"os"
|
||||
"strconv"
|
||||
"strings"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/jesseduffield/lazygit/pkg/utils"
|
||||
)
|
||||
|
||||
// Test describes an integration tests that will be run against the lazygit gui.
|
||||
|
||||
// our unit tests will use this description to avoid a panic caused by attempting
|
||||
// to get the test's name via it's file's path.
|
||||
const unitTestDescription = "test test"
|
||||
|
||||
type IntegrationTest struct {
|
||||
name string
|
||||
description string
|
||||
extraCmdArgs string
|
||||
skip bool
|
||||
setupRepo func(shell *Shell)
|
||||
setupConfig func(config *config.AppConfig)
|
||||
run func(
|
||||
shell *Shell,
|
||||
input *Input,
|
||||
assert *Assert,
|
||||
keys config.KeybindingConfig,
|
||||
)
|
||||
}
|
||||
|
||||
var _ integrationTypes.IntegrationTest = &IntegrationTest{}
|
||||
|
||||
type NewIntegrationTestArgs struct {
|
||||
// Briefly describes what happens in the test and what it's testing for
|
||||
Description string
|
||||
// prepares a repo for testing
|
||||
SetupRepo func(shell *Shell)
|
||||
// takes a config and mutates. The mutated context will end up being passed to the gui
|
||||
SetupConfig func(config *config.AppConfig)
|
||||
// runs the test
|
||||
Run func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig)
|
||||
// additional args passed to lazygit
|
||||
ExtraCmdArgs string
|
||||
// for when a test is flakey
|
||||
Skip bool
|
||||
}
|
||||
|
||||
func NewIntegrationTest(args NewIntegrationTestArgs) *IntegrationTest {
|
||||
name := ""
|
||||
if args.Description != unitTestDescription {
|
||||
// this panics if we're in a unit test for our integration tests,
|
||||
// so we're using "test test" as a sentinel value
|
||||
name = testNameFromFilePath()
|
||||
}
|
||||
|
||||
return &IntegrationTest{
|
||||
name: name,
|
||||
description: args.Description,
|
||||
extraCmdArgs: args.ExtraCmdArgs,
|
||||
skip: args.Skip,
|
||||
setupRepo: args.SetupRepo,
|
||||
setupConfig: args.SetupConfig,
|
||||
run: args.Run,
|
||||
}
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Name() string {
|
||||
return self.name
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Description() string {
|
||||
return self.description
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) ExtraCmdArgs() string {
|
||||
return self.extraCmdArgs
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) Skip() bool {
|
||||
return self.skip
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) SetupConfig(config *config.AppConfig) {
|
||||
self.setupConfig(config)
|
||||
}
|
||||
|
||||
func (self *IntegrationTest) SetupRepo(shell *Shell) {
|
||||
self.setupRepo(shell)
|
||||
}
|
||||
|
||||
// I want access to all contexts, the model, the ability to press a key, the ability to log,
|
||||
func (self *IntegrationTest) Run(gui integrationTypes.GuiDriver) {
|
||||
shell := NewShell()
|
||||
assert := NewAssert(gui)
|
||||
keys := gui.Keys()
|
||||
input := NewInput(gui, keys, assert, KeyPressDelay())
|
||||
|
||||
self.run(shell, input, assert, keys)
|
||||
|
||||
if KeyPressDelay() > 0 {
|
||||
// the dev would want to see the final state if they're running in slow mode
|
||||
input.Wait(2000)
|
||||
}
|
||||
}
|
||||
|
||||
func testNameFromFilePath() string {
|
||||
path := utils.FilePath(3)
|
||||
name := strings.Split(path, "integration/tests/")[1]
|
||||
|
||||
return name[:len(name)-len(".go")]
|
||||
}
|
||||
|
||||
// this is the delay in milliseconds between keypresses
|
||||
// defaults to zero
|
||||
func KeyPressDelay() int {
|
||||
delayStr := os.Getenv("KEY_PRESS_DELAY")
|
||||
if delayStr == "" {
|
||||
return 0
|
||||
}
|
||||
|
||||
delay, err := strconv.Atoi(delayStr)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
return delay
|
||||
}
|
105
pkg/integration/components/test_test.go
Normal file
105
pkg/integration/components/test_test.go
Normal file
@ -0,0 +1,105 @@
|
||||
package components
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
type fakeGuiDriver struct {
|
||||
failureMessage string
|
||||
pressedKeys []string
|
||||
}
|
||||
|
||||
var _ integrationTypes.GuiDriver = &fakeGuiDriver{}
|
||||
|
||||
type GuiDriver interface {
|
||||
PressKey(string)
|
||||
Keys() config.KeybindingConfig
|
||||
CurrentContext() types.Context
|
||||
Model() *types.Model
|
||||
Fail(message string)
|
||||
// These two log methods are for the sake of debugging while testing. There's no need to actually
|
||||
// commit any logging.
|
||||
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
|
||||
Log(message string)
|
||||
// logs in the actual UI (in the commands panel)
|
||||
LogUI(message string)
|
||||
CheckedOutRef() *models.Branch
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) PressKey(key string) {
|
||||
self.pressedKeys = append(self.pressedKeys, key)
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Keys() config.KeybindingConfig {
|
||||
return config.KeybindingConfig{}
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) CurrentContext() types.Context {
|
||||
return nil
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Model() *types.Model {
|
||||
return &types.Model{Commits: []*models.Commit{}}
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Fail(message string) {
|
||||
self.failureMessage = message
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) Log(message string) {
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) LogUI(message string) {
|
||||
}
|
||||
|
||||
func (self *fakeGuiDriver) CheckedOutRef() *models.Branch {
|
||||
return nil
|
||||
}
|
||||
|
||||
func TestAssertionFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.PressKeys("a")
|
||||
input.PressKeys("b")
|
||||
assert.CommitCount(2)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys)
|
||||
assert.Equal(t, "Expected 2 commits present, but got 0", driver.failureMessage)
|
||||
}
|
||||
|
||||
func TestManualFailure(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
assert.Fail("blah")
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.Equal(t, "blah", driver.failureMessage)
|
||||
}
|
||||
|
||||
func TestSuccess(t *testing.T) {
|
||||
test := NewIntegrationTest(NewIntegrationTestArgs{
|
||||
Description: unitTestDescription,
|
||||
Run: func(shell *Shell, input *Input, assert *Assert, keys config.KeybindingConfig) {
|
||||
input.PressKeys("a")
|
||||
input.PressKeys("b")
|
||||
assert.CommitCount(0)
|
||||
},
|
||||
})
|
||||
driver := &fakeGuiDriver{}
|
||||
test.Run(driver)
|
||||
assert.EqualValues(t, []string{"a", "b"}, driver.pressedKeys)
|
||||
assert.Equal(t, "", driver.failureMessage)
|
||||
}
|
@ -7,11 +7,13 @@ import (
|
||||
"os/exec"
|
||||
"testing"
|
||||
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// see docs/Integration_Tests.md
|
||||
// Deprecated: This file is part of the old way of doing things. See pkg/integration/cmd/runner/main.go for the new way
|
||||
|
||||
// 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.
|
||||
//
|
||||
@ -20,15 +22,15 @@ import (
|
||||
// as an env var.
|
||||
|
||||
func main() {
|
||||
mode := integration.GetModeFromEnv()
|
||||
mode := deprecated.GetModeFromEnv()
|
||||
speedEnv := os.Getenv("SPEED")
|
||||
includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true"
|
||||
selectedTestName := os.Args[1]
|
||||
|
||||
err := integration.RunTests(
|
||||
err := deprecated.RunTests(
|
||||
log.Printf,
|
||||
runCmdInTerminal,
|
||||
func(test *integration.Test, f func(*testing.T) error) {
|
||||
func(test *deprecated.IntegrationTest, f func(*testing.T) error) {
|
||||
if selectedTestName != "" && test.Name != selectedTestName {
|
||||
return
|
||||
}
|
@ -10,21 +10,23 @@ import (
|
||||
"github.com/jesseduffield/gocui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/deprecated"
|
||||
"github.com/jesseduffield/lazygit/pkg/secureexec"
|
||||
)
|
||||
|
||||
// this program lets you manage integration tests in a TUI. See docs/Integration_Tests.md for more info.
|
||||
// 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 []*integration.Test
|
||||
tests []*deprecated.IntegrationTest
|
||||
itemIdx int
|
||||
testDir string
|
||||
editing bool
|
||||
g *gocui.Gui
|
||||
}
|
||||
|
||||
func (app *App) getCurrentTest() *integration.Test {
|
||||
func (app *App) getCurrentTest() *deprecated.IntegrationTest {
|
||||
if len(app.tests) > 0 {
|
||||
return app.tests[app.itemIdx]
|
||||
}
|
||||
@ -49,7 +51,7 @@ func (app *App) refreshTests() {
|
||||
}
|
||||
|
||||
func (app *App) loadTests() {
|
||||
tests, err := integration.LoadTests(app.testDir)
|
||||
tests, err := deprecated.LoadTests(app.testDir)
|
||||
if err != nil {
|
||||
log.Panicln(err)
|
||||
}
|
||||
@ -61,7 +63,7 @@ func (app *App) loadTests() {
|
||||
}
|
||||
|
||||
func main() {
|
||||
rootDir := integration.GetRootDirectory()
|
||||
rootDir := deprecated.GetRootDirectory()
|
||||
testDir := filepath.Join(rootDir, "test", "integration")
|
||||
|
||||
app := &App{testDir: testDir}
|
||||
@ -106,7 +108,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=record go run test/runner/main.go %s", currentTest.Name))
|
||||
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
|
||||
@ -120,7 +122,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=sandbox go run test/runner/main.go %s", currentTest.Name))
|
||||
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
|
||||
@ -134,7 +136,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true go run test/runner/main.go %s", currentTest.Name))
|
||||
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
|
||||
@ -148,7 +150,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true MODE=updateSnapshot go run test/runner/main.go %s", currentTest.Name))
|
||||
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
|
||||
@ -162,7 +164,7 @@ func main() {
|
||||
return nil
|
||||
}
|
||||
|
||||
cmd := secureexec.Command("sh", "-c", fmt.Sprintf("INCLUDE_SKIPPED=true SPEED=1 go run test/runner/main.go %s", currentTest.Name))
|
||||
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
|
@ -1,7 +1,7 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package gui
|
||||
package deprecated
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
@ -13,10 +13,11 @@ import (
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration"
|
||||
"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
|
||||
@ -25,10 +26,10 @@ import (
|
||||
// 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/gui_test.go -run /commit
|
||||
// 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/gui_test.go -run /commit
|
||||
// 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
|
||||
@ -44,7 +45,7 @@ func Test(t *testing.T) {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
mode := integration.GetModeFromEnv()
|
||||
mode := GetModeFromEnv()
|
||||
speedEnv := os.Getenv("SPEED")
|
||||
includeSkipped := os.Getenv("INCLUDE_SKIPPED") != ""
|
||||
|
||||
@ -52,10 +53,10 @@ func Test(t *testing.T) {
|
||||
parallelIndex := tryConvert(os.Getenv("PARALLEL_INDEX"), 0)
|
||||
testNumber := 0
|
||||
|
||||
err := integration.RunTests(
|
||||
err := RunTests(
|
||||
t.Logf,
|
||||
runCmdHeadless,
|
||||
func(test *integration.Test, f func(*testing.T) error) {
|
||||
func(test *IntegrationTest, f func(*testing.T) error) {
|
||||
defer func() { testNumber += 1 }()
|
||||
if testNumber%parallelTotal != parallelIndex {
|
||||
return
|
||||
@ -78,6 +79,15 @@ func Test(t *testing.T) {
|
||||
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,
|
||||
@ -94,12 +104,3 @@ func runCmdHeadless(cmd *exec.Cmd) error {
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func tryConvert(numStr string, defaultVal int) int {
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
564
pkg/integration/deprecated/integration.go
Normal file
564
pkg/integration/deprecated/integration.go
Normal file
@ -0,0 +1,564 @@
|
||||
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 := ioutil.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, 1)
|
||||
|
||||
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 := ioutil.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 := ioutil.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
|
||||
}
|
79
pkg/integration/go_test.go
Normal file
79
pkg/integration/go_test.go
Normal file
@ -0,0 +1,79 @@
|
||||
//go:build !windows
|
||||
// +build !windows
|
||||
|
||||
package integration
|
||||
|
||||
// this is the new way of running tests. See pkg/integration/integration_tests/commit.go
|
||||
// for an example
|
||||
|
||||
import (
|
||||
"io"
|
||||
"io/ioutil"
|
||||
"os"
|
||||
"os/exec"
|
||||
"strconv"
|
||||
"testing"
|
||||
|
||||
"github.com/creack/pty"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
func TestIntegration(t *testing.T) {
|
||||
if testing.Short() {
|
||||
t.Skip("Skipping integration tests in short mode")
|
||||
}
|
||||
|
||||
mode := GetModeFromEnv()
|
||||
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 *components.IntegrationTest, f func() error) {
|
||||
defer func() { testNumber += 1 }()
|
||||
if testNumber%parallelTotal != parallelIndex {
|
||||
return
|
||||
}
|
||||
|
||||
t.Run(test.Name(), func(t *testing.T) {
|
||||
err := f()
|
||||
assert.NoError(t, err)
|
||||
})
|
||||
},
|
||||
mode,
|
||||
includeSkipped,
|
||||
)
|
||||
|
||||
assert.NoError(t, err)
|
||||
}
|
||||
|
||||
func runCmdHeadless(cmd *exec.Cmd) error {
|
||||
cmd.Env = append(
|
||||
cmd.Env,
|
||||
"HEADLESS=true",
|
||||
"TERM=xterm",
|
||||
)
|
||||
|
||||
f, err := pty.StartWithSize(cmd, &pty.Winsize{Rows: 100, Cols: 100})
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
_, _ = io.Copy(ioutil.Discard, f)
|
||||
|
||||
return f.Close()
|
||||
}
|
||||
|
||||
func tryConvert(numStr string, defaultVal int) int {
|
||||
num, err := strconv.Atoi(numStr)
|
||||
if err != nil {
|
||||
return defaultVal
|
||||
}
|
||||
|
||||
return num
|
||||
}
|
@ -1,7 +1,6 @@
|
||||
package integration
|
||||
|
||||
import (
|
||||
"encoding/json"
|
||||
"errors"
|
||||
"fmt"
|
||||
"io/ioutil"
|
||||
@ -9,65 +8,47 @@ import (
|
||||
"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"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// This package is for running our integration test suite. See docs/Integration_Tests.md for more info
|
||||
// this is the integration runner for the new and improved integration interface
|
||||
|
||||
type Test struct {
|
||||
Name string `json:"name"`
|
||||
Speed float64 `json:"speed"`
|
||||
Description string `json:"description"`
|
||||
ExtraCmdArgs string `json:"extraCmdArgs"`
|
||||
Skip bool `json:"skip"`
|
||||
}
|
||||
var Tests = tests.Tests
|
||||
|
||||
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
|
||||
// Default: if a snapshot test fails, the we'll be asked whether we want to update it
|
||||
ASK_TO_UPDATE_SNAPSHOT = iota
|
||||
// fails the test if the snapshots don't match
|
||||
CHECK_SNAPSHOT
|
||||
// runs the test and updates the snapshot
|
||||
UPDATE_SNAPSHOT
|
||||
// This just makes use of the setup step of the test to get you into
|
||||
// a lazygit session. Then you'll be able to do whatever you want. Useful
|
||||
// when you want to test certain things without needing to manually set
|
||||
// up the situation yourself.
|
||||
// fails the test if the snapshots don't match
|
||||
SANDBOX
|
||||
)
|
||||
|
||||
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")
|
||||
}
|
||||
}
|
||||
const LAZYGIT_TEST_NAME_ENV_VAR = "LAZYGIT_TEST_NAME"
|
||||
|
||||
type (
|
||||
logf func(format string, formatArgs ...interface{})
|
||||
)
|
||||
|
||||
// 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{}),
|
||||
logf logf,
|
||||
runCmd func(cmd *exec.Cmd) error,
|
||||
fnWrapper func(test *Test, f func(*testing.T) error),
|
||||
fnWrapper func(test *components.IntegrationTest, f func() error),
|
||||
mode Mode,
|
||||
speedEnv string,
|
||||
onFail func(t *testing.T, expected string, actual string, prefix string),
|
||||
includeSkipped bool,
|
||||
) error {
|
||||
rootDir := GetRootDirectory()
|
||||
@ -76,121 +57,88 @@ func RunTests(
|
||||
return err
|
||||
}
|
||||
|
||||
testDir := filepath.Join(rootDir, "test", "integration")
|
||||
testDir := filepath.Join(rootDir, "test", "integration_new")
|
||||
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
err = osCommand.Cmd.New("go build -o " + tempLazygitPath()).Run()
|
||||
err = osCommand.Cmd.New(fmt.Sprintf("go build -o %s pkg/integration/cmd/injector/main.go", tempLazygitPath())).Run()
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
tests, err := LoadTests(testDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
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)
|
||||
fnWrapper(test, func() 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)
|
||||
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(test, actualRepoDir, rootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
findOrCreateDir(testPath)
|
||||
prepareIntegrationTestDir(actualDir)
|
||||
findOrCreateDir(actualRepoDir)
|
||||
err := createFixture(testPath, actualRepoDir)
|
||||
if err != nil {
|
||||
configDir := filepath.Join(testPath, "used_config")
|
||||
|
||||
cmd, err := getLazygitCommand(test, testPath, rootDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = runCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
switch mode {
|
||||
case UPDATE_SNAPSHOT:
|
||||
if err := updateSnapshot(actualDir, expectedDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
configDir := filepath.Join(testPath, "used_config")
|
||||
|
||||
cmd, err := getLazygitCommand(testPath, rootDir, mode, speed, test.ExtraCmdArgs)
|
||||
if err != nil {
|
||||
logf("Test passed: %s", test.Name())
|
||||
case CHECK_SNAPSHOT:
|
||||
if err := compareSnapshots(logf, configDir, actualDir, expectedDir, test.Name()); err != nil {
|
||||
return err
|
||||
}
|
||||
logf("Test passed: %s", test.Name())
|
||||
case ASK_TO_UPDATE_SNAPSHOT:
|
||||
if _, err := os.Stat(expectedDir); os.IsNotExist(err) {
|
||||
if err := updateSnapshot(actualDir, expectedDir); err != nil {
|
||||
return err
|
||||
}
|
||||
logf("No existing snapshot found for %s. Created snapshot.", test.Name())
|
||||
|
||||
err = runCmd(cmd)
|
||||
if err != nil {
|
||||
return err
|
||||
return nil
|
||||
}
|
||||
|
||||
if mode == UPDATE_SNAPSHOT || mode == RECORD {
|
||||
// create/update snapshot
|
||||
err = oscommands.CopyDir(actualDir, expectedDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
if err := compareSnapshots(logf, configDir, actualDir, expectedDir, test.Name()); err != nil {
|
||||
logf("%s", 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 {
|
||||
// prompt user whether to update the snapshot (Y/N)
|
||||
if promptUserToUpdateSnapshot() {
|
||||
if err := updateSnapshot(actualDir, expectedDir); 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 := ioutil.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
|
||||
logf("Snapshot updated: %s", test.Name())
|
||||
} else {
|
||||
return err
|
||||
}
|
||||
}
|
||||
|
||||
logf("Test passed: %s", test.Name())
|
||||
case SANDBOX:
|
||||
logf("Session exited")
|
||||
}
|
||||
|
||||
return nil
|
||||
@ -200,48 +148,148 @@ func RunTests(
|
||||
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)
|
||||
func promptUserToUpdateSnapshot() bool {
|
||||
fmt.Println("Test failed. Update snapshot? (y/n)")
|
||||
var input string
|
||||
fmt.Scanln(&input)
|
||||
return input == "y"
|
||||
}
|
||||
|
||||
func updateSnapshot(actualDir string, expectedDir string) error {
|
||||
// create/update snapshot
|
||||
err := oscommands.CopyDir(actualDir, expectedDir)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
var actualFiles []os.FileInfo
|
||||
actualFiles, err = ioutil.ReadDir(actualDir)
|
||||
if err := renameSpecialPaths(expectedDir); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
return err
|
||||
}
|
||||
|
||||
func compareSnapshots(logf logf, configDir string, actualDir string, expectedDir string, testName string) error {
|
||||
// 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.
|
||||
expectedDirCopy := filepath.Join(os.TempDir(), "expected_dir_test", testName)
|
||||
err := oscommands.CopyDir(expectedDir, expectedDirCopy)
|
||||
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)
|
||||
defer func() {
|
||||
err := os.RemoveAll(expectedDirCopy)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
}()
|
||||
|
||||
if err := restoreSpecialPaths(expectedDirCopy); err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
err = validateSameRepos(expectedDirCopy, actualDir)
|
||||
if 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(expectedDirCopy)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
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(expectedDirCopy, f.Name())
|
||||
|
||||
actualRepo, expectedRepo, err := generateSnapshots(actualRepoPath, expectedRepoPath)
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
|
||||
if expectedRepo != actualRepo {
|
||||
// get the log file and print it
|
||||
bytes, err := ioutil.ReadFile(filepath.Join(configDir, "development.log"))
|
||||
if err != nil {
|
||||
return err
|
||||
}
|
||||
logf("%s", string(bytes))
|
||||
|
||||
return errors.New(getDiff(f.Name(), actualRepo, expectedRepo))
|
||||
}
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFileName(f os.FileInfo) string {
|
||||
return f.Name()
|
||||
func createFixture(test *components.IntegrationTest, actualDir string, rootDir string) error {
|
||||
if err := os.Chdir(actualDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
shell := components.NewShell()
|
||||
shell.RunCommand("git init")
|
||||
shell.RunCommand(`git config user.email "CI@example.com"`)
|
||||
shell.RunCommand(`git config user.name "CI"`)
|
||||
|
||||
test.SetupRepo(shell)
|
||||
|
||||
// changing directory back to rootDir after the setup is done
|
||||
if err := os.Chdir(rootDir); err != nil {
|
||||
panic(err)
|
||||
}
|
||||
|
||||
return nil
|
||||
}
|
||||
|
||||
func prepareIntegrationTestDir(actualDir string) {
|
||||
// remove contents of integration test directory
|
||||
dir, err := ioutil.ReadDir(actualDir)
|
||||
func getLazygitCommand(test *components.IntegrationTest, testPath string, rootDir string) (*exec.Cmd, error) {
|
||||
osCommand := oscommands.NewDummyOSCommand()
|
||||
|
||||
templateConfigDir := filepath.Join(rootDir, "test", "default_test_config")
|
||||
actualRepoDir := filepath.Join(testPath, "actual", "repo")
|
||||
|
||||
configDir := filepath.Join(testPath, "used_config")
|
||||
|
||||
err := os.RemoveAll(configDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.Mkdir(actualDir, 0o777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
return nil, err
|
||||
}
|
||||
for _, d := range dir {
|
||||
os.RemoveAll(filepath.Join(actualDir, d.Name()))
|
||||
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, test.ExtraCmdArgs())
|
||||
|
||||
cmdObj := osCommand.Cmd.New(cmdStr)
|
||||
|
||||
cmdObj.AddEnvVars(fmt.Sprintf("%s=%s", LAZYGIT_TEST_NAME_ENV_VAR, test.Name()))
|
||||
|
||||
return cmdObj.GetCmd(), nil
|
||||
}
|
||||
|
||||
func GetModeFromEnv() Mode {
|
||||
switch os.Getenv("MODE") {
|
||||
case "", "ask":
|
||||
return ASK_TO_UPDATE_SNAPSHOT
|
||||
case "check":
|
||||
return CHECK_SNAPSHOT
|
||||
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")
|
||||
}
|
||||
}
|
||||
|
||||
@ -270,90 +318,22 @@ func GetRootDirectory() string {
|
||||
}
|
||||
}
|
||||
|
||||
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, 1)
|
||||
|
||||
return speeds
|
||||
}
|
||||
|
||||
func LoadTests(testDir string) ([]*Test, error) {
|
||||
paths, err := filepath.Glob(filepath.Join(testDir, "/*/test.json"))
|
||||
func generateSnapshots(actualDir string, expectedDir string) (string, string, error) {
|
||||
actual, err := generateSnapshot(actualDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
tests := make([]*Test, len(paths))
|
||||
|
||||
for i, path := range paths {
|
||||
data, err := ioutil.ReadFile(path)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
|
||||
test := &Test{}
|
||||
|
||||
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)
|
||||
expected, err := generateSnapshot(expectedDir)
|
||||
if err != nil {
|
||||
if os.IsNotExist(err) {
|
||||
err = os.MkdirAll(path, 0o777)
|
||||
if err != nil {
|
||||
panic(err)
|
||||
}
|
||||
} else {
|
||||
panic(err)
|
||||
}
|
||||
return "", "", err
|
||||
}
|
||||
|
||||
return actual, expected, nil
|
||||
}
|
||||
|
||||
// note that we don't actually store this snapshot in the lazygit repo.
|
||||
@ -425,41 +405,6 @@ func generateSnapshot(dir string) (string, error) {
|
||||
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{}
|
||||
|
||||
@ -519,44 +464,75 @@ func restoreSpecialPaths(dir string) error {
|
||||
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"))
|
||||
// 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 nil, err
|
||||
return err
|
||||
}
|
||||
|
||||
if exists {
|
||||
templateConfigDir = filepath.Join(testPath, "config")
|
||||
}
|
||||
|
||||
configDir := filepath.Join(testPath, "used_config")
|
||||
|
||||
err = os.RemoveAll(configDir)
|
||||
var actualFiles []os.FileInfo
|
||||
actualFiles, err = ioutil.ReadDir(actualDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
}
|
||||
err = oscommands.CopyDir(templateConfigDir, configDir)
|
||||
if err != nil {
|
||||
return nil, err
|
||||
return 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))
|
||||
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 cmdObj.GetCmd(), nil
|
||||
return nil
|
||||
}
|
||||
|
||||
func getFileName(f os.FileInfo) string {
|
||||
return f.Name()
|
||||
}
|
||||
|
||||
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)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
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 getDiff(prefix string, expected string, actual string) string {
|
||||
mockT := &MockTestingT{}
|
||||
assert.Equal(mockT, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual))
|
||||
return mockT.err
|
||||
}
|
||||
|
||||
type MockTestingT struct {
|
||||
err string
|
||||
}
|
||||
|
||||
func (self *MockTestingT) Errorf(format string, args ...interface{}) {
|
||||
self.err += fmt.Sprintf(format, args...)
|
||||
}
|
||||
|
41
pkg/integration/tests/branch/suggestions.go
Normal file
41
pkg/integration/tests/branch/suggestions.go
Normal file
@ -0,0 +1,41 @@
|
||||
package branch
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Suggestions = components.NewIntegrationTest(components.NewIntegrationTestArgs{
|
||||
Description: "Checking out a branch with name suggestions",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *components.Shell) {
|
||||
shell.
|
||||
EmptyCommit("my commit message").
|
||||
NewBranch("new-branch").
|
||||
NewBranch("new-branch-2").
|
||||
NewBranch("new-branch-3").
|
||||
NewBranch("branch-to-checkout").
|
||||
NewBranch("other-new-branch-2").
|
||||
NewBranch("other-new-branch-3")
|
||||
},
|
||||
Run: func(shell *components.Shell, input *components.Input, assert *components.Assert, keys config.KeybindingConfig) {
|
||||
input.SwitchToBranchesWindow()
|
||||
assert.CurrentViewName("localBranches")
|
||||
|
||||
input.PressKeys(keys.Branches.CheckoutBranchByName)
|
||||
assert.CurrentViewName("confirmation")
|
||||
|
||||
input.Type("branch-to")
|
||||
|
||||
input.PressKeys(keys.Universal.TogglePanel)
|
||||
assert.CurrentViewName("suggestions")
|
||||
|
||||
// we expect the first suggestion to be the branch we want because it most
|
||||
// closely matches what we typed in
|
||||
input.Confirm()
|
||||
|
||||
assert.CurrentBranchName("branch-to-checkout")
|
||||
},
|
||||
})
|
32
pkg/integration/tests/commit/commit.go
Normal file
32
pkg/integration/tests/commit/commit.go
Normal file
@ -0,0 +1,32 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var Commit = components.NewIntegrationTest(components.NewIntegrationTestArgs{
|
||||
Description: "Staging a couple files and committing",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *components.Shell) {
|
||||
shell.CreateFile("myfile", "myfile content")
|
||||
shell.CreateFile("myfile2", "myfile2 content")
|
||||
},
|
||||
Run: func(shell *components.Shell, input *components.Input, assert *components.Assert, keys config.KeybindingConfig) {
|
||||
assert.CommitCount(0)
|
||||
|
||||
input.Select()
|
||||
input.NextItem()
|
||||
input.Select()
|
||||
input.PressKeys(keys.Files.CommitChanges)
|
||||
|
||||
commitMessage := "my commit message"
|
||||
input.Type(commitMessage)
|
||||
input.Confirm()
|
||||
|
||||
assert.CommitCount(1)
|
||||
assert.HeadCommitMessage(commitMessage)
|
||||
},
|
||||
})
|
38
pkg/integration/tests/commit/new_branch.go
Normal file
38
pkg/integration/tests/commit/new_branch.go
Normal file
@ -0,0 +1,38 @@
|
||||
package commit
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var NewBranch = components.NewIntegrationTest(components.NewIntegrationTestArgs{
|
||||
Description: "Creating a new branch from a commit",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *components.Shell) {
|
||||
shell.
|
||||
EmptyCommit("commit 1").
|
||||
EmptyCommit("commit 2").
|
||||
EmptyCommit("commit 3")
|
||||
},
|
||||
Run: func(shell *components.Shell, input *components.Input, assert *components.Assert, keys config.KeybindingConfig) {
|
||||
assert.CommitCount(3)
|
||||
|
||||
input.SwitchToCommitsWindow()
|
||||
assert.CurrentViewName("commits")
|
||||
input.NextItem()
|
||||
|
||||
input.PressKeys(keys.Universal.New)
|
||||
|
||||
assert.CurrentViewName("confirmation")
|
||||
|
||||
branchName := "my-branch-name"
|
||||
input.Type(branchName)
|
||||
input.Confirm()
|
||||
|
||||
assert.CommitCount(2)
|
||||
assert.HeadCommitMessage("commit 2")
|
||||
assert.CurrentBranchName(branchName)
|
||||
},
|
||||
})
|
41
pkg/integration/tests/interactive_rebase/one.go
Normal file
41
pkg/integration/tests/interactive_rebase/one.go
Normal file
@ -0,0 +1,41 @@
|
||||
package interactive_rebase
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
)
|
||||
|
||||
var One = components.NewIntegrationTest(components.NewIntegrationTestArgs{
|
||||
Description: "Begins an interactive rebase, then fixups, drops, and squashes some commits",
|
||||
ExtraCmdArgs: "",
|
||||
Skip: false,
|
||||
SetupConfig: func(config *config.AppConfig) {},
|
||||
SetupRepo: func(shell *components.Shell) {
|
||||
shell.
|
||||
CreateNCommits(5) // these will appears at commit 05, 04, 04, down to 01
|
||||
},
|
||||
Run: func(shell *components.Shell, input *components.Input, assert *components.Assert, keys config.KeybindingConfig) {
|
||||
input.SwitchToCommitsWindow()
|
||||
assert.CurrentViewName("commits")
|
||||
|
||||
input.NavigateToListItemContainingText("commit 02")
|
||||
input.PressKeys(keys.Universal.Edit)
|
||||
assert.SelectedLineContains("YOU ARE HERE")
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Commits.MarkCommitAsFixup)
|
||||
assert.SelectedLineContains("fixup")
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Universal.Remove)
|
||||
assert.SelectedLineContains("drop")
|
||||
|
||||
input.PreviousItem()
|
||||
input.PressKeys(keys.Commits.SquashDown)
|
||||
assert.SelectedLineContains("squash")
|
||||
|
||||
input.ContinueRebase()
|
||||
|
||||
assert.CommitCount(2)
|
||||
},
|
||||
})
|
18
pkg/integration/tests/tests.go
Normal file
18
pkg/integration/tests/tests.go
Normal file
@ -0,0 +1,18 @@
|
||||
package tests
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/branch"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/commit"
|
||||
"github.com/jesseduffield/lazygit/pkg/integration/tests/interactive_rebase"
|
||||
)
|
||||
|
||||
// Here is where we lists the actual tests that will run. When you create a new test,
|
||||
// be sure to add it to this list.
|
||||
|
||||
var Tests = []*components.IntegrationTest{
|
||||
commit.Commit,
|
||||
commit.NewBranch,
|
||||
branch.Suggestions,
|
||||
interactive_rebase.One,
|
||||
}
|
31
pkg/integration/types/types.go
Normal file
31
pkg/integration/types/types.go
Normal file
@ -0,0 +1,31 @@
|
||||
package types
|
||||
|
||||
import (
|
||||
"github.com/jesseduffield/lazygit/pkg/commands/models"
|
||||
"github.com/jesseduffield/lazygit/pkg/config"
|
||||
"github.com/jesseduffield/lazygit/pkg/gui/types"
|
||||
)
|
||||
|
||||
// these interfaces are used by the gui package so that it knows what it needs
|
||||
// to provide to a test in order for the test to run.
|
||||
|
||||
type IntegrationTest interface {
|
||||
Run(GuiDriver)
|
||||
SetupConfig(config *config.AppConfig)
|
||||
}
|
||||
|
||||
// this is the interface through which our integration tests interact with the lazygit gui
|
||||
type GuiDriver interface {
|
||||
PressKey(string)
|
||||
Keys() config.KeybindingConfig
|
||||
CurrentContext() types.Context
|
||||
Model() *types.Model
|
||||
Fail(message string)
|
||||
// These two log methods are for the sake of debugging while testing. There's no need to actually
|
||||
// commit any logging.
|
||||
// logs to the normal place that you log to i.e. viewable with `lazygit --logs`
|
||||
Log(message string)
|
||||
// logs in the actual UI (in the commands panel)
|
||||
LogUI(message string)
|
||||
CheckedOutRef() *models.Branch
|
||||
}
|
@ -128,3 +128,10 @@ func StackTrace() string {
|
||||
n := runtime.Stack(buf, false)
|
||||
return fmt.Sprintf("%s\n", buf[:n])
|
||||
}
|
||||
|
||||
// returns the path of the file that calls the function.
|
||||
// 'skip' is the number of stack frames to skip.
|
||||
func FilePath(skip int) string {
|
||||
_, path, _, _ := runtime.Caller(skip)
|
||||
return path
|
||||
}
|
||||
|
@ -2,7 +2,7 @@
|
||||
|
||||
# How to use:
|
||||
# 1) find a commit that is working fine.
|
||||
# 2) Create an integration test capturing the fact that it works (Don't commit it). See https://github.com/jesseduffield/lazygit/blob/master/docs/Integration_Tests.md
|
||||
# 2) Create an integration test capturing the fact that it works (Don't commit it). See https://github.com/jesseduffield/lazygit/blob/master/pkg/integration/README.md
|
||||
# 3) checkout the commit that's known to be failing
|
||||
# 4) run this script supplying the commit sha / tag name that works and the name of the newly created test
|
||||
|
||||
|
@ -1 +0,0 @@
|
||||
file0
|
@ -1 +0,0 @@
|
||||
ref: refs/heads/new-branch-3
|
Binary file not shown.
@ -1,8 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 commit (initial): file0
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from master to new-branch
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch to new-branch-2
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch-2 to new-branch-3
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from new-branch-3 to old-branch
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from old-branch to old-branch-2
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 checkout: moving from old-branch-2 to old-branch-3
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675450 +1000 checkout: moving from old-branch-3 to new-branch-3
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 commit (initial): file0
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8 CI <CI@example.com> 1617675445 +1000 branch: Created from HEAD
|
Binary file not shown.
Binary file not shown.
@ -1,2 +0,0 @@
|
||||
xŤÍA
|
||||
Â0Fa×9Ĺ왉“Ä€�ĐUŹ‘4°Đ)<ľ=‚ŰÇoé�Dő4v€Wř�cŚ%‹>ř–«V¶ąTˇQŐšôŻľÓ4Ó}šźř¦öŢpYz{�x >8UGgafsÔc2đ'7uÝŔćÜď+ö
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
75e9e90a1d58c37d97d46a543dfbfd0f33fc52d8
|
@ -1 +0,0 @@
|
||||
test0
|
@ -1 +0,0 @@
|
||||
{"KeyEvents":[{"Timestamp":639,"Mod":0,"Key":259,"Ch":0},{"Timestamp":1752,"Mod":0,"Key":256,"Ch":99},{"Timestamp":2183,"Mod":0,"Key":256,"Ch":110},{"Timestamp":2271,"Mod":0,"Key":256,"Ch":101},{"Timestamp":2327,"Mod":0,"Key":256,"Ch":119},{"Timestamp":2599,"Mod":0,"Key":256,"Ch":45},{"Timestamp":3583,"Mod":0,"Key":9,"Ch":9},{"Timestamp":3880,"Mod":0,"Key":258,"Ch":0},{"Timestamp":4175,"Mod":0,"Key":13,"Ch":13},{"Timestamp":4815,"Mod":0,"Key":256,"Ch":113}],"ResizeEvents":[{"Timestamp":0,"Width":272,"Height":74}]}
|
@ -1,21 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd $1
|
||||
|
||||
git init
|
||||
|
||||
git config user.email "CI@example.com"
|
||||
git config user.name "CI"
|
||||
|
||||
echo test0 > file0
|
||||
git add .
|
||||
git commit -am file0
|
||||
|
||||
git checkout -b new-branch
|
||||
git checkout -b new-branch-2
|
||||
git checkout -b new-branch-3
|
||||
git checkout -b old-branch
|
||||
git checkout -b old-branch-2
|
||||
git checkout -b old-branch-3
|
@ -1 +0,0 @@
|
||||
{ "description": "Checking out a branch with name suggestions", "speed": 100 }
|
@ -1 +0,0 @@
|
||||
commit
|
Binary file not shown.
@ -1,5 +0,0 @@
|
||||
0000000000000000000000000000000000000000 3df3d8761bc0f0828596b11845aeac175b7b7393 CI <CI@example.com> 1617671339 +1000 commit (initial): myfile1
|
||||
3df3d8761bc0f0828596b11845aeac175b7b7393 a7d53cc21fd53100f955377be379423b0e386274 CI <CI@example.com> 1617671339 +1000 commit: myfile2
|
||||
a7d53cc21fd53100f955377be379423b0e386274 4ba4f1ed711a9081fab21bc222469aa5176a01f8 CI <CI@example.com> 1617671339 +1000 commit: myfile3
|
||||
4ba4f1ed711a9081fab21bc222469aa5176a01f8 1440bc6cc888a09dca2329d1060eec6de78d9d21 CI <CI@example.com> 1617671339 +1000 commit: myfile4
|
||||
1440bc6cc888a09dca2329d1060eec6de78d9d21 e7560e2cd4783a261ad32496cefed2d9f69a46e7 CI <CI@example.com> 1617671342 +1000 commit: commit
|
@ -1,5 +0,0 @@
|
||||
0000000000000000000000000000000000000000 3df3d8761bc0f0828596b11845aeac175b7b7393 CI <CI@example.com> 1617671339 +1000 commit (initial): myfile1
|
||||
3df3d8761bc0f0828596b11845aeac175b7b7393 a7d53cc21fd53100f955377be379423b0e386274 CI <CI@example.com> 1617671339 +1000 commit: myfile2
|
||||
a7d53cc21fd53100f955377be379423b0e386274 4ba4f1ed711a9081fab21bc222469aa5176a01f8 CI <CI@example.com> 1617671339 +1000 commit: myfile3
|
||||
4ba4f1ed711a9081fab21bc222469aa5176a01f8 1440bc6cc888a09dca2329d1060eec6de78d9d21 CI <CI@example.com> 1617671339 +1000 commit: myfile4
|
||||
1440bc6cc888a09dca2329d1060eec6de78d9d21 e7560e2cd4783a261ad32496cefed2d9f69a46e7 CI <CI@example.com> 1617671342 +1000 commit: commit
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1,3 +0,0 @@
|
||||
x�ÍA
|
||||
Â0@Q×9Åì™iÆI
|
||||
"BW=FšL°Ð!R"èííÜ~üÜÌÖÄrê»* J®˜d £Æ¬¥DÕÀû"\S¾.½û³í0Íp›æ‡~’½6½äfw ¡ �¼áLˆèŽzLºþÉ�}ëº)¹2r,Ï
|
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
Binary file not shown.
@ -1 +0,0 @@
|
||||
e7560e2cd4783a261ad32496cefed2d9f69a46e7
|
@ -1 +0,0 @@
|
||||
test1
|
@ -1 +0,0 @@
|
||||
test2
|
@ -1 +0,0 @@
|
||||
test3
|
@ -1 +0,0 @@
|
||||
test4
|
@ -1 +0,0 @@
|
||||
test5
|
@ -1 +0,0 @@
|
||||
{"KeyEvents":[{"Timestamp":527,"Mod":0,"Key":256,"Ch":32},{"Timestamp":830,"Mod":0,"Key":256,"Ch":99},{"Timestamp":1127,"Mod":0,"Key":256,"Ch":99},{"Timestamp":1190,"Mod":0,"Key":256,"Ch":111},{"Timestamp":1335,"Mod":0,"Key":256,"Ch":109},{"Timestamp":1447,"Mod":0,"Key":256,"Ch":109},{"Timestamp":1583,"Mod":0,"Key":256,"Ch":105},{"Timestamp":1606,"Mod":0,"Key":256,"Ch":116},{"Timestamp":1935,"Mod":0,"Key":13,"Ch":13},{"Timestamp":2353,"Mod":0,"Key":27,"Ch":0}],"ResizeEvents":[{"Timestamp":0,"Width":127,"Height":35}]}
|
@ -1,24 +0,0 @@
|
||||
#!/bin/sh
|
||||
|
||||
set -e
|
||||
|
||||
cd $1
|
||||
|
||||
git init
|
||||
|
||||
git config user.email "CI@example.com"
|
||||
git config user.name "CI"
|
||||
|
||||
echo test1 > myfile1
|
||||
git add .
|
||||
git commit -am "myfile1"
|
||||
echo test2 > myfile2
|
||||
git add .
|
||||
git commit -am "myfile2"
|
||||
echo test3 > myfile3
|
||||
git add .
|
||||
git commit -am "myfile3"
|
||||
echo test4 > myfile4
|
||||
git add .
|
||||
git commit -am "myfile4"
|
||||
echo test5 > myfile5
|
@ -1 +0,0 @@
|
||||
{ "description": "stage a file and commit the change", "speed": 15 }
|
@ -1 +0,0 @@
|
||||
file2
|
@ -1 +0,0 @@
|
||||
ref: refs/heads/lol
|
Binary file not shown.
@ -1,4 +0,0 @@
|
||||
0000000000000000000000000000000000000000 9901fd9b7766be600bed07f55f1794a759527a98 CI <CI@example.com> 1617674232 +1000 commit (initial): file0
|
||||
9901fd9b7766be600bed07f55f1794a759527a98 0029f9bf66e346d47ede6a501abb5b82bee60096 CI <CI@example.com> 1617674232 +1000 commit: file1
|
||||
0029f9bf66e346d47ede6a501abb5b82bee60096 e1cb250774fb8606d33062518d0ae03831130249 CI <CI@example.com> 1617674232 +1000 commit: file2
|
||||
e1cb250774fb8606d33062518d0ae03831130249 0029f9bf66e346d47ede6a501abb5b82bee60096 CI <CI@example.com> 1617674249 +1000 checkout: moving from master to lol
|
@ -1 +0,0 @@
|
||||
0000000000000000000000000000000000000000 0029f9bf66e346d47ede6a501abb5b82bee60096 CI <CI@example.com> 1617674249 +1000 branch: Created from 0029f9bf66e346d47ede6a501abb5b82bee60096
|
@ -1,3 +0,0 @@
|
||||
0000000000000000000000000000000000000000 9901fd9b7766be600bed07f55f1794a759527a98 CI <CI@example.com> 1617674232 +1000 commit (initial): file0
|
||||
9901fd9b7766be600bed07f55f1794a759527a98 0029f9bf66e346d47ede6a501abb5b82bee60096 CI <CI@example.com> 1617674232 +1000 commit: file1
|
||||
0029f9bf66e346d47ede6a501abb5b82bee60096 e1cb250774fb8606d33062518d0ae03831130249 CI <CI@example.com> 1617674232 +1000 commit: file2
|
Binary file not shown.
Binary file not shown.
Some files were not shown because too many files have changed in this diff Show More
Loading…
x
Reference in New Issue
Block a user