assert

package
v2.5.0 Latest Latest
Warning

This package is not in the latest version of its module.

Go to latest
Published: Feb 25, 2026 License: MIT Imports: 20 Imported by: 0

Documentation

Overview

Package assert provides always-on runtime assertions for detecting programming bugs.

Unlike test assertions, these assertions are intended to remain enabled in production code. They are designed for detecting invariant violations, programming errors, and impossible states - NOT for input validation or expected error conditions.

Design Philosophy

Assertions are for catching bugs, not for handling user input:

  • Use assertions for conditions that should NEVER be false if the code is correct
  • Use error returns for conditions that CAN legitimately fail (I/O, user input, etc.)
  • Assertions return errors so callers can stop execution immediately

Good assertion usage:

a := assert.New(ctx, logger, "transaction", "create")
if err := a.NotNil(ctx, config, "config must be loaded before server starts"); err != nil {
    return err
}
if err := a.That(ctx, len(items) > 0, "processItems called with empty slice"); err != nil {
    return err
}

Bad assertion usage (use error returns instead):

// DON'T: User input validation
_ = a.That(ctx, email != "", "email is required") // Use validation errors

// DON'T: I/O that can fail
_ = a.NoError(ctx, file.Read(), "file must read") // Use proper error handling

Core Assertion Methods

The package provides five core assertion methods on Asserter:

a.That(ctx context.Context, ok bool, msg string, kv ...any) error
    Returns an error if ok is false. General-purpose assertion.

a.NotNil(ctx context.Context, v any, msg string, kv ...any) error
    Returns an error if v is nil. Handles both untyped nil and typed nil (nil interface
    values with concrete types).

a.NotEmpty(ctx context.Context, s string, msg string, kv ...any) error
    Returns an error if s is an empty string.

a.NoError(ctx context.Context, err error, msg string, kv ...any) error
    Returns an error if err is not nil. Automatically includes the error in context.

a.Never(ctx context.Context, msg string, kv ...any) error
    Always returns an error. Use for unreachable code paths.

Key-Value Context

All assertion methods accept optional key-value pairs to provide context in logs and errors:

if err := a.That(ctx, balance >= 0, "balance must not be negative",
    "account_id", accountID,
    "balance", balance,
); err != nil {
    return err
}

The error message will include:

assertion failed: balance must not be negative
    assertion=That
    account_id=550e8400-e29b-41d4-a716-446655440000
    balance=-100

Odd numbers of key-value arguments are handled gracefully with a "MISSING_VALUE" marker.

Domain Predicates

The package includes predicate functions for common domain validations:

// Numeric predicates
assert.Positive(n int64) bool        // n > 0
assert.NonNegative(n int64) bool     // n >= 0
assert.NotZero(n int64) bool         // n != 0
assert.InRange(n, min, max int64) bool // min <= n <= max

// String predicates
assert.ValidUUID(s string) bool      // valid UUID format

// Financial predicates (using shopspring/decimal)
assert.ValidAmount(d decimal.Decimal) bool    // exponent in [-18, 18]
assert.ValidScale(scale int) bool             // scale in [0, 18]
assert.PositiveDecimal(d decimal.Decimal) bool    // d > 0
assert.NonNegativeDecimal(d decimal.Decimal) bool // d >= 0

Use predicates with Asserter:

if err := a.That(ctx, assert.Positive(count), "count must be positive", "count", count); err != nil {
    return err
}
if err := a.That(ctx, assert.ValidUUID(id), "invalid UUID", "id", id); err != nil {
    return err
}

Usage Examples

Pre-conditions (validate inputs at function entry):

func ProcessTransaction(ctx context.Context, tx *Transaction) error {
    a := assert.New(ctx, logger, "transaction", "process")
    if err := a.NotNil(ctx, tx, "transaction must not be nil"); err != nil {
        return err
    }
    if err := a.NotEmpty(ctx, tx.ID, "transaction must have ID", "tx", tx); err != nil {
        return err
    }
    // ... rest of function
}

Post-conditions (validate outputs before return):

func CreateAccount(ctx context.Context, name string) (*Account, error) {
    a := assert.New(ctx, logger, "account", "create")
    acc := &Account{ID: uuid.New(), Name: name}
    if err := a.NotNil(ctx, acc.ID, "created account must have ID"); err != nil {
        return nil, err
    }
    return acc, nil
}

