DSL Guide¶
Suites And Cases¶
describe forms can be nested. Tests are registered when the file is loaded.
Because Common Lisp already exports CL:DESCRIBE, test packages should import
cl-weave:describe with :shadowing-import-from.
Table Tests¶
(it-each ((1 2 3)
(13 21 34))
"adds ~A and ~A"
(left right total)
(expect (+ left right) :to-be total))
(describe-each ((:json "application/json")
(:sexp "application/s-expression"))
"~A reporter"
(reporter content-type)
(it "declares its content type"
(expect content-type :to-satisfy #'stringp)))
(describe-each ((:json "application/json"))
"~A reporter with fixtures"
(reporter content-type)
(before-each
(setf (gethash :content-type *test-context*) content-type))
(it-each ((:ok :ok))
"runs generated case ~A"
(actual expected)
(expect actual :to-be expected)))
(it-only-each ((1 2 3))
"focuses generated case ~A and ~A"
(left right total)
(expect (+ left right) :to-be total))
(it-skip-each ((:slow :case))
"skips generated case ~A"
(kind label)
"blocked by upstream")
(it-todo-each ((:parser :stream) (:ffi :crash-boundary))
"documents generated todo ~A"
(area label)
"needs design")
it-each expands into independent it forms at macro expansion time.
describe-each expands into independent describe forms, so nested
fixtures and cases keep the same semantics as hand-written suites. Table forms
compose with canonical modifiers such as it-only-each,
it-concurrent-each, it-sequential-each, it-fails-each, it-skip-each,
and it-todo-each, and with the suite-level table variants
describe-only-each, describe-skip-each, and describe-todo-each. Fixture
hooks use the canonical Lisp names.
ai-contract.md is the machine-readable normalization
contract for agents. Runtime metadata also exposes referenceDocuments,
supportChannels, securityContacts, lifecycle,
runtimeSupport, and releaseProcess so external tools can discover
canonical docs, support routing, disclosure paths,
platform support, release policy, and project status without scraping prose.
Conditional Runs¶
(it-skip-if (not (probe-file #P"/tmp/service.sock"))
"talks to a local service"
(expect (probe-file #P"/tmp/service.sock") :to-be-truthy))
(it-run-if (member :sbcl *features*)
"uses SBCL allocation counters"
(expect (lambda () (list :ok)) :to-allocate-under 4096))
(describe-run-if (member :linux *features*)
"linux-only integration"
(it "checks a platform boundary"
(expect :ok :to-be :ok)))
it-skip-if and describe-skip-if register skipped tests or suites when the
condition is true. it-run-if and describe-run-if register skipped tests or
suites when the condition is false.
Conditions are evaluated while the test file registers tests; skipped branches
emit ordinary :skip events with deterministic reasons, and their hooks and
bodies are not executed.
Fixtures¶
(defvar *state*)
(describe "with fixture"
(before-all
(setf *state* (make-hash-table)))
(before-all
(setf (gethash :created *state*) t))
(before-each
(setf (gethash :trace *state*) nil))
(around-each (next)
(let ((*state* *state*))
(unwind-protect
(funcall next)
(remhash :scratch *state*))))
(after-each
(remhash :trace *state*))
(after-all
(setf *state* nil))
(it "uses dynamic state"
(setf (gethash :x *state*) 1)
(expect (gethash :x *state*) :to-be 1)))
before-all / after-all bodies run once around a suite. before-each /
after-each bodies run around every test in the current suite and nested suites.
around-each receives a continuation for the remaining around hooks and test
body, so special variables can be dynamically rebound around only the case.
Use unwind-protect inside around-each when the fixture owns cleanup.
after-each and after-all teardown runs even when a test body or hook exits
non-locally — including SBCL timeouts and debugger restarts — so fixtures that
release resources in a teardown hook are not skipped when a case times out.
When a hook itself fails, cl-weave signals a hook-failure condition whose
readers hook-failure-phase and hook-failure-causes identify which lifecycle
phase failed and the underlying conditions it collected.
Fixture hooks intentionally use canonical Lisp names rather than camelCase
aliases, because Common Lisp uppercases unescaped symbols while reading source.
CPS Continuation Helpers¶
Use with-continuation-result when testing callback/CPS APIs that receive a
continuation function. The macro binds the continuation name supplied in the
binding list, runs the form, asserts that the continuation was called, and then
exposes the first value passed to it.
(it "tests a CPS parser"
(with-continuation-result (node next calledp)
(parse-token-cps "42" #'next)
(expect calledp :to-be-truthy)
(expect node :to-equal '(:number 42))))
Use with-continuation-values when the continuation carries multiple values:
(with-continuation-values (values next)
(decode-cps input #'next)
(expect values :to-equal '(:ok (:amount 100))))
Skipping¶
(describe-skip "upstream-dependent suite" "waiting for upstream behavior"
(it "documents a blocked case"
(expect :unreachable :to-be :reachable)))
(it-skip "documents a pending case" "waiting for upstream behavior")
Skipped suites report selected descendant cases as :skip without running suite
hooks or test bodies. Skipped cases use the same event status and do not fail
run-all.
Focus And Todo¶
(describe-only "focused suite"
(it "runs inside focused suite"
(expect :selected :to-be :selected)))
(it-only "focuses a single case"
(expect (+ 40 2) :to-be 42))
(it-todo "documents a missing edge case" "needs property generator")
(it-todo-each ((:ast) (:ffi))
"documents future coverage for ~A"
(area)
"needs generator")
(describe-todo "future protocol" "needs design"
(it "documents the expected shape"
(expect :draft :to-be :stable)))
(describe-todo-each ((:json) (:sexp))
"future ~A reporter"
(reporter)
"needs snapshot contract"
(it "documents pending reporter behavior"
(expect reporter :to-satisfy #'keywordp)))
When any suite or case is focused, run-all executes only the focused path.
Todo suites report selected descendant cases as :todo without running suite
hooks or test bodies. Todo cases use the same event status and do not fail
run-all.
Retry And Timeout¶
(it "eventually observes an external state" (:retry 2 :timeout-ms 500)
(expect (probe-state) :to-be :ready))
(it "supports retry options" (:retry 1)
(expect (+ 20 22) :to-be 42))
(it-fails "documents a known parser bug" (:retry 1)
(expect (parse-fragment input) :to-be :accepted))
:retry is the number of extra attempts after the first attempt. Fixtures and
dynamic *test-context* are recreated for every attempt. :timeout-ms fails the
case if a single attempt exceeds the configured wall-clock budget. Timeout
failures are reported as test-timeout conditions.
CLI and CI runs can set suite-wide defaults with --retry,
CL_WEAVE_RETRY, --test-timeout-ms, CL_WEAVE_TEST_TIMEOUT_MS,
CL_WEAVE_TEST_TIMEOUT, --max-workers, or CL_WEAVE_MAX_WORKERS. Per-test
options take priority over
global defaults, so :retry 0 disables a global retry budget for one case,
:timeout-ms replaces the global per-attempt timeout, and --max-workers
bounds adjacent concurrent worker batches.
it-fails inverts one runnable case only when its test attempt signals
assertion-failure. An implementation error or timeout remains visible as
:error or :fail; an unexpectedly passing body is reported as :fail with
expected-failure-missed.
Interactive Restarts¶
Every runnable attempt installs Common Lisp restarts while the body and
before-each / after-each hooks are active:
(handler-bind ((cl-weave:assertion-failure
(lambda (condition)
(declare (ignore condition))
(invoke-restart 'cl-weave:retry-test))))
(cl-weave:run-all))
continue-test records the current attempt as :pass, skip-test records it
as :skip with an optional reason, and retry-test reruns the attempt while
consuming the configured :retry budget. If no handler or debugger invokes a
restart, CI behavior is unchanged and the original failure, error, or timeout is
reported normally.
Concurrent Tests¶
(it-concurrent "fetches account metadata" (:timeout-ms 1000)
(expect (fetch-account) :to-satisfy #'account-ready-p))
(it "uses option form when macros generate cases" (:execution-mode :concurrent :retry 1)
(expect (probe-cache) :to-be :warm))
(describe-concurrent "parallel-safe API checks"
(it "fetches account" (expect (fetch-account) :to-satisfy #'account-ready-p))
(it-sequential "uses shared rate-limit bucket"
(expect (probe-rate-limit) :to-be :available)))
it-concurrent and (:execution-mode :concurrent) mark a case as safe
to run beside adjacent concurrent cases. describe-concurrent sets the same
execution mode for descendant cases, and
it-sequential opts a single case back out. describe-sequential is the
suite-level inverse — it forces sequential execution for its descendants, which
is useful for nesting a serial group inside an otherwise concurrent parent.
Report order
stays deterministic: events are emitted in the selected definition order. When
:bail is enabled, concurrent batching is disabled so fast-fail behavior
remains exact. run-all :max-workers N, --max-workers N, and
CL_WEAVE_MAX_WORKERS=N bound the number of worker threads used for each
adjacent concurrent batch.