Skip to content

Commit 064b7fc

Browse files
committed
Merge TASK-007: CI test for public-header hygiene
2 parents 7c2f460 + f3c0292 commit 064b7fc

7 files changed

Lines changed: 423 additions & 17 deletions

File tree

.github/workflows/verify-build.yml

Lines changed: 30 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -253,6 +253,24 @@ jobs:
253253
debug: debug
254254
coverage: nocoverage
255255
shell: bash
256+
# TASK-007: dedicated header-hygiene gate. Runs `make check-hygiene`
257+
# (preprocesses <httpserver.hpp> against the staged install and greps
258+
# for forbidden backend headers). Surfaces this gate as its own named
259+
# GitHub Actions check so reviewers see header-hygiene status
260+
# independently of the broader `make check` log. Until M5 lands the
261+
# check is informational (HEADER_HYGIENE_STRICT defaults to "no");
262+
# TASK-020 flips it to strict.
263+
- test-group: extra
264+
os: ubuntu-latest
265+
os-type: ubuntu
266+
build-type: header-hygiene
267+
compiler-family: gcc
268+
c-compiler: gcc-14
269+
cc-compiler: g++-14
270+
debug: nodebug
271+
coverage: nocoverage
272+
linking: dynamic
273+
shell: bash
256274
- test-group: basic
257275
os: windows-latest
258276
os-type: windows
@@ -632,7 +650,18 @@ jobs:
632650
run: |
633651
cd build ;
634652
make check;
635-
if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' }}
653+
if: ${{ matrix.build-type != 'iwyu' && matrix.compiler-family != 'arm-cross' && matrix.build-type != 'header-hygiene' }}
654+
655+
- name: Run header-hygiene check
656+
# TASK-007: dedicated public-header hygiene gate. Runs the
657+
# preprocessor-grep target (Layer 2) against a staged install and
658+
# reports any forbidden backend headers reaching <httpserver.hpp>.
659+
# Currently informational (HEADER_HYGIENE_STRICT=no) -- TASK-020
660+
# flips this to strict when M5 closes the umbrella.
661+
run: |
662+
cd build
663+
make check-hygiene
664+
if: ${{ matrix.build-type == 'header-hygiene' }}
636665

637666
- name: Print tests results
638667
shell: bash

Makefile.am

Lines changed: 131 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,8 @@ endif
4040

4141
EXTRA_DIST = libhttpserver.pc.in $(DX_CONFIG) scripts/extract-release-notes.sh scripts/validate-version.sh \
4242
test/headers/consumer_direct.cpp test/headers/consumer_detail.cpp test/headers/consumer_umbrella.cpp \
43-
test/headers/consumer_post_umbrella.cpp
43+
test/headers/consumer_post_umbrella.cpp \
44+
test/headers/consumer_umbrella_no_backend.cpp
4445

4546
# ---------------------------------------------------------------------------
4647
# Header-hygiene checks (TASK-002)
@@ -131,45 +132,160 @@ CHECK_INSTALL_STAGE = $(abs_top_builddir)/.install-stage
131132

132133
check-install-layout:
133134
@echo "=== check-install-layout: staged install must hide details/ and *_impl.hpp ==="
134-
@rm -rf $(CHECK_INSTALL_STAGE)
135-
@$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_INSTALL_STAGE) >check-install.log 2>&1 || { \
136-
echo "FAIL: staged install failed"; \
137-
cat check-install.log; \
138-
rm -f check-install.log; \
135+
@if test "$(CHECK_INSTALL_SHARED)" != "yes"; then \
139136
rm -rf $(CHECK_INSTALL_STAGE); \
140-
exit 1; \
141-
}
142-
@rm -f check-install.log
137+
$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_INSTALL_STAGE) >check-install.log 2>&1 || { \
138+
echo "FAIL: staged install failed"; \
139+
cat check-install.log; \
140+
rm -f check-install.log; \
141+
rm -rf $(CHECK_INSTALL_STAGE); \
142+
exit 1; \
143+
}; \
144+
rm -f check-install.log; \
145+
fi
143146
@leaked_details=`find $(CHECK_INSTALL_STAGE) -type d -name details 2>/dev/null`; \
144147
if test -n "$$leaked_details"; then \
145148
echo "FAIL: details/ directory leaked into install:"; \
146149
echo "$$leaked_details"; \
147-
rm -rf $(CHECK_INSTALL_STAGE); \
150+
if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \
148151
exit 1; \
149152
fi
150153
@leaked_impl=`find $(CHECK_INSTALL_STAGE) -name '*_impl.hpp' 2>/dev/null`; \
151154
if test -n "$$leaked_impl"; then \
152155
echo "FAIL: *_impl.hpp file leaked into install:"; \
153156
echo "$$leaked_impl"; \
154-
rm -rf $(CHECK_INSTALL_STAGE); \
157+
if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \
155158
exit 1; \
156159
fi
157160
@umbrella_count=`find $(CHECK_INSTALL_STAGE) -name 'httpserver.hpp' | wc -l | tr -d ' '`; \
158161
if test "$$umbrella_count" != "1"; then \
159162
echo "FAIL: expected exactly 1 installed httpserver.hpp, got $$umbrella_count"; \
160-
rm -rf $(CHECK_INSTALL_STAGE); \
163+
if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi; \
161164
exit 1; \
162165
fi
163-
@rm -rf $(CHECK_INSTALL_STAGE)
166+
@if test "$(CHECK_INSTALL_SHARED)" != "yes"; then rm -rf $(CHECK_INSTALL_STAGE); fi
164167
@echo " PASS: staged install layout is clean"
165168

