diff --git a/mod/tigron/.golangci.yml b/mod/tigron/.golangci.yml index 4e312afa579..1c17cd11206 100644 --- a/mod/tigron/.golangci.yml +++ b/mod/tigron/.golangci.yml @@ -13,11 +13,23 @@ issues: linters: default: all disable: - - cyclop - - exhaustruct - - funlen - - godox - - nonamedreturns + # These are the linters that we know we do not want + - cyclop # provided by revive + - exhaustruct # does not serve much of a purpose + - funlen # provided by revive + - gocognit # provided by revive + - goconst # provided by revive + - godox # not helpful unless we could downgrade it to warning / info + - ginkgolinter # no ginkgo + - gomodguard # we use depguard instead + - ireturn # too annoying with not enough value + - lll # provided by golines + - nonamedreturns # named returns are occasionally useful + - prealloc # premature optimization + - promlinter # no prometheus + - sloglint # no slog + - testifylint # no testify + - zerologlint # no zerolog settings: depguard: rules: @@ -51,7 +63,7 @@ formatters: gofumpt: extra-rules: true golines: - max-len: 100 + max-len: 120 tab-len: 4 shorten-comments: true enable: diff --git a/mod/tigron/expect/comparators.go b/mod/tigron/expect/comparators.go index 17edc209588..36b09de5e45 100644 --- a/mod/tigron/expect/comparators.go +++ b/mod/tigron/expect/comparators.go @@ -14,13 +14,12 @@ limitations under the License. */ +//revive:disable:package-comments // annoying false positive behavior +//nolint:thelper // FIXME: remove when we move to tig.T package expect import ( - "encoding/hex" - "fmt" "regexp" - "strings" "testing" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" @@ -29,7 +28,6 @@ import ( // All can be used as a parameter for expected.Output to group a set of comparators. func All(comparators ...test.Comparator) test.Comparator { - //nolint:thelper return func(stdout, info string, t *testing.T) { t.Helper() @@ -39,57 +37,35 @@ 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. +// 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 { - //nolint:thelper return func(stdout, info string, t *testing.T) { t.Helper() - assertive.Check(t, strings.Contains(stdout, compare), - fmt.Sprintf("Output does not contain: %q", compare), - info) + assertive.Contains(assertive.WithFailLater(t), stdout, compare, info) } } -// DoesNotContain is to be used for expected.Output to ensure a comparison string is NOT found in -// the output. +// 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 { - //nolint:thelper return func(stdout, info string, t *testing.T) { t.Helper() - assertive.Check(t, !strings.Contains(stdout, compare), - fmt.Sprintf("Output should not contain: %q", compare), info) + assertive.DoesNotContain(assertive.WithFailLater(t), stdout, compare, info) } } // Equals is to be used for expected.Output to ensure it is exactly the output. func Equals(compare string) test.Comparator { - //nolint:thelper return func(stdout, info string, t *testing.T) { t.Helper() - - hexdump := hex.Dump([]byte(stdout)) - assertive.Check( - t, - compare == stdout, - fmt.Sprintf("Output is not equal to: %q", compare), - "\n"+hexdump, - info, - ) + assertive.IsEqual(assertive.WithFailLater(t), stdout, compare, info) } } // Match is to be used for expected.Output to ensure we match a regexp. -// Provisional - expected use, but have not seen it so far. func Match(reg *regexp.Regexp) test.Comparator { - //nolint:thelper return func(stdout, info string, t *testing.T) { t.Helper() - assertive.Check( - t, - reg.MatchString(stdout), - fmt.Sprintf("Output does not match: %q", reg.String()), - info, - ) + assertive.Match(assertive.WithFailLater(t), stdout, reg, info) } } diff --git a/mod/tigron/internal/assertive/assertive.go b/mod/tigron/internal/assertive/assertive.go index 4f0f4d6536a..735c58c52f9 100644 --- a/mod/tigron/internal/assertive/assertive.go +++ b/mod/tigron/internal/assertive/assertive.go @@ -14,156 +14,274 @@ limitations under the License. */ +//revive:disable:add-constant,package-comments package assertive import ( + "bufio" "errors" + "fmt" + "os" + "regexp" + "runtime" "strings" "time" + + "github.com/containerd/nerdctl/mod/tigron/internal/formatter" + "github.com/containerd/nerdctl/mod/tigron/tig" +) + +// TODO: once debugging output will be cleaned-up, reintroduce hexdump. + +const ( + expectedSuccessDecorator = "✅️ does verify:\t\t" + expectedFailDecorator = "❌ does not verify:\t" + receivedDecorator = "👀 testing:\t\t" + hyperlinkDecorator = "🔗" ) -type testingT interface { - Helper() - FailNow() - Fail() - Log(args ...interface{}) +// ErrorIsNil fails a test if err is not nil. +func ErrorIsNil(testing tig.T, err error, msg ...string) { + testing.Helper() + + evaluate(testing, errors.Is(err, nil), err, "is ``", msg...) } -// ErrorIsNil immediately fails a test if err is not nil. -func ErrorIsNil(t testingT, err error, msg ...string) { - t.Helper() +// ErrorIs fails a test if err is not the comparison error. +func ErrorIs(testing tig.T, err, expected error, msg ...string) { + testing.Helper() - if err != nil { - t.Log("expecting nil error, but got:", err) - failNow(t, msg...) - } + evaluate(testing, errors.Is(err, expected), err, fmt.Sprintf("is `%v`", expected), msg...) } -// ErrorIs immediately fails a test if err is not the comparison error. -func ErrorIs(t testingT, err, compErr error, msg ...string) { - t.Helper() +// IsEqual fails a test if the two interfaces are not equal. +func IsEqual[T comparable](testing tig.T, actual, expected T, msg ...string) { + testing.Helper() - if !errors.Is(err, compErr) { - t.Log("expected error to be:", compErr, "- instead it is:", err) - failNow(t, msg...) - } + evaluate(testing, actual == expected, actual, fmt.Sprintf("= `%v`", expected), msg...) } -// IsEqual immediately fails a test if the two interfaces are not equal. -func IsEqual(t testingT, actual, expected interface{}, msg ...string) { - t.Helper() +// IsNotEqual fails a test if the two interfaces are equal. +func IsNotEqual[T comparable](testing tig.T, actual, expected T, msg ...string) { + testing.Helper() - if !isEqual(t, actual, expected) { - t.Log("expected:", actual, " - to be equal to:", expected) - failNow(t, msg...) - } + evaluate(testing, actual != expected, actual, fmt.Sprintf("!= `%v`", expected), msg...) } -// IsNotEqual immediately fails a test if the two interfaces are equal. -func IsNotEqual(t testingT, actual, expected interface{}, msg ...string) { - t.Helper() +// Contains fails a test if the actual string does not contain the other string. +func Contains(testing tig.T, actual, contains string, msg ...string) { + testing.Helper() - if isEqual(t, actual, expected) { - t.Log("expected:", actual, " - to be equal to:", expected) - failNow(t, msg...) - } + evaluate( + testing, + strings.Contains(actual, contains), + actual, + fmt.Sprintf("~= `%v`", contains), + msg...) } -// StringContains immediately fails a test if the actual string does not contain the other string. -func StringContains(t testingT, actual, contains string, msg ...string) { - t.Helper() +// DoesNotContain fails a test if the actual string contains the other string. +func DoesNotContain(testing tig.T, actual, contains string, msg ...string) { + testing.Helper() - if !strings.Contains(actual, contains) { - t.Log("expected:", actual, " - to contain:", contains) - failNow(t, msg...) - } + evaluate( + testing, + !strings.Contains(actual, contains), + actual, + fmt.Sprintf("! ~= `%v`", contains), + msg...) } -// StringDoesNotContain immediately fails a test if the actual string contains the other string. -func StringDoesNotContain(t testingT, actual, contains string, msg ...string) { - t.Helper() +// HasSuffix fails a test if the string does not end with suffix. +func HasSuffix(testing tig.T, actual, suffix string, msg ...string) { + testing.Helper() - if strings.Contains(actual, contains) { - t.Log("expected:", actual, " - to NOT contain:", contains) - failNow(t, msg...) - } + evaluate( + testing, + strings.HasSuffix(actual, suffix), + actual, + fmt.Sprintf("`%v` $", suffix), + msg...) } -// StringHasSuffix immediately fails a test if the string does not end with suffix. -func StringHasSuffix(t testingT, actual, suffix string, msg ...string) { - t.Helper() +// HasPrefix fails a test if the string does not start with prefix. +func HasPrefix(testing tig.T, actual, prefix string, msg ...string) { + testing.Helper() - if !strings.HasSuffix(actual, suffix) { - t.Log("expected:", actual, " - to end with:", suffix) - failNow(t, msg...) - } + evaluate( + testing, + strings.HasPrefix(actual, prefix), + actual, + fmt.Sprintf("^ `%v`", prefix), + msg...) } -// StringHasPrefix immediately fails a test if the string does not start with prefix. -func StringHasPrefix(t testingT, actual, prefix string, msg ...string) { - t.Helper() +// Match fails a test if the string does not match the regexp. +func Match(testing tig.T, actual string, reg *regexp.Regexp, msg ...string) { + testing.Helper() - if !strings.HasPrefix(actual, prefix) { - t.Log("expected:", actual, " - to start with:", prefix) - failNow(t, msg...) - } + evaluate(testing, reg.MatchString(actual), actual, fmt.Sprintf("`%v`", reg), msg...) } -// DurationIsLessThan immediately fails a test if the duration is more than the reference. -func DurationIsLessThan(t testingT, actual, expected time.Duration, msg ...string) { - t.Helper() +// DoesNotMatch fails a test if the string does match the regexp. +func DoesNotMatch(testing tig.T, actual string, reg *regexp.Regexp, msg ...string) { + testing.Helper() - if actual >= expected { - t.Log("expected:", actual, " - to be less than:", expected) - failNow(t, msg...) - } + evaluate(testing, !reg.MatchString(actual), actual, fmt.Sprintf("`%v`", reg), msg...) } -// True immediately fails a test if the boolean is not true... -func True(t testingT, comp bool, msg ...string) bool { - t.Helper() +// IsLessThan fails a test if the actual is more or equal than the reference. +func IsLessThan[T ~int | ~float64 | time.Duration]( + testing tig.T, + actual, expected T, + msg ...string, +) { + testing.Helper() - if !comp { - failNow(t, msg...) - } + evaluate(testing, actual < expected, actual, fmt.Sprintf("< `%v`", expected), msg...) +} + +// IsMoreThan fails a test if the actual is less or equal than the reference. +func IsMoreThan[T ~int | ~float64 | time.Duration]( + testing tig.T, + actual, expected T, + msg ...string, +) { + testing.Helper() + + evaluate(testing, actual > expected, actual, fmt.Sprintf("< `%v`", expected), msg...) +} + +// True fails a test if the boolean is not true... +func True(testing tig.T, comp bool, msg ...string) bool { + testing.Helper() + + evaluate(testing, comp, comp, true, msg...) return comp } -// Check marks a test as failed if the boolean is not true (safe in go routines) -// -//nolint:varnamelen -func Check(t testingT, comp bool, msg ...string) bool { - t.Helper() +// 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. +func WithFailLater(t tig.T) tig.T { + return &failLater{ + t, + } +} - if !comp { - for _, m := range msg { - t.Log(m) +// WithSilentSuccess (used to wrap a *testing.T struct) will not log debugging assertive information +// when the result is +// a success. +// In some cases, this is convenient to avoid crowding the display with successful checks info. +func WithSilentSuccess(t tig.T) tig.T { + return &silentSuccess{ + t, + } +} + +type failLater struct { + tig.T +} +type silentSuccess struct { + tig.T +} + +func evaluate(testing tig.T, isSuccess bool, actual, expected any, msg ...string) { + testing.Helper() + + decorate(testing, isSuccess, actual, expected, msg...) + + if !isSuccess { + if _, ok := testing.(*failLater); ok { + testing.Fail() + } else { + testing.FailNow() } + } +} + +func decorate(testing tig.T, isSuccess bool, actual, expected any, msg ...string) { + testing.Helper() - t.Fail() + header := "\t" + + hyperlink := getTopFrameFile() + if hyperlink != "" { + msg = append([]string{hyperlink + "\n"}, msg...) } - return comp + 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), + ) + } + + if _, ok := testing.(*silentSuccess); !isSuccess || !ok { + testing.Log(header + strings.Join(msg, "\n") + "\n") + } } -//nolint:varnamelen -func failNow(t testingT, msg ...string) { - t.Helper() +func getTopFrameFile() string { + // Get the frames. + //nolint:mnd // Whatever mnd... + pc := make([]uintptr, 20) + //nolint:mnd // Whatever mnd... + n := runtime.Callers(2, pc) + callersFrames := runtime.CallersFrames(pc[:n]) + + var file string - if len(msg) > 0 { - for _, m := range msg { - t.Log(m) + var lineNumber int + + var frame runtime.Frame + for range 20 { + frame, _ = callersFrames.Next() + if !strings.Contains(frame.Function, "/") { + break } + + file = frame.File + lineNumber = frame.Line } - t.FailNow() -} + if file == "" { + return "" + } + + //nolint:gosec // file is coming from runtime frames so, fine + source, err := os.Open(file) + if err != nil { + return "" + } -func isEqual(t testingT, actual, expected interface{}) bool { - t.Helper() + defer func() { + _ = source.Close() + }() + + index := 1 + scanner := bufio.NewScanner(source) + + var line string + + for ; scanner.Err() == nil && index <= lineNumber; index++ { + if !scanner.Scan() { + break + } + + line = strings.Trim(scanner.Text(), "\t ") + } - // FIXME: this is risky and limited. Right now this is fine internally, but do better if this - // becomes public. - return actual == expected + return hyperlinkDecorator + " " + (&formatter.OSC8{ + Text: line, + Location: "file://" + file, + Line: frame.Line, + }).String() } diff --git a/mod/tigron/internal/assertive/assertive_test.go b/mod/tigron/internal/assertive/assertive_test.go index 6d236c0718b..ccb987e3ab3 100644 --- a/mod/tigron/internal/assertive/assertive_test.go +++ b/mod/tigron/internal/assertive/assertive_test.go @@ -14,35 +14,199 @@ limitations under the License. */ +//revive:disable:add-constant package assertive_test import ( "errors" "fmt" + "regexp" "testing" + "time" "github.com/containerd/nerdctl/mod/tigron/internal/assertive" + "github.com/containerd/nerdctl/mod/tigron/internal/mimicry" + "github.com/containerd/nerdctl/mod/tigron/internal/mocks" + "github.com/containerd/nerdctl/mod/tigron/tig" ) -func TestY(t *testing.T) { +func TestAssertivePass(t *testing.T) { t.Parallel() - var err error + var nilErr error + //nolint:err113 // Fine, this is a test + notNilErr := errors.New("some error") - assertive.ErrorIsNil(t, err) + assertive.ErrorIsNil(t, nilErr, "a nil error should pass ErrorIsNil") + assertive.ErrorIs(t, nilErr, nil, "a nil error should pass ErrorIs(err, nil)") + assertive.ErrorIs( + t, + fmt.Errorf("neh %w", notNilErr), + notNilErr, + "an error wrapping another should match with ErrorIs", + ) - //nolint:err113 - someErr := errors.New("test error") + assertive.IsEqual(t, "foo", "foo", "= should work as expected (on string)") + assertive.IsNotEqual(t, "foo", "else", "!= should work as expected (on string)") - err = fmt.Errorf("wrap: %w", someErr) - assertive.ErrorIs(t, err, someErr) + assertive.IsEqual(t, true, true, "= should work as expected (on bool)") + assertive.IsNotEqual(t, true, false, "!= should work as expected (on bool)") - foo := "foo" - assertive.IsEqual(t, foo, "foo") + assertive.IsEqual(t, 1, 1, "= should work as expected (on int)") + assertive.IsNotEqual(t, 1, 0, "!= should work as expected (on int)") - bar := 10 - assertive.IsEqual(t, bar, 10) + assertive.IsEqual(t, -1.0, -1, "= should work as expected (on float)") + assertive.IsNotEqual(t, -1.0, 0, "!= should work as expected (on float)") - baz := true - assertive.IsEqual(t, baz, true) + type foo struct { + name string + } + + assertive.IsEqual(t, foo{}, foo{}, "= should work as expected (on struct)") + assertive.IsEqual( + t, + foo{name: "foo"}, + foo{name: "foo"}, + "= should work as expected (on struct)", + ) + assertive.IsNotEqual( + t, + foo{name: "bar"}, + foo{name: "foo"}, + "!= should work as expected (on struct)", + ) + + assertive.Contains(t, "foo", "o", "⊂ should work") + assertive.DoesNotContain(t, "foo", "a", "¬⊂ should work") + assertive.HasPrefix(t, "foo", "f", "prefix should work") + assertive.HasSuffix(t, "foo", "o", "suffix should work") + assertive.Match(t, "foo", regexp.MustCompile("^[fo]{3,}$"), "match should work") + assertive.DoesNotMatch(t, "foo", regexp.MustCompile("^[abc]{3,}$"), "match should work") + + assertive.True(t, true, "is true should work as expected") + + assertive.IsLessThan(t, time.Minute, time.Hour, "< should work (duration)") + assertive.IsMoreThan(t, time.Minute, time.Second, "< should work (duration)") + assertive.IsLessThan(t, 1, 2, "< should work (int)") + assertive.IsMoreThan(t, 2, 1, "> should work (int)") + assertive.IsLessThan(t, -1.2, 2, "< should work (float)") + assertive.IsMoreThan(t, 2, -1.2, "> should work (float)") +} + +func TestAssertiveFailBehavior(t *testing.T) { + t.Parallel() + + mockT := &mocks.MockT{} + + var nilErr error + //nolint:err113 // Fine, this is a test + notNilErr := errors.New("some error") + + assertive.ErrorIsNil(mockT, notNilErr, "a nil error should pass ErrorIsNil") + assertive.ErrorIs(mockT, notNilErr, nil, "a nil error should pass ErrorIs(err, nil)") + assertive.ErrorIs( + mockT, + fmt.Errorf("neh %w", nilErr), + nilErr, + "an error wrapping another should match with ErrorIs", + ) + + assertive.IsEqual(mockT, "foo", "else", "= should work as expected (on string)") + assertive.IsNotEqual(mockT, "foo", "foo", "!= should work as expected (on string)") + + assertive.IsEqual(mockT, true, false, "= should work as expected (on bool)") + assertive.IsNotEqual(mockT, true, true, "!= should work as expected (on bool)") + + assertive.IsEqual(mockT, 1, 0, "= should work as expected (on int)") + assertive.IsNotEqual(mockT, 1, 1, "!= should work as expected (on int)") + + assertive.IsEqual(mockT, -1.0, 0, "= should work as expected (on float)") + assertive.IsNotEqual(mockT, -1.0, -1, "!= should work as expected (on float)") + + type foo struct { + name string + } + + assertive.IsEqual(mockT, foo{}, foo{name: "foo"}, "= should work as expected (on struct)") + assertive.IsEqual( + mockT, + foo{name: "bar"}, + foo{name: "foo"}, + "= should work as expected (on struct)", + ) + assertive.IsNotEqual( + mockT, + foo{name: ""}, + foo{name: ""}, + "!= should work as expected (on struct)", + ) + + assertive.Contains(mockT, "foo", "a", "⊂ should work") + assertive.DoesNotContain(mockT, "foo", "o", "¬⊂ should work") + assertive.HasPrefix(mockT, "foo", "o", "prefix should work") + assertive.HasSuffix(mockT, "foo", "f", "suffix should work") + assertive.Match(mockT, "foo", regexp.MustCompile("^[abc]{3,}$"), "match should work") + assertive.DoesNotMatch(mockT, "foo", regexp.MustCompile("^[fo]{3,}$"), "match should work") + + assertive.True(mockT, false, "is true should work as expected") + + assertive.IsLessThan(mockT, time.Hour, time.Minute, "< should work (duration)") + assertive.IsMoreThan(mockT, time.Second, time.Minute, "< should work (duration)") + assertive.IsLessThan(mockT, 2, 1, "< should work (int)") + assertive.IsMoreThan(mockT, 1, 2, "> should work (int)") + assertive.IsLessThan(mockT, 2, -1.2, "< should work (float)") + assertive.IsMoreThan(mockT, -1.2, 2, "> should work (float)") + + if len(mockT.Report(tig.T.FailNow)) != 27 { + t.Error("we should have called FailNow as many times as we have asserts here") + } + + if len(mockT.Report(tig.T.Fail)) != 0 { + t.Error("we should NOT have called Fail") + } +} + +func TestAssertiveFailLater(t *testing.T) { + t.Parallel() + + mockT := &mocks.MockT{} + + assertive.True(assertive.WithFailLater(mockT), false, "is true should work as expected") + + if len(mockT.Report(tig.T.FailNow)) != 0 { + t.Log(mimicry.PrintCall(mockT.Report(tig.T.FailNow)[0])) + t.Error("we should NOT have called FailNow") + } + + if len(mockT.Report(tig.T.Fail)) != 1 { + t.Error("we should have called Fail") + } +} + +func TestAssertiveSilentSuccess(t *testing.T) { + t.Parallel() + + mockT := &mocks.MockT{} + + assertive.True(mockT, true, "is true should work as expected") + assertive.True(mockT, false, "is true should work as expected") + + if len(mockT.Report(tig.T.Log)) != 2 { + t.Error("we should have called Log on both success and failure") + } + + mockT.Reset() + + assertive.True(assertive.WithSilentSuccess(mockT), true, "is true should work as expected") + + if len(mockT.Report(tig.T.Log)) != 0 { + t.Log(mimicry.PrintCall(mockT.Report(tig.T.Log)[0])) + t.Error("we should NOT have called Log on success") + } + + assertive.True(assertive.WithSilentSuccess(mockT), false, "is true should work as expected") + + if len(mockT.Report(tig.T.Log)) != 1 { + t.Error("we should still have called Log on failure") + } } diff --git a/mod/tigron/internal/com/command_test.go b/mod/tigron/internal/com/command_test.go index 04034f037b8..2f62f1ef246 100644 --- a/mod/tigron/internal/com/command_test.go +++ b/mod/tigron/internal/com/command_test.go @@ -159,7 +159,7 @@ func TestBasicFail(t *testing.T) { assertive.ErrorIs(t, err, com.ErrExecutionFailed) assertive.IsEqual(t, 127, res.ExitCode) assertive.IsEqual(t, "", res.Stdout) - assertive.StringHasSuffix(t, res.Stderr, "does-not-exist: command not found\n") + assertive.HasSuffix(t, res.Stderr, "does-not-exist: command not found\n") } func TestWorkingDir(t *testing.T) { @@ -187,7 +187,7 @@ func TestWorkingDir(t *testing.T) { t.Skip("skipping last check on windows, see note") } - assertive.StringHasSuffix(t, res.Stdout, dir+"\n") + assertive.HasSuffix(t, res.Stdout, dir+"\n") } func TestEnvBlacklist(t *testing.T) { @@ -206,8 +206,8 @@ func TestEnvBlacklist(t *testing.T) { assertive.ErrorIsNil(t, err) assertive.IsEqual(t, 0, res.ExitCode) - assertive.StringContains(t, res.Stdout, "FOO=BAR") - assertive.StringContains(t, res.Stdout, "FOOBAR=BARBAR") + assertive.Contains(t, res.Stdout, "FOO=BAR") + assertive.Contains(t, res.Stdout, "FOOBAR=BARBAR") command = &com.Command{ Binary: "env", @@ -222,8 +222,8 @@ func TestEnvBlacklist(t *testing.T) { assertive.ErrorIsNil(t, err) assertive.IsEqual(t, res.ExitCode, 0) - assertive.StringDoesNotContain(t, res.Stdout, "FOO=BAR") - assertive.StringContains(t, res.Stdout, "FOOBAR=BARBAR") + assertive.DoesNotContain(t, res.Stdout, "FOO=BAR") + assertive.Contains(t, res.Stdout, "FOOBAR=BARBAR") // On windows, with mingw, SYSTEMROOT,TERM and HOME (possibly others) will be forcefully added // to the environment regardless, so, we can't test "*" blacklist @@ -272,10 +272,10 @@ func TestEnvAdd(t *testing.T) { assertive.ErrorIsNil(t, err) assertive.IsEqual(t, res.ExitCode, 0) - assertive.StringContains(t, res.Stdout, "FOO=REPLACE") - assertive.StringContains(t, res.Stdout, "BAR=NEW") - assertive.StringContains(t, res.Stdout, "BAZ=OLD") - assertive.StringContains(t, res.Stdout, "BLED=EXPLICIT") + assertive.Contains(t, res.Stdout, "FOO=REPLACE") + assertive.Contains(t, res.Stdout, "BAR=NEW") + assertive.Contains(t, res.Stdout, "BAZ=OLD") + assertive.Contains(t, res.Stdout, "BLED=EXPLICIT") } func TestStdoutStderr(t *testing.T) { @@ -322,7 +322,7 @@ func TestTimeoutPlain(t *testing.T) { assertive.IsEqual(t, res.ExitCode, -1) assertive.IsEqual(t, res.Stdout, "one") assertive.IsEqual(t, res.Stderr, "") - assertive.DurationIsLessThan(t, end.Sub(start), 2*time.Second) + assertive.IsLessThan(t, end.Sub(start), 2*time.Second) } func TestTimeoutDelayed(t *testing.T) { @@ -351,7 +351,7 @@ func TestTimeoutDelayed(t *testing.T) { assertive.IsEqual(t, res.ExitCode, -1) assertive.IsEqual(t, res.Stdout, "one") assertive.IsEqual(t, res.Stderr, "") - assertive.DurationIsLessThan(t, end.Sub(start), 2*time.Second) + assertive.IsLessThan(t, end.Sub(start), 2*time.Second) } func TestPTYStdout(t *testing.T) { diff --git a/mod/tigron/internal/formatter/doc.go b/mod/tigron/internal/formatter/doc.go new file mode 100644 index 00000000000..e03daea02b6 --- /dev/null +++ b/mod/tigron/internal/formatter/doc.go @@ -0,0 +1,18 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package formatter provides simple formatting helpers for internal consumption. +package formatter diff --git a/mod/tigron/internal/formatter/formatter.go b/mod/tigron/internal/formatter/formatter.go new file mode 100644 index 00000000000..765a3be575f --- /dev/null +++ b/mod/tigron/internal/formatter/formatter.go @@ -0,0 +1,95 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package formatter + +import ( + "fmt" + "strings" + "unicode/utf8" +) + +const ( + maxLineLength = 110 + maxLines = 100 + kMaxLength = 7 +) + +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. +// +//nolint:mnd // Too annoying +func Table(data [][]any) 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]) + "..." + } + + for _, line := range chunk(value, maxLineLength-kMaxLength-7) { + output += fmt.Sprintf( + "| %-*s | %-*s |\n", + kMaxLength, + key, + maxLineLength-kMaxLength-7, + line, + ) + key = "" + } + } + + output += fmt.Sprintf("+%s+", strings.Repeat("-", maxLineLength-2)) + + return output +} diff --git a/mod/tigron/internal/formatter/osc8.go b/mod/tigron/internal/formatter/osc8.go new file mode 100644 index 00000000000..4a4874a3a9d --- /dev/null +++ b/mod/tigron/internal/formatter/osc8.go @@ -0,0 +1,31 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package formatter + +import "fmt" + +// OSC8 hyperlinks implementation. +type OSC8 struct { + Location string `json:"location"` + Line int `json:"line"` + Text string `json:"text"` +} + +func (o *OSC8) String() string { + // FIXME: not sure if any desktop software does support line numbers anchors? + return fmt.Sprintf("\x1b]8;;%s#%d:1\x07%s\x1b]8;;\x07"+"\u001b[0m", o.Location, o.Line, o.Text) +} diff --git a/mod/tigron/internal/mimicry/doc.go b/mod/tigron/internal/mimicry/doc.go new file mode 100644 index 00000000000..289acf8e59e --- /dev/null +++ b/mod/tigron/internal/mimicry/doc.go @@ -0,0 +1,21 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package mimicry provides a very rough and rudimentary mimicry library to help with internal tigron testing. +// It does not require generation, does not abuse reflect (too much), and keeps the amount of boilerplate baloney to a +// minimum. +// This is NOT a generic mock library. Use something else if you need one. +package mimicry diff --git a/mod/tigron/internal/mimicry/doc.md b/mod/tigron/internal/mimicry/doc.md new file mode 100644 index 00000000000..4e37122350a --- /dev/null +++ b/mod/tigron/internal/mimicry/doc.md @@ -0,0 +1,118 @@ +# [INTERNAL] [EXPERIMENTAL] Mimicry + +## Creating a Mock + +```golang +package mymock + +import "github.com/containerd/nerdctl/mod/tigron/internal/mimicry" + +// Let's assume we want to mock the following, likely defined somewhere else +// type InterfaceToBeMocked interface { +// SomeMethod(one string, two int) error +// } + +// Compile time ensure the mock does fulfill the interface +var _ InterfaceToBeMocked = &MyMock{} + +type MyMock struct { + // Embed mimicry core + mimicry.Core +} + +// First, describe function parameters and return values. +type ( + MyMockSomeMethodIn struct { + one string + two int + } + + MyMockSomeMethodOut = error +) + +// Satisfy the interface + wire-in the handler mechanism + +func (m *MyMock) SomeMethod(one string, two int) error { + // Call mimicry method Retrieve that will record the call, and return a custom handler if one is defined + if handler := m.Retrieve(); handler != nil { + // Call the optional handler if there is one. + return handler.(mimicry.Function[MyMockSomeMethodIn, MyMockSomeMethodOut])(MyMockSomeMethodIn{ + one: one, + two: two, + }) + } + + return nil +} +``` + + +## Using a Mock + +For consumers, the simplest way to use the mock is to inspect calls after the fact: + +```golang +package mymock + +import "testing" + +// This is the code you want to test, that does depend on the interface we are mocking. +// func functionYouWantToTest(o InterfaceToBeMocked, i int) { +// o.SomeMethod("lala", i) +// } + +func TestOne(t *testing.T) { + // Create the mock from above + mocky := &MyMock{} + + // Call the function you want to test + functionYouWantToTest(mocky, 42) + functionYouWantToTest(mocky, 123) + + // Now you can inspect the calls log for that function. + report := mocky.Report(InterfaceToBeMocked.SomeMethod) + t.Log("Number of times it was called:", len(report)) + t.Log("Inspecting the last call:") + t.Log(mimicry.PrintCall(report[len(report)-1])) +} +``` + +## Using handlers + +Implementing handlers allows active interception of the calls for more elaborate scenarios. + +```golang +package main_test + +import "testing" + +// The method you want to test against the mock +// func functionYouWantToTest(o InterfaceToBeMocked, i int) { +// o.SomeMethod("lala", i) +// } + +func TestTwo(t *testing.T) { + // Create the base mock + mocky := &MyMock{} + + // Declare a custom handler for the method `SomeMethod` + mocky.Register(InterfaceToBeMocked.SomeMethod, func(in MyMockSomeMethodIn) MyMockSomeMethodOut { + t.Log("Got parameters", in) + + // We want to fail on that + if in.two == 42 { + // Print out the callstack + report := mocky.Report(InterfaceToBeMocked.SomeMethod) + t.Log(mimicry.PrintCall(report[len(report)-1])) + t.Error("We do not want to ever receive 42. Inspect trace above.") + }else{ + t.Log("all fine - we did not see 42") + } + + return nil + }) + + functionYouWantToTest(mocky, 123) + functionYouWantToTest(mocky, 42) +} +``` diff --git a/mod/tigron/internal/mimicry/mimicry.go b/mod/tigron/internal/mimicry/mimicry.go new file mode 100644 index 00000000000..60deb6c1a34 --- /dev/null +++ b/mod/tigron/internal/mimicry/mimicry.go @@ -0,0 +1,147 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mimicry + +import ( + "reflect" + "runtime" + "strings" + "time" +) + +const callStackMaxDepth = 5 + +var _ Mocked = &Core{} + +// Mocked is the interface representing a fully-mocking struct (both for Designer and Consumer). +type Mocked interface { + Consumer + Designer +} + +// Function is a generics for any mockable function. +type Function[IN any, OUT any] = func(IN) OUT + +// Consumer is the mock interface exposed to mock users. +// It defines a handful of methods to register a custom handler, get and reset calls reports. +type Consumer interface { + Register(fun, handler any) + Report(fun any) []*Call + Reset() +} + +// Designer is the mock interface that mock creators can use to write function boilerplate. +type Designer interface { + Retrieve(args ...any) any +} + +// Core is a concrete implementation that any mock struct can embed to satisfy Mocked. +// FIXME: this is not safe to use concurrently. +type Core struct { + mockedFunctions map[string]any + callsList map[string][]*Call +} + +// Reset does reset the callStack records for all functions. +func (mi *Core) Reset() { + mi.callsList = make(map[string][]*Call) +} + +// Report returns all Calls made to the referenced function. +func (mi *Core) Report(fun any) []*Call { + fid := getFunID(fun) + + if mi.callsList == nil { + mi.callsList = make(map[string][]*Call) + } + + ret, ok := mi.callsList[fid] + if !ok { + ret = []*Call{} + } + + return ret +} + +// Retrieve returns a registered custom handler for that function if there is one. +func (mi *Core) Retrieve(args ...any) any { + // Get the frames. + pc := make([]uintptr, callStackMaxDepth) + //nolint:mnd // Whatever mnd... + n := runtime.Callers(2, pc) + callersFrames := runtime.CallersFrames(pc[:n]) + // This is the frame associate with the mock currently calling retrieve, so, extract the short + // name of it. + frame, _ := callersFrames.Next() + nm := strings.Split(frame.Function, ".") + fid := nm[len(nm)-1] + + // Initialize callsList if need be + if mi.callsList == nil { + mi.callsList = make(map[string][]*Call) + } + + // Now, get the remaining frames until we hit the go library or the call stack depth limit. + frames := []*Frame{} + + for range callStackMaxDepth { + frame, _ = callersFrames.Next() + if isStd(frame.Function) { + break + } + + frames = append(frames, &Frame{ + File: frame.File, + Function: frame.Function, + Line: frame.Line, + }) + } + + // Stuff into the call list. + mi.callsList[fid] = append(mi.callsList[fid], &Call{ + Time: time.Now(), + Args: args, + Frames: frames, + }) + + // See if we have a registered handler and return it if so. + if ret, ok := mi.mockedFunctions[fid]; ok { + return ret + } + + return nil +} + +// Register does declare an explicit handler for that function. +func (mi *Core) Register(fun, handler any) { + if mi.mockedFunctions == nil { + mi.mockedFunctions = make(map[string]any) + } + + mi.mockedFunctions[getFunID(fun)] = handler +} + +func getFunID(fun any) string { + // The point of keeping only the func name is to avoid type mismatch dependent on what interface + // is used by the + // consumer. + origin := runtime.FuncForPC(reflect.ValueOf(fun).Pointer()).Name() + seg := strings.Split(origin, ".") + origin = seg[len(seg)-1] + + return origin +} diff --git a/mod/tigron/internal/mimicry/print.go b/mod/tigron/internal/mimicry/print.go new file mode 100644 index 00000000000..96c333c63da --- /dev/null +++ b/mod/tigron/internal/mimicry/print.go @@ -0,0 +1,59 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mimicry + +import ( + "strings" + "time" + + "github.com/containerd/nerdctl/mod/tigron/internal/formatter" +) + +const ( + maxLineLength = 110 + sourceLineAround = 2 + breakpointDecorator = "🔴" + frameDecorator = "⬆️" +) + +// PrintCall does fancy format a Call. +func PrintCall(call *Call) string { + sectionSeparator := strings.Repeat("_", maxLineLength) + + debug := [][]any{ + {"Arguments", call.Args}, + {"Time", call.Time.Format(time.RFC3339)}, + } + + output := []string{ + formatter.Table(debug), + sectionSeparator, + } + + marker := breakpointDecorator + for _, v := range call.Frames { + output = append(output, + v.String(), + sectionSeparator, + v.Excerpt(sourceLineAround, marker), + sectionSeparator, + ) + marker = frameDecorator + } + + return "\n" + strings.Join(output, "\n") +} diff --git a/mod/tigron/internal/mimicry/stack.go b/mod/tigron/internal/mimicry/stack.go new file mode 100644 index 00000000000..d712a5a71ed --- /dev/null +++ b/mod/tigron/internal/mimicry/stack.go @@ -0,0 +1,130 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package mimicry + +import ( + "bufio" + "fmt" + "os" + "path/filepath" + "strings" + "time" + "unicode/utf8" + + "github.com/containerd/nerdctl/mod/tigron/internal/formatter" +) + +const ( + hyperlinkDecorator = "🔗" + intoDecorator = "↪" +) + +// Call is used to store information about a call to a function of the mocked struct, including +// arguments, time, and +// frames. +type Call struct { + Time time.Time + Args []any + Frames []*Frame +} + +// A Frame stores information about a call code-path: file, line number and function name. +type Frame struct { + File string + Function string + Line int +} + +// String returns an OSC8 hyperlink pointing to the source along with package and function +// information. +// FIXME: we are mixing formatting concerns here. +// FIXME: this is gibberish to read. +func (f *Frame) String() string { + cwd, _ := os.Getwd() + + rel, err := filepath.Rel(cwd, f.File) + if err != nil { + rel = f.File + } + + spl := strings.Split(f.Function, ".") + fun := spl[len(spl)-1] + mod := strings.Join(spl[:len(spl)-1], ".") + + return hyperlinkDecorator + " " + (&formatter.OSC8{ + Location: "file://" + f.File, + Line: f.Line, + Text: fmt.Sprintf("%s:%d", rel, f.Line), + }).String() + + fmt.Sprintf( + "\n%6s package %q\n", + intoDecorator, + mod, + ) + + fmt.Sprintf( + "%8s func %s", + " "+intoDecorator, + fun, + ) +} + +// Excerpt will return the source code content associated with the frame + a few lines around. +func (f *Frame) Excerpt(add int, marker string) string { + source, err := os.Open(f.File) + if err != nil { + return "" + } + + defer func() { + _ = source.Close() + }() + + index := 1 + scanner := bufio.NewScanner(source) + + for ; scanner.Err() == nil && index < f.Line-add; index++ { + if !scanner.Scan() { + break + } + + _ = scanner.Text() + } + + capt := []string{} + + for ; scanner.Err() == nil && index <= f.Line+add; index++ { + if !scanner.Scan() { + break + } + + line := scanner.Text() + if index == f.Line { + line = fmt.Sprintf("%6d %s %s", index, marker, line) + } else { + // FIXME: see other similar note. Rune counting is not display-width, so... + line = fmt.Sprintf("%6d %*s %s", index, utf8.RuneCountInString(marker), "", line) + } + + capt = append(capt, line) + } + + return strings.Join(capt, "\n") +} + +func isStd(in string) bool { + return !strings.Contains(in, "/") +} diff --git a/mod/tigron/internal/mocks/doc.go b/mod/tigron/internal/mocks/doc.go new file mode 100644 index 00000000000..abc96ad625d --- /dev/null +++ b/mod/tigron/internal/mocks/doc.go @@ -0,0 +1,18 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package mocks provides a collection of tigron internal mocks to ease testing. +package mocks diff --git a/mod/tigron/internal/mocks/t.go b/mod/tigron/internal/mocks/t.go new file mode 100644 index 00000000000..7665fd4fe3b --- /dev/null +++ b/mod/tigron/internal/mocks/t.go @@ -0,0 +1,95 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +//nolint:forcetypeassert +//revive:disable:exported,max-public-structs,package-comments +package mocks + +// FIXME: type asserts... + +import ( + "github.com/containerd/nerdctl/mod/tigron/internal/mimicry" + "github.com/containerd/nerdctl/mod/tigron/tig" +) + +type T interface { + tig.T + mimicry.Consumer +} + +type ( + THelperIn struct{} + THelperOut struct{} + + TFailIn struct{} + TFailOut struct{} + + TFailNowIn struct{} + TFailNowOut struct{} + + TLogIn []any + TLogOut struct{} + + TNameIn struct{} + TNameOut = string + + TTempDirIn struct{} + TTempDirOut = string +) + +type MockT struct { + mimicry.Core +} + +func (m *MockT) Helper() { + if handler := m.Retrieve(); handler != nil { + handler.(mimicry.Function[THelperIn, THelperOut])(THelperIn{}) + } +} + +func (m *MockT) FailNow() { + if handler := m.Retrieve(); handler != nil { + handler.(mimicry.Function[TFailNowIn, TFailNowOut])(TFailNowIn{}) + } +} + +func (m *MockT) Fail() { + if handler := m.Retrieve(); handler != nil { + handler.(mimicry.Function[TFailIn, TFailOut])(TFailIn{}) + } +} + +func (m *MockT) Log(args ...any) { + if handler := m.Retrieve(args...); handler != nil { + handler.(mimicry.Function[TLogIn, TLogOut])(args) + } +} + +func (m *MockT) Name() string { + if handler := m.Retrieve(); handler != nil { + return handler.(mimicry.Function[TNameIn, TNameOut])(TNameIn{}) + } + + return "" +} + +func (m *MockT) TempDir() string { + if handler := m.Retrieve(); handler != nil { + return handler.(mimicry.Function[TTempDirIn, TTempDirOut])(TTempDirIn{}) + } + + return "" +} diff --git a/mod/tigron/test/command.go b/mod/tigron/test/command.go index a8ed09e4524..ef41610338a 100644 --- a/mod/tigron/test/command.go +++ b/mod/tigron/test/command.go @@ -234,7 +234,7 @@ func (gc *GenericCommand) Run(expect *Expected) { // Range through the expected errors and confirm they are seen on stderr for _, expectErr := range expect.Errors { - assertive.StringContains(gc.t, result.Stderr, expectErr.Error(), + assertive.Contains(gc.t, result.Stderr, expectErr.Error(), fmt.Sprintf("Expected error: %q to be found in stderr\n", expectErr.Error()), debug) } diff --git a/mod/tigron/test/data_test.go b/mod/tigron/test/data_test.go index 3c9ea730206..9e39ed677ca 100644 --- a/mod/tigron/test/data_test.go +++ b/mod/tigron/test/data_test.go @@ -56,10 +56,10 @@ func TestDataIdentifier(t *testing.T) { two := dataObj.Identifier() assertive.IsEqual(t, one, two) - assertive.StringHasPrefix(t, one, "testdataidentifier") + assertive.HasPrefix(t, one, "testdataidentifier") three := dataObj.Identifier("Some Add ∞ Funky∞Prefix") - assertive.StringHasPrefix(t, three, "testdataidentifier-some-add-funky-prefix") + assertive.HasPrefix(t, three, "testdataidentifier-some-add-funky-prefix") } func TestDataIdentifierThatIsReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyReallyLong( @@ -73,7 +73,7 @@ func TestDataIdentifierThatIsReallyReallyReallyReallyReallyReallyReallyReallyRea two := dataObj.Identifier() assertive.IsEqual(t, one, two) - assertive.StringHasPrefix(t, one, "testdataidentifier") + assertive.HasPrefix(t, one, "testdataidentifier") assertive.IsEqual(t, len(one), identifierMaxLength) three := dataObj.Identifier("Add something") diff --git a/mod/tigron/tig/doc.go b/mod/tigron/tig/doc.go new file mode 100644 index 00000000000..6349b286a57 --- /dev/null +++ b/mod/tigron/tig/doc.go @@ -0,0 +1,20 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +// Package tig defines interfaces for third-party packages that tigron needs to interact with. +// The main upside of expressing our expectations instead of depending directly on concrete implementations is +// evidently the ability to mock easily, which in turn makes testing much easier. +package tig diff --git a/mod/tigron/tig/t.go b/mod/tigron/tig/t.go new file mode 100644 index 00000000000..f6256b72404 --- /dev/null +++ b/mod/tigron/tig/t.go @@ -0,0 +1,40 @@ +/* + Copyright The containerd Authors. + + Licensed under the Apache License, Version 2.0 (the "License"); + you may not use this file except in compliance with the License. + You may obtain a copy of the License at + + http://www.apache.org/licenses/LICENSE-2.0 + + Unless required by applicable law or agreed to in writing, software + distributed under the License is distributed on an "AS IS" BASIS, + WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + See the License for the specific language governing permissions and + limitations under the License. +*/ + +package tig + +// T is what Tigron needs from a testing implementation (*testing.T obviously satisfies it). +// +// Expert note: using the testing.TB interface instead is tempting, but not possible, as the go authors made it +// impossible to implement (by declaring a private method on it): +// https://cs.opensource.google/go/go/+/refs/tags/go1.24.2:src/testing/testing.go;l=913-939 +// Generally speaking, interfaces in go should be defined by the consumer, not the producer. +// Depending on producers' interfaces make them much harder to change for the producer, harder (or impossible) to mock, +// and decreases modularity. +// On the other hand, consumer defined interfaces allows to remove direct dependencies on implementation and encourages +// depending on abstraction instead, and reduces the interface size of what has to be mocked to just what is actually +// needed. +// This is a fundamental difference compared to traditional compiled languages that forces code to declare which +// interfaces it implements, while go interfaces are more a form of duck-typing. +// See https://www.airs.com/blog/archives/277 for more. +type T interface { + Helper() + FailNow() + Fail() + Log(args ...any) + Name() string + TempDir() string +}