Source code

Building and testing

How to work on the server itself: building it, running the tests, and what the tests check. To use the server in a project, Installing is enough.

Build

git clone --recursive https://github.com/Kiyomi-Computation-Systems/chez-http-server.git
cd chez-http-server
make build

make help lists every target. The ones you will use most:

TargetDoes
make buildbuilds the chez-libuv shim and assembles the library path
make testruns every suite, tests/test-*.sps
make run PROGRAM=... [ARGS="..."]runs a program on the library path
make devstarts a Chez REPL with everything on the library path
make check-pinschecks every submodule sits at its pinned commit, and Akku.manifest at the same versions
make prodrebuilds the shim optimized and runs the suite on it
make cleanremoves build artifacts

Test

make test

For the test run you also need the clients the suites drive the server with: curl, ab (apache2-utils on Linux), python3 and awk, plus pcre2 and zlib where pkg-config sees them (brew install pcre2, or apt install libpcre2-dev zlib1g-dev). make test builds vtest2 from its pinned source and downloads oha, checked by its sha256, the first time.

To run one suite:

make test TESTS=tests/test-examples.sps

Every suite exits with one of four statuses: 0 all passed, 1 a check failed, 2 the suite itself broke or ran no check, 3 a vendored file no longer matches its recorded checksum. make test runs them all, keeps going past a failure, and reports ALL SUITES PASSED only when every one passed.

What the suites check

KindChecksAgainst
the pure coreevery row of the connection's transition table, the limits, the serializer, targets, Hostthe table written out as data, RFC 9110 and 9112
the parser corporahow the server answers each request, however its bytes are split into readsllhttp's fixtures, libhtp's streams
strictness and robustnesswhich requests are refused, and with whatKestrel's, Apache httpd's and lighttpd's tests; AWS http-desync-guardian; request-smuggling vectors
the shellsockets, timeouts, backpressure, drain, upgradesa real libuv loop on loopback
real clientsthat what they see is rightcurl, ApacheBench, vtest2 scripts, an oha load test
allocationthe bytes a request allocatesa ceiling the suite fails above
the examplesthat each example answers as its page sayseach started as its own process

The corpus suites try a sample of the ways to split each input by default; CORPUS=full make test tries every split.

The vendored corpora live in test/vendor/, each with a PROVENANCE file giving its source and the sha256 of every file. They are never edited by hand; make corpus-refresh re-vendors one from an upstream checkout.

In Docker

With Docker installed, one script gives you every tool the build, the tests and the docs site need, without installing any of them on your machine:

scripts/docker-env make test    # run one command
scripts/docker-env              # or open a shell