166-
check-local: check-headers check-install-layout
169+
# ---------------------------------------------------------------------------
170+
# Header-hygiene preprocessor gate (TASK-007).
171+
#
172+
# This is the preprocessor-grep half of the TASK-007 enforcement (the
173+
# compile-time half lives as `header_hygiene` in test/Makefile.am).
174+
#
175+
# Procedure:
176+
# 1. Stage `make install DESTDIR=$(CHECK_HYGIENE_STAGE)` to get a
177+
# pristine public include tree -- exactly what packagers and
178+
# downstream consumers see.
179+
# 2. Preprocess test/headers/consumer_umbrella_no_backend.cpp using
180+
# ONLY -I$(CHECK_HYGIENE_STAGE)$(includedir) plus $(CPPFLAGS) (so
181+
# e.g. /opt/homebrew/include is on the search path -- the grep
182+
# below NEEDS to resolve <microhttpd.h> if the umbrella pulls it
183+
# in, otherwise we couldn't detect the leak).
184+
# 3. Grep the cpp output for `# <line> "<file>"` line markers that
185+
# name any forbidden backend header. The line-marker filter
186+
# avoids false positives from substrings in code or comments.
187+
#
188+
# HEADER_HYGIENE_STRICT controls whether a leak is fatal:
189+
# - "no" (default until M5): leaks are reported as EXPECTED-FAIL
190+
# and exit 0. This keeps `make check` green during M2-M5
191+
# while making M2-M5 progress visible in CI logs.
192+
# - "yes" (TASK-020 close-out): leaks are fatal. Set this from the
193+
# command line (`make check-hygiene HEADER_HYGIENE_STRICT=yes`)
194+
# or flip the default below.
195+
#
196+
# Cross-reference: keep HEADER_HYGIENE_FORBIDDEN in sync with the
197+
# #ifdef ladder in test/unit/header_hygiene_test.cpp.
198+
# ---------------------------------------------------------------------------
199+
200+
HEADER_HYGIENE_FORBIDDEN = microhttpd\.h|pthread\.h|gnutls/gnutls\.h|sys/socket\.h|sys/uio\.h
201+
CHECK_HYGIENE_STAGE = $(abs_top_builddir)/.hygiene-stage
202+
CHECK_HYGIENE_CXX = $(CXX) -std=c++20 -E -I$(CHECK_HYGIENE_STAGE)$(includedir) $(CPPFLAGS)
203+
HEADER_HYGIENE_STRICT ?= no
204+
205+
# Sentinel file: only re-run the staged install when headers have changed.
206+
# This is an mtime gate used exclusively for standalone `make check-hygiene`
207+
# invocations — it avoids paying a full `make install` cost on every
208+
# repeated standalone run. When check-local drives check-hygiene it sets
209+
# CHECK_HYGIENE_SHARED=yes and passes CHECK_HYGIENE_STAGE pointing at its
210+
# own pre-built shared stage, so this stamp target is bypassed entirely.
211+
HYGIENE_STAMP = $(CHECK_HYGIENE_STAGE)/.hygiene-stamp
167212

