Development¶
Build, test and formatting commands for working on cl-cli itself.
Contribution process, review expectations and the code of conduct are org-wide and live in nerima-lisp/.github.
Environment¶
The dev shell provides SBCL, ECL, rlwrap, and every tool the
shell-verification suite pipes generated output through: bash, zsh,
fish, nushell, pwsh, elvish, and mandoc. It also exports the
CL_*_SOURCE_DIR variables that point the test loader at the sibling
packages, so no checkout layout is assumed.
Without Nix, install SBCL and load the system through ASDF. cl-cli itself
depends on uiop (ships with every modern ASDF) on every implementation but
SBCL, and additionally on
cl-host-kit on SBCL.
The optional SBCL-only cl-cli/concurrent system additionally depends on
cl-concurrent-kit.
Commands¶
$ nix flake check # the full gate: SBCL, ECL, formatting, docs
$ nix run .#test # SBCL suite only, fastest feedback
$ nix fmt # format Nix sources (treefmt/nixfmt)
$ nix build .#docs # render the documentation site with --strict
$ SYSTEM=$(nix eval --raw --expr 'builtins.currentSystem')
$ nix build ".#checks.${SYSTEM}.coverage" --no-link --print-out-paths
$ nix build ".#checks.${SYSTEM}.coverage-gate" --no-link
Granularity lives in the flake's checks.* attributes rather than in extra CI
jobs, so nix flake check runs exactly what CI runs:
| Check | What it covers |
|---|---|
default |
SBCL, core suite plus shell verification |
ecl |
ECL, core suite only |
formatting |
every tracked .nix file is nixfmt-clean |
docs |
mkdocs build --strict — broken links and unlisted pages fail |
paredit-lint |
every .lisp/.asd file is a balanced S-expression document |
coverage |
sb-cover HTML for reachable branches in cl-cli |
coverage-gate |
expression and branch coverage stay at or above 96% |
nix flake check only evaluates the outputs for the system it runs on. The
flake declares x86_64-linux and aarch64-darwin. CI runs on the former; the
latter is a development output without a CI gate. Other hosts need a builder
for one of those systems. See
Compatibility.
The coverage check returns a store path containing cover-index.html. The
coverage-gate check parses the same report and rejects regressions below 96%
for either aggregate expressions or branches. The project target remains 100%
of reachable branches inside function bodies; sb-cover's raw expression
percentage also includes top-level forms and macro-expansion helpers.
The flake is generated by
cl-nix-forge's mkPackageFlake,
the org preset every nerima-lisp package shares, so those checks are the org
standard's own output table plus the two entries this repository adds.
To run a suite without Nix:
run-tests.lisp loads the suite and runs it, exiting non-zero on any failure —
it is the same file nix flake check and nix run .#test invoke, so the local
command and the CI gate cannot drift apart. Outside Nix, the test dependencies
have to be reachable: put them on CL_SOURCE_REGISTRY, check them out next to
this repository as ../cl-weave/ and friends, or point CL_WEAVE_SOURCE_DIR
and its siblings at them.
The suite is split in two¶
run-tests.lisp prints which half it ran on every run, because a suite that
quietly shrinks when a dependency goes missing reads exactly like a suite that
passed.
cl-cli/testis the portable core and runs everywhere. Its dependencies are all portable Common Lisp.cl-cli/test/shell-verificationadditionally pipes every generated completion script and man page through the realbash,zsh,fish,nushell,pwsh,elvish, andmandocthat will consume them. It depends oncl-process-kitfor subprocess timeouts, whose owncl-log-kitdependency hard-codessb-thread:*(nerima-lisp/cl-log-kit#1, open upstream), so this half runs under SBCL only.
The split lives at the system boundary precisely so that a non-SBCL implementation runs the core suite for real instead of failing to compile the whole thing. Both implementations are release-blocking.
The SBCL test system also loads the optional cl-cli/concurrent API and its
focused batch-parsing tests. ECL deliberately excludes that system and keeps
the portable boundary intact.
Two suites are gated rather than run everywhere: the fuzz suite needs a harness
timeout capability cl-weave does not offer on every implementation, and the
benchmark budgets are absolute millisecond thresholds calibrated against SBCL.
Both report as skipped, with a reason, rather than as failures. See
Compatibility for the supported implementation matrix.
cl-weave is intentionally test-only. The CPS tests use
with-continuation-values to verify multiple-value continuations, while the
fuzz suite uses describe-run-if only when the host exposes a timeout
capability. On hosts with that capability, the common runner calls its Lisp
API as (run-all :timeout-ms 120000 :pass-with-no-tests nil), giving each test
a two-minute fail-safe. Hosts without the capability omit :timeout-ms and
are bounded by the enclosing Nix process timeout instead. Both paths reject an
empty loaded registry. The CLI flag spelling --test-timeout-ms is not used
inside Lisp. Keeping cl-weave out of the runtime ASDF systems preserves the
portable parser boundary.
Writing tests¶
Test files are named after the source file they exercise:
t/<source>-test.lisp for src/<source>.lisp, or
t/<source>-<aspect>-test.lisp when one source has several aspects worth
separating (t/parser-value-storage-count-test.lisp and
t/parser-value-storage-key-value-test.lisp both cover
src/parser-value-storage.lisp). Shared fixtures and assertion helpers are
not tests and are named t/helpers-<concern>.lisp.
Add or update a focused test in the file matching the source you changed, and
register any new file in cl-cli.asd. run-tests.lisp is the loader, not a
place to put tests. A test that shells out to a real tool belongs in
t/completion-commands-shell-verification-test.lisp under the
cl-cli/test/shell-verification system; everything else goes in the portable
cl-cli/test.
The SBCL-only t/concurrent-test.lisp exercises
cl-cli/concurrent:parse-argv-batch and is registered conditionally in
cl-cli.asd.
Cover both the success path and the expected failure mode when relevant.
Prefer an assertion that would fail if the behavior were wrong over one that
merely records what the code prints today. A completion renderer emitting
e:'app' shipped broken because a test asserted that exact string; the
question to ask is "what property does this encode", not "does this match".
If a test cannot run on a given implementation, gate it on the capability that is actually missing and let it report as a skip with a reason. Do not widen a threshold or delete an assertion so that a red run turns green.
Changing the public surface¶
Keep public exports intentional: add one only when it is reusable outside a single local consumer. Preserve constructor fail-fast behavior for invalid specs, and reuse the shared normalization helpers instead of re-implementing string or list validation in several places.
When the user-visible surface changes, update API Reference and the matching guide page in the same pull request. Release history is not kept in the tree: it goes into the GitHub Release description when the release is cut.
Releasing¶
The org-wide procedure is in RELEASE_STANDARD.md.
Two things are specific to this repository:
:versionappears five times incl-cli.asd— incl-cli,cl-cli/demo,cl-cli/concurrent,cl-cli/testandcl-cli/test/shell-verification— and all five must match. The+demo-version+literal indemo/main.lispmust also matchcl-cli/demo;t/demo-test.lispchecks that equality.flake.nixreads the first:versionline, andrelease.ymlrefuses to publish a tag that disagrees with it.- A release verified only by the portable core has not exercised the generated completion scripts. Confirm the runner's own line about which half it loaded.
After the checks pass, create the release tag locally as an annotated tag: