diff --git a/main.go b/main.go index 651d2be09..8684b8cd6 100644 --- a/main.go +++ b/main.go @@ -1,30 +1,19 @@ 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/integration" - "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. +// we'll overwrite them if they haven't been set by the build script and if Go itself has set corresponding values in the binary var ( commit string version = DEFAULT_VERSION @@ -33,8 +22,13 @@ var ( ) func main() { - updateBuildInfo() + cliArgs := parseCliArgsAndEnvVars() + buildInfo := getBuildInfo() + app.Start(cliArgs, buildInfo, nil) +} + +func parseCliArgsAndEnvVars() *app.CliArgs { flaggy.DefaultParser.ShowVersionWithVersionFlag = false repoPath := "" @@ -46,20 +40,20 @@ func main() { 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") + printVersionInfo := false + flaggy.Bool(&printVersionInfo, "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)") + 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)") - logFlag := false - flaggy.Bool(&logFlag, "l", "logs", "Tail lazygit logs (intended to be used when `lazygit --debug` is called in a separate terminal tab)") + tailLogs := false + flaggy.Bool(&tailLogs, "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") + printDefaultConfig := false + flaggy.Bool(&printDefaultConfig, "c", "config", "Print the default config") - configDirFlag := false - flaggy.Bool(&configDirFlag, "cd", "print-config-dir", "Print the config directory") + 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") @@ -70,158 +64,68 @@ func main() { 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)") + customConfigFile := "" + flaggy.String(&customConfigFile, "ucf", "use-config-file", "Comma separated list to custom config file(s)") flaggy.Parse() if os.Getenv("DEBUG") == "TRUE" { - debuggingFlag = true + debug = true } - 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") + return &app.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, } - - 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()) - } - - if test, ok := integration.CurrentIntegrationTest(); ok { - test.SetupConfig(appConfig) - } - - 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 +func getBuildInfo() *app.BuildInfo { + buildInfo := &app.BuildInfo{ + Commit: commit, + Date: date, + Version: version, + BuildSource: buildSource, } - 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 + if buildInfo.Version != DEFAULT_VERSION { + return buildInfo } - buildInfo, ok := debug.ReadBuildInfo() + goBuildInfo, ok := debug.ReadBuildInfo() if !ok { - return + return buildInfo } - revision, ok := lo.Find(buildInfo.Settings, func(setting debug.BuildSetting) bool { + revision, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool { return setting.Key == "vcs.revision" }) if ok { - commit = revision.Value + buildInfo.Commit = revision.Value // if lazygit was built from source we'll show the version as the // abbreviated commit hash - version = utils.ShortSha(revision.Value) + buildInfo.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 { + time, ok := lo.Find(goBuildInfo.Settings, func(setting debug.BuildSetting) bool { return setting.Key == "vcs.time" }) if ok { - date = time.Value + buildInfo.Date = time.Value } + + return buildInfo } diff --git a/pkg/app/app.go b/pkg/app/app.go index 418c1406e..36fbc3e97 100644 --- a/pkg/app/app.go +++ b/pkg/app/app.go @@ -24,6 +24,7 @@ import ( "github.com/jesseduffield/lazygit/pkg/gui" "github.com/jesseduffield/lazygit/pkg/gui/types" "github.com/jesseduffield/lazygit/pkg/i18n" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/jesseduffield/lazygit/pkg/updates" ) @@ -38,7 +39,12 @@ 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 types.StartArgs, + test integrationTypes.Test, +) { app, err := NewApp(config, common) if err == nil { diff --git a/pkg/app/run.go b/pkg/app/run.go new file mode 100644 index 000000000..aaa133792 --- /dev/null +++ b/pkg/app/run.go @@ -0,0 +1,163 @@ +package app + +import ( + "bytes" + "fmt" + "log" + "os" + "path/filepath" + "runtime" + "strings" + + "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/integration" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/jesseduffield/lazygit/pkg/logs" + "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 +} + +// only used when running integration tests +type TestConfig struct { + Test integrationTypes.Test +} + +func Start(cliArgs *CliArgs, buildInfo *BuildInfo, test integrationTypes.Test) { + 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 test, ok := integration.CurrentIntegrationTest(); ok { + test.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, types.NewStartArgs(cliArgs.FilterPath, parsedGitArg), test) +} + +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") +} diff --git a/pkg/config/app_config.go b/pkg/config/app_config.go index 3591166c7..9806bcf58 100644 --- a/pkg/config/app_config.go +++ b/pkg/config/app_config.go @@ -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 diff --git a/pkg/gui/gui_adapter_impl.go b/pkg/gui/gui_adapter_impl.go new file mode 100644 index 000000000..dc811f92e --- /dev/null +++ b/pkg/gui/gui_adapter_impl.go @@ -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 GuiAdapterImpl struct { + gui *Gui +} + +var _ integrationTypes.GuiAdapter = &GuiAdapterImpl{} + +func (self *GuiAdapterImpl) 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 *GuiAdapterImpl) Keys() config.KeybindingConfig { + return self.gui.Config.GetUserConfig().Keybinding +} + +func (self *GuiAdapterImpl) CurrentContext() types.Context { + return self.gui.c.CurrentContext() +} + +func (self *GuiAdapterImpl) Model() *types.Model { + return self.gui.State.Model +} + +func (self *GuiAdapterImpl) 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 *GuiAdapterImpl) Log(message string) { + self.gui.c.Log.Warn(message) +} + +// logs in the actual UI (in the commands panel) +func (self *GuiAdapterImpl) LogUI(message string) { + self.gui.c.LogAction(message) +} + +func (self *GuiAdapterImpl) CheckedOutRef() *models.Branch { + return self.gui.helpers.Refs.GetCheckedOutRef() +} diff --git a/pkg/gui/services/custom_commands/resolver.go b/pkg/gui/services/custom_commands/resolver.go index 35ebbd9d1..1b80987c1 100644 --- a/pkg/gui/services/custom_commands/resolver.go +++ b/pkg/gui/services/custom_commands/resolver.go @@ -1,6 +1,10 @@ package custom_commands import ( + "bytes" + "fmt" + "text/template" + "github.com/jesseduffield/lazygit/pkg/common" "github.com/jesseduffield/lazygit/pkg/config" ) @@ -101,3 +105,30 @@ func (self *Resolver) resolveMenuOption(option *config.CustomCommandMenuOption, Value: value, }, nil } + +func main() { + fmt.Println(ResolveTemplate("old approach: {{index .PromptResponses 0}}, new approach: {{ .Form.a }}", CustomCommandObject{ + PromptResponses: []string{"a"}, + Form: map[string]string{"a": "B"}, + })) +} + +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 +} diff --git a/pkg/gui/test_mode.go b/pkg/gui/test_mode.go index 7f57a4be8..0252aa198 100644 --- a/pkg/gui/test_mode.go +++ b/pkg/gui/test_mode.go @@ -10,6 +10,10 @@ import ( "github.com/jesseduffield/lazygit/pkg/utils" ) +type IntegrationTest interface { + Run(guiAdapter *GuiAdapterImpl) +} + func (gui *Gui) handleTestMode() { if integration.PlayingIntegrationTest() { test, ok := integration.CurrentIntegrationTest() @@ -21,17 +25,7 @@ func (gui *Gui) handleTestMode() { go func() { time.Sleep(time.Millisecond * 100) - shell := &integration.ShellImpl{} - assert := &AssertImpl{gui: gui} - keys := gui.Config.GetUserConfig().Keybinding - input := NewInputImpl(gui, keys, assert, integration.KeyPressDelay()) - - test.Run( - shell, - input, - assert, - gui.c.UserConfig.Keybinding, - ) + test.Run(&GuiAdapterImpl{gui: gui}) gui.g.Update(func(*gocui.Gui) error { return gocui.ErrQuit diff --git a/pkg/integration/env.go b/pkg/integration/env.go index 0c0d0b196..6102923fc 100644 --- a/pkg/integration/env.go +++ b/pkg/integration/env.go @@ -2,9 +2,9 @@ package integration import ( "os" - "strconv" "github.com/jesseduffield/generics/slices" + "github.com/jesseduffield/lazygit/pkg/integration/integration_tests" "github.com/jesseduffield/lazygit/pkg/integration/types" ) @@ -18,35 +18,20 @@ func IntegrationTestName() string { return os.Getenv("LAZYGIT_TEST_NAME") } +func PlayingIntegrationTest() bool { + return IntegrationTestName() != "" +} + func CurrentIntegrationTest() (types.Test, bool) { if !PlayingIntegrationTest() { return nil, false } - return slices.Find(Tests, func(test types.Test) bool { + return slices.Find(integration_tests.Tests, func(test types.Test) bool { return test.Name() == IntegrationTestName() }) } -func PlayingIntegrationTest() bool { - return IntegrationTestName() != "" -} - -// 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 -} - // OLD integration test format stuff func Replaying() bool { diff --git a/pkg/integration/helpers/assert.go b/pkg/integration/helpers/assert.go new file mode 100644 index 000000000..eced1187d --- /dev/null +++ b/pkg/integration/helpers/assert.go @@ -0,0 +1,106 @@ +package helpers + +import ( + "fmt" + "strings" + "time" + + "github.com/jesseduffield/lazygit/pkg/gui/types" + integrationTypes "github.com/jesseduffield/lazygit/pkg/integration/types" +) + +type AssertImpl struct { + gui integrationTypes.GuiAdapter +} + +var _ integrationTypes.Assert = &AssertImpl{} + +func (self *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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 *AssertImpl) 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) +} + +func (self *AssertImpl) Fail(message string) { + self.gui.Fail(message) +} diff --git a/pkg/integration/helpers/input.go b/pkg/integration/helpers/input.go new file mode 100644 index 000000000..fa4875ea1 --- /dev/null +++ b/pkg/integration/helpers/input.go @@ -0,0 +1,153 @@ +package helpers + +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 InputImpl struct { + gui integrationTypes.GuiAdapter + keys config.KeybindingConfig + assert integrationTypes.Assert + pushKeyDelay int +} + +func NewInputImpl(gui integrationTypes.GuiAdapter, keys config.KeybindingConfig, assert integrationTypes.Assert, pushKeyDelay int) *InputImpl { + return &InputImpl{ + gui: gui, + keys: keys, + assert: assert, + pushKeyDelay: pushKeyDelay, + } +} + +var _ integrationTypes.Input = &InputImpl{} + +func (self *InputImpl) PressKeys(keyStrs ...string) { + for _, keyStr := range keyStrs { + self.pressKey(keyStr) + } +} + +func (self *InputImpl) pressKey(keyStr string) { + self.Wait(self.pushKeyDelay) + + self.gui.PressKey(keyStr) +} + +func (self *InputImpl) SwitchToStatusWindow() { + self.pressKey(self.keys.Universal.JumpToBlock[0]) +} + +func (self *InputImpl) SwitchToFilesWindow() { + self.pressKey(self.keys.Universal.JumpToBlock[1]) +} + +func (self *InputImpl) SwitchToBranchesWindow() { + self.pressKey(self.keys.Universal.JumpToBlock[2]) +} + +func (self *InputImpl) SwitchToCommitsWindow() { + self.pressKey(self.keys.Universal.JumpToBlock[3]) +} + +func (self *InputImpl) SwitchToStashWindow() { + self.pressKey(self.keys.Universal.JumpToBlock[4]) +} + +func (self *InputImpl) Type(content string) { + for _, char := range content { + self.pressKey(string(char)) + } +} + +func (self *InputImpl) Confirm() { + self.pressKey(self.keys.Universal.Confirm) +} + +func (self *InputImpl) Cancel() { + self.pressKey(self.keys.Universal.Return) +} + +func (self *InputImpl) Select() { + self.pressKey(self.keys.Universal.Select) +} + +func (self *InputImpl) NextItem() { + self.pressKey(self.keys.Universal.NextItem) +} + +func (self *InputImpl) PreviousItem() { + self.pressKey(self.keys.Universal.PrevItem) +} + +func (self *InputImpl) ContinueMerge() { + self.PressKeys(self.keys.Universal.CreateRebaseOptionsMenu) + self.assert.SelectedLineContains("continue") + self.Confirm() +} + +func (self *InputImpl) ContinueRebase() { + self.ContinueMerge() +} + +func (self *InputImpl) Wait(milliseconds int) { + time.Sleep(time.Duration(milliseconds) * time.Millisecond) +} + +func (self *InputImpl) LogUI(message string) { + self.gui.LogUI(message) +} + +func (self *InputImpl) Log(message string) { + self.gui.LogUI(message) +} + +// 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 *InputImpl) 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)) +} diff --git a/pkg/integration/shell.go b/pkg/integration/helpers/shell.go similarity index 98% rename from pkg/integration/shell.go rename to pkg/integration/helpers/shell.go index 70da9c27b..fdfb0d231 100644 --- a/pkg/integration/shell.go +++ b/pkg/integration/helpers/shell.go @@ -1,4 +1,4 @@ -package integration +package helpers import ( "fmt" diff --git a/pkg/integration/helpers/test_impl.go b/pkg/integration/helpers/test_impl.go new file mode 100644 index 000000000..7d9efa56f --- /dev/null +++ b/pkg/integration/helpers/test_impl.go @@ -0,0 +1,107 @@ +package helpers + +import ( + "os" + "strconv" + "strings" + + "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/integration/types" + "github.com/jesseduffield/lazygit/pkg/utils" +) + +type TestImpl struct { + name string + description string + extraCmdArgs string + skip bool + setupRepo func(shell types.Shell) + setupConfig func(config *config.AppConfig) + run func( + shell types.Shell, + input types.Input, + assert types.Assert, + keys config.KeybindingConfig, + ) +} + +type NewTestArgs struct { + Description string + SetupRepo func(shell types.Shell) + SetupConfig func(config *config.AppConfig) + Run func(shell types.Shell, input types.Input, assert types.Assert, keys config.KeybindingConfig) + ExtraCmdArgs string + Skip bool +} + +func NewTest(args NewTestArgs) *TestImpl { + return &TestImpl{ + name: testNameFromFilePath(), + description: args.Description, + extraCmdArgs: args.ExtraCmdArgs, + skip: args.Skip, + setupRepo: args.SetupRepo, + setupConfig: args.SetupConfig, + run: args.Run, + } +} + +var _ types.Test = (*TestImpl)(nil) + +func (self *TestImpl) Name() string { + return self.name +} + +func (self *TestImpl) Description() string { + return self.description +} + +func (self *TestImpl) ExtraCmdArgs() string { + return self.extraCmdArgs +} + +func (self *TestImpl) Skip() bool { + return self.skip +} + +func (self *TestImpl) SetupConfig(config *config.AppConfig) { + self.setupConfig(config) +} + +func (self *TestImpl) SetupRepo(shell types.Shell) { + self.setupRepo(shell) +} + +// I want access to all contexts, the model, the ability to press a key, the ability to log, +func (self *TestImpl) Run( + gui types.GuiAdapter, +) { + shell := &ShellImpl{} + assert := &AssertImpl{gui: gui} + keys := gui.Keys() + input := NewInputImpl(gui, keys, assert, KeyPressDelay()) + + self.run(shell, input, assert, keys) +} + +func testNameFromFilePath() string { + path := utils.FilePath(3) + name := strings.Split(path, "integration/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 +} diff --git a/pkg/integration/integration.go b/pkg/integration/integration.go index 853dae0d9..4119f47a3 100644 --- a/pkg/integration/integration.go +++ b/pkg/integration/integration.go @@ -10,15 +10,13 @@ import ( "testing" "github.com/jesseduffield/lazygit/pkg/commands/oscommands" + "github.com/jesseduffield/lazygit/pkg/integration/helpers" "github.com/jesseduffield/lazygit/pkg/integration/integration_tests" "github.com/jesseduffield/lazygit/pkg/integration/types" ) // this is the integration runner for the new and improved integration interface -// re-exporting this so that clients only need to import one package -var Tests = integration_tests.Tests - func RunTestsNew( logf func(format string, formatArgs ...interface{}), runCmd func(cmd *exec.Cmd) error, @@ -41,7 +39,7 @@ func RunTestsNew( return err } - for _, test := range Tests { + for _, test := range integration_tests.Tests { test := test fnWrapper(test, func(t *testing.T) error { //nolint: thelper @@ -141,7 +139,7 @@ func createFixtureNew(test types.Test, actualDir string, rootDir string) error { panic(err) } - shell := &ShellImpl{} + shell := &helpers.ShellImpl{} shell.RunCommand("git init") shell.RunCommand(`git config user.email "CI@example.com"`) shell.RunCommand(`git config user.name "CI"`) diff --git a/pkg/integration/integration_tests/branch/suggestions.go b/pkg/integration/integration_tests/branch/suggestions.go index 47c360514..a925280e7 100644 --- a/pkg/integration/integration_tests/branch/suggestions.go +++ b/pkg/integration/integration_tests/branch/suggestions.go @@ -2,10 +2,11 @@ package branch import ( "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/integration/helpers" "github.com/jesseduffield/lazygit/pkg/integration/types" ) -var Suggestions = types.NewTest(types.NewTestArgs{ +var Suggestions = helpers.NewTest(helpers.NewTestArgs{ Description: "Checking out a branch with name suggestions", ExtraCmdArgs: "", Skip: false, diff --git a/pkg/integration/integration_tests/commit/commit.go b/pkg/integration/integration_tests/commit/commit.go index f0af9462a..9fd3bb356 100644 --- a/pkg/integration/integration_tests/commit/commit.go +++ b/pkg/integration/integration_tests/commit/commit.go @@ -2,10 +2,11 @@ package commit import ( "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/integration/helpers" "github.com/jesseduffield/lazygit/pkg/integration/types" ) -var Commit = types.NewTest(types.NewTestArgs{ +var Commit = helpers.NewTest(helpers.NewTestArgs{ Description: "Staging a couple files and committing", ExtraCmdArgs: "", Skip: false, diff --git a/pkg/integration/integration_tests/commit/new_branch.go b/pkg/integration/integration_tests/commit/new_branch.go index 218e95180..9669937fa 100644 --- a/pkg/integration/integration_tests/commit/new_branch.go +++ b/pkg/integration/integration_tests/commit/new_branch.go @@ -2,10 +2,11 @@ package commit import ( "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/integration/helpers" "github.com/jesseduffield/lazygit/pkg/integration/types" ) -var NewBranch = types.NewTest(types.NewTestArgs{ +var NewBranch = helpers.NewTest(helpers.NewTestArgs{ Description: "Creating a new branch from a commit", ExtraCmdArgs: "", Skip: false, diff --git a/pkg/integration/integration_tests/interactive_rebase/one.go b/pkg/integration/integration_tests/interactive_rebase/one.go index 92780b24d..d8899569c 100644 --- a/pkg/integration/integration_tests/interactive_rebase/one.go +++ b/pkg/integration/integration_tests/interactive_rebase/one.go @@ -2,10 +2,11 @@ package interactive_rebase import ( "github.com/jesseduffield/lazygit/pkg/config" + "github.com/jesseduffield/lazygit/pkg/integration/helpers" "github.com/jesseduffield/lazygit/pkg/integration/types" ) -var One = types.NewTest(types.NewTestArgs{ +var One = helpers.NewTest(helpers.NewTestArgs{ Description: "Begins an interactive rebase, then fixups, drops, and squashes some commits", ExtraCmdArgs: "", Skip: false, diff --git a/pkg/integration/types/types.go b/pkg/integration/types/types.go index 60fd4f353..2e6ea34f6 100644 --- a/pkg/integration/types/types.go +++ b/pkg/integration/types/types.go @@ -1,12 +1,17 @@ package types import ( - "strings" - + "github.com/jesseduffield/lazygit/pkg/commands/models" "github.com/jesseduffield/lazygit/pkg/config" - "github.com/jesseduffield/lazygit/pkg/utils" + "github.com/jesseduffield/lazygit/pkg/gui/types" ) +// TODO: refactor this so that we don't have code spread around so much. We want +// our TestImpl struct to take the dependencies it needs from the gui and then +// create the input, assert, shell structs itself. That way, we can potentially +// ditch these interfaces so that we don't need to keep updating them every time +// we add a method to the concrete struct. + type Test interface { Name() string Description() string @@ -16,7 +21,7 @@ type Test interface { // so that they appear when lazygit runs SetupConfig(config *config.AppConfig) // this is called upon lazygit starting - Run(Shell, Input, Assert, config.KeybindingConfig) + Run(GuiAdapter) // e.g. '-debug' ExtraCmdArgs() string // for tests that are flakey and when we don't have time to fix them @@ -81,6 +86,7 @@ type Input interface { } // through this interface we assert on the state of the lazygit gui +// implementation is at pkg/gui/assert.go type Assert interface { WorkingTreeFileCount(int) CommitCount(int) @@ -93,80 +99,17 @@ type Assert interface { Fail(errorMessage string) } -type TestImpl 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, - ) -} - -type NewTestArgs struct { - Description string - SetupRepo func(shell Shell) - SetupConfig func(config *config.AppConfig) - Run func(shell Shell, input Input, assert Assert, keys config.KeybindingConfig) - ExtraCmdArgs string - Skip bool -} - -func NewTest(args NewTestArgs) *TestImpl { - return &TestImpl{ - name: testNameFromFilePath(), - description: args.Description, - extraCmdArgs: args.ExtraCmdArgs, - skip: args.Skip, - setupRepo: args.SetupRepo, - setupConfig: args.SetupConfig, - run: args.Run, - } -} - -var _ Test = (*TestImpl)(nil) - -func (self *TestImpl) Name() string { - return self.name -} - -func (self *TestImpl) Description() string { - return self.description -} - -func (self *TestImpl) ExtraCmdArgs() string { - return self.extraCmdArgs -} - -func (self *TestImpl) Skip() bool { - return self.skip -} - -func (self *TestImpl) SetupConfig(config *config.AppConfig) { - self.setupConfig(config) -} - -func (self *TestImpl) SetupRepo(shell Shell) { - self.setupRepo(shell) -} - -func (self *TestImpl) Run( - shell Shell, - input Input, - assert Assert, - keys config.KeybindingConfig, -) { - self.run(shell, input, assert, keys) -} - -func testNameFromFilePath() string { - path := utils.FilePath(3) - name := strings.Split(path, "integration/integration_tests/")[1] - - return name[:len(name)-len(".go")] +type GuiAdapter 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 } diff --git a/test/runner/main.go b/test/runner/main.go index ea6af59e8..691f0de4f 100644 --- a/test/runner/main.go +++ b/test/runner/main.go @@ -8,11 +8,11 @@ import ( "testing" "github.com/jesseduffield/lazygit/pkg/integration" + "github.com/jesseduffield/lazygit/pkg/integration/integration_tests" + "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/stretchr/testify/assert" ) -// Deprecated: This file is part of the old way of doing things. See test/runner_new/main.go for the new way - // see docs/Integration_Tests.md // This file can be invoked directly, but you might find it easier to go through // test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests. @@ -23,15 +23,28 @@ import ( func main() { mode := integration.GetModeFromEnv() - speedEnv := os.Getenv("SPEED") includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true" selectedTestName := os.Args[1] - err := integration.RunTests( + // check if our given test name actually exists + if selectedTestName != "" { + found := false + for _, test := range integration_tests.Tests { + if test.Name() == selectedTestName { + found = true + break + } + } + if !found { + log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/tests.go`?", selectedTestName) + } + } + + err := integration.RunTestsNew( log.Printf, runCmdInTerminal, - func(test *integration.Test, f func(*testing.T) error) { - if selectedTestName != "" && test.Name != selectedTestName { + func(test types.Test, f func(*testing.T) error) { + if selectedTestName != "" && test.Name() != selectedTestName { return } if err := f(nil); err != nil { @@ -39,7 +52,6 @@ func main() { } }, mode, - speedEnv, func(_t *testing.T, expected string, actual string, prefix string) { //nolint:thelper assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual)) }, diff --git a/test/runner_new/main.go b/test/runner_old/main.go similarity index 70% rename from test/runner_new/main.go rename to test/runner_old/main.go index 19a5dba03..ea6af59e8 100644 --- a/test/runner_new/main.go +++ b/test/runner_old/main.go @@ -8,10 +8,11 @@ import ( "testing" "github.com/jesseduffield/lazygit/pkg/integration" - "github.com/jesseduffield/lazygit/pkg/integration/types" "github.com/stretchr/testify/assert" ) +// Deprecated: This file is part of the old way of doing things. See test/runner_new/main.go for the new way + // see docs/Integration_Tests.md // This file can be invoked directly, but you might find it easier to go through // test/lazyintegration/main.go, which provides a convenient gui wrapper to integration tests. @@ -22,28 +23,15 @@ import ( func main() { mode := integration.GetModeFromEnv() + speedEnv := os.Getenv("SPEED") includeSkipped := os.Getenv("INCLUDE_SKIPPED") == "true" selectedTestName := os.Args[1] - // check if our given test name actually exists - if selectedTestName != "" { - found := false - for _, test := range integration.Tests { - if test.Name() == selectedTestName { - found = true - break - } - } - if !found { - log.Fatalf("test %s not found. Perhaps you forgot to add it to `pkg/integration/integration_tests/tests.go`?", selectedTestName) - } - } - - err := integration.RunTestsNew( + err := integration.RunTests( log.Printf, runCmdInTerminal, - func(test types.Test, f func(*testing.T) error) { - if selectedTestName != "" && test.Name() != selectedTestName { + func(test *integration.Test, f func(*testing.T) error) { + if selectedTestName != "" && test.Name != selectedTestName { return } if err := f(nil); err != nil { @@ -51,6 +39,7 @@ func main() { } }, mode, + speedEnv, func(_t *testing.T, expected string, actual string, prefix string) { //nolint:thelper assert.Equal(MockTestingT{}, expected, actual, fmt.Sprintf("Unexpected %s. Expected:\n%s\nActual:\n%s\n", prefix, expected, actual)) },