Unreachable code:

switch status {
case Active:
    return handleActive()
case Inactive:
    return handleInactive()
case Deleted:
    return handleDeleted()
default:
    return a.Never(ctx, "unhandled status", "status", status)
}

Goroutine Halting

In goroutines, use Halt to stop execution after a failed assertion:

go func() {
    a := assert.New(ctx, logger, "transaction", "sync")
    if err := a.That(ctx, ready, "sync not ready"); err != nil {
        a.Halt(err)
    }
    // ... rest of goroutine
}()

Observability Integration

Failed assertions emit telemetry signals:

  1. Metrics: Records assertion_failed_total with component/operation/assertion labels. Initialize with InitAssertionMetrics(metricsFactory).

  2. Tracing: Records assertion.failed span events (with stack traces in non-prod). Automatically uses the span from the context.

Stack Traces

Stack traces are included in logs and trace events only in non-production environments (ENV != production and GO_ENV != production).

Index

Constants

View Source
const AssertionSpanEventName = constant.EventAssertionFailed

AssertionSpanEventName is the event name used when recording assertion failures on spans.

Variables

View Source
var ErrAssertionFailed = errors.New("assertion failed")

ErrAssertionFailed is the sentinel error for failed assertions.

Functions

func BalanceIsZero

func BalanceIsZero(available, onHold decimal.Decimal) bool

BalanceIsZero returns true if both available and onHold balances are exactly zero.

func BalanceSufficientForRelease

func BalanceSufficientForRelease(onHold, releaseAmount decimal.Decimal) bool

BalanceSufficientForRelease returns true if the available on-hold balance is sufficient to release the specified amount.

func DateAfter

func DateAfter(date, reference time.Time) bool

DateAfter returns true if date is strictly after reference time.

func DateNotInFuture

func DateNotInFuture(date time.Time) bool

DateNotInFuture returns true if the date is not in the future (i.e., <= now). Zero time is considered valid (returns true).

func DebitsEqualCredits

func DebitsEqualCredits(debits, credits decimal.Decimal) bool

DebitsEqualCredits returns true if debits and credits are exactly equal. This validates the fundamental double-entry accounting invariant: for every transaction, total debits MUST equal total credits.

Note: Uses decimal.Equal() for exact comparison without floating point issues. Even a tiny difference indicates a bug in amount calculation.

Example:

a.That(ctx, assert.DebitsEqualCredits(debitTotal, creditTotal),
    "double-entry violation: debits must equal credits",
    "debits", debitTotal, "credits", creditTotal)

func InRange

func InRange(n, minVal, maxVal int64) bool

InRange returns true if min <= n <= max.

Note: If min > max (inverted range), always returns false. This is fail-safe behavior - callers should ensure min <= max for correct results.

Example:

a.That(ctx, assert.InRange(page, 1, 1000), "page out of range", "page", page)

func InRangeInt

func InRangeInt(n, minVal, maxVal int) bool

InRangeInt returns true if min <= n <= max. This is the int variant of InRange (which uses int64).

Note: If min > max (inverted range), always returns false. This is fail-safe behavior - callers should ensure min <= max for correct results.

Example:

a.That(ctx, assert.InRangeInt(cfg.PoolSize, 1, 100), "POOL_SIZE out of range", "value", cfg.PoolSize)

func InitAssertionMetrics

func InitAssertionMetrics(factory *metrics.MetricsFactory)

InitAssertionMetrics initializes assertion metrics with the provided MetricsFactory. This should be called once during application startup after telemetry is initialized.

func NonNegative

func NonNegative(n int64) bool

NonNegative returns true if n >= 0.

Example:

a.That(ctx, assert.NonNegative(balance), "balance must not be negative", "balance", balance)

func NonNegativeDecimal

func NonNegativeDecimal(amount decimal.Decimal) bool

NonNegativeDecimal returns true if amount >= 0.

Example:

a.That(ctx, assert.NonNegativeDecimal(balance), "balance must not be negative", "balance", balance)

func NonZeroTotals

func NonZeroTotals(debits, credits decimal.Decimal) bool

