asserts
This commit is contained in:
parent
991fe531fe
commit
1e7719dbd0
2
.vscode/settings.json
vendored
2
.vscode/settings.json
vendored
@ -3,7 +3,7 @@
|
||||
"go.lintTool": "golangci-lint",
|
||||
"go.lintFlags": ["--fast"],
|
||||
"go.lintOnSave": "workspace",
|
||||
"go.testFlags": ["-race"],
|
||||
"go.testFlags": ["-race", "-v"],
|
||||
"go.coverOnSave": false,
|
||||
|
||||
// ── Editor ─────────────────────────────────────────────────────────────────
|
||||
|
||||
18
Makefile
18
Makefile
@ -15,6 +15,22 @@ LDFLAGS := -ldflags "\
|
||||
CMDS := $(shell find cmd -mindepth 1 -maxdepth 1 -type d 2>/dev/null)
|
||||
BINS := $(patsubst cmd/%,bin/%,$(CMDS))
|
||||
|
||||
# ── Build tags ─────────────────────────────────────────────────────────────────
|
||||
# Pass TAGS=<comma-separated> to select optional build variants for the binary:
|
||||
# make build TAGS=assert_disable # disable assertions (production)
|
||||
# make build TAGS=result_goexit # Goexit-based result exits
|
||||
# make build TAGS=assert_disable,result_goexit
|
||||
#
|
||||
# Available tags:
|
||||
# assert_disable pkg/assert: compile out all assertion checks (zero cost)
|
||||
# result_goexit pkg/result: use runtime.Goexit instead of panic for exits
|
||||
#
|
||||
# Tests always run with the default tags (assertions on, panic exits).
|
||||
# The only reason to pass TAGS to a test target is profiling, where assertion
|
||||
# overhead must be excluded from the measurement.
|
||||
TAGS ?=
|
||||
TAGS_FLAG = $(if $(TAGS),-tags $(TAGS))
|
||||
|
||||
# ── Default target ─────────────────────────────────────────────────────────────
|
||||
help: ## Show this help message
|
||||
@grep -E '^[a-zA-Z_-]+:.*?## .*$$' Makefile | \
|
||||
@ -46,7 +62,7 @@ build: $(BINS) ## Compile all cmd/* binaries to ./bin/ (stamped with version, co
|
||||
|
||||
bin/%: cmd/%
|
||||
@mkdir -p bin
|
||||
go build $(LDFLAGS) -o $@ ./$<
|
||||
go build $(TAGS_FLAG) $(LDFLAGS) -o $@ ./$<
|
||||
|
||||
# ── Test ───────────────────────────────────────────────────────────────────────
|
||||
test: ## Run all tests
|
||||
|
||||
25
pkg/assert/assert.go
Normal file
25
pkg/assert/assert.go
Normal file
@ -0,0 +1,25 @@
|
||||
//go:build !assert_disable
|
||||
|
||||
package assert
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"runtime"
|
||||
)
|
||||
|
||||
// That panics if condition is false, reporting msg as the violated invariant.
|
||||
func That(condition bool, msg string) {
|
||||
if !condition {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
panic(fmt.Sprintf("assertion violated at %s:%d: %s", file, line, msg))
|
||||
}
|
||||
}
|
||||
|
||||
// Thatf panics if condition is false, reporting a formatted string as the
|
||||
// violated invariant. Arguments are formatted as in [fmt.Sprintf].
|
||||
func Thatf(condition bool, format string, args ...any) {
|
||||
if !condition {
|
||||
_, file, line, _ := runtime.Caller(1)
|
||||
panic(fmt.Sprintf("assertion violated at %s:%d: %s", file, line, fmt.Sprintf(format, args...)))
|
||||
}
|
||||
}
|
||||
11
pkg/assert/assert_disabled.go
Normal file
11
pkg/assert/assert_disabled.go
Normal file
@ -0,0 +1,11 @@
|
||||
//go:build assert_disable
|
||||
|
||||
package assert
|
||||
|
||||
// That is a no-op in assert_disable builds; the compiler eliminates the call
|
||||
// and its condition entirely.
|
||||
func That(_ bool, _ string) {}
|
||||
|
||||
// Thatf is a no-op in assert_disable builds; the compiler eliminates the call
|
||||
// and its condition entirely.
|
||||
func Thatf(_ bool, _ string, _ ...any) {}
|
||||
19
pkg/assert/assert_disabled_test.go
Normal file
19
pkg/assert/assert_disabled_test.go
Normal file
@ -0,0 +1,19 @@
|
||||
//go:build assert_disable
|
||||
|
||||
package assert_test
|
||||
|
||||
import (
|
||||
"testing"
|
||||
|
||||
"gitea.djmil.dev/go/template/pkg/assert"
|
||||
)
|
||||
|
||||
// In a disabled build, That and Thatf must not panic regardless of condition.
|
||||
|
||||
func TestThat_disabled(t *testing.T) {
|
||||
assert.That(false, "must be silently ignored")
|
||||
}
|
||||
|
||||
func TestThatf_disabled(t *testing.T) {
|
||||
assert.Thatf(false, "must be silently ignored: %d", 42)
|
||||
}
|
||||
59
pkg/assert/assert_test.go
Normal file
59
pkg/assert/assert_test.go
Normal file
@ -0,0 +1,59 @@
|
||||
//go:build !assert_disable
|
||||
|
||||
package assert_test
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
|
||||
"gitea.djmil.dev/go/template/pkg/assert"
|
||||
)
|
||||
|
||||
func TestThat_passes(t *testing.T) {
|
||||
assert.That(true, "should not panic")
|
||||
}
|
||||
|
||||
func TestThat_panics(t *testing.T) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatal("expected panic, got none")
|
||||
}
|
||||
msg, ok := r.(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected string panic value, got %T", r)
|
||||
}
|
||||
if !strings.Contains(msg, "ring must be non-empty") {
|
||||
t.Errorf("panic message %q does not contain invariant text", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "assert_test.go") {
|
||||
t.Errorf("panic message %q does not contain caller file", msg)
|
||||
}
|
||||
}()
|
||||
assert.That(false, "ring must be non-empty")
|
||||
}
|
||||
|
||||
func TestThatf_passes(t *testing.T) {
|
||||
n := 1
|
||||
assert.Thatf(n > 0, "should not panic: %d", n)
|
||||
}
|
||||
|
||||
func TestThatf_panics(t *testing.T) {
|
||||
defer func() {
|
||||
r := recover()
|
||||
if r == nil {
|
||||
t.Fatal("expected panic, got none")
|
||||
}
|
||||
msg, ok := r.(string)
|
||||
if !ok {
|
||||
t.Fatalf("expected string panic value, got %T", r)
|
||||
}
|
||||
if !strings.Contains(msg, "len=5 cap=3") {
|
||||
t.Errorf("panic message %q does not contain formatted args", msg)
|
||||
}
|
||||
if !strings.Contains(msg, "assert_test.go") {
|
||||
t.Errorf("panic message %q does not contain caller file", msg)
|
||||
}
|
||||
}()
|
||||
assert.Thatf(false, "buffer overflowed: len=%d cap=%d", 5, 3)
|
||||
}
|
||||
18
pkg/assert/doc.go
Normal file
18
pkg/assert/doc.go
Normal file
@ -0,0 +1,18 @@
|
||||
// Package assert provides compile-time-removable invariant checks.
|
||||
//
|
||||
// Use assert to document programmer contracts — preconditions, postconditions,
|
||||
// and internal invariants that should be logically impossible to violate if the
|
||||
// code is correct. A triggered assertion is always a bug, never a user error.
|
||||
//
|
||||
// Assertions are enabled by default (dev/debug builds). Disable them in
|
||||
// production builds by passing -tags assert_disable to the Go toolchain:
|
||||
//
|
||||
// go build -tags assert_disable ./...
|
||||
//
|
||||
// In a disabled build both [That] and [Thatf] are compiled out entirely — no
|
||||
// condition is evaluated and no allocation occurs.
|
||||
//
|
||||
// Contrast with [gitea.djmil.dev/go/template/pkg/result], which handles runtime
|
||||
// correctness (I/O, parsing, external inputs). Use result for things that can
|
||||
// legitimately fail; use assert for things that must never fail.
|
||||
package assert
|
||||
Loading…
Reference in New Issue
Block a user