diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index f3bf83c..e3569c7 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -41,6 +41,9 @@ jobs: exit 1 fi + - name: Check Generated Stubs + run: make check-generate + - name: Test (Production Mode) run: make test diff --git a/Makefile b/Makefile index d6edc57..85ba0db 100644 --- a/Makefile +++ b/Makefile @@ -14,7 +14,7 @@ TAGS_DEBUG := -tags debug GENERATOR_CMD := cmd/gen-release/main.go GENERATOR_BIN := gen-release -.PHONY: all test test-debug fmt tidy generate build-generator help +.PHONY: all test test-debug fmt tidy generate check-generate build-generator help all: test ## Run all tests (release mode) @@ -26,6 +26,14 @@ generate: build-generator ## Generate release stubs from debug code by running g @echo "Generating release stubs..." ./$(GENERATOR_BIN) -in bumert_debug.go +check-generate: generate ## Fail if committed release stubs differ from freshly generated ones + @echo "Checking generated release stubs are up to date..." + @if ! git diff --quiet bumert_release.go; then \ + echo "Release stubs are out of date. Run 'make generate' and commit the result."; \ + git diff bumert_release.go; \ + exit 1; \ + fi + test: generate ## Run tests without debug assertions (generates stubs first) @echo "Running tests (release mode)..." $(GOTEST) $(PKG) diff --git a/README.md b/README.md index b2b9404..0dece86 100644 --- a/README.md +++ b/README.md @@ -83,6 +83,32 @@ To **enable** assertions, you must include the `debug` build tag: **Running/Testing without the tag** (e.g., `go test ./...` or `make test` or `go build main.go`) produces a binary where all `bumert` checks are **completely absent**. +## Important: the check is free, the argument is not + +The "zero overhead" guarantee covers the assertion machinery itself. In a release build the calls are inlined away to nothing, including the interface boxing, so a check like `Should(ptr).NotBeNil()` compiles down to zero instructions and performs no allocations. + +What it does **not** do is remove the expression you pass in. Go evaluates function arguments before the call, and a no-op cannot un-evaluate them. This is different from a C-style `assert()` macro, which the preprocessor deletes entirely under `NDEBUG`. So with `bumert` the value being asserted is still computed in production: + +```go +// The pointer load is free and the whole line disappears in release builds. +bumert.Should(ptr).NotBeNil() + +// expensiveCheck() STILL RUNS in release builds, even though the assertion +// itself is a no-op. Only the assertion around the result is removed. +bumert.Assertf(expensiveCheck(), "must hold") +``` + +If the thing you want to assert is expensive or has side effects, defer it behind a closure with `TrueFn`. The closure is never called in release builds, so the work is skipped there: + +```go +cfg := loadConfig() +bumert.Should(cfg).TrueFn(func() bool { + return cfg.IsValid() // expensive; only runs with -tags debug +}) +``` + +Rule of thumb: pass plain values and pointers directly, and put anything costly behind `TrueFn`. + ## Panic Behavior When the `debug` tag is enabled, a **failed assertion will cause a panic**. This is intentional and follows the common pattern for assertion libraries in Go (and other languages). It immediately halts execution at the point of the violated assumption during development or debugging, making issues easy to spot. The panic message includes the file and line number of the failed assertion, along with the failure reason (e.g., "Expected to be greater than ").