NonZeroTotals returns true if both debits and credits are non-zero. A transaction with zero totals is meaningless and indicates a bug.

Example:

a.That(ctx, assert.NonZeroTotals(debitTotal, creditTotal),
    "transaction totals must be non-zero",
    "debits", debitTotal, "credits", creditTotal)

func NotZero

func NotZero(n int64) bool

NotZero returns true if n != 0.

Example:

a.That(ctx, assert.NotZero(divisor), "divisor must not be zero", "divisor", divisor)

func Positive

func Positive(n int64) bool

Positive returns true if n > 0.

Example:

a.That(ctx, assert.Positive(count), "count must be positive", "count", count)

func PositiveDecimal

func PositiveDecimal(amount decimal.Decimal) bool

PositiveDecimal returns true if amount > 0.

Example:

a.That(ctx, assert.PositiveDecimal(price), "price must be positive", "price", price)

func PositiveInt

func PositiveInt(n int) bool

PositiveInt returns true if n > 0. This is the int variant of Positive (which uses int64).

Example:

a.That(ctx, assert.PositiveInt(cfg.MaxWorkers), "MAX_WORKERS must be positive", "value", cfg.MaxWorkers)

func ResetAssertionMetrics

func ResetAssertionMetrics()

ResetAssertionMetrics clears the assertion metrics singleton (useful for tests).

func TransactionCanBeReverted

func TransactionCanBeReverted(status string, hasParent bool) bool

TransactionCanBeReverted returns true if a transaction can be reverted. The transaction can only be reverted if:

  • Status is APPROVED
  • It has no parent transaction (i.e., it is not a reversal of another transaction)

This ensures only original transactions can be reverted, not reversals.

func TransactionCanTransitionTo

func TransactionCanTransitionTo(current, target string) bool

TransactionCanTransitionTo returns true if transitioning from current to target is valid. The transaction state machine only allows: PENDING -> APPROVED or PENDING -> CANCELED.

Note: This is for forward transitions only. Revert is a separate operation.

Example:

a.That(ctx, assert.TransactionCanTransitionTo(current, next),
    "invalid status transition",
    "current", current,
    "next", next)

func TransactionHasOperations

func TransactionHasOperations(operations []string) bool

TransactionHasOperations returns true if the transaction has operations.

func TransactionOperationsMatch

func TransactionOperationsMatch(operations, allowed []string) bool

TransactionOperationsMatch returns true if the operations match allowed operations.

func ValidAmount

func ValidAmount(amount decimal.Decimal) bool

ValidAmount returns true if the decimal's exponent is within reasonable bounds. The exponent must be in the range [-18, 18] to align with supported precision for financial calculations (scale up to 18 decimal places).

Note: This validates exponent bounds only, not coefficient size. For user-facing validation, consider additional bounds checks on the coefficient.

Example:

a.That(ctx, assert.ValidAmount(amount), "amount has invalid precision", "amount", amount)

func ValidPort

func ValidPort(port string) bool

ValidPort returns true if port is a valid network port number (1-65535). The port must be a numeric string representing a value in the valid range.

