From 96310288eee4ec7ff45990fe8178c15a3299a037 Mon Sep 17 00:00:00 2001 From: Jesse Duffield Date: Mon, 26 Dec 2022 17:15:33 +1100 Subject: [PATCH] allow chaining matchers --- pkg/integration/components/input.go | 7 +- pkg/integration/components/matcher.go | 142 +++++++++++++----- pkg/integration/tests/bisect/basic.go | 8 +- .../tests/bisect/from_other_branch.go | 2 +- pkg/integration/tests/branch/rebase.go | 2 +- .../tests/branch/rebase_and_drop.go | 2 +- .../tests/cherry_pick/cherry_pick.go | 2 +- .../cherry_pick/cherry_pick_conflicts.go | 2 +- pkg/integration/tests/commit/staged.go | 8 +- .../tests/commit/staged_without_hooks.go | 16 +- pkg/integration/tests/commit/unstaged.go | 4 +- .../tests/diff/diff_and_apply_patch.go | 2 +- .../tests/file/dir_with_untracked_file.go | 2 +- 13 files changed, 132 insertions(+), 67 deletions(-) diff --git a/pkg/integration/components/input.go b/pkg/integration/components/input.go index 796d1bf9d..39f1991c1 100644 --- a/pkg/integration/components/input.go +++ b/pkg/integration/components/input.go @@ -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, "" } diff --git a/pkg/integration/components/matcher.go b/pkg/integration/components/matcher.go index c8d47933c..9885b7d5e 100644 --- a/pkg/integration/components/matcher.go +++ b/pkg/integration/components/matcher.go @@ -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) } diff --git a/pkg/integration/tests/bisect/basic.go b/pkg/integration/tests/bisect/basic.go index 52e272c3a..c6ad88c5b 100644 --- a/pkg/integration/tests/bisect/basic.go +++ b/pkg/integration/tests/bisect/basic.go @@ -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")) }, }) diff --git a/pkg/integration/tests/bisect/from_other_branch.go b/pkg/integration/tests/bisect/from_other_branch.go index 36dad4705..cb31af972 100644 --- a/pkg/integration/tests/bisect/from_other_branch.go +++ b/pkg/integration/tests/bisect/from_other_branch.go @@ -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( diff --git a/pkg/integration/tests/branch/rebase.go b/pkg/integration/tests/branch/rebase.go index ad0e40925..860331215 100644 --- a/pkg/integration/tests/branch/rebase.go +++ b/pkg/integration/tests/branch/rebase.go @@ -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"), diff --git a/pkg/integration/tests/branch/rebase_and_drop.go b/pkg/integration/tests/branch/rebase_and_drop.go index 5c1afe141..479d240d9 100644 --- a/pkg/integration/tests/branch/rebase_and_drop.go +++ b/pkg/integration/tests/branch/rebase_and_drop.go @@ -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"), diff --git a/pkg/integration/tests/cherry_pick/cherry_pick.go b/pkg/integration/tests/cherry_pick/cherry_pick.go index e306a016e..e79e35b19 100644 --- a/pkg/integration/tests/cherry_pick/cherry_pick.go +++ b/pkg/integration/tests/cherry_pick/cherry_pick.go @@ -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")) }, }) diff --git a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go index 2aed02334..4ed310e1d 100644 --- a/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go +++ b/pkg/integration/tests/cherry_pick/cherry_pick_conflicts.go @@ -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")) }, }) diff --git a/pkg/integration/tests/commit/staged.go b/pkg/integration/tests/commit/staged.go index b018d9dfe..4a0c3eafc 100644 --- a/pkg/integration/tests/commit/staged.go +++ b/pkg/integration/tests/commit/staged.go @@ -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" diff --git a/pkg/integration/tests/commit/staged_without_hooks.go b/pkg/integration/tests/commit/staged_without_hooks.go index 43d065db2..e2f0752f0 100644 --- a/pkg/integration/tests/commit/staged_without_hooks.go +++ b/pkg/integration/tests/commit/staged_without_hooks.go @@ -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() diff --git a/pkg/integration/tests/commit/unstaged.go b/pkg/integration/tests/commit/unstaged.go index a3c5b2812..6e7e2a307 100644 --- a/pkg/integration/tests/commit/unstaged.go +++ b/pkg/integration/tests/commit/unstaged.go @@ -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) diff --git a/pkg/integration/tests/diff/diff_and_apply_patch.go b/pkg/integration/tests/diff/diff_and_apply_patch.go index 6e85b3911..e963bb296 100644 --- a/pkg/integration/tests/diff/diff_and_apply_patch.go +++ b/pkg/integration/tests/diff/diff_and_apply_patch.go @@ -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 diff --git a/pkg/integration/tests/file/dir_with_untracked_file.go b/pkg/integration/tests/file/dir_with_untracked_file.go index acdb94499..b85ea273b 100644 --- a/pkg/integration/tests/file/dir_with_untracked_file.go +++ b/pkg/integration/tests/file/dir_with_untracked_file.go @@ -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"))