// Copyright (c) 2025 Gustavo "Guz" L. de Mello // Copyright (c) 2025 The Lored.dev Contributors // // Contents of this file, expect as otherwise noted, are dual-licensed under the // Apache License, Version 2.0 or // the MIT license , at you option. // // You may use this file in compliance with the licenses. // // Unless required by applicable law or agreed to in writing, this file distributed // under the licenses is distributed on as "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS // OF ANY KIND, either express or implied. // // An original copy of this file can be found at http://code.capytal.cc/loreddev/x/tinyssert/tinyssert.go. // Package tinyssert is a minimal set of assertions functions for testing and simulation // testing, all in one file. // // The most simple way of using the package is importing it directly and using the // alias functions: // // package main // // import ( // "log" // "code.capytal.cc/loreddev/x/tinyssert" // ) // // func main() { // expected := "value" // value := "value" // log.Println(tinyssert.Equal(expected, value)) // "true" // } // // You can create your own "assert" variable and have more control // over how asserts work, see the [New] constructor for more information: // // package main // // import ( // "log/slog" // "code.capytal.cc/loreddev/x/tinyssert" // ) // // var logger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) // var assert = tinyssert.NewAssertions(tinyssert.WithLogger(logger)) // // func main() { // expected := "value" // value := "not value" // assert.Equal(expected, value) // "expected \"value\", got \"not value\"" with the call stack and returns false // } // // Preferably, when using assertions inside production code or libraries, you can use // the assertions via dependency injection. This provides a easy way to disable // assertions in production (see [NewDisabled]) while being able to test an API without // changing it: // // package main // // import ( // "flag" // "log/slog" // // "code.capytal.cc/loreddev/x/tinyssert" // ) // // var debug = flag.Bool("debug", false, "Run the application in debug mode") // // func init() { // flag.Parse() // } // // func main() { // logger := slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) // assert := tinyssert.NewDisabled() // if *debug { // assert := tinyssert.New(tinyssert.WithLogger(logger)) // } // // app := App{logger: logger, assert: assert} // // app.Start() // } // // type App struct { // logger: *slog.Logger // assert: tinyssert.Assertions // } // // function (app *App) Start() { // app.assert.OK(app.logger, "Logger must be initialized before the application") // // // ... // } package tinyssert import ( "fmt" "io" "log/slog" "os" "path" "reflect" "runtime" "strings" "unicode" "unicode/utf8" ) // Assertions represents all the API provided by the [assert] package. Implementation // of this interface can have extra logic on their state such as panicking or using // [testing.T.FailNow] to halt the program from continuing on failed assertions instead // of just return false. // // `msg` argument(s) is(/are) optional, the first argument should always be a string. // The string can have formatting verbs, with the remaining arguments being used to fill // those verbs. So for example: // // if argument > 0 { // tinyssert.OK(value, "Since %d is greater than 0, this should always be ok", argument) // } type Assertions interface { // Asserts that the value is not zero-valued, is nil, or panics, aka. "is ok". Ok(v any, msg ...any) // Asserts that the actual value is equal to the expected value. Equal(expected, actual any, msg ...any) // Asserts that the actual value is not equal to the expected value. NotEqual(notExpected, actual any, msg ...any) // Asserts that the value is nil. Nil(v any, msg ...any) // Asserts that the value is not nil. NotNil(v any, msg ...any) // Asserts that the value is a boolean true. True(b bool, msg ...any) // Asserts that the value is a boolean false. False(b bool, msg ...any) // Asserts that the value is zero-valued. Zero(v any, msg ...any) // Asserts that the value is not zero-valued. NotZero(v any, msg ...any) // Asserts that the function panics. Panic(fn func(), msg ...any) // Asserts that the function does not panics. NotPanic(fn func(), msg ...any) // Logs the formatted failure message and/or marks the test as failed if possible, // depending of what is possible to the implementation. Fail(f Failure) // Panics with the formatted failure message and/or marks the test as failed, // depending of what is possible to the implementation. FailNow(f Failure) // Gets the caller stack. CallerInfo() []string } // AssertionsErr is the same as [Assertions], but it returns [Failure] on it's method, useful // for assertions that can also be returned as errors. type AssertionsErr interface { Assertions // Asserts that the value is not zero-valued, is nil, or panics, aka. "is ok". // Returns a Failure if the assertion fails, otherwise returns nil. OkErr(v any, msg ...any) Failure // Asserts that the actual value is equal to the expected value. // Returns a Failure if the assertion fails, otherwise returns nil. EqualErr(expected, actual any, msg ...any) Failure // Asserts that the actual value is not equal to the expected value. // Returns a Failure if the assertion fails, otherwise returns nil. NotEqualErr(notExpected, actual any, msg ...any) Failure // Asserts that the value is nil. // Returns a Failure if the assertion fails, otherwise returns nil. NilErr(v any, msg ...any) Failure // Asserts that the value is not nil. // Returns a Failure if the assertion fails, otherwise returns nil. NotNilErr(v any, msg ...any) Failure // Asserts that the value is a boolean true. // Returns a Failure if the assertion fails, otherwise returns nil. TrueErr(b bool, msg ...any) Failure // Asserts that the value is a boolean false. // Returns a Failure if the assertion fails, otherwise returns nil. FalseErr(b bool, msg ...any) Failure // Asserts that the value is zero-valued. // Returns a Failure if the assertion fails, otherwise returns nil. ZeroErr(v any, msg ...any) Failure // Asserts that the value is not zero-valued. NotZeroErr(v any, msg ...any) Failure // Asserts that the function panics. // Returns a Failure if the assertion fails, otherwise returns nil. PanicErr(fn func(), msg ...any) Failure // Asserts that the function does not panics. // Returns a Failure if the assertion fails, otherwise returns nil. NotPanicErr(fn func(), msg ...any) Failure } // New constructs a new implementation of [Assertions]. Use `opts` to customize the behaviour // of the implementation. func New(opts ...Option) AssertionsErr { a := &assertions{ panic: false, log: slog.New(slog.NewTextHandler(io.Discard, &slog.HandlerOptions{})), test: nil, helper: nil, } for _, opt := range opts { opt(a) } if th, ok := a.test.(helperT); ok { a.helper = th } return a } // Option is used in new constructor functions (such as [New] and [NewDisabled]) to customize // the behaviour of the implementation. type Option = func(*assertions) // WithPanic sets the implementation to panic when an assertion fails. If used together with // [WithTest], the implementation should use [testing.T.FailNow] instead of panic. func WithPanic() Option { return func(a *assertions) { a.panic = true } } // WithTest provides a [TestingT] implementation, such as the provided by the [testing] stadard package // to be used to log and mark tests as failed if possible. // // If the implementation has an Errorf method, it will be used instead of any logger provided by [WithLogger]. // // If the implementation has an FailNow method, it will be used instead of panic. func WithTest(t TestingT) Option { return func(a *assertions) { a.test = t } } // WithLogger provides an [slog.Logger] instance to be used by the [Assertions] implementation to log // failed assertions. // // If used together with [WithTest], the logger is not used if the [TestingT] implementation provided has // an Errorf method. func WithLogger(l *slog.Logger) Option { return func(a *assertions) { a.log = l } } type assertions struct { panic bool test TestingT helper helperT log *slog.Logger group string } // TestingT is a wrapper interface around [testing.T]. type TestingT interface { Errorf(format string, args ...any) } type helperT interface { Helper() } var _ Assertions = (*assertions)(nil) func (a *assertions) EqualErr(expected, actual any, msg ...any) Failure { if a.equal(expected, actual) { return nil } return a.fail(fmt.Sprintf("expected %v (right), got %v (left)", expected, actual), msg...) } func (a *assertions) Equal(expected, actual any, msg ...any) { _ = a.EqualErr(expected, actual, msg...) } func (a *assertions) NotEqualErr(notExpected, actual any, msg ...any) Failure { if !a.equal(notExpected, actual) { return nil } return a.fail(fmt.Sprintf("expected to %v (right) and %v (left) to be not-equal", notExpected, actual), msg...) } func (a *assertions) NotEqual(notExpected, actual any, msg ...any) { _ = a.NotEqualErr(notExpected, actual, msg...) } func (a *assertions) equal(ex, ac any) bool { if nex, nac := a.OkErr(ex), a.OkErr(ac); (nex != nil) != (nac != nil) { return false } if reflect.DeepEqual(ex, ac) { return true } ev, av := reflect.ValueOf(ex), reflect.ValueOf(ac) if ev == av { return true } if av.Type().ConvertibleTo(ev.Type()) { return reflect.DeepEqual(ex, av.Convert(ev.Type()).Interface()) } if fmt.Sprintf("%#v", ex) == fmt.Sprintf("%#v", ac) { return true } return false } func (a *assertions) OkErr(v any, msg ...any) Failure { if a.nil(v) { return a.fail("expected not-nil value", msg...) } if a.zero(v) { return a.fail("expected non-zero value", msg...) } if f, ok := v.(func()); ok { if a.panics(f) { return a.fail("expected to not panic") } } return nil } func (a *assertions) Ok(v any, msg ...any) { _ = a.OkErr(v, msg...) } func (a *assertions) NilErr(v any, msg ...any) Failure { if a.nil(v) { return nil } return a.fail("expected nil value", msg...) } func (a *assertions) Nil(v any, msg ...any) { _ = a.NilErr(v, msg...) } func (a *assertions) NotNilErr(v any, msg ...any) Failure { if !a.nil(v) { return nil } return a.fail("expected not-nil value", msg...) } func (a *assertions) NotNil(v any, msg ...any) { _ = a.NotNilErr(v, msg...) } func (a *assertions) nil(v any) bool { if v == nil { return true } rv := reflect.ValueOf(v) rk := rv.Kind() if rk >= reflect.Chan && rk <= reflect.Slice && rv.IsNil() { return true } return false } func (a *assertions) TrueErr(v bool, msg ...any) Failure { if v { return nil } return a.fail("expected true", msg...) } func (a *assertions) True(v bool, msg ...any) { _ = a.TrueErr(v, msg...) } func (a *assertions) FalseErr(v bool, msg ...any) Failure { if !v { return nil } return a.fail("expected false", msg...) } func (a *assertions) False(v bool, msg ...any) { _ = a.FalseErr(v, msg...) } func (a *assertions) ZeroErr(v any, msg ...any) Failure { if a.zero(v) { return nil } return a.fail("expected zero value", msg...) } func (a *assertions) Zero(v any, msg ...any) { _ = a.ZeroErr(v, msg...) } func (a *assertions) NotZeroErr(v any, msg ...any) Failure { if !a.zero(v) { return nil } return a.fail("expected non-zero value", msg...) } func (a *assertions) NotZero(v any, msg ...any) { _ = a.NotZeroErr(v, msg...) } func (a *assertions) zero(v any) bool { if v != nil && !reflect.DeepEqual(v, reflect.Zero(reflect.TypeOf(v)).Interface()) { return false } return true } func (a *assertions) PanicErr(fn func(), msg ...any) Failure { if a.panics(fn) { return nil } return a.fail("expected function to panic", msg...) } func (a *assertions) Panic(fn func(), msg ...any) { _ = a.PanicErr(fn, msg...) } func (a *assertions) NotPanicErr(fn func(), msg ...any) Failure { if !a.panics(fn) { return nil } return a.fail("expected function to not panic", msg...) } func (a *assertions) NotPanic(fn func(), msg ...any) { _ = a.NotPanicErr(fn, msg...) } func (a *assertions) panics(fn func()) bool { var r any func() { defer func() { r = recover() }() fn() }() return r != nil } func (a *assertions) fail(reason string, msg ...any) Failure { if a.helper != nil { a.helper.Helper() } f := failure{ reason: reason, message: fmtMessage(msg), callerInfo: a.CallerInfo(), } if n, ok := a.test.(interface { Name() string }); ok { f.test = n.Name() } if a.panic { a.FailNow(f) } else { a.Fail(f) } return f } func (a *assertions) Fail(f Failure) { if ft, ok := a.test.(interface { Fail() }); ok { a.test.Errorf("ASSERTION FAILED:\n%s", f.String()) ft.Fail() } else { a.log.Error("ASSERTION FAILED", slog.String("reason", f.Reason()), slog.String("message", f.Message()), slog.String("test", f.Test()), slog.Any("caller", f.CallerInfo()), ) } } func (a *assertions) FailNow(f Failure) { if ft, ok := a.test.(interface { FailNow() }); ok { a.test.Errorf("ASSERTION FAILED:\n%s", f.String()) ft.FailNow() } else { panic(f.String()) } } func fmtMessage(msg ...any) string { switch len(msg) { case 0: return "" case 1: if s, ok := msg[0].(string); ok { return s } return fmt.Sprintf("%v", msg[0]) default: var m string if s, ok := msg[0].(string); ok { m = s } else { m = fmt.Sprintf("%v", msg[0]) } return fmt.Sprintf(m, msg[1:]...) } } func (as *assertions) CallerInfo() []string { callers := []string{} for i := 0; ; i++ { pc, file, line, ok := runtime.Caller(i) if !ok { // We reached the end of the call stack break } // Edge case found in https://github.com/stretchr/testify/issues/180 if file == "" { break } f := runtime.FuncForPC(pc) if f == nil { break } name := f.Name() if name == "testing.Runner" { break } filename := path.Base(file) dirname := path.Base(path.Dir(file)) if (dirname != "assert" && dirname != "mock" && dirname != "require") || filename == "mock_test.go" { callers = append(callers, fmt.Sprintf("%s:%d", file, line)) } // Remove the package s := strings.Split(name, ".") name = s[len(s)-1] if isTest(name, "Test") || isTest(name, "Benchmark") || isTest(name, "Example") { break } } return callers } func isTest(name, prefix string) bool { if !strings.HasPrefix(name, prefix) { return false } if len(name) == len(prefix) { return true } r, _ := utf8.DecodeRuneInString(name[len(prefix):]) return !unicode.IsLower(r) } type failure struct { reason string message string test string callerInfo []string } var _ Failure = failure{} func (e failure) Reason() string { return e.reason } func (e failure) Message() string { return e.message } func (e failure) Error() string { if e.message != "" { return fmt.Sprintf("assertion failed, %s: %s", e.reason, e.message) } return fmt.Sprintf("assertion failed, %s", e.reason) } func (e failure) String() string { c := map[string]string{ "Reason": e.reason, } if e.message != "" { c["Message"] = e.message } if e.test != "" { c["Test"] = e.test } c["Stack Trace"] = e.StackTrace() var out string for k, m := range c { var s string for _, l := range strings.Split(m, "\n") { s += fmt.Sprintf("\t%s\n", l) } out += fmt.Sprintf("\t%s:\n%s", k, s) } return out } func (e failure) Test() string { return e.test } func (e failure) CallerInfo() []string { return e.callerInfo } // StackTrace returns the CallerInfo strings as a formatted stack trace. func (e failure) StackTrace() string { return strings.Join(e.callerInfo, "\n\t") } type Failure interface { Reason() string Message() string Test() string StackTrace() string CallerInfo() []string error fmt.Stringer } type disabledAssertions struct{} // NewDisabled creates a new implementation of Assertions that always a nil error and // never panics or marks the test as failed, with the exception of Fail, FailNow and // CallerInfo, which uses their corresponding [Fail], [FailNow] and [CallerInfo] top-level // functions. // // The `opts` argument does nothing, and is just available to make the function signature // equal to [New]. func NewDisabled(opts ...Option) AssertionsErr { _ = opts return &disabledAssertions{} } func (*disabledAssertions) Ok(any, ...any) {} func (*disabledAssertions) Equal(_, _ any, _ ...any) {} func (*disabledAssertions) NotEqual(_, _ any, _ ...any) {} func (*disabledAssertions) Nil(any, ...any) {} func (*disabledAssertions) NotNil(any, ...any) {} func (*disabledAssertions) True(bool, ...any) {} func (*disabledAssertions) False(bool, ...any) {} func (*disabledAssertions) Zero(any, ...any) {} func (*disabledAssertions) NotZero(any, ...any) {} func (*disabledAssertions) Panic(func(), ...any) {} func (*disabledAssertions) NotPanic(func(), ...any) {} func (*disabledAssertions) OkErr(any, ...any) Failure { return nil } func (*disabledAssertions) EqualErr(_, _ any, _ ...any) Failure { return nil } func (*disabledAssertions) NotEqualErr(_, _ any, _ ...any) Failure { return nil } func (*disabledAssertions) NilErr(any, ...any) Failure { return nil } func (*disabledAssertions) NotNilErr(any, ...any) Failure { return nil } func (*disabledAssertions) TrueErr(bool, ...any) Failure { return nil } func (*disabledAssertions) FalseErr(bool, ...any) Failure { return nil } func (*disabledAssertions) ZeroErr(any, ...any) Failure { return nil } func (*disabledAssertions) NotZeroErr(any, ...any) Failure { return nil } func (*disabledAssertions) PanicErr(func(), ...any) Failure { return nil } func (*disabledAssertions) NotPanicErr(func(), ...any) Failure { return nil } func (*disabledAssertions) Fail(f Failure) { Default.Fail(f) } func (*disabledAssertions) FailNow(f Failure) { Default.FailNow(f) } func (*disabledAssertions) CallerInfo() []string { return Default.CallerInfo() } var ( // DefaultLogger is the default [slog.Logger] used by [Default] DefaultLogger = slog.New(slog.NewTextHandler(os.Stdout, &slog.HandlerOptions{})) // Default implementation of [Assertions] used by the top-level API functions. Default = New(WithLogger(DefaultLogger)) ) // Ok asserts that the value is not zero-valued, is nil, or panics, aka. "is ok". // // Logs the failure message with [DefaultLogger]. func Ok(obj any, msg ...any) { Default.Ok(obj, msg...) } // OkErr asserts that the value is not zero-valued, is nil, or panics, aka. "is ok". // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func OkErr(obj any, msg ...any) Failure { return Default.OkErr(obj, msg...) } // Equal asserts that the actual value is equal to the expected value. // // Logs the failure message with [DefaultLogger]. func Equal(expected, actual any, msg ...any) { Default.Equal(expected, actual, msg...) } // EqualErr asserts that the actual value is equal to the expected value. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func EqualErr(expected, actual any, msg ...any) Failure { return Default.EqualErr(expected, actual, msg...) } // NotEqual asserts that the actual value is not equal to the expected value. // // Logs the failure message with [DefaultLogger]. func NotEqual(notExpected, actual any, msg ...any) { Default.NotEqual(notExpected, actual, msg...) } // NotEqualErr asserts that the actual value is not equal to the expected value. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func NotEqualErr(notExpected, actual any, msg ...any) Failure { return Default.NotEqualErr(notExpected, actual, msg...) } // Nil asserts that the value is nil. // // Logs the failure message with [DefaultLogger]. func Nil(v any, msg ...any) { Default.Nil(v, msg...) } // NilErr asserts that the value is nil. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func NilErr(v any, msg ...any) Failure { return Default.NilErr(v, msg...) } // NotNil asserts that the value is not nil. // // Logs the failure message with [DefaultLogger]. func NotNil(v any, msg ...any) { Default.NotNil(v, msg...) } // NotNilErr asserts that the value is not nil. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func NotNilErr(v any, msg ...any) Failure { return Default.NotNilErr(v, msg...) } // True asserts that the value is a boolean true. // // Logs the failure message with [DefaultLogger]. func True(v bool, msg ...any) { Default.True(v, msg...) } // TrueErr asserts that the value is a boolean true. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func TrueErr(v bool, msg ...any) Failure { return Default.TrueErr(v, msg...) } // False asserts that the value is a boolean false. // // Logs the failure message with [DefaultLogger]. func False(v bool, msg ...any) { Default.False(v, msg...) } // FalseErr asserts that the value is a boolean false. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func FalseErr(v bool, msg ...any) Failure { return Default.FalseErr(v, msg...) } // Zero asserts that the value is zero-valued. // // Logs the failure message with [DefaultLogger]. func Zero(v any, msg ...any) { Default.Zero(v, msg...) } // ZeroErr asserts that the value is zero-valued. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func ZeroErr(v any, msg ...any) Failure { return Default.ZeroErr(v, msg...) } // NotZero asserts that the value is not zero-valued. // // Logs the failure message with [DefaultLogger]. func NotZero(v any, msg ...any) { Default.NotZero(v, msg...) } // NotZeroErr asserts that the value is not zero-valued. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func NotZeroErr(v any, msg ...any) Failure { return Default.NotZeroErr(v, msg...) } // Panic asserts that the function panics. // // Logs the failure message with [DefaultLogger]. func Panic(fn func(), msg ...any) { Default.Panic(fn, msg...) } // PanicErr asserts that the function panics. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func PanicErr(fn func(), msg ...any) Failure { return Default.PanicErr(fn, msg...) } // NotPanic asserts that the function does not panics. // // Logs the failure message with [DefaultLogger]. func NotPanic(fn func(), msg ...any) { Default.NotPanic(fn, msg...) } // NotPanicErr asserts that the function does not panics. // Returns a Failure if the assertion fails, otherwise returns nil. // // Logs the failure message with [DefaultLogger]. func NotPanicErr(fn func(), msg ...any) Failure { return Default.NotPanicErr(fn, msg...) } // Fail logs the formatted failure message using [DefaultLogger]. func Fail(f Failure) { Default.Fail(f) } // FailNow panics with the formatted failure message. func FailNow(f Failure) { Default.FailNow(f) } // CallerInfo gets the caller stack. func CallerInfo() []string { return Default.CallerInfo() }