diff --git a/mod/tigron/.golangci.yml b/mod/tigron/.golangci.yml index 22d2a86dd71..ab1817e4d1a 100644 --- a/mod/tigron/.golangci.yml +++ b/mod/tigron/.golangci.yml @@ -37,6 +37,7 @@ linters: - cyclop # provided by revive - exhaustruct # does not serve much of a purpose - errcheck # provided by revive + - errchkjson # forces handling of json err (eg: prevents _), which is too much - forcetypeassert # provided by revive - funlen # provided by revive - gocognit # provided by revive @@ -65,7 +66,7 @@ linters: arguments: [60] - name: function-length # Default is 50, 75 - arguments: [80, 180] + arguments: [80, 200] - name: cyclomatic # Default is 10 arguments: [30] @@ -93,11 +94,17 @@ linters: files: - $all allow: + # Allowing go standard library and tigron itself - $gostd - github.com/containerd/nerdctl/mod/tigron + # We use creack as our base for pty - github.com/creack/pty + # Used for display width computation in internal/formatter + - golang.org/x/text/width + # errgroups and term (make raw) are used by internal/pipes - golang.org/x/sync - golang.org/x/term + # EXPERIMENTAL: for go routines leakage detection - go.uber.org/goleak staticcheck: checks: diff --git a/mod/tigron/expect/comparators.go b/mod/tigron/expect/comparators.go index d7f944f32a8..7bf87803f41 100644 --- a/mod/tigron/expect/comparators.go +++ b/mod/tigron/expect/comparators.go @@ -30,11 +30,11 @@ import ( // All can be used as a parameter for expected.Output to group a set of comparators. func All(comparators ...test.Comparator) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() for _, comparator := range comparators { - comparator(stdout, info, t) + comparator(stdout, "", t) } } } @@ -42,47 +42,47 @@ func All(comparators ...test.Comparator) test.Comparator { // Contains can be used as a parameter for expected.Output and ensures a comparison string is found contained in the // output. func Contains(compare string) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() - assertive.Contains(assertive.WithFailLater(t), stdout, compare, info) + assertive.Contains(assertive.WithFailLater(t), stdout, compare, "Inspecting output (contains)") } } // DoesNotContain is to be used for expected.Output to ensure a comparison string is NOT found in the output. func DoesNotContain(compare string) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() - assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, info) + assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, "Inspecting output (does not contain)") } } // Equals is to be used for expected.Output to ensure it is exactly the output. func Equals(compare string) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() - assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, info) + assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, "Inspecting output (equals)") } } // Match is to be used for expected.Output to ensure we match a regexp. func Match(reg *regexp.Regexp) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() - assertive.Match(assertive.WithFailLater(t), stdout, reg, info) + assertive.Match(assertive.WithFailLater(t), stdout, reg, "Inspecting output (match)") } } // JSON allows to verify that the output can be marshalled into T, and optionally can be further verified by a provided // method. func JSON[T any](obj T, verifier func(T, string, tig.T)) test.Comparator { - return func(stdout, info string, t *testing.T) { + return func(stdout, _ string, t *testing.T) { t.Helper() err := json.Unmarshal([]byte(stdout), &obj) - assertive.ErrorIsNil(assertive.WithFailLater(t), err, "failed to unmarshal JSON from stdout") + assertive.ErrorIsNil(assertive.WithSilentSuccess(t), err, "Unmarshalling JSON from stdout must succeed") if verifier != nil && err == nil { - verifier(obj, info, t) + verifier(obj, "Inspecting output (JSON)", t) } } } diff --git a/mod/tigron/go.mod b/mod/tigron/go.mod index 6979d174148..7d355696bef 100644 --- a/mod/tigron/go.mod +++ b/mod/tigron/go.mod @@ -5,8 +5,9 @@ go 1.23.0 require ( github.com/creack/pty v1.1.24 go.uber.org/goleak v1.3.0 - golang.org/x/sync v0.12.0 + golang.org/x/sync v0.13.0 golang.org/x/term v0.30.0 + golang.org/x/text v0.24.0 ) require golang.org/x/sys v0.31.0 // indirect diff --git a/mod/tigron/go.sum b/mod/tigron/go.sum index b6ba6f6badb..fdf139e7e15 100644 --- a/mod/tigron/go.sum +++ b/mod/tigron/go.sum @@ -8,11 +8,13 @@ github.com/stretchr/testify v1.8.0 h1:pSgiaMZlXftHpm5L7V1+rVB+AZJydKsMxsQBIJw4PK github.com/stretchr/testify v1.8.0/go.mod h1:yNjHg4UonilssWZ8iaSj1OCr/vHnekPRkoO+kdMU+MU= go.uber.org/goleak v1.3.0 h1:2K3zAYmnTNqV73imy9J1T3WC+gmCePx2hEGkimedGto= go.uber.org/goleak v1.3.0/go.mod h1:CoHD4mav9JJNrW/WLlf7HGZPjdw8EucARQHekz1X6bE= -golang.org/x/sync v0.12.0 h1:MHc5BpPuC30uJk597Ri8TV3CNZcTLu6B6z4lJy+g6Jw= -golang.org/x/sync v0.12.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= +golang.org/x/sync v0.13.0 h1:AauUjRAJ9OSnvULf/ARrrVywoJDy0YS2AwQ98I37610= +golang.org/x/sync v0.13.0/go.mod h1:1dzgHSNfp02xaA81J2MS99Qcpr2w7fw1gpm99rleRqA= golang.org/x/sys v0.31.0 h1:ioabZlmFYtWhL+TRYpcnNlLwhyxaM9kWTDEmfnprqik= golang.org/x/sys v0.31.0/go.mod h1:BJP2sWEmIv4KK5OTEluFJCKSidICx8ciO85XgH3Ak8k= golang.org/x/term v0.30.0 h1:PQ39fJZ+mfadBm0y5WlL4vlM7Sx1Hgf13sMIY2+QS9Y= golang.org/x/term v0.30.0/go.mod h1:NYYFdzHoI5wRh/h5tDMdMqCqPJZEuNqVR5xJLd/n67g= +golang.org/x/text v0.24.0 h1:dd5Bzh4yt5KYA8f9CJHCP4FB4D51c2c6JvN37xJJkJ0= +golang.org/x/text v0.24.0/go.mod h1:L8rBsPeo2pSS+xqN0d5u2ikmjtmoJbDBT1b7nHvFCdU= gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA= gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM= diff --git a/mod/tigron/internal/assertive/assertive.go b/mod/tigron/internal/assertive/assertive.go index 735c58c52f9..bc9b2df4563 100644 --- a/mod/tigron/internal/assertive/assertive.go +++ b/mod/tigron/internal/assertive/assertive.go @@ -31,12 +31,12 @@ import ( "github.com/containerd/nerdctl/mod/tigron/tig" ) -// TODO: once debugging output will be cleaned-up, reintroduce hexdump. - const ( + markLineLength = 20 expectedSuccessDecorator = "โœ…๏ธ does verify:\t\t" - expectedFailDecorator = "โŒ does not verify:\t" + expectedFailDecorator = "โŒ FAILED!\t\t" receivedDecorator = "๐Ÿ‘€ testing:\t\t" + annotationDecorator = "๐Ÿ–Š๏ธ" hyperlinkDecorator = "๐Ÿ”—" ) @@ -163,8 +163,8 @@ func True(testing tig.T, comp bool, msg ...string) bool { // WithFailLater will allow an assertion to not fail the test immediately. // Failing later is necessary when asserting inside go routines, and also if you want many -// successive asserts to all -// evaluate instead of stopping at the first failing one. +// successive asserts to all evaluate instead of stopping at the first failing one. +// FIXME: it should be possible to have both WithFailLater and WithSilentSuccess at the same time. func WithFailLater(t tig.T) tig.T { return &failLater{ t, @@ -205,49 +205,76 @@ func evaluate(testing tig.T, isSuccess bool, actual, expected any, msg ...string func decorate(testing tig.T, isSuccess bool, actual, expected any, msg ...string) { testing.Helper() - header := "\t" + if _, ok := testing.(*silentSuccess); !isSuccess || !ok { + head := strings.Repeat("<", markLineLength) + footer := strings.Repeat(">", markLineLength) + header := "\t" - hyperlink := getTopFrameFile() - if hyperlink != "" { - msg = append([]string{hyperlink + "\n"}, msg...) - } + custom := fmt.Sprintf("\t%s %s", annotationDecorator, strings.Join(msg, "\n")) - msg = append(msg, fmt.Sprintf("\t%s`%v`", receivedDecorator, actual)) + msg = append([]string{"", head}, custom) - if isSuccess { - msg = append(msg, - fmt.Sprintf("\t%s%v", expectedSuccessDecorator, expected), - ) - } else { - msg = append(msg, - fmt.Sprintf("\t%s%v", expectedFailDecorator, expected), - ) - } + msg = append([]string{getTopFrameFile()}, msg...) - if _, ok := testing.(*silentSuccess); !isSuccess || !ok { - testing.Log(header + strings.Join(msg, "\n") + "\n") + msg = append(msg, fmt.Sprintf("\t%s`%v`", receivedDecorator, actual)) + + if isSuccess { + msg = append(msg, + fmt.Sprintf("\t%s%v", expectedSuccessDecorator, expected), + ) + } else { + msg = append(msg, + fmt.Sprintf("\t%s%v", expectedFailDecorator, expected), + ) + } + + testing.Log(header + strings.Join(msg, "\n") + "\n" + footer + "\n") } } +// XXX FIXME #expert +// Because of how golang testing works, the upper frame is the one from where t.Run is being called, +// as (presumably) the passed function is starting with its own stack in a go routine. +// In the case of subtests, t.Run being called from inside Tigron will make it so that the top frame +// is case.go around line 233 (where we call Command.Run(), which is the one calling assertive). +// To possibly address this: +// plan a. just drop entirely OSC8 links and source extracts and trash all of this +// plan b. get the top frame from the root test, and pass it to subtests on a custom property, the somehow into here +// plan c. figure out a hack to call t.Run from the test file without ruining the Tigron UX +// Dereference t.Run? Return a closure to be called from the top? w/o disabling inlining in the right place? +// Short term, blacklisting /tigron (and /nerdtest) will at least prevent the wrong links from appearing in the output. func getTopFrameFile() string { - // Get the frames. + // Get the frames. Skip the first two frames - current one and caller. //nolint:mnd // Whatever mnd... - pc := make([]uintptr, 20) + pc := make([]uintptr, 40) //nolint:mnd // Whatever mnd... n := runtime.Callers(2, pc) callersFrames := runtime.CallersFrames(pc[:n]) - var file string + var ( + file string + lineNumber int + frame runtime.Frame + ) - var lineNumber int + more := true + for more { + frame, more = callersFrames.Next() - var frame runtime.Frame - for range 20 { - frame, _ = callersFrames.Next() + // Once we are in the go main stack, bail out if !strings.Contains(frame.Function, "/") { break } + // XXX see note above + if strings.Contains(frame.File, "/tigron") { + continue + } + + if strings.Contains(frame.File, "/nerdtest") { + continue + } + file = frame.File lineNumber = frame.Line } @@ -282,6 +309,6 @@ func getTopFrameFile() string { return hyperlinkDecorator + " " + (&formatter.OSC8{ Text: line, Location: "file://" + file, - Line: frame.Line, + Line: lineNumber, }).String() } diff --git a/mod/tigron/internal/formatter/formatter.go b/mod/tigron/internal/formatter/formatter.go index 765a3be575f..43ce9fe56c4 100644 --- a/mod/tigron/internal/formatter/formatter.go +++ b/mod/tigron/internal/formatter/formatter.go @@ -19,77 +19,99 @@ package formatter import ( "fmt" "strings" - "unicode/utf8" + + "golang.org/x/text/width" ) const ( maxLineLength = 110 maxLines = 100 kMaxLength = 7 + spacer = " " ) -func chunk(s string, length int) []string { - var chunks []string - - lines := strings.Split(s, "\n") - - for x := 0; x < maxLines && x < len(lines); x++ { - line := lines[x] - if utf8.RuneCountInString(line) < length { - chunks = append(chunks, line) - - continue - } - - for index := 0; index < utf8.RuneCountInString(line); index += length { - end := index + length - if end > utf8.RuneCountInString(line) { - end = utf8.RuneCountInString(line) - } - - chunks = append(chunks, string([]rune(line)[index:end])) - } - } - - if len(chunks) == maxLines { - chunks = append(chunks, "...") - } - - return chunks -} - -// Table formats a `n x 2` dataset into a series of rows. -// FIXME: the problem with full-width emoji is that they are going to eff-up the maths and display -// here... -// Maybe the csv writer could be cheat-used to get the right widths. +// Table formats a `n x 2` dataset into a series of n rows by 2 columns. // //nolint:mnd // Too annoying -func Table(data [][]any) string { +func Table(data [][]any, mark string) string { var output string for _, row := range data { key := fmt.Sprintf("%v", row[0]) value := strings.ReplaceAll(fmt.Sprintf("%v", row[1]), "\t", " ") - output += fmt.Sprintf("+%s+\n", strings.Repeat("-", maxLineLength-2)) - - if utf8.RuneCountInString(key) > kMaxLength { - key = string([]rune(key)[:kMaxLength-3]) + "..." - } + output += fmt.Sprintf("+%s+\n", strings.Repeat(mark, maxLineLength-2)) - for _, line := range chunk(value, maxLineLength-kMaxLength-7) { + for _, line := range chunk(value, maxLineLength-kMaxLength-7, maxLines) { output += fmt.Sprintf( - "| %-*s | %-*s |\n", - kMaxLength, - key, - maxLineLength-kMaxLength-7, + "| %s | %s |\n", + // Keys longer than one line of kMaxLength will be striped to one line + chunk(key, kMaxLength, 1)[0], line, ) key = "" } } - output += fmt.Sprintf("+%s+", strings.Repeat("-", maxLineLength-2)) + output += fmt.Sprintf("+%s+", strings.Repeat(mark, maxLineLength-2)) return output } + +// chunk does take a string and split it in lines of maxLength size, accounting for characters display width. +func chunk(s string, maxLength, maxLines int) []string { + chunks := []string{} + + runes := []rune(s) + + size := 0 + start := 0 + + for index := range runes { + var segment string + + switch width.LookupRune(runes[index]).Kind() { + case width.EastAsianWide, width.EastAsianFullwidth: + size += 2 + case width.EastAsianAmbiguous, width.Neutral, width.EastAsianHalfwidth, width.EastAsianNarrow: + size++ + default: + size++ + } + + switch { + case runes[index] == '\n': + // Met a line-break. Pad to size (removing the line break) + segment = string(runes[start:index]) + segment += strings.Repeat(spacer, maxLength-size+1) + start = index + 1 + size = 0 + case size == maxLength: + // Line is full. Add the segment. + segment = string(runes[start : index+1]) + start = index + 1 + size = 0 + case size > maxLength: + // Last char was double width. Push it back to next line, and pad with a single space. + segment = string(runes[start:index]) + spacer + start = index + size = 2 + case index == len(runes)-1: + // End of string. Pad it to size. + segment = string(runes[start : index+1]) + segment += strings.Repeat(spacer, maxLength-size) + default: + continue + } + + chunks = append(chunks, segment) + } + + if len(chunks) > maxLines { + chunks = append(chunks[0:maxLines], "...") + } else if len(chunks) == 0 { + chunks = []string{strings.Repeat(spacer, maxLength)} + } + + return chunks +} diff --git a/mod/tigron/internal/mimicry/print.go b/mod/tigron/internal/mimicry/print.go index 96c333c63da..2f1c357ca4e 100644 --- a/mod/tigron/internal/mimicry/print.go +++ b/mod/tigron/internal/mimicry/print.go @@ -40,7 +40,7 @@ func PrintCall(call *Call) string { } output := []string{ - formatter.Table(debug), + formatter.Table(debug, "-"), sectionSeparator, } diff --git a/mod/tigron/test/case.go b/mod/tigron/test/case.go index 3ec3574a4d7..6a703e964a0 100644 --- a/mod/tigron/test/case.go +++ b/mod/tigron/test/case.go @@ -17,10 +17,13 @@ package test import ( + "encoding/json" + "fmt" "slices" "testing" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" + "github.com/containerd/nerdctl/mod/tigron/internal/formatter" ) // Case describes an entire test-case, including data, setup and cleanup routines, command and @@ -62,9 +65,15 @@ type Case struct { parent *Case } +const ( + startDecorator = "๐Ÿš€" + cleanDecorator = "๐Ÿงฝ" + setupDecorator = "๐Ÿ—" + subinDecorator = "โคต๏ธ" + suboutDecorator = "โ†ฉ๏ธ" +) + // Run prepares and executes the test, and any possible subtests. -// -//nolint:gocognit func (test *Case) Run(t *testing.T) { t.Helper() // Run the test @@ -72,17 +81,17 @@ func (test *Case) Run(t *testing.T) { testRun := func(subT *testing.T) { subT.Helper() - assertive.True(subT, test.t == nil, "You cannot run a test multiple times") + silentT := assertive.WithSilentSuccess(subT) + + assertive.True(silentT, test.t == nil, "You cannot run a test multiple times") + assertive.True(silentT, test.Description != "" || test.parent == nil, + "A subtest description cannot be empty") + assertive.True(silentT, test.Command == nil || test.Expected != nil, + "Expectations for a test command cannot be nil. You may want to use `Setup` instead"+ + "of `Command`.") // Attach testing.T test.t = subT - assertive.True( - test.t, - test.Description != "" || test.parent == nil, - "A test description cannot be empty", - ) - assertive.True(test.t, test.Command == nil || test.Expected != nil, - "Expectations for a test command cannot be nil. You may want to use Setup instead.") // Ensure we have env if test.Env == nil { @@ -168,49 +177,83 @@ func (test *Case) Run(t *testing.T) { } // Execute cleanups now - test.t.Log("") - test.t.Log("======================== Pre-test cleanup ========================") - - for _, cleanup := range cleanups { - cleanup(test.Data, test.helpers) - } - - // Register the cleanups, in reverse - test.t.Cleanup(func() { - test.t.Log("") - test.t.Log("======================== Post-test cleanup ========================") - - slices.Reverse(cleanups) + if len(cleanups) > 0 { + test.t.Log( + "\n\n" + formatter.Table( + [][]any{{cleanDecorator, fmt.Sprintf("%q: initial cleanup", test.t.Name())}}, + "=", + ) + "\n", + ) for _, cleanup := range cleanups { cleanup(test.Data, test.helpers) } - }) - // Run the setups - test.t.Log("") - test.t.Log("======================== Test setup ========================") + // Register the cleanups, in reverse + test.t.Cleanup(func() { + test.t.Helper() + test.t.Log( + "\n\n" + formatter.Table( + [][]any{{cleanDecorator, fmt.Sprintf("%q: post-cleanup", test.t.Name())}}, + "=", + ) + "\n", + ) + + slices.Reverse(cleanups) - for _, setup := range setups { - setup(test.Data, test.helpers) + for _, cleanup := range cleanups { + cleanup(test.Data, test.helpers) + } + }) + } + + // Run the setups + if len(setups) > 0 { + test.t.Log( + "\n\n" + formatter.Table( + [][]any{{setupDecorator, fmt.Sprintf("%q: setup", test.t.Name())}}, + "=", + ) + "\n", + ) + + for _, setup := range setups { + setup(test.Data, test.helpers) + } } // Run the command if any, with expectations // Note: if we have a command, we already know we DO have Expected - test.t.Log("") - test.t.Log("======================== Test Run ========================") - if test.Command != nil { - test.Command(test.Data, test.helpers).Run(test.Expected(test.Data, test.helpers)) + cmd := test.Command(test.Data, test.helpers) + + debugConfig, _ := json.MarshalIndent(test.Config.(*config).config, "", " ") + debugData, _ := json.MarshalIndent(test.Data.(*data).labels, "", " ") + + test.t.Log( + "\n\n" + formatter.Table( + [][]any{ + {startDecorator, fmt.Sprintf("%q: starting test!", test.t.Name())}, + {"cwd", test.Data.TempDir()}, + {"config", string(debugConfig)}, + {"data", string(debugData)}, + }, + "=", + ) + "\n", + ) + + cmd.Run(test.Expected(test.Data, test.helpers)) } - // Now go for the subtests - test.t.Log("") - test.t.Log("======================== Processing subtests ========================") + if len(test.SubTests) > 0 { + // Now go for the subtests + test.t.Logf("\n%s๏ธ %q: into subtests prep", subinDecorator, test.t.Name()) + + for _, subTest := range test.SubTests { + subTest.parent = test + subTest.Run(test.t) + } - for _, subTest := range test.SubTests { - subTest.parent = test - subTest.Run(test.t) + test.t.Logf("\n%s๏ธ %q: done with subtests prep", suboutDecorator, test.t.Name()) } } diff --git a/mod/tigron/test/command.go b/mod/tigron/test/command.go index 67289079867..0c1970d8d81 100644 --- a/mod/tigron/test/command.go +++ b/mod/tigron/test/command.go @@ -14,14 +14,14 @@ limitations under the License. */ -//nolint:revive +//revive:disable:exported,package-comments,add-constant // TODO. package test import ( "context" - "fmt" "io" "os" + "strconv" "strings" "testing" "time" @@ -29,9 +29,17 @@ import ( "github.com/containerd/nerdctl/mod/tigron/internal" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" "github.com/containerd/nerdctl/mod/tigron/internal/com" + "github.com/containerd/nerdctl/mod/tigron/internal/formatter" ) -const defaultExecutionTimeout = 3 * time.Minute +const ( + defaultExecutionTimeout = 3 * time.Minute + commandDecorator = "โš™๏ธ" + errorDecorator = "๐Ÿšซ" + exitDecorator = "โš ๏ธ" + stdoutDecorator = "๐ŸŸข" + stderrDecorator = "๐ŸŸ " +) // CustomizableCommand is an interface meant for people who want to heavily customize the base // command of their test case. @@ -71,7 +79,6 @@ type CustomizableCommand interface { read(key ConfigKey) ConfigValue } -//nolint:ireturn func NewGenericCommand() CustomizableCommand { genericCom := &GenericCommand{ Env: map[string]string{}, @@ -151,49 +158,56 @@ func (gc *GenericCommand) Signal(sig os.Signal) error { } func (gc *GenericCommand) Run(expect *Expected) { - if gc.t != nil { - gc.t.Helper() - } + gc.t.Helper() + + var debug [][]any if !gc.async { _ = gc.cmd.Run(context.WithValue(context.Background(), com.LoggerKey, gc.t)) } + debug = append(debug, + []any{"โžก๏ธ", commandDecorator + " " + gc.cmd.Binary + " " + strings.Join(gc.cmd.Args, " ")}, + ) + + // Wait for the command and if Err is not nil, append it to the debug information result, err := gc.cmd.Wait() + if err != nil { + debug = append(debug, []any{"", errorDecorator + " " + err.Error()}) + } + + // If we were requested to perform expectation matching, add non-empty debugging information if result != nil { gc.rawStdErr = result.Stderr + + if result.ExitCode != 0 { + debug = append(debug, []any{"", exitDecorator + " " + strconv.Itoa(result.ExitCode)}) + } + + if result.Stdout != "" { + debug = append(debug, []any{"", stdoutDecorator + " " + result.Stdout}) + } + + if result.Stderr != "" { + debug = append(debug, []any{"", stderrDecorator + " " + result.Stderr}) + } + + if result.Signal != nil { + debug = append(debug, []any{"Signal", result.Signal.String()}) + } + + debug = append(debug, + []any{"Limit", gc.cmd.Timeout}, + []any{"Environ", strings.Join(result.Environ, "\n")}, + ) } - // Check our expectations, if any + // Print the command info + gc.t.Log("\n\n" + formatter.Table(debug, "-") + "\n") + + // Now, check our expectations, if any if expect != nil { - // Build the debug string - separator := "=================================" - debugCommand := gc.cmd.Binary + " " + strings.Join(gc.cmd.Args, " ") - debugTimeout := gc.cmd.Timeout - debugWD := gc.cmd.WorkingDir - - // FIXME: this is ugly af. Do better. - debug := fmt.Sprintf( - "\n%s\n| Command:\t%s\n| Working Dir:\t%s\n| Timeout:\t%s\n%s\n"+ - "%s\n%s\n| Stderr:\n%s\n%s\n%s\n| Stdout:\n%s\n%s\n%s\n| Exit Code: %d\n| Signaled: %v\n| Err: %v\n%s", - separator, - debugCommand, - debugWD, - debugTimeout, - separator, - "\t"+strings.Join(result.Environ, "\n\t"), - separator, - separator, - result.Stderr, - separator, - separator, - result.Stdout, - separator, - result.ExitCode, - result.Signal, - err, - separator, - ) + assertT := assertive.WithSilentSuccess(gc.t) // ExitCode goes first switch expect.ExitCode { @@ -203,44 +217,67 @@ func (gc *GenericCommand) Run(expect *Expected) { // ExitCodeGenericFail means we expect an error (excluding timeout, cancellation, // signalling). assertive.ErrorIs( - gc.t, + assertT, err, com.ErrExecutionFailed, - "Command should have failed", - debug, + "Command should fail", ) case internal.ExitCodeTimeout: assertive.ErrorIs( gc.t, err, com.ErrTimeout, - "Command should have timed out", - debug, + "Command should time-out", ) case internal.ExitCodeSignaled: assertive.ErrorIs( gc.t, err, com.ErrSignaled, - "Command should have been signaled", - debug, + "Command should get signaled", ) case internal.ExitCodeSuccess: - assertive.ErrorIsNil(gc.t, err, "Command should have succeeded", debug) + assertive.ErrorIsNil( + assertT, + err, + "Command should succeed", + ) default: - assertive.IsEqual(gc.t, expect.ExitCode, result.ExitCode, - fmt.Sprintf("Expected exit code: %d\n", expect.ExitCode), debug) + exc := -1 + if result != nil { + exc = result.ExitCode + } + + assertive.IsEqual( + assertT, + expect.ExitCode, + exc, + "Exit code should match expectation", + ) } + // Switch to fail later mode so that we get ALL subsequent asserts failures from there on. + // Also does allow using assert(ive) in go routines. + assertT = assertive.WithFailLater(gc.t) + // Range through the expected errors and confirm they are seen on stderr for _, expectErr := range expect.Errors { - assertive.Contains(gc.t, result.Stderr, expectErr.Error(), - fmt.Sprintf("Expected error: %q to be found in stderr\n", expectErr.Error()), debug) + assertive.Contains( + assertT, + gc.rawStdErr, + expectErr.Error(), + "Stderr should contain expected error", + ) } // Finally, check the output if we are asked to + // FIXME: we cannot pass on assertT further for now until we move to tig.T if expect.Output != nil { - expect.Output(result.Stdout, debug, gc.t) + expect.Output( + result.Stdout, + "", + gc.t, + ) } } } @@ -263,7 +300,6 @@ func (gc *GenericCommand) withConfig(config Config) { gc.Config = config } -//nolint:ireturn func (gc *GenericCommand) Clone() TestableCommand { // Copy the command and return a new one - with almost everything from the parent command clone := *gc @@ -287,7 +323,6 @@ func (gc *GenericCommand) T() *testing.T { return gc.t } -//nolint:ireturn func (gc *GenericCommand) clear() TestableCommand { comcopy := *gc // Reset internal command diff --git a/mod/tigron/test/helpers.go b/mod/tigron/test/helpers.go index 681226047fa..c148be6e5ac 100644 --- a/mod/tigron/test/helpers.go +++ b/mod/tigron/test/helpers.go @@ -32,6 +32,7 @@ type helpersInternal struct { // Ensure will run a command and make sure it is successful. func (help *helpersInternal) Ensure(args ...string) { + help.t.Helper() help.Command(args...).Run(&Expected{ ExitCode: internal.ExitCodeSuccess, }) @@ -39,6 +40,7 @@ func (help *helpersInternal) Ensure(args ...string) { // Anyhow will run a command regardless of outcome (may or may not fail). func (help *helpersInternal) Anyhow(args ...string) { + help.t.Helper() help.Command(args...).Run(&Expected{ ExitCode: internal.ExitCodeNoCheck, }) @@ -46,6 +48,7 @@ func (help *helpersInternal) Anyhow(args ...string) { // Fail will run a command and make sure it does fail. func (help *helpersInternal) Fail(args ...string) { + help.t.Helper() help.Command(args...).Run(&Expected{ ExitCode: internal.ExitCodeGenericFail, }) @@ -55,6 +58,7 @@ func (help *helpersInternal) Fail(args ...string) { func (help *helpersInternal) Capture(args ...string) string { var ret string + help.t.Helper() help.Command(args...).Run(&Expected{ //nolint:thelper Output: func(stdout, _ string, _ *testing.T) { @@ -67,6 +71,7 @@ func (help *helpersInternal) Capture(args ...string) string { // Err will run a command with no expectation and return Stderr. func (help *helpersInternal) Err(args ...string) string { + help.t.Helper() cmd := help.Command(args...) cmd.Run(nil) diff --git a/pkg/testutil/nerdtest/command.go b/pkg/testutil/nerdtest/command.go index b979963d662..dbeed949e7c 100644 --- a/pkg/testutil/nerdtest/command.go +++ b/pkg/testutil/nerdtest/command.go @@ -105,6 +105,7 @@ type nerdCommand struct { } func (nc *nerdCommand) Run(expect *test.Expected) { + nc.T().Helper() nc.prep() if getTarget() == targetDocker { // We are not in the business of testing docker *error* output, so, spay expectation here diff --git a/pkg/testutil/nerdtest/utilities.go b/pkg/testutil/nerdtest/utilities.go index 59311e5fe19..56cae946170 100644 --- a/pkg/testutil/nerdtest/utilities.go +++ b/pkg/testutil/nerdtest/utilities.go @@ -45,6 +45,7 @@ func IsDocker() bool { // InspectContainer is a helper that can be used inside custom commands or Setup func InspectContainer(helpers test.Helpers, name string) dockercompat.Container { + helpers.T().Helper() var dc []dockercompat.Container cmd := helpers.Command("container", "inspect", name) cmd.Run(&test.Expected{ @@ -58,6 +59,7 @@ func InspectContainer(helpers test.Helpers, name string) dockercompat.Container } func InspectVolume(helpers test.Helpers, name string) native.Volume { + helpers.T().Helper() var dc []native.Volume cmd := helpers.Command("volume", "inspect", name) cmd.Run(&test.Expected{ @@ -71,6 +73,7 @@ func InspectVolume(helpers test.Helpers, name string) native.Volume { } func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { + helpers.T().Helper() var dc []dockercompat.Network cmd := helpers.Command("network", "inspect", name) cmd.Run(&test.Expected{ @@ -84,6 +87,7 @@ func InspectNetwork(helpers test.Helpers, name string) dockercompat.Network { } func InspectImage(helpers test.Helpers, name string) dockercompat.Image { + helpers.T().Helper() var dc []dockercompat.Image cmd := helpers.Command("image", "inspect", name) cmd.Run(&test.Expected{ @@ -102,6 +106,7 @@ const ( ) func EnsureContainerStarted(helpers test.Helpers, con string) { + helpers.T().Helper() started := false for i := 0; i < maxRetry && !started; i++ { helpers.Command("container", "inspect", con).