This commit is contained in:
djmil 2026-07-29 18:53:22 +00:00
parent 991fe531fe
commit ef1175d00c
7 changed files with 150 additions and 2 deletions

View File

@ -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

View File

@ -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
View 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...)))
}
}

View 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) {}

View 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
View 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
View 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