From 3b80f31dbb6649a0147f63b5de16c5476df61f37 Mon Sep 17 00:00:00 2001 From: James Harris Date: Mon, 25 Nov 2024 10:08:55 +1000 Subject: [PATCH] Fix issues with test runner command. --- internal/runner/assertion.go | 9 --- internal/runner/bless.go | 121 ++++----------------------------- internal/runner/interface.go | 11 ++- internal/runner/runner.go | 94 +++++++++++++++++++------ internal/runner/runner_test.go | 4 +- internal/runner/shell.go | 44 ++++++++++++ run.go | 15 ++-- 7 files changed, 151 insertions(+), 147 deletions(-) create mode 100644 internal/runner/shell.go diff --git a/internal/runner/assertion.go b/internal/runner/assertion.go index add087a..0416323 100644 --- a/internal/runner/assertion.go +++ b/internal/runner/assertion.go @@ -1,14 +1,5 @@ package runner -import ( - "strings" -) - -var ( - separator = strings.Repeat("=", 10) - newLine = []byte("\n") -) - // assertionExecutor is an impelmentation of [test.AssertionVisitor] that // performs assertions within the context of a test. type assertionExecutor[T TestingT[T]] struct { diff --git a/internal/runner/bless.go b/internal/runner/bless.go index df1c790..2c46cb9 100644 --- a/internal/runner/bless.go +++ b/internal/runner/bless.go @@ -1,107 +1,32 @@ package runner import ( - "bytes" "fmt" "io" "os" - "regexp" - "strings" "github.com/dogmatiq/aureus/internal/test" ) -// BlessStrategy is a strategy for blessing failed tests. -type BlessStrategy interface { - // bless updates the file containing an assertion's expected output to match - // its actual output. - // - // a is the assertion that failed. r is the file containing the blessed - // output that will replace the current expectation. - bless( - t LoggerT, - a test.Assertion, - r *os.File, - ) -} - -// BlessDisabled is a [BlessStrategy] that explicitly disables blessing of -// failed tests. -// -// It implies that the -aureus.bless flag on the command line is ignored. -type BlessDisabled struct{} - -func (*BlessDisabled) bless(LoggerT, test.Assertion, *os.File) {} - -// BlessAvailable is a [BlessStrategy] that instructs the user that blessing may -// be activated by using the -aureus.bless flag on the command line. -type BlessAvailable struct { - PackagePath string -} +// BlessStrategy is a strategy for accepting failed test output as the new +// expectation, known as "blessing" the output. +type BlessStrategy int -func (s *BlessAvailable) bless( - t LoggerT, - _ test.Assertion, - _ *os.File, -) { - t.Helper() - - atoms := strings.Split(t.Name(), "/") - for i, atom := range atoms { - atoms[i] = "^" + regexp.QuoteMeta(atom) + "$" - } - pattern := strings.Join(atoms, "/") - - var w bytes.Buffer - w.WriteString("To accept the current output as correct, use the -aureus.bless flag:") - w.WriteString("\n\n") - w.WriteString(" go test ") - - if s.PackagePath == "" { - // this will work, typically, but some packages may complain about not - // understanding the -aureus.bless flag - w.WriteString("./...") - } else { - w.WriteString(shellQuote(s.PackagePath)) - } - - w.WriteString(" -aureus.bless -run ") - w.WriteString(shellQuote(pattern)) - - logSection( - t, - "BLESS", - w.Bytes(), - false, - ) -} +const ( + // BlessAvailable is a [BlessStrategy] that instructs the user that blessing + // may be activated by using the -aureus.bless flag on the command line. + BlessAvailable BlessStrategy = iota -// BlessEnabled is a [BlessStrategy] that explicitly enables blessing of failed -// tests. -type BlessEnabled struct{} + // BlessEnabled is a [BlessStrategy] that explicitly enables blessing of + // failed tests. + BlessEnabled -func (s *BlessEnabled) bless( - t LoggerT, - a test.Assertion, - r *os.File, -) { - t.Helper() - - message := `The current output has been blessed. Future runs will consider this output correct.` - if err := edit(a, r); err != nil { - message = "Unable to bless output: " + err.Error() - } - - logSection( - t, - "BLESS", - []byte(message), - false, - ) - -} + // BlessDisabled is a [BlessStrategy] that explicitly disables blessing of + // failed tests. + BlessDisabled +) -func edit(a test.Assertion, r *os.File) error { +func bless(a test.Assertion, r *os.File) error { // TODO: Tests are loaded using an [fs.FS], so the file system is not // necessarily the host file system. // @@ -229,19 +154,3 @@ func fileSize(f *os.File) (int64, error) { } return stat.Size(), nil } - -func shellQuote(s string) string { - var w strings.Builder - w.WriteByte('\'') - - for _, r := range s { - if r == '\'' { - w.WriteString("'\"'\"'") - } else { - w.WriteRune(r) - } - } - - w.WriteByte('\'') - return w.String() -} diff --git a/internal/runner/interface.go b/internal/runner/interface.go index 0fa854a..23014a6 100644 --- a/internal/runner/interface.go +++ b/internal/runner/interface.go @@ -7,9 +7,18 @@ type LoggerT interface { Log(...any) } +// FailerT is the subset of the [testing.TB] that supports logging and failure +// reporting. +type FailerT interface { + LoggerT + SkipNow() + Fail() + Failed() bool +} + // TestingT is a constraint for types that are compatible with [testing.T]. type TestingT[T any] interface { - LoggerT + FailerT SkipNow() Fail() diff --git a/internal/runner/runner.go b/internal/runner/runner.go index 8bec284..ac9412f 100644 --- a/internal/runner/runner.go +++ b/internal/runner/runner.go @@ -17,6 +17,7 @@ type Runner[T TestingT[T]] struct { GenerateOutput OutputGenerator[T] TrimSpace bool // TODO: make this a loader concern BlessStrategy BlessStrategy + PackagePath string } // Run makes the assertions described by all documents within a [TestSuite]. @@ -49,10 +50,10 @@ func (r *Runner[T]) assert(t T, a test.Assertion) { t.Helper() logSection( t, - "INPUT", + fmt.Sprintf("INPUT (%s)", location(a.Input)), a.Input.Data, + "\x1b[2m", r.TrimSpace, - location(a.Input), ) f, err := generateOutput(t, r.GenerateOutput, a.Input, a.Output) @@ -79,8 +80,20 @@ func (r *Runner[T]) assert(t T, a test.Assertion) { return } + messages := []string{ + "\x1b[1mTo run this test again, use:\n\n" + + " \x1b[2m" + r.goTestCommand(t) + "\x1b[0m", + } + if diff == nil { - logSection(t, "OUTPUT", a.Output.Data, r.TrimSpace, location(a.Output)) + logSection( + t, + fmt.Sprintf("OUTPUT (%s)", location(a.Output)), + a.Output.Data, + "\x1b[33;2m", + r.TrimSpace, + messages..., + ) return } @@ -90,9 +103,40 @@ func (r *Runner[T]) assert(t T, a test.Assertion) { return } - logSection(t, "OUTPUT DIFF", diff, true) - r.BlessStrategy.bless(t, a, f) - t.Fail() + switch r.BlessStrategy { + case BlessAvailable: + t.Fail() + messages = append( + messages, + "\x1b[1mTo \x1b[33maccept this output as correct\x1b[37m from now on, add the \x1b[33m-aureus.bless\x1b[37m flag:\n\n"+ + " \x1b[2m"+r.goTestCommand(t)+" -aureus.bless\x1b[0m", + ) + + case BlessDisabled: + t.Fail() + + case BlessEnabled: + if err := bless(a, f); err != nil { + t.Log("unable to bless output:", err) + t.Fail() + return + } + + messages = append( + messages, + "The current \x1b[33moutput has been blessed\x1b[0m. Future runs will consider this output correct.", + ) + } + + logSection( + t, + "OUTPUT DIFF", + diff, + "", + true, + messages..., + ) + } func location(c test.Content) string { @@ -111,14 +155,17 @@ func log(t LoggerT, fn func(w *strings.Builder)) { func logSection( t LoggerT, - name string, - data []byte, + title string, + body []byte, + bodyANSI string, trimSpace bool, - extra ...any, + messages ...string, ) { t.Helper() log(t, func(w *strings.Builder) { + w.WriteString("\x1b[0m") + w.WriteString("\n") w.WriteString("\n") @@ -127,15 +174,7 @@ func logSection( w.WriteString("\x1b[7m") // inverse w.WriteString(" ") - w.WriteString(name) - - if len(extra) > 0 { - w.WriteString(" (") - for _, v := range extra { - fmt.Fprint(w, v) - } - w.WriteByte(')') - } + w.WriteString(title) w.WriteString(" ") w.WriteString("\x1b[27m") // reset inverse @@ -144,17 +183,25 @@ func logSection( w.WriteString("\x1b[1m│\x1b[0m\n") if trimSpace { - data = bytes.TrimSpace(data) + body = bytes.TrimSpace(body) } - for _, line := range bytes.Split(data, newLine) { + for _, line := range bytes.Split(body, newLine) { w.WriteString("\x1b[1m│\x1b[0m ") + w.WriteString(bodyANSI) w.Write(line) - w.WriteByte('\n') + w.WriteString("\x1b[0m\n") } w.WriteString("\x1b[1m│\x1b[0m\n") w.WriteString("\x1b[1m╰────\x1b[0m────\x1b[2m──┈\x1b[0m\n") + + for _, t := range messages { + w.WriteString("\n") + w.WriteString("\x1b[33m✦\x1b[0m ") + w.WriteString(t) + w.WriteString("\x1b[0m\n") + } }) } @@ -180,3 +227,8 @@ func computeDiff( return diff.ColorDiff(wantLoc, wantData, gotLoc, gotData), nil } + +var ( + separator = strings.Repeat("=", 10) + newLine = []byte("\n") +) diff --git a/internal/runner/runner_test.go b/internal/runner/runner_test.go index b8ea7ad..da3cb3a 100644 --- a/internal/runner/runner_test.go +++ b/internal/runner/runner_test.go @@ -28,7 +28,7 @@ func TestRunner(t *testing.T) { ) error { return prettyPrint(in, out) }, - BlessStrategy: &BlessDisabled{}, + BlessStrategy: BlessDisabled, } runner.Run(t, tst) @@ -49,7 +49,7 @@ func TestRunner(t *testing.T) { ) error { return prettyPrint(in, out) }, - BlessStrategy: &BlessDisabled{}, + BlessStrategy: BlessDisabled, } x := &testingT{T: t} diff --git a/internal/runner/shell.go b/internal/runner/shell.go new file mode 100644 index 0000000..1c79ef7 --- /dev/null +++ b/internal/runner/shell.go @@ -0,0 +1,44 @@ +package runner + +import ( + "fmt" + "regexp" + "strings" +) + +func (r *Runner[T]) goTestCommand(t LoggerT) string { + pkg := "./..." + if r.PackagePath != "" { + pkg = shellQuote(r.PackagePath) + } + + return fmt.Sprintf( + "go test %s -run %s -v -count 1", + pkg, + shellQuote(testNamePattern(t)), + ) +} + +func testNamePattern(t LoggerT) string { + atoms := strings.Split(t.Name(), "/") + for i, atom := range atoms { + atoms[i] = "^" + regexp.QuoteMeta(atom) + "$" + } + return strings.Join(atoms, "/") +} + +func shellQuote(s string) string { + var w strings.Builder + w.WriteByte('\'') + + for _, r := range s { + if r == '\'' { + w.WriteString("'\"'\"'") + } else { + w.WriteRune(r) + } + } + + w.WriteByte('\'') + return w.String() +} diff --git a/run.go b/run.go index ebbc6d0..2d65f72 100644 --- a/run.go +++ b/run.go @@ -38,12 +38,10 @@ func Run[T runner.TestingT[T]]( t.Helper() opts := runOptions{ - Dir: "./testdata", - Recursive: true, - TrimSpace: true, - BlessStrategy: &runner.BlessAvailable{ - PackagePath: guessPackagePath(), - }, + Dir: "./testdata", + Recursive: true, + TrimSpace: true, + BlessStrategy: runner.BlessAvailable, } if cliflags.Get().Bless { @@ -76,6 +74,7 @@ func Run[T runner.TestingT[T]]( }, TrimSpace: opts.TrimSpace, BlessStrategy: opts.BlessStrategy, + PackagePath: guessPackagePath(), } tests := test.Merge(fileTests, markdownTests) @@ -133,9 +132,9 @@ func TrimSpace(on bool) RunOption { func Bless(on bool) RunOption { return func(o *runOptions) { if on { - o.BlessStrategy = &runner.BlessEnabled{} + o.BlessStrategy = runner.BlessEnabled } else { - o.BlessStrategy = &runner.BlessDisabled{} + o.BlessStrategy = runner.BlessDisabled } } }