Caddy Onboarding LabChapter VII — Your First Contribution0/6Contents
Chapter VII

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.


walkthrough

The default unit-test shape: colocated, table-driven

modules/caddyhttp/fileserver/staticfiles_test.go · lines 3550
line 35

This 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.

lines 36–40

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.

lines 41–50

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,		},
step 1 of 3
walkthrough

The other flavor of caddytest: a real running server

caddytest/integration/handler_test.go · lines 1132
lines 11–12

caddytest.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.

lines 13–24

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.

lines 26–31

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.

line 32

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)}
step 1 of 4
walkthrough

Reproducing CI's test job locally

.github/workflows/ci.yml · lines 104138
lines 109–114

First 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.

lines 116–120

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.

lines 132–138

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
step 1 of 3
checkpoint

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?