168-
.PHONY: check-headers check-install-layout
213+
$(HYGIENE_STAMP): $(wildcard $(top_srcdir)/src/httpserver/*.hpp)
214+
@rm -rf $(CHECK_HYGIENE_STAGE)
215+
@$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(CHECK_HYGIENE_STAGE) >check-hygiene-install.log 2>&1 || { \
216+
echo "FAIL: staged install failed"; cat check-hygiene-install.log; \
217+
rm -f check-hygiene-install.log; rm -rf $(CHECK_HYGIENE_STAGE); exit 1; }
218+
@rm -f check-hygiene-install.log
219+
@touch $(HYGIENE_STAMP)
220+
221+
check-hygiene:
222+
@echo "=== check-hygiene: <httpserver.hpp> must not transitively include backend headers ==="
223+
@if test "$(CHECK_HYGIENE_SHARED)" != "yes"; then \
224+
$(MAKE) $(AM_MAKEFLAGS) $(HYGIENE_STAMP); \
225+
else \
226+
if ! test -d "$(CHECK_HYGIENE_STAGE)"; then \
227+
echo "FAIL: CHECK_HYGIENE_SHARED=yes but stage dir '$(CHECK_HYGIENE_STAGE)' does not exist."; \
228+
echo " Always pair CHECK_HYGIENE_SHARED=yes with CHECK_HYGIENE_STAGE=<valid-dir>."; \
229+
exit 1; \
230+
fi; \
231+
fi
232+
@status=0; \
233+
if ! $(CHECK_HYGIENE_CXX) $(top_srcdir)/test/headers/consumer_umbrella_no_backend.cpp >check-hygiene.i 2>check-hygiene.err; then \
234+
if test "$(HEADER_HYGIENE_STRICT)" = "yes"; then \
235+
echo "FAIL: preprocessor failed"; cat check-hygiene.err; \
236+
status=1; \
237+
else \
238+
echo "EXPECTED-FAIL (informational until M5): preprocessor failed against staged install."; \
239+
echo " This is expected while M2-M5 are in flight (e.g. webserver.hpp still"; \
240+
echo " references private detail headers that aren't shipped)."; \
241+
echo " Tail of preprocessor diagnostics:"; \
242+
sed 's/^/ /' check-hygiene.err | tail -10; \
243+
fi; \
244+
else \
245+
leaks=`grep -hE '^# [0-9]+ "[^"]*/($(HEADER_HYGIENE_FORBIDDEN))"' check-hygiene.i | awk '{print $$3}' | sort -u`; \
246+
if test -n "$$leaks"; then \
247+
if test "$(HEADER_HYGIENE_STRICT)" = "yes"; then \
248+
echo "FAIL: forbidden headers leaked through <httpserver.hpp>:"; \
249+
echo "$$leaks"; \
250+
status=1; \
251+
else \
252+
echo "EXPECTED-FAIL (informational until M5): forbidden headers currently leak through <httpserver.hpp>:"; \
253+
echo "$$leaks"; \
254+
fi; \
255+
else \
256+
echo " PASS: no forbidden headers reached the consumer TU"; \
257+
fi; \
258+
fi; \
259+
rm -f check-hygiene.i check-hygiene.err; \
260+
exit $$status
261+
262+
# check-local runs check-install-layout and check-hygiene against a single
263+
# shared staged install to avoid paying two full `make install` costs on
264+
# every `make check`. Both sub-checks can still be invoked standalone (they
265+
# will do their own install when CHECK_*_SHARED is not set).
266+
check-local: check-headers
267+
@echo "=== Shared staged install for check-install-layout and check-hygiene ==="
268+
@rm -rf $(abs_top_builddir)/.shared-check-stage
269+
@$(MAKE) $(AM_MAKEFLAGS) install DESTDIR=$(abs_top_builddir)/.shared-check-stage >check-shared-install.log 2>&1 || { \
270+
echo "FAIL: shared staged install failed"; cat check-shared-install.log; \
271+
rm -f check-shared-install.log; rm -rf $(abs_top_builddir)/.shared-check-stage; exit 1; }
272+
@rm -f check-shared-install.log
273+
@$(MAKE) $(AM_MAKEFLAGS) check-install-layout \
274+
CHECK_INSTALL_STAGE=$(abs_top_builddir)/.shared-check-stage \
275+
CHECK_INSTALL_SHARED=yes
276+
@$(MAKE) $(AM_MAKEFLAGS) check-hygiene \
277+
CHECK_HYGIENE_STAGE=$(abs_top_builddir)/.shared-check-stage \
278+
CHECK_HYGIENE_SHARED=yes
279+
@rm -rf $(abs_top_builddir)/.shared-check-stage
280+
281+
.PHONY: check-headers check-install-layout check-hygiene
169282

170283
MOSTLYCLEANFILES = $(DX_CLEANFILES) *.gcda *.gcno *.gcov
171284
DISTCLEANFILES = DIST_REVISION
172285

286+
clean-local:
287+
rm -rf $(CHECK_HYGIENE_STAGE) $(abs_top_builddir)/.shared-check-stage $(CHECK_INSTALL_STAGE)
288+
173289
pkgconfigdir = $(libdir)/pkgconfig
174290
pkgconfig_DATA = libhttpserver.pc
175291

Lines changed: 51 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,51 @@
1+
### TASK-007: CI test for public-header hygiene
2+
3+
**Milestone:** M1 - Foundation
4+
**Component:** CI / Test infrastructure
5+
**Estimate:** S
6+
7+
**Goal:**
8+
Lock in the "no backend headers leak through `<httpserver.hpp>`" invariant with a CI gate so a future commit can't silently regress it.
9+
10+
**Action Items:**
11+
- [x] Add a test program `test/header_hygiene.cpp` containing only `#include <httpserver.hpp>` and `int main(){}`. *(Implemented as `test/unit/header_hygiene_test.cpp` for test-tree symmetry; `test/headers/consumer_umbrella_no_backend.cpp` is the parallel source consumed by the preprocessor-grep target.)*
12+
- [x] In `Makefile.am`, build it without `-I` flags pointing at libmicrohttpd / pthread / gnutls headers (use only the installed-header path). *(Per-target `header_hygiene_CPPFLAGS = -I$(top_srcdir)/src $(CPPFLAGS)` overrides `AM_CPPFLAGS`, dropping `-DHTTPSERVER_COMPILATION` and `-I$(top_srcdir)/src/httpserver/`. The preprocessor-grep target uses ONLY the staged `DESTDIR` install include path.)*
13+
- [x] Run `g++ -E test/header_hygiene.cpp -I<install-prefix>/include` and `grep -E 'microhttpd\.h|pthread\.h|gnutls/gnutls\.h|sys/socket\.h|sys/uio\.h'` — expect zero matches. *(See `check-hygiene` in top-level `Makefile.am`. Today the grep finds matches; that's the EXPECTED-FAIL state until M5.)*
14+
- [x] Wire the check into `make check` (or a dedicated `make hygiene` target invoked by CI). *(Both: the runtime sentinel `header_hygiene` runs as part of `make check` (XFAIL until M5); the preprocessor-grep `check-hygiene` runs via `check-local` and also stands alone as a target for CI.)*
15+
- [x] Add a CI job that fails if any of the forbidden headers appear in the preprocessed output. *(Added `header-hygiene` matrix entry in `.github/workflows/verify-build.yml` running `make check-hygiene`. Currently informational; flips to fatal at TASK-020 by setting `HEADER_HYGIENE_STRICT=yes`.)*
16+
17+
**Dependencies:**
18+
- Blocked by: TASK-002
19+
- Blocks: None (informational gate; will fail until M2-M5 land, that's expected and intended)
20+
21+
**Acceptance Criteria:**
22+
- `grep -lE 'microhttpd\.h|pthread\.h|gnutls\.h|sys/socket\.h' src/httpserver/*.hpp` returns no results once M2-M5 land (PRD §3.1 acceptance).
23+
- The hygiene test is invoked by `make check` and fails loudly when violated.
24+
- Typecheck passes.
25+
26+
**Related Requirements:** PRD-HDR-REQ-001..003
27+
**Related Decisions:** §9 testing item 1
28+
29+
**Status:** Done (informational gate landed; full enforcement at TASK-020)
30+
31+
---
32+
33+
**Implementation Notes (TASK-007 close-out):**
34+
35+
- **Strategy:** Option (c) from the plan -- "implement the test machinery now, mark it XFAIL until M5." Rejected (a) "leave `make check` red" (would block every PR for weeks); rejected (b) "narrow the grep to today's leaks" (encodes a binary invariant as a moving target, four chances to forget).
36+
- **Two layers of enforcement, both wired into `make check`:**
37+
- *Layer 1 (compile-time sentinel):* `test/unit/header_hygiene_test.cpp` includes `<httpserver.hpp>` then checks well-known include-guard macros (`MHD_VERSION`, `_PTHREAD_H{,_}`, `GNUTLS_GNUTLS_H`, `_SYS_SOCKET_H{,_}`, `_SYS_UIO_H{,_}`). At runtime it prints the leaked headers and exits 1. Marked `XFAIL_TESTS` in `test/Makefile.am` so `make check` stays green.
38+
- *Layer 2 (preprocessor grep):* `make check-hygiene` in the top-level `Makefile.am` stages `make install DESTDIR=$(CHECK_HYGIENE_STAGE)` and preprocesses `test/headers/consumer_umbrella_no_backend.cpp` against ONLY the staged include path, then greps cpp line markers for forbidden headers. Default `HEADER_HYGIENE_STRICT=no` makes it informational; flipping to `yes` makes it fatal.
39+
- **CI:** dedicated `header-hygiene` matrix entry in `.github/workflows/verify-build.yml` invokes `make check-hygiene` so the gate surfaces as its own GitHub Actions check.
40+
- **`<sys/uio.h>` rationale:** PRD-HDR-REQ-001..003 don't name `<sys/uio.h>` directly, but TASK-004 introduced `iovec_entry` specifically to avoid exposing it. Listing it here is a hardening assertion that TASK-004's intent isn't regressed.
41+
- **Why preprocessor-grep currently fails ahead of leak detection:** the staged install does not ship `details/` headers (per TASK-002); `webserver.hpp` still references `httpserver/details/http_endpoint.hpp` until TASK-014's PIMPL split. The `check-hygiene` recipe treats this preprocessor failure as EXPECTED-FAIL in informational mode, with diagnostics so M2-M5 progress remains visible.
42+
43+
**M5 close-out (TASK-020 owner: zero ambiguity):**
44+
45+
When TASK-020 makes `<httpserver.hpp>` clean of backend headers:
46+
47+
1. Run `make check-hygiene HEADER_HYGIENE_STRICT=yes` from the build dir -- confirm exit 0 and `PASS: no forbidden headers reached the consumer TU`.
48+
2. Run `make check` -- expect Automake to report `XPASS: header_hygiene` (treated as a hard error by default), confirming the sentinel now passes.
49+
3. In `test/Makefile.am`, delete the line `XFAIL_TESTS = header_hygiene` and the comment block above it. Re-run `make check` -- expect `PASS: header_hygiene` and overall green.
50+
4. In `Makefile.am`, change `HEADER_HYGIENE_STRICT ?= no` to `HEADER_HYGIENE_STRICT ?= yes` (or remove the conditional and inline the strict path). Re-run `make check` to confirm `check-hygiene` is green.
51+
5. Mark this task `Status: Done (full enforcement)` and tick the M5 acceptance criterion (`grep -lE '...' src/httpserver/*.hpp` returns no results).

specs/tasks/M3-request/TASK-020.md

Lines changed: 31 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,31 @@
1+
### TASK-020: Final public-header backend-include sweep
2+
3+
**Milestone:** M3 - Webserver internal & Request Refactor
4+
**Component:** Public headers (sweep)
5+
**Estimate:** S
6+
7+
**Goal:**
8+
Verify and lock the "no backend headers in public surface" invariant after PIMPL splits and accessor refactors land, removing any straggler includes that survived earlier tasks.
9+
10+
**Action Items:**
11+
- [ ] `grep -lE 'microhttpd\.h|pthread\.h|gnutls/gnutls\.h|sys/socket\.h|sys/uio\.h' src/httpserver/*.hpp`. Each file that turns up: route the include into the corresponding `details/*_impl.hpp` or `.cpp` file.
12+
- [ ] Verify after the sweep that the grep returns zero results.
13+
- [ ] Ensure the hygiene CI test from TASK-007 now passes. **Specifically:**
14+
- [ ] In `test/Makefile.am`, delete the line `XFAIL_TESTS = header_hygiene` (and the explanatory comment block above it). After this edit, `make check` should report `PASS: header_hygiene` -- not `XFAIL` and not `XPASS`.
15+
- [ ] In `Makefile.am`, change `HEADER_HYGIENE_STRICT ?= no` to `HEADER_HYGIENE_STRICT ?= yes` (or remove the conditional and inline the strict-mode path). Verify `make check-hygiene` exits 0 with `PASS: no forbidden headers reached the consumer TU`.
16+
- [ ] Run `make check-hygiene HEADER_HYGIENE_STRICT=yes` from the build dir as a final smoke check.
17+
18+
**Dependencies:**
19+
- Blocked by: TASK-014, TASK-015, TASK-019
20+
- Blocks: None (gating outcome that the rest of the project relies on)
21+
22+
**Acceptance Criteria:**
23+
- `grep -lE 'microhttpd\.h|pthread\.h|gnutls\.h|sys/socket\.h' src/httpserver/*.hpp` returns no results (PRD §3.1 acceptance).
24+
- A test program containing only `#include <httpserver.hpp>` and `int main(){}` compiles without `-I` to libmicrohttpd / pthread / gnutls (PRD §3.1 acceptance).
25+
- TASK-007's hygiene test (red until now) goes green.
26+
- Typecheck passes.
27+
28+
**Related Requirements:** PRD-HDR-REQ-001, PRD-HDR-REQ-002, PRD-HDR-REQ-003
29+
**Related Decisions:** §2.2, §5.5
30+
31+
**Status:** Not Started

0 commit comments

Comments
 (0)