Your First Contribution
This module turns the codebase knowledge from earlier stops into an actual contribution: it shows where CONTRIBUTING.md expects you to start (an issue, not a PR), how Caddy's two test flavors — colocated _test.go unit tests and caddytest/integration end-to-end tests driven by .caddyfiletest fixtures — are built and run, and how to reproduce CI's build/test/lint gates locally before you ever push. It closes with the PR norms (small, squashed, AI-disclosed, fully explainable) that decide whether a technically-correct change actually gets merged.
The default unit-test shape: colocated, table-driven
modules/caddyhttp/fileserver/staticfiles_test.go · lines 35–50This file is staticfiles_test.go, sitting in the same directory and declaring the same package fileserver as staticfiles.go. That's the repo's convention for unit tests: colocate the _test.go file with the code it exercises, in the same package, so the test can call unexported functions directly — here, the lower-case fileHidden.
An anonymous struct type declares exactly the shape of one test case — inputs (inputHide, inputPath) and the expected output (expect) — then opens a slice literal of them. This is the table-driven test pattern: one function body, many rows of data, instead of one function per scenario.
Each {...} is one row: a hide-pattern list, a candidate path, and the expected true/false. The real table runs to a dozen-plus rows covering dotfiles, glob prefixes, and repeated names, and every single row — the first one shown here and the last one further down — is fed through the identical call actual := fileHidden(tc.inputPath, tc.inputHide) and compared to tc.expect. That single-call, table-driven shape is the default for colocated tests across this codebase: no server, no fixtures, just a function and its inputs.
func TestFileHidden(t *testing.T) { for i, tc := range []struct { inputHide []string inputPath string expect bool }{ { inputHide: nil, inputPath: "", expect: false, }, { inputHide: []string{".gitignore"}, inputPath: "/.gitignore", expect: true, },
The other flavor of caddytest: a real running server
caddytest/integration/handler_test.go · lines 11–32caddytest.NewTester is the same caddytest package as the fixture-comparison test, but this constructor only builds an HTTP client (cookie jar plus a transport that force-dials 127.0.0.1) — it doesn't start a Caddy process yet.
InitServer hands this literal Caddyfile text to the harness with configType "caddyfile". Under the hood that's the exact Admin API flow from an earlier module: it POSTs the raw config to /load on the admin endpoint. If nothing is answering there yet, the harness boots one in-process by setting os.Args to caddy run --config ... --adapter caddyfile and calling caddycmd.Main() — the same command-line boot sequence from "From Command Line to JSON: Booting Caddy", just invoked as a goroutine instead of a separate OS process.
This is the part a colocated unit test can't do: a real http.NewRequest goes out over the network to the now-running server, and AssertResponseCode reads the actual status code back — the whole listener-to-handler path from the "End to end" module, exercised by the test instead of by curl.
One catch: this test never actually runs under CI's default test command. initServer's very first line is if testing.Short() { tc.t.SkipNow() } — before it ever tries to reach an admin endpoint — and ci.yml's test job runs go test ... -short -race ./.... So a green CI test job does not prove this test executed; you have to drop -short locally to exercise it.
func TestBrowse(t *testing.T) { tester := caddytest.NewTester(t) tester.InitServer(` { skip_install_trust admin localhost:2999 http_port 9080 https_port 9443 grace_period 1ns } http://localhost:9080 { file_server browse } `, "caddyfile") req, err := http.NewRequest(http.MethodGet, "http://localhost:9080/", nil) if err != nil { t.Fail() return } tester.AssertResponseCode(req, 200)}
Reproducing CI's test job locally
.github/workflows/ci.yml · lines 104–138First it builds the actual cmd/caddy binary with go build -trimpath -ldflags="-w -s" -v — the same thing you'd run locally as cd cmd/caddy && go build to confirm a change compiles at all, before any test even runs.
Then it starts and stops that freshly built binary as a live smoke test — a real process boot/shutdown check, repeated across the linux, mac, and windows entries of the job's matrix, so a change that compiles but panics on startup fails here on every OS before a single Go test runs.
The command that actually runs the test suite is go test -coverprofile="cover-profile.out" -short -race ./... — that's the exact invocation to reproduce locally. -race turns on Go's data-race detector (slower, but catches concurrency bugs); -short is the flag that makes caddytest.Tester.initServer skip immediately instead of spinning up a real server, so this command alone does not exercise the caddytest/integration suite.
- name: Get dependencies run: | go get -v -t -d ./... # mkdir test-results - name: Build Caddy working-directory: ./cmd/caddy env: CGO_ENABLED: 0 run: | go build -trimpath -ldflags="-w -s" -v - name: Smoke test Caddy working-directory: ./cmd/caddy run: | ./caddy start ./caddy stop - name: Publish Build Artifact uses: actions/upload-artifact@bbbca2ddaa5d8feaa63e36b76fdaad77386f024f # v7.0.0 with: name: caddy_${{ runner.os }}_go${{ matrix.go }}_${{ steps.vars.outputs.short_sha }} path: ${{ matrix.CADDY_BIN_PATH }} compression-level: 0 # Commented bits below were useful to allow the job to continue # even if the tests fail, so we can publish the report separately # For info about set-output, see https://stackoverflow.com/questions/57850553/github-actions-check-steps-status - name: Run tests # id: step_test # continue-on-error: true run: | # (go test -coverprofile=cover-profile.out -race ./... 2>&1) > test-results/test-result.out go test -coverprofile="cover-profile.out" -short -race ./... # echo "status=$?" >> $GITHUB_OUTPUT
Check your understanding
Answered in place — nothing is graded, everything is explained. 0 / 3 passed
Per CONTRIBUTING.md, what should you do before writing a nontrivial code change and opening a PR for it?
ci.yml's test job runs `go test -coverprofile="cover-profile.out" -short -race ./...`. What does that mean for a caddytest/integration test that calls `Tester.InitServer`?
In a caddytest/integration/caddyfile_adapt/*.caddyfiletest fixture, what separates the input Caddyfile text from the expected JSON output?