This directory contains useful test code for testing various bits
of Apache functionality. This stuff is for the developers only,
so we might remove it on public releases.
+
+
+Python / pytest test suites
+===========================
+
+There are two complementary Python (pytest) test suites in this directory.
+Both run against an already-built httpd; they are independent of each other.
+
+ pyhttpd / modules/ httpd's own test framework. Covers HTTP/2, mod_md
+ (ACME), HTTP/1, proxy and core behaviour. Drives the
+ server with external clients (curl, nghttp, h2load) and
+ is configured via pyhttpd/config.ini (generated by
+ httpd's configure). Framework code lives in pyhttpd/;
+ tests live in modules/{core,http1,http2,md,proxy}/.
+
+ pytest_suite/ A self-contained port of the classic Perl Apache::Test
+ suite (core HTTP, per-module tests, security/CVE
+ regressions, SSL/TLS, PHP). Drives the server with an
+ in-process Python HTTP client (httpx) and raw sockets.
+ It is fully self-contained: it ships its own conf
+ templates, document root, C test modules and helper
+ framework under pytest_suite/, and reads nothing outside
+ that directory. See pytest_suite/README.md for details,
+ including how to add new tests.
+
+The two suites coexist: pytest_suite/ has its own conftest.py and
+pyproject.toml, so pytest treats it as its own rootdir and does NOT pick up
+this directory's pyhttpd conftest.py. Run pytest_suite from inside
+pytest_suite/ (or via the unified runner below) -- do not point pytest at the
+top-level test/ directory expecting to run both at once.
+
+
+Unified runner: run-all-tests.sh
+---------------------------------
+
+run-all-tests.sh runs BOTH suites against the same built httpd and reports a
+combined result. pytest_suite runs first, then the pyhttpd modules/ tests.
+
+ ./run-all-tests.sh # run both suites (pytest_suite first)
+ ./run-all-tests.sh --only=pysuite # only the pytest_suite tests
+ ./run-all-tests.sh --only=pyhttpd # only the pyhttpd modules/ tests
+ ./run-all-tests.sh --apxs=/path/to/apxs # point at a specific httpd build
+ ./run-all-tests.sh -k status -v # flags pass through to BOTH suites
+
+How it finds the httpd build:
+ * pytest_suite is located via apxs -- from --apxs, the $APXS environment
+ variable, the "prefix" in pyhttpd/config.ini, or apxs on $PATH.
+ * the pyhttpd tests use pyhttpd/config.ini (built by httpd's configure), via
+ the system "pytest".
+
+Selecting tests:
+ * Flag arguments (-v, -k NAME, -x, ...) are passed to both suites.
+ * Positional test paths are passed only to pytest_suite (they are
+ pytest_suite paths). Select pyhttpd tests with the PYHTTPD_TARGETS
+ environment variable, e.g.
+ PYHTTPD_TARGETS="modules/http2" ./run-all-tests.sh --only=pyhttpd
+ By default the pyhttpd side runs every modules/* directory whose conftest
+ imports successfully in your environment; a directory whose optional
+ dependency is missing (e.g. modules/md needs pyOpenSSL) is skipped with a
+ note rather than aborting the run.
+
+Environment overrides:
+ APXS path to apxs (default: pyhttpd/config.ini prefix, else PATH)
+ PHP_FPM path to a php-fpm binary; enables pytest_suite's PHP tests
+ PYHTTPD_TARGETS pyhttpd test path(s) to run (default: auto-detected modules/*)
+
+The runner exits non-zero if either suite has failures.
+
+
+Running a suite directly
+------------------------
+
+Both runtests.sh scripts create their own virtualenv on first run and can be
+invoked from any directory -- the paths below are just the convenient way to
+type them. The venv is (re)built automatically whenever it is missing or its
+pyproject.toml has changed, using `uv sync` if uv is installed and otherwise
+`python3 -m venv` + pip (reading the dependency list from pyproject.toml -- no
+uv required). To force a clean rebuild yourself, `rm -rf <suite>/.venv`.
+
+pytest_suite (self-contained; the venv holds only pytest + httpx):
+
+ ./pytest_suite/runtests.sh --apxs=/path/to/apxs # all tests
+ ./pytest_suite/runtests.sh --php-fpm=/path/to/php-fpm tests/t/php # PHP tests
+ ./pytest_suite/runtests.sh -k rewrite -v # any pytest args pass through
+
+pyhttpd tests (need pyhttpd/config.ini from httpd's configure, plus curl,
+nghttp2/h2load, and -- for modules/md -- pyOpenSSL and an ACME test server):
+
+ ./pyhttpd/runtests.sh modules/http2 # all HTTP/2 tests
+ ./pyhttpd/runtests.sh modules/core -k test_001 # a subset
+
+Other contents
+--------------
+
+ unit/ C-based unit tests (libcheck / httpdunit).
+ clients/ helper client programs used by the tests.
+ *.c small standalone C test programs.
--- /dev/null
+#!/bin/sh
+#
+# runtests.sh -- run the pyhttpd modules/ test suite.
+#
+# Manages a local .venv under pyhttpd/ so that pytest and the CGI helper scripts
+# (which httpd forks) both use the same Python with all required packages
+# (cryptography, python-multipart, websockets, etc.) available.
+#
+# Usage:
+# ./runtests.sh # run all modules/ tests
+# ./runtests.sh modules/http1 # run a specific suite
+# ./runtests.sh -k post -v # any pytest args pass through
+#
+# Environment:
+# PYHTTPD_TARGETS space-separated list of test paths (default: modules)
+#
+set -eu
+
+here="$(cd "$(dirname "$0")" && pwd)"
+
+# --- ensure the venv exists and is current ----------------------------------
+# We invoke .venv/bin/pytest directly rather than `uv run` so the suite works
+# even where `uv run` is shimmed/unavailable.
+#
+# Create $here/.venv on first run, and rebuild it when pyproject.toml is newer
+# than the venv (i.e. dependencies changed). Prefer uv (which reads
+# pyproject.toml + uv.lock); otherwise fall back to python3 -m venv + pip,
+# taking the dependency list straight from pyproject.toml so there is no second
+# copy to keep in sync. Absolute paths throughout, so this behaves identically
+# regardless of the caller's cwd. This block is kept byte-for-byte identical in
+# pytest_suite/runtests.sh and pyhttpd/runtests.sh -- edit both together.
+PYTEST="$here/.venv/bin/pytest"
+if [ ! -x "$PYTEST" ] || [ "$here/pyproject.toml" -nt "$here/.venv" ]; then
+ if command -v uv >/dev/null 2>&1; then
+ echo "runtests.sh: (re)creating $here/.venv via 'uv sync'..." >&2
+ uv sync --project "$here"
+ elif command -v python3 >/dev/null 2>&1; then
+ echo "runtests.sh: (re)creating $here/.venv via python3 + pip..." >&2
+ python3 -m venv "$here/.venv"
+ # Read [project].dependencies from pyproject.toml (one entry per line,
+ # double-quoted) so the install list never drifts from the manifest.
+ deps=$(awk -F'"' '/^dependencies = \[/{f=1; next} f && /^\]/{f=0} f && NF>=2 {print $2}' "$here/pyproject.toml")
+ # shellcheck disable=SC2086 # deps is an intentional word-split list
+ "$here/.venv/bin/pip" install --quiet $deps
+ else
+ echo "runtests.sh: ERROR: $PYTEST not found and neither 'uv' nor 'python3' is installed." >&2
+ exit 1
+ fi
+ # Mark the venv as freshly built so the staleness check above won't retrigger
+ # until pyproject.toml changes again.
+ touch "$here/.venv"
+fi
+
+# Prepend the venv's bin dir so that CGI scripts forked by httpd also resolve
+# python3 to the venv's interpreter (which has all packages installed), and so
+# that any shim wrappers earlier on PATH are shadowed.
+export PATH="$here/.venv/bin:$PATH"
+
+# The modules/ test suite lives in test/, a sibling of this script's directory
+# (test/pyhttpd/) -- cd there so both the default target and any
+# PYHTTPD_TARGETS/positional path the caller supplies resolve the same way
+# regardless of where runtests.sh was invoked from.
+cd "$(dirname "$here")"
+
+# Only fall back to the "modules" default when the caller gave no positional
+# test path of their own -- otherwise it would always tag along after theirs
+# (`pytest modules modules/http1`), silently widening any subset selection
+# back out to the full suite. A positional path is recognized by actually
+# existing on disk (relative to test/, our cwd at this point) -- this avoids
+# both having to enumerate every pytest flag that takes a separate-word value
+# (-k, -m, -p, --tb, --maxfail, -n from pytest-xdist, ...) and misdetecting a
+# -k/-m expression that happens to contain '/' (this suite's own parametrize
+# IDs look like "/006/006.css", so "-k 006/006" is a realistic selector, and
+# it does not exist as a path).
+have_path=0
+for arg in "$@"; do
+ case "$arg" in
+ -*) ;;
+ # Strip a trailing ::nodeid (pytest's file::Class::test node-selector
+ # syntax) before checking existence -- only the file/dir part is real.
+ *) [ -e "${arg%%::*}" ] && have_path=1 ;;
+ esac
+done
+
+if [ -n "${PYHTTPD_TARGETS:-}" ]; then
+ targets="$PYHTTPD_TARGETS"
+elif [ "$have_path" = 1 ]; then
+ targets=""
+else
+ targets="modules"
+fi
+
+echo "runtests.sh: $PYTEST $targets $*" >&2
+# shellcheck disable=SC2086 # $targets is an intentional word-split path list
+exec "$PYTEST" $targets "$@"
here="$(cd "$(dirname "$0")" && pwd)"
cd "$here"
-# --- locate the virtualenv's pytest -----------------------------------------
+# --- ensure the venv exists and is current ----------------------------------
# We invoke .venv/bin/pytest directly rather than `uv run` so the suite works
-# even where `uv run` is shimmed/unavailable. Create the venv with `uv sync`
-# (or `python -m venv .venv && .venv/bin/pip install -e .`) if it's missing.
+# even where `uv run` is shimmed/unavailable.
+#
+# Create $here/.venv on first run, and rebuild it when pyproject.toml is newer
+# than the venv (i.e. dependencies changed). Prefer uv (which reads
+# pyproject.toml + uv.lock); otherwise fall back to python3 -m venv + pip,
+# taking the dependency list straight from pyproject.toml so there is no second
+# copy to keep in sync. Absolute paths throughout, so this behaves identically
+# regardless of the caller's cwd. This block is kept byte-for-byte identical in
+# pytest_suite/runtests.sh and pyhttpd/runtests.sh -- edit both together.
PYTEST="$here/.venv/bin/pytest"
-if [ ! -x "$PYTEST" ]; then
+if [ ! -x "$PYTEST" ] || [ "$here/pyproject.toml" -nt "$here/.venv" ]; then
if command -v uv >/dev/null 2>&1; then
- echo "runtests.sh: .venv not found; running 'uv sync' to create it..." >&2
- uv sync
+ echo "runtests.sh: (re)creating $here/.venv via 'uv sync'..." >&2
+ uv sync --project "$here"
elif command -v python3 >/dev/null 2>&1; then
- echo "runtests.sh: .venv not found; creating it with python3 + pip..." >&2
- python3 -m venv .venv
- .venv/bin/pip install --quiet -e .
+ echo "runtests.sh: (re)creating $here/.venv via python3 + pip..." >&2
+ python3 -m venv "$here/.venv"
+ # Read [project].dependencies from pyproject.toml (one entry per line,
+ # double-quoted) so the install list never drifts from the manifest.
+ deps=$(awk -F'"' '/^dependencies = \[/{f=1; next} f && /^\]/{f=0} f && NF>=2 {print $2}' "$here/pyproject.toml")
+ # shellcheck disable=SC2086 # deps is an intentional word-split list
+ "$here/.venv/bin/pip" install --quiet $deps
else
echo "runtests.sh: ERROR: $PYTEST not found and neither 'uv' nor 'python3' is installed." >&2
exit 1
fi
+ # Mark the venv as freshly built so the staleness check above won't retrigger
+ # until pyproject.toml changes again.
+ touch "$here/.venv"
fi
# --- discover apxs / httpd / php-fpm ----------------------------------------
rm -f "$here/t/logs/cgisock"* 2>/dev/null || true
# --- run --------------------------------------------------------------------
-# shellcheck disable=SC2086 # auto_args is an intentional word-split flag list
echo "runtests.sh: $PYTEST $auto_args $*" >&2
+# shellcheck disable=SC2086 # auto_args is an intentional word-split flag list
exec "$PYTEST" $auto_args "$@"