* test/pytest_suite: Import/translate gitignore from the svn:ignore orignals. [skip ci] * test/pytest_suite/apache_pytest/fpm.py (PhpFpm.generate_conf): Set register_argc_argv so PHP tests see $_SERVER['argc']/['argv'] under FPM, which mod_php's config for this never actually reached. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * test/pytest_suite/tests/t/ab/test_base.py (_ab_path): Don't resolve() the httpd path before deriving ab's; it follows the check/bin/httpd symlink out of the check/ tree, so ab was never found there. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * test/pytest_suite/.gitignore: Ignore generated .htaccess files, t/conf/cacheroot/, t/htdocs/modules/autoindex2/, t/htdocs/modules/substitute/test.txt, and t/state/. * test/pytest_suite/t/htdocs/apache/cfg_getline/.htaccess, authz_core/a/.htaccess, authz_core/a/b/.htaccess, authz_core/a/b/c/.htaccess: Remove stale generated content that had been committed by mistake. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Remove files (missed in previous commit r1936969) - * test/pytest_suite/t/htdocs/apache/cfg_getline/.htaccess, authz_core/a/.htaccess, authz_core/a/b/.htaccess, authz_core/a/b/c/.htaccess: Remove stale generated content that had been committed by mistake. Fix false negatives checking error_log with test_proxy_beacon: [skip ci] * test/pytest_suite/conftest.py (framework): Record error_log's size before httpd starts, as session_log_start. * test/pytest_suite/tests/t/modules/test_proxy_beacon.py (test_proxy_beacon): Scope the "added backend" log check to session_log_start instead of the per-test window or byte 0. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> * test/pytest_suite/apache_pytest/config.py (_build_vars): Add @STATEDIR@ config template variable. * test/pytest_suite/t/conf/cache.conf.in, tests/t/modules/test_cache.py: Use @STATEDIR@/cacheroot/ for CacheRoot, instead of t/conf/cacheroot. Assisted-by: Claude Sonnet 5 <noreply@anthropic.com> Arrange the testsuite to run it on windows. used claude ai for the investigation. the bat, use set APACHE_TEST_PERL=C:/Strawberry/perl/bin/perl.exe and --httpd=..\httpd.exe and --conf=..\httpd.conf to an install httpd in the box. build the test modules using cmake, that is for windows where we don't have apxs. Arrange python detection. Arrange to pass more tests on windows. rollback, won't help. skip some tests that won't pass on windows. arrange others and add --with-perl to runtests.bat. * test/pytest_suite: Ignore generate file t/basic1. [skip ci] * test/pytest_suite: Remove some left-behind generated files, update svn:ignore and .gitignore to ignore them. Submitted by: jorton, jfclere git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/branches/2.4.x@1938621 13f79535-47bb-0310-9956-ffa450edef68
Apache httpd test suite — Python / pytest
Provenance / how this fits in
test/. Thispytest_suite/is a self-contained snapshot ported from thehttpd-testsrepository (the Python port of the classic PerlApache::Testsuite). It coexists alongsidetest/pyhttpd/andtest/modules/— it does not use pyhttpd's framework or its rootconftest.py. It has its ownconftest.pyandpyproject.toml, so pytest treats this directory as its own rootdir; run it from here (or viatest/run-all-tests.sh), never by pointing pytest attest/as a whole. The two suites are complementary: this one covers the classic core/modules/ security/SSL/PHP tests; pyhttpd covers HTTP/2, mod_md (ACME), HTTP/1 and proxy. To re-sync from upstream, re-copy the source tree (source files only — exclude.venv/, caches, and generated artifacts; see.gitignore).
A self-contained pytest port of the Apache httpd
integration test suite (historically the Perl Apache::Test framework). It
generates an httpd configuration, compiles the bundled C test modules, starts a
private httpd instance (and optionally PHP-FPM), exercises it over HTTP/HTTPS,
and shuts everything down — all driven by ordinary pytest test functions.
Everything the suite needs at runtime lives under this directory; it
does not read anything from the rest of the repository. The only external
inputs are the Apache httpd build under test (located via apxs) and,
optionally, a php-fpm binary for the PHP tests.
Quick start
# 1. Create the virtualenv (pytest + httpx). Needs `uv` (https://docs.astral.sh/uv/);
# reads pyproject.toml + uv.lock. runtests.sh also does this for you on first run.
uv sync
# 2. Run the whole suite against your httpd build.
./runtests.sh --apxs /path/to/your/httpd/bin/apxs
# ...or let it auto-detect apxs / httpd / php-fpm from $PATH or env vars:
APXS=$HOME/root/httpd/bin/apxs PHP_FPM=/usr/sbin/php-fpm8.3 ./runtests.sh
runtests.sh is the self-evident entry point: it locates the venv's pytest,
fills in --apxs / --php-fpm from environment variables or $PATH, clears a
stale CGI socket, and forwards any extra arguments to pytest.
./runtests.sh tests/t/modules # run one category
./runtests.sh tests/t/modules/test_alias.py # run one file
./runtests.sh -k rewrite -v # any pytest args pass through
./runtests.sh --php-fpm /opt/local/sbin/php-fpm83 tests/t/php # PHP tests
What you need
| Requirement | How it's provided | Notes |
|---|---|---|
| Python ≥ 3.11 + pytest + httpx | uv sync (uses pyproject.toml / uv.lock) |
The only Python deps. |
| A built Apache httpd | --apxs / --httpd (or APXS / HTTPD env) |
The server under test. Built with shared modules so the suite can load them. |
php-fpm (optional) |
--php-fpm (or PHP_FPM env) |
Any PHP 7/8 version. Without it, the PHP tests skip. |
openssl CLI |
auto (on $PATH) |
Used to generate the test SSL CA/certs. |
The suite probes the httpd build (httpd -v/-V/-l and its installed
httpd.conf) to learn its version, MPM, and available modules, then skips
any test whose required module/version isn't present. A passing run therefore
reports a mix of passed and skipped — skips are expected and fine.
Command-line options
These are added on top of pytest's own options:
| Option | Meaning |
|---|---|
--apxs PATH |
Path to apxs. Used to locate httpd, the inherited httpd.conf, the install prefix, and to compile the C test modules. |
--httpd PATH |
Path to the httpd binary (defaults to apxs -q SBINDIR/httpd). |
--php-fpm PATH |
Path to a php-fpm binary. Enables the PHP tests by routing *.php to a managed FPM pool via mod_proxy_fcgi. Version/location-agnostic. |
--php-fpm-port N |
TCP port for the managed php-fpm pool (default 8999). |
--defines "A B" |
Extra -D defines passed to httpd (e.g. LDAP). |
Layout
python/
├── runtests.sh # entry point (this is how you run the suite)
├── pyproject.toml # deps + pytest config
├── conftest.py # fixtures: server lifecycle, the `http` client, need_* gating
├── apache_pytest/ # the framework (port of Apache::Test internals)
│ ├── probe.py # inspect the httpd build (version, MPM, modules)
│ ├── config.py # generate httpd.conf from t/conf/*.conf.in
│ ├── cmodules.py # compile c-modules/ via apxs
│ ├── sslca.py # generate the SSL test CA + certs
│ ├── fpm.py # manage a php-fpm daemon
│ ├── server.py # start/stop httpd
│ ├── client.py # the `http` fixture (HTTP/HTTPS + raw sockets)
│ ├── rawsocket.py # raw-socket helper for protocol tests
│ └── testapi.py # t_cmp() + need_* markers used by tests
├── tests/
│ ├── test_framework_smoke.py # framework self-tests
│ ├── test_config_parity.py # dev-only: diff vs the Perl reference (skips if absent)
│ └── t/ # the translated suite, mirroring the historical t/ layout
│ ├── apache/ # core HTTP/protocol tests
│ ├── modules/ # per-module tests (proxy, rewrite, headers, ...)
│ ├── ssl/ # TLS / client-cert tests
│ ├── security/ # CVE regression tests
│ ├── php/ # PHP tests (need --php-fpm)
│ ├── http11/ filter/ protocol/ apr/ ab/
│ └── ...
├── t/ # runtime assets (self-contained copy)
│ ├── conf/ # *.conf.in templates + ssl/
│ ├── htdocs/ # document root served by the test server
│ └── php-fpm/ # php-fpm pool assets
└── c-modules/ # C test-module sources, compiled via apxs at runtime
Generated files (t/conf/*.conf, t/logs/, t/conf/ssl/ca/) are produced at
run time and are safe to delete between runs.
Adding a new test
Tests are plain pytest functions. They receive the running server through the
http fixture and assert on its responses. Put a test in the category
directory that matches what it exercises, in a file named test_<name>.py, with
functions named test_*.
1. Minimal example
tests/t/modules/test_status.py:
import re
from apache_pytest import need_module
@need_module("status") # skip unless mod_status is loaded
def test_server_status(http):
servername = http.vars("servername")
body = http.GET_BODY("/server-status")
assert re.search(f"Apache Server Status for {servername}", body, re.I)
That's the whole pattern:
httpis the fixture giving you the running server. Request methods return anhttpx.Response.@need_module("x")(and friends, below) gate the test — if the requirement isn't met, the test is skipped at collection time. This is the Python equivalent of the Perlplan tests => N, need_module 'x'.- Use plain
assert. Add a message for context:assert cond, "what failed".
2. The http fixture API
Requests (bare paths are resolved against the running server; absolute URLs pass through):
| Method | Returns | Notes |
|---|---|---|
http.GET(path, **kw) |
Response |
also HEAD, OPTIONS, PUT, POST |
http.POST(path, content=b"...", **kw) |
Response |
content= is the request body |
http.GET_BODY(path) |
str |
response text |
http.GET_RC(path) |
int |
status code (returns 500 on a transport/TLS error, like LWP) |
http.POST_BODY(path, content=...) |
str |
Useful keyword args (forwarded to httpx): headers={...}, redirect_ok=True
(follow 3xx — off by default), cert="client_ok" (present a client certificate;
see SSL below), auth=httpx.BasicAuth(u, p).
Server introspection / configuration:
| Call | Purpose |
|---|---|
http.vars("servername") |
a value from the generated config's vars table (servername, port, documentroot, t_logs, ...) |
http.have_module("rewrite") |
is a module loaded? (runtime check, in addition to the @need_module gate) |
http.have_min_apache_version("2.4.50") |
runtime version gate |
http.apxs("INCLUDEDIR") |
query the build's apxs |
http.scheme("https") |
switch subsequent requests to HTTPS (the mod_ssl vhost) |
http.module("proxy_http_reverse") |
target a specific configured virtual host by module name |
http.vhost_url("mod_headers", "/x") / http.hostport("mod_ssl") |
build a URL / host:port for a named vhost |
Comparison helper (a faithful port of Perl's t_cmp): t_cmp(received, expected) returns a bool. If expected is a compiled regex it does a
re.search; otherwise it compares by string equality. Use it when you want the
regex-or-equal behavior:
from apache_pytest import t_cmp
assert t_cmp(r.status_code, 200), "status"
assert t_cmp(r.headers.get("Allow", ""), re.compile("OPTIONS")), "Allow header"
3. Requirement markers (skip gating)
Import from apache_pytest and apply as decorators. A test is skipped (at
collection time) unless every marker is satisfied by the probed httpd build:
from apache_pytest import (
need_module, need_min_apache_version, need_cgi, need_ssl, need_php, need_lwp,
)
@need_module("proxy", "setenvif") # all named modules must be loaded
@need_min_apache_version("2.4.49") # server must be >= this version
def test_something(http):
...
| Marker | Satisfied when |
|---|---|
need_module("x", ...) |
every named module is loaded (bare name, mod_x, or mod_x.c all accepted; bundled C test modules count) |
need_min_apache_version("2.4.x") |
server version ≥ given |
need_cgi() |
mod_cgi or mod_cgid present |
need_ssl() |
mod_ssl present |
need_php() |
a PHP SAPI module or --php-fpm (with mod_proxy_fcgi) is available |
need_lwp() |
always (kept for 1:1 readability with the Perl originals) |
For conditional logic inside a test (not whole-test gating), use the runtime
checks and pytest.skip:
def test_versioned(http):
if not http.have_min_apache_version("2.4.60"):
pytest.skip("needs >= 2.4.60")
...
4. Parametrizing (loops in the Perl original)
Where a Perl test looped over cases, use @pytest.mark.parametrize:
import pytest
from apache_pytest import need_module, t_cmp
CASES = [("/index.html", 200), ("/missing", 404)]
@need_module("alias")
@pytest.mark.parametrize(("path", "code"), CASES, ids=lambda c: str(c))
def test_alias(http, path, code):
assert t_cmp(http.GET(path).status_code, code), path
5. SSL / client certificates
Switch the client to HTTPS and (optionally) present one of the generated test
client certs (client_ok, client_snakeoil, client_revoked, client_colon):
from apache_pytest import need_ssl
@need_ssl()
def test_client_cert(http):
http.scheme("https")
assert http.GET_RC("/require/asf/index.html", cert="client_ok") == 200
assert http.GET_RC("/require/asf/index.html", cert=None) != 200
The framework generates the CA/server/client certificates automatically and the client trusts the test CA. TLS 1.3 post-handshake auth works.
6. Raw sockets (protocol-level tests)
For tests that send hand-crafted (often malformed) requests and read the raw response — e.g. many CVE regressions:
import re
from apache_pytest import need_module, t_cmp
@need_module("proxy")
def test_bad_request(http):
http.module("cve_2011_3368") # select the vhost
sock = http.vhost_socket() # raw socket to that vhost's port
try:
sock.print(
"GET @localhost/foo HTTP/1.1\r\n"
f"Host: {http.hostport()}\r\n\r\n"
)
line = sock.getline() or ""
assert t_cmp(line, re.compile(r"^HTTP/1\.. 400")), "rejected"
finally:
sock.close()
VhostSocket offers .print(data), .getline(), .read(), .connected,
.socket_trace(True), and .close(); it's also a context manager.
Tip for HTTP/1.1 raw requests: after sending, half-close the write side (
sock._sock.shutdown(socket.SHUT_WR)) so a keep-alive connection doesn't block on the read timeout.
7. Server-side config and document root
If a test needs server configuration or files served from disk:
- Static files / scripts go under
t/htdocs/(the document root). A request for/foo/bar.htmlis served fromt/htdocs/foo/bar.html. - Per-module httpd config lives in the
t/conf/*.conf.intemplates, which use@TOKEN@placeholders (@SERVERROOT@,@DOCUMENTROOT@,@PORT@,@SSL_MODULE@, ...) and<VirtualHost module_name>blocks that the framework rewrites to allocated ports. Add config to the relevant.conf.in(most general-purpose config is int/conf/extra.conf.in); a request can target a named vhost withhttp.module("name")/http.vhost_url("name"). - CGI / helper scripts can be shipped as
*.PLtemplates undert/htdocs/; they're generated into executable scripts at run time.
8. Conventions
- Mirror the historical layout: a test for
t/<category>/<name>.tbecomestests/t/<category>/test_<name>.py(hyphens in the name → underscores, e.g.CVE-2011-3368.t→test_CVE_2011_3368.py). - Keep a short module docstring noting what's tested (and, for ports, the Perl
original). If the docstring quotes Perl containing backslashes, make it a raw
string (
r"""..."""). - A new test must end passing or skipping — never erroring. If it can't run
in some environment, gate it with a
need_*marker orpytest.skipwith a clear reason rather than letting it fail.
Notes & gotchas
uv runvs the venv.runtests.shinvokes.venv/bin/pytestdirectly so it works in environments whereuv runis shimmed/unavailable. If you call pytest yourself, use.venv/bin/pytest(or activate the venv).- Same-named test modules across categories (e.g.
ssl/test_all.pyandphp/test_all.py) coexist thanks to--import-mode=importlib+pythonpath=["."]inpyproject.toml. - Stale CGI socket. A killed run can leave
t/logs/cgisock*, which breaks a fresh start;runtests.shremoves it automatically. - No orphans. The server is launched in its own process group and reaped on
teardown (including after a failed start); php-fpm is stopped likewise. After
a run,
pgrep -f 'bin/httpd'andpgrep -f php-fpmshould be empty. test_config_parity.pyis a development-only check that diffs the generated config against a freshly Perl-generated reference. It skips cleanly when the PerlApache::Testframework isn't present, so it's inert in a released, standalone copy of this directory.