mirror of
https://github.com/jesseduffield/lazygit.git
synced 2025-03-21 21:47:32 +02:00
We need to fetch our list of tests both outside of our test binary and within. We need to get the list from within so that we can run the code that drives the test and runs assertions. To get the list of tests we need to know where the root of the lazygit repo is, given that the tests live in files under that root. So far, we've used this GetLazyRootDirectory() function for that, but it assumes that we're not in a test directory (it just looks for the first .git dir it can find). Because we didn't want to properly fix this before, we've been setting the working directory of the test command to the lazygit root, and using the --path CLI arg to override it when the test itself ran. This was a terrible hack. Now, we're passing the lazygit root directory as an env var to the integration test, so that we can set the working directory to the actual path of the test repo; removing the need to use the --path arg.
410 lines
9.7 KiB
Go
410 lines
9.7 KiB
Go
package clients
|
|
|
|
import (
|
|
"fmt"
|
|
"log"
|
|
"os"
|
|
"os/exec"
|
|
"path/filepath"
|
|
"strings"
|
|
|
|
"github.com/jesseduffield/gocui"
|
|
"github.com/jesseduffield/lazycore/pkg/utils"
|
|
"github.com/jesseduffield/lazygit/pkg/gui"
|
|
"github.com/jesseduffield/lazygit/pkg/gui/style"
|
|
"github.com/jesseduffield/lazygit/pkg/integration/components"
|
|
"github.com/jesseduffield/lazygit/pkg/integration/tests"
|
|
"github.com/samber/lo"
|
|
)
|
|
|
|
// This program lets you run integration tests from a TUI. See pkg/integration/README.md for more info.
|
|
|
|
var SLOW_INPUT_DELAY = 600
|
|
|
|
func RunTUI(raceDetector bool) {
|
|
rootDir := utils.GetLazyRootDirectory()
|
|
testDir := filepath.Join(rootDir, "test", "integration")
|
|
|
|
app := newApp(testDir)
|
|
app.loadTests()
|
|
|
|
g, err := gocui.NewGui(gocui.NewGuiOpts{
|
|
OutputMode: gocui.OutputTrue,
|
|
RuneReplacements: 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.KeyArrowDown, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
if app.itemIdx < len(app.filteredTests)-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)
|
|
}
|
|
|
|
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
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, true, false, raceDetector, 0)
|
|
|
|
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
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, false, raceDetector, 0)
|
|
|
|
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
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, false, raceDetector, SLOW_INPUT_DELAY)
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("list", 'd', gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
currentTest := app.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
suspendAndRunTest(currentTest, false, true, raceDetector, 0)
|
|
|
|
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 := exec.Command("sh", "-c", fmt.Sprintf("code -r pkg/integration/tests/%s.go", 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 := exec.Command("sh", "-c", fmt.Sprintf("code test/_results/%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
|
|
}
|
|
|
|
app.filteredTests = app.allTests
|
|
app.renderTests()
|
|
app.editorView.TextArea.Clear()
|
|
app.editorView.Clear()
|
|
app.editorView.Reset()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
if err := g.SetKeybinding("editor", gocui.KeyEnter, gocui.ModNone, func(*gocui.Gui, *gocui.View) error {
|
|
app.filtering = false
|
|
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
|
|
app.renderTests()
|
|
|
|
return nil
|
|
}); err != nil {
|
|
log.Panicln(err)
|
|
}
|
|
|
|
err = g.MainLoop()
|
|
g.Close()
|
|
switch err {
|
|
case gocui.ErrQuit:
|
|
return
|
|
default:
|
|
log.Panicln(err)
|
|
}
|
|
}
|
|
|
|
type app struct {
|
|
allTests []*components.IntegrationTest
|
|
filteredTests []*components.IntegrationTest
|
|
itemIdx int
|
|
testDir string
|
|
filtering bool
|
|
g *gocui.Gui
|
|
listView *gocui.View
|
|
editorView *gocui.View
|
|
}
|
|
|
|
func newApp(testDir string) *app {
|
|
return &app{testDir: testDir, allTests: tests.GetTests(utils.GetLazyRootDirectory())}
|
|
}
|
|
|
|
func (self *app) getCurrentTest() *components.IntegrationTest {
|
|
self.adjustCursor()
|
|
if len(self.filteredTests) > 0 {
|
|
return self.filteredTests[self.itemIdx]
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (self *app) loadTests() {
|
|
self.filteredTests = self.allTests
|
|
|
|
self.adjustCursor()
|
|
}
|
|
|
|
func (self *app) adjustCursor() {
|
|
self.itemIdx = utils.Clamp(self.itemIdx, 0, len(self.filteredTests)-1)
|
|
}
|
|
|
|
func (self *app) filterWithString(needle string) {
|
|
if needle == "" {
|
|
self.filteredTests = self.allTests
|
|
} else {
|
|
self.filteredTests = lo.Filter(self.allTests, func(test *components.IntegrationTest, _ int) bool {
|
|
return strings.Contains(test.Name(), needle)
|
|
})
|
|
}
|
|
|
|
self.renderTests()
|
|
self.g.Update(func(g *gocui.Gui) error { return nil })
|
|
}
|
|
|
|
func (self *app) renderTests() {
|
|
self.listView.Clear()
|
|
for _, test := range self.filteredTests {
|
|
fmt.Fprintln(self.listView, test.Name())
|
|
}
|
|
}
|
|
|
|
func (self *app) wrapEditor(f func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool) func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
|
return func(v *gocui.View, key gocui.Key, ch rune, mod gocui.Modifier) bool {
|
|
matched := f(v, key, ch, mod)
|
|
if matched {
|
|
self.filterWithString(v.TextArea.GetContent())
|
|
}
|
|
return matched
|
|
}
|
|
}
|
|
|
|
func suspendAndRunTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
|
if err := gocui.Screen.Suspend(); err != nil {
|
|
panic(err)
|
|
}
|
|
|
|
runTuiTest(test, sandbox, waitForDebugger, raceDetector, inputDelay)
|
|
|
|
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 (self *app) layout(g *gocui.Gui) error {
|
|
maxX, maxY := g.Size()
|
|
descriptionViewHeight := 7
|
|
keybindingsViewHeight := 3
|
|
editorViewHeight := 3
|
|
if !self.filtering {
|
|
editorViewHeight = 0
|
|
} else {
|
|
descriptionViewHeight = 0
|
|
keybindingsViewHeight = 0
|
|
}
|
|
g.Cursor = self.filtering
|
|
g.FgColor = gocui.ColorGreen
|
|
listView, err := g.SetView("list", 0, 0, maxX-1, maxY-descriptionViewHeight-keybindingsViewHeight-editorViewHeight-1, 0)
|
|
if err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
|
|
if self.listView == nil {
|
|
self.listView = listView
|
|
}
|
|
|
|
listView.Highlight = true
|
|
listView.SelBgColor = gocui.ColorBlue
|
|
self.renderTests()
|
|
listView.Title = "Tests"
|
|
listView.FgColor = gocui.ColorDefault
|
|
if _, err := g.SetCurrentView("list"); err != nil {
|
|
return err
|
|
}
|
|
}
|
|
|
|
descriptionView, err := g.SetViewBeneath("description", "list", descriptionViewHeight)
|
|
if err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
descriptionView.Title = "Test description"
|
|
descriptionView.Wrap = true
|
|
descriptionView.FgColor = gocui.ColorDefault
|
|
}
|
|
|
|
keybindingsView, err := g.SetViewBeneath("keybindings", "description", keybindingsViewHeight)
|
|
if err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
keybindingsView.Title = "Keybindings"
|
|
keybindingsView.Wrap = true
|
|
keybindingsView.FgColor = gocui.ColorDefault
|
|
fmt.Fprintln(keybindingsView, "up/down: navigate, enter: run test, t: run test slow, s: sandbox, d: debug test, o: open test file, shift+o: open test snapshot directory, forward-slash: filter")
|
|
}
|
|
|
|
editorView, err := g.SetViewBeneath("editor", "keybindings", editorViewHeight)
|
|
if err != nil {
|
|
if !gocui.IsUnknownView(err) {
|
|
return err
|
|
}
|
|
|
|
if self.editorView == nil {
|
|
self.editorView = editorView
|
|
}
|
|
|
|
editorView.Title = "Filter"
|
|
editorView.FgColor = gocui.ColorDefault
|
|
editorView.Editable = true
|
|
editorView.Editor = gocui.EditorFunc(self.wrapEditor(gocui.SimpleEditor))
|
|
}
|
|
|
|
currentTest := self.getCurrentTest()
|
|
if currentTest == nil {
|
|
return nil
|
|
}
|
|
|
|
descriptionView.Clear()
|
|
fmt.Fprint(descriptionView, currentTest.Description())
|
|
|
|
return nil
|
|
}
|
|
|
|
func quit(g *gocui.Gui, v *gocui.View) error {
|
|
return gocui.ErrQuit
|
|
}
|
|
|
|
func runTuiTest(test *components.IntegrationTest, sandbox bool, waitForDebugger bool, raceDetector bool, inputDelay int) {
|
|
err := components.RunTests(components.RunTestArgs{
|
|
Tests: []*components.IntegrationTest{test},
|
|
Logf: log.Printf,
|
|
RunCmd: runCmdInTerminal,
|
|
TestWrapper: runAndPrintError,
|
|
Sandbox: sandbox,
|
|
WaitForDebugger: waitForDebugger,
|
|
RaceDetector: raceDetector,
|
|
CodeCoverageDir: "",
|
|
InputDelay: inputDelay,
|
|
MaxAttempts: 1,
|
|
})
|
|
if err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
}
|
|
|
|
func runAndPrintError(test *components.IntegrationTest, f func() error) {
|
|
if err := f(); err != nil {
|
|
log.Println(err.Error())
|
|
}
|
|
}
|