The command runs in a container built from docker/dev.Dockerfile: the Ubuntu and packages CI uses, and Chez Scheme 10.4.1 built from source.

  • The first run is slow. It builds the image first, mostly compiling Chez, which takes a few minutes. Later runs start in about a second. The image is rebuilt only when docker/dev.Dockerfile or the Chez version changes.
  • A fresh clone needs nothing else. The container has the network, so make test clones the submodules and fetches oha itself.
  • It mounts this checkout and nothing else of your machine (in a linked worktree, also the main repository's .git): none of your git config but the repository's own, no keys, no tokens, and no environment variables but the terminal's and your Docker proxy settings. Edit, commit and push outside it. It runs as you, so the files it writes are yours.
  • It is not a sandbox. It reaches the network, your machine's own services included, and your machine later runs what it writes into the checkout, .git included.
  • To reach a server it runs, give a port with -p. scripts/docker-env -p 8080 make run PROGRAM=examples/hello.sps serves on your machine's 127.0.0.1:8080, and curl http://127.0.0.1:8080/ from outside answers Hello. -p sets the port inside too, so for another one give -p 3000, not CHEZ_HTTP_PORT.
  • Set a variable inside it with env, after -p: scripts/docker-env -p 8080 env CHEZ_HTTP_DRAIN_TIMEOUT=5 make run PROGRAM=examples/hello.sps.
  • One toolchain at a time. The container shares build/ with your machine, and each toolchain records paths the others lack. Run make clean when you switch between it, your machine's own tools and the Guix container.

make check-docker, run outside the container, tests the script, then checks the container from inside: the tools, Chez and libuv at their floors, the isolation, then make test and make check-site. Last, it serves an example through -p and asks it from your machine.

On a Guix host

With Guix installed, one script gives you every tool the build, the tests and the docs site need, without installing any of them into your system:

scripts/guix-env make test    # run one command
scripts/guix-env              # or open a shell

The command runs in a container built from the Guix commit pinned in channels.scm and the packages in manifest.scm: Chez Scheme 10.4.1, libuv 1.52.1, gcc, curl, ab, what vtest2 builds with, and cmark-gfm.

  • The first run is slow. Guix fetches its pinned commit and the packages, then builds Chez from source, which alone takes about two minutes. Later runs start in a few seconds, so batch commands: scripts/guix-env sh -c 'make test && make check-site'.
  • A fresh clone needs nothing else. The container has the network, so make test clones the submodules and fetches oha itself.
  • It sees this checkout and nothing else of your home directory (in a linked worktree, also the main repository's .git): no git identity, no keys, no tokens. Edit, commit and push outside it.
  • It sets CHEZ and CC, so no CHEZ= is needed.
  • A server it runs answers on your machine. It shares the network, so after scripts/guix-env make run PROGRAM=examples/hello.sps, curl http://127.0.0.1:8080/ from outside answers Hello.
  • Your environment stays outside, except TERM, COLORTERM and LANG. Set a variable inside instead: scripts/guix-env env CHEZ_HTTP_PORT=3000 make run PROGRAM=examples/hello.sps.

make check-guix, run outside the container, tests the script, then checks the container from inside: the pinned tools, Chez and libuv at their floors, the isolation, and finally make test and make check-site. To move the pin, change the commit in channels.scm (guix describe -f channels prints yours) and run make check-guix.

The docs site

These pages render to a small static site:

make site          # writes build/site/; open build/site/index.html
make check-site    # every page listed, every link and anchor resolving

The site is built by build-site.sps and the libraries in site/, through chez-cmark-gfm, which needs the system cmark-gfm library (brew install cmark-gfm, or apt install cmark-gfm). make site fetches chez-cmark-gfm the first time, as Akku.lock pins it, or uses .akku/ after akku install, which also installs the server's dependencies and builds chez-libuv's shim; make check-akku-lock checks Akku.lock is what akku lock makes of Akku.manifest. A new page goes in docs/ and in site/pages.sls, the nav's order; make check-site fails until both name it.

Each release tag publishes the site to GitHub Pages, through .github/workflows/pages.yml, after make check-site passes; it also runs by hand from the Actions tab. That job has only Chez and cmark-gfm: make check-site clones chez-srfi alone and builds no shim, and CI's site job builds the site the same way on every pull request.

The Akku package

Akku.manifest also makes the server an Akku package: the libraries under src/, depending on chez-libuv, chez-httparse, chez-uri (the uriparse repository) and byte-machine. Each range starts at the version vendored under vendor/, the one the suites ran against; a later patch release meets it too. make check-pins fails when a range, a vendored copy and Akku.lock disagree, so a pin moves in all three at once: the submodule, its range, then akku lock. It also fails when the package's version is not the latest release in CHANGELOG.md, so bump it in Akku.manifest with each release.

make check-akku needs akku. It checks Akku.lock with make check-akku-lock, then installs this checkout into a scratch project as someone using the server would, with the dependencies Akku.lock locks, and checks that exactly src/'s libraries land, that chez-libuv's install hook builds the shim, and that examples/hello.sps answers on Akku's library path alone.

House rules

  • Functional core, imperative shell. Rules go in the pure libraries under src/http-server/, which import no libuv; effects go in src/http-server/private/. make check-imports enforces it. The facade, src/http-server.sls, and the testing library, src/http-server/testing.sls, sit over both.
  • A test is finished when it has been seen to fail. Break the code it guards, watch the check go red, then put the code back.
  • Every exported procedure has a signature comment, Typed Racket style: ;; reason-status : Symbol -> Nat.
  • Comments describe the code as it is, not how it came to be.

CI runs make test and make check-site on Linux for every pull request. CONTRIBUTING.md has the rest of what a pull request needs.