Note: Port 0 is invalid for configuration purposes (it's used for dynamic allocation). Empty strings, non-numeric values, and out-of-range values return false.

Example:

a.That(ctx, assert.ValidPort(cfg.DBPort), "DB_PORT must be valid port", "port", cfg.DBPort)

func ValidSSLMode

func ValidSSLMode(mode string) bool

ValidSSLMode returns true if mode is a valid PostgreSQL SSL mode. Valid modes are: disable, allow, prefer, require, verify-ca, verify-full. Empty string is also valid (uses PostgreSQL default).

Note: SSL modes are case-sensitive per PostgreSQL documentation. Unknown modes will cause connection failures.

Example:

a.That(ctx, assert.ValidSSLMode(cfg.DBSSLMode), "DB_SSLMODE invalid", "mode", cfg.DBSSLMode)

func ValidScale

func ValidScale(scale int) bool

ValidScale returns true if scale is in the range [0, 18]. Scale represents the number of decimal places for financial amounts.

Example:

a.That(ctx, assert.ValidScale(scale), "invalid scale", "scale", scale)

func ValidTransactionStatus

func ValidTransactionStatus(status string) bool

ValidTransactionStatus returns true if status is a valid transaction status. Valid statuses are: CREATED, APPROVED, PENDING, CANCELED, NOTED.

Note: Statuses are case-sensitive and must match exactly.

Example:

a.That(ctx, assert.ValidTransactionStatus(tran.Status.Code),
    "invalid transaction status",
    "status", tran.Status.Code)

func ValidUUID

func ValidUUID(s string) bool

ValidUUID returns true if s is a valid UUID string.

Note: Accepts both canonical (with hyphens) and non-canonical (without hyphens) UUID formats per RFC 4122. Empty strings return false.

Example:

a.That(ctx, assert.ValidUUID(id), "invalid UUID format", "id", id)

Types

type Asserter

type Asserter struct {
	// contains filtered or unexported fields
}

Asserter evaluates invariants and emits telemetry on failure.

func New

func New(ctx context.Context, logger Logger, component, operation string) *Asserter

New creates an Asserter with context, logging, and labels. component and operation are used for telemetry labeling.

func (*Asserter) Halt

func (asserter *Asserter) Halt(err error)

Halt terminates the current goroutine if err is not nil. Use this after a failed assertion in goroutines to prevent further execution.

func (*Asserter) Never

func (asserter *Asserter) Never(ctx context.Context, msg string, kv ...any) error

Never always returns an error. Use for code paths that should be unreachable.

Example:

return asserter.Never(ctx, "unhandled status", "status", status)

func (*Asserter) NoError

func (asserter *Asserter) NoError(ctx context.Context, err error, msg string, kv ...any) error

NoError returns an error if err is not nil. The error message and type are automatically included in the assertion context for debugging.

Example:

if err := asserter.NoError(ctx, err, "compute must succeed", "input", input); err != nil {
	return err
}

func (*Asserter) NotEmpty

func (asserter *Asserter) NotEmpty(ctx context.Context, s, msg string, kv ...any) error

NotEmpty returns an error if s is an empty string.

Example:

if err := asserter.NotEmpty(ctx, userID, "userID must be provided"); err != nil {
	return err
}

func (*Asserter) NotNil

func (asserter *Asserter) NotNil(ctx context.Context, v any, msg string, kv ...any) error

NotNil returns an error if v is nil. This function correctly handles both untyped nil and typed nil (nil interface values with concrete types).

Example:

if err := asserter.NotNil(ctx, config, "config must be initialized"); err != nil {
	return err
}

func (*Asserter) That

func (asserter *Asserter) That(ctx context.Context, ok bool, msg string, kv ...any) error

That returns an error if ok is false. Use for general-purpose assertions.

Example:

if err := asserter.That(ctx, len(items) > 0, "items must not be empty", "count", len(items)); err != nil {
	return err
}

type AssertionError

type AssertionError struct {
	Assertion string
	Message   string
	Component string
	Operation string
	Details   string
}

AssertionError represents a failed assertion with rich context.

func (*AssertionError) Error

func (entry *AssertionError) Error() string

Error returns the formatted assertion failure message.

func (*AssertionError) Unwrap

func (entry *AssertionError) Unwrap() error

Unwrap returns the sentinel assertion error for errors.Is.

type AssertionMetrics

type AssertionMetrics struct {
	// contains filtered or unexported fields
}

AssertionMetrics provides assertion-related metrics using OpenTelemetry. It wraps lib-uncommons' MetricsFactory for consistent metric handling.

func GetAssertionMetrics

func GetAssertionMetrics() *AssertionMetrics

GetAssertionMetrics returns the singleton AssertionMetrics instance. Returns nil if InitAssertionMetrics has not been called.

func (*AssertionMetrics) RecordAssertionFailed

func (am *AssertionMetrics) RecordAssertionFailed(
	ctx context.Context,
	component, operation, assertion string,
)

RecordAssertionFailed increments the assertion_failed_total counter with labels. If metrics are not initialized, this is a no-op.

type Logger

type Logger interface {
	Log(ctx context.Context, level log.Level, msg string, fields ...log.Field)
}

Logger defines the minimal logging interface required by assertions. This interface is satisfied by uncommons/log.Logger.

Jump to

Keyboard shortcuts

? : This menu
/ : Search site
f or F : Jump to
y or Y : Canonical URL