1
0
mirror of https://github.com/jesseduffield/lazygit.git synced 2025-01-20 05:19:24 +02:00

allow chaining matchers

This commit is contained in:
Jesse Duffield 2022-12-26 17:15:33 +11:00
parent c841ba8237
commit 96310288ee
13 changed files with 132 additions and 67 deletions

View File

@ -177,8 +177,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
self.assert.assertWithRetries(func() (bool, string) {
matchIndex = -1
var matches []string
lines := view.ViewBufferLines()
// first we look for a duplicate on the current screen. We won't bother looking beyond that though.
for i, line := range view.ViewBufferLines() {
for i, line := range lines {
ok, _ := matcher.test(line)
if ok {
matches = append(matches, line)
@ -186,9 +187,9 @@ func (self *Input) NavigateToListItem(matcher *matcher) {
}
}
if len(matches) > 1 {
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Lines:\n%s", len(matches), matcher.name, strings.Join(matches, "\n"))
return false, fmt.Sprintf("Found %d matches for `%s`, expected only a single match. Matching lines:\n%s", len(matches), matcher.name(), strings.Join(matches, "\n"))
} else if len(matches) == 0 {
return false, fmt.Sprintf("Could not find item matching: %s", matcher.name)
return false, fmt.Sprintf("Could not find item matching: %s. Lines:\n%s", matcher.name(), strings.Join(lines, "\n"))
} else {
return true, ""
}

View File

@ -4,34 +4,118 @@ import (
"fmt"
"regexp"
"strings"
"github.com/samber/lo"
)
// for making assertions on string values
type matcher struct {
rules []matcherRule
// this is printed when there's an error so that it's clear what the context of the assertion is
prefix string
}
type matcherRule struct {
// e.g. "contains 'foo'"
name string
// returns a bool that says whether the test passed and if it returns false, it
// also returns a string of the error message
testFn func(string) (bool, string)
// this is printed when there's an error so that it's clear what the context of the assertion is
prefix string
}
func NewMatcher(name string, testFn func(string) (bool, string)) *matcher {
return &matcher{name: name, testFn: testFn}
rules := []matcherRule{{name: name, testFn: testFn}}
return &matcher{rules: rules}
}
func (self *matcher) name() string {
if len(self.rules) == 0 {
return "anything"
}
return strings.Join(
lo.Map(self.rules, func(rule matcherRule, _ int) string { return rule.name }),
", ",
)
}
func (self *matcher) test(value string) (bool, string) {
ok, message := self.testFn(value)
if ok {
// if there are no rules, then we pass the test by default
if len(self.rules) == 0 {
return true, ""
}
if self.prefix != "" {
return false, self.prefix + " " + message
for _, rule := range self.rules {
ok, message := rule.testFn(value)
if ok {
continue
}
if self.prefix != "" {
return false, self.prefix + " " + message
}
return false, message
}
return false, message
return true, ""
}
func (self *matcher) Contains(target string) *matcher {
rule := matcherRule{
name: fmt.Sprintf("contains '%s'", target),
testFn: func(value string) (bool, string) {
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
},
}
self.rules = append(self.rules, rule)
return self
}
func (self *matcher) DoesNotContain(target string) *matcher {
rule := matcherRule{
name: fmt.Sprintf("does not contain '%s'", target),
testFn: func(value string) (bool, string) {
return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value)
},
}
self.rules = append(self.rules, rule)
return self
}
func (self *matcher) MatchesRegexp(target string) *matcher {
rule := matcherRule{
name: fmt.Sprintf("matches regular expression '%s'", target),
testFn: func(value string) (bool, string) {
matched, err := regexp.MatchString(target, value)
if err != nil {
return false, fmt.Sprintf("Unexpected error parsing regular expression '%s': %s", target, err.Error())
}
return matched, fmt.Sprintf("Expected '%s' to match regular expression '%s'", value, target)
},
}
self.rules = append(self.rules, rule)
return self
}
func (self *matcher) Equals(target string) *matcher {
rule := matcherRule{
name: fmt.Sprintf("equals '%s'", target),
testFn: func(value string) (bool, string) {
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
},
}
self.rules = append(self.rules, rule)
return self
}
func (self *matcher) context(prefix string) *matcher {
@ -40,42 +124,24 @@ func (self *matcher) context(prefix string) *matcher {
return self
}
func Contains(target string) *matcher {
return NewMatcher(
fmt.Sprintf("contains '%s'", target),
func(value string) (bool, string) {
return strings.Contains(value, target), fmt.Sprintf("Expected '%s' to be found in '%s'", target, value)
},
)
// this matcher has no rules meaning it always passes the test. Use this
// when you don't care what value you're dealing with.
func Anything() *matcher {
return &matcher{}
}
func NotContains(target string) *matcher {
return NewMatcher(
fmt.Sprintf("does not contain '%s'", target),
func(value string) (bool, string) {
return !strings.Contains(value, target), fmt.Sprintf("Expected '%s' to NOT be found in '%s'", target, value)
},
)
func Contains(target string) *matcher {
return Anything().Contains(target)
}
func DoesNotContain(target string) *matcher {
return Anything().DoesNotContain(target)
}
func MatchesRegexp(target string) *matcher {
return NewMatcher(
fmt.Sprintf("matches regular expression '%s'", target),
func(value string) (bool, string) {
matched, err := regexp.MatchString(target, value)
if err != nil {
return false, fmt.Sprintf("Unexpected error parsing regular expression '%s': %s", target, err.Error())
}
return matched, fmt.Sprintf("Expected '%s' to match regular expression '%s'", value, target)
},
)
return Anything().MatchesRegexp(target)
}
func Equals(target string) *matcher {
return NewMatcher(
fmt.Sprintf("equals '%s'", target),
func(value string) (bool, string) {
return target == value, fmt.Sprintf("Expected '%s' to equal '%s'", value, target)
},
)
return Anything().Equals(target)
}

View File

@ -52,15 +52,13 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
// lazygit will land us in the commit between our good and bad commits.
assert.CurrentView().
Name("commits").
SelectedLine(Contains("commit 05")).
SelectedLine(Contains("<-- current"))
SelectedLine(Contains("commit 05").Contains("<-- current"))
markCommitAsBad()
assert.CurrentView().
Name("commits").
SelectedLine(Contains("commit 04")).
SelectedLine(Contains("<-- current"))
SelectedLine(Contains("commit 04").Contains("<-- current"))
markCommitAsGood()
@ -68,6 +66,6 @@ var Basic = NewIntegrationTest(NewIntegrationTestArgs{
input.Alert(Equals("Bisect complete"), MatchesRegexp("(?s)commit 05.*Do you want to reset"))
assert.CurrentView().Name("commits").Content(Contains("commit 04"))
assert.View("information").Content(NotContains("bisecting"))
assert.View("information").Content(DoesNotContain("bisecting"))
},
})

View File

@ -44,7 +44,7 @@ var FromOtherBranch = NewIntegrationTest(NewIntegrationTestArgs{
input.Alert(Equals("Bisect complete"), MatchesRegexp(`(?s)commit 08.*Do you want to reset`))
assert.View("information").Content(NotContains("bisecting"))
assert.View("information").Content(DoesNotContain("bisecting"))
// back in master branch which just had the one commit
assert.CurrentView().Name("commits").Lines(

View File

@ -47,7 +47,7 @@ var Rebase = NewIntegrationTest(NewIntegrationTestArgs{
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
assert.View("information").Content(NotContains("rebasing"))
assert.View("information").Content(DoesNotContain("rebasing"))
assert.View("commits").TopLines(
Contains("second-change-branch unrelated change"),

View File

@ -72,7 +72,7 @@ var RebaseAndDrop = NewIntegrationTest(NewIntegrationTestArgs{
input.AcceptConfirmation(Equals("continue"), Contains("all merge conflicts resolved. Continue?"))
assert.View("information").Content(NotContains("rebasing"))
assert.View("information").Content(DoesNotContain("rebasing"))
assert.View("commits").TopLines(
Contains("to keep"),

View File

@ -70,6 +70,6 @@ var CherryPick = NewIntegrationTest(NewIntegrationTestArgs{
assert.View("information").Content(Contains("2 commits copied"))
input.Press(keys.Universal.Return)
assert.View("information").Content(NotContains("commits copied"))
assert.View("information").Content(DoesNotContain("commits copied"))
},
})

View File

@ -83,6 +83,6 @@ var CherryPickConflicts = NewIntegrationTest(NewIntegrationTestArgs{
assert.View("information").Content(Contains("2 commits copied"))
input.Press(keys.Universal.Return)
assert.View("information").Content(NotContains("commits copied"))
assert.View("information").Content(DoesNotContain("commits copied"))
},
})

View File

@ -27,17 +27,17 @@ var Staged = NewIntegrationTest(NewIntegrationTestArgs{
// we start with both lines having been staged
assert.View("stagingSecondary").Content(Contains("+myfile content"))
assert.View("stagingSecondary").Content(Contains("+with a second line"))
assert.View("staging").Content(NotContains("+myfile content"))
assert.View("staging").Content(NotContains("+with a second line"))
assert.View("staging").Content(DoesNotContain("+myfile content"))
assert.View("staging").Content(DoesNotContain("+with a second line"))
// unstage the selected line
input.PrimaryAction()
// the line should have been moved to the main view
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content"))
assert.View("stagingSecondary").Content(Contains("+with a second line"))
assert.View("staging").Content(Contains("+myfile content"))
assert.View("staging").Content(NotContains("+with a second line"))
assert.View("staging").Content(DoesNotContain("+with a second line"))
input.Press(keys.Files.CommitChanges)
commitMessage := "my commit message"

View File

@ -25,19 +25,19 @@ var StagedWithoutHooks = NewIntegrationTest(NewIntegrationTestArgs{
input.Enter()
assert.CurrentView().Name("stagingSecondary")
// we start with both lines having been staged
assert.View("stagingSecondary").Content(Contains("+myfile content"))
assert.View("stagingSecondary").Content(Contains("+with a second line"))
assert.View("staging").Content(NotContains("+myfile content"))
assert.View("staging").Content(NotContains("+with a second line"))
assert.View("stagingSecondary").Content(
Contains("+myfile content").Contains("+with a second line"),
)
assert.View("staging").Content(
DoesNotContain("+myfile content").DoesNotContain("+with a second line"),
)
// unstage the selected line
input.PrimaryAction()
// the line should have been moved to the main view
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
assert.View("stagingSecondary").Content(Contains("+with a second line"))
assert.View("staging").Content(Contains("+myfile content"))
assert.View("staging").Content(NotContains("+with a second line"))
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content").Contains("+with a second line"))
assert.View("staging").Content(Contains("+myfile content").DoesNotContain("+with a second line"))
input.Press(keys.Files.CommitChangesWithoutHook)
assert.InCommitMessagePanel()

View File

@ -23,10 +23,10 @@ var Unstaged = NewIntegrationTest(NewIntegrationTestArgs{
assert.CurrentView().Name("files").SelectedLine(Contains("myfile"))
input.Enter()
assert.CurrentView().Name("staging")
assert.View("stagingSecondary").Content(NotContains("+myfile content"))
assert.View("stagingSecondary").Content(DoesNotContain("+myfile content"))
// stage the first line
input.PrimaryAction()
assert.View("staging").Content(NotContains("+myfile content"))
assert.View("staging").Content(DoesNotContain("+myfile content"))
assert.View("stagingSecondary").Content(Contains("+myfile content"))
input.Press(keys.Files.CommitChanges)

View File

@ -54,7 +54,7 @@ var DiffAndApplyPatch = NewIntegrationTest(NewIntegrationTestArgs{
input.Press(keys.Universal.DiffingMenu)
input.Menu(Equals("Diffing"), Contains("exit diff mode"))
assert.View("information").Content(NotContains("building patch"))
assert.View("information").Content(DoesNotContain("building patch"))
input.Press(keys.Universal.CreatePatchOptionsMenu)
// adding the regex '$' here to distinguish the menu item from the 'apply patch in reverse' item

View File

@ -25,7 +25,7 @@ var DirWithUntrackedFile = NewIntegrationTest(NewIntegrationTestArgs{
assert.CommitCount(1)
assert.MainView().
Content(NotContains("error: Could not access")).
Content(DoesNotContain("error: Could not access")).
// we show baz because it's a modified file but we don't show bar because it's untracked
// (though it would be cool if we could show that too)
Content(Contains("baz"))