svn commit: r1936270 - in httpd/httpd/trunk/test: . pyhttpd pytest_suite

[email protected] Fri, 17 Jul 2026 15:49:01 -0000
Newsgroups gmane.comp.apache.cvs
Message-ID <178430334181.3899497.12131379018520141880@svn03-he-fi>
Author: jim
Date: Fri Jul 17 15:49:01 2026
New Revision: 1936270

Log:
Make the Python test suites more robust:
  o Call location independence
  o Fully support pip or uv
  o Better reporting of test results when both suites are run
  o Support postitional args for test cases in a reliable manner

Modified:
   httpd/httpd/trunk/test/README
   httpd/httpd/trunk/test/pyhttpd/runtests.sh
   httpd/httpd/trunk/test/pytest_suite/runtests.sh
   httpd/httpd/trunk/test/run-all-tests.sh

Modified: httpd/httpd/trunk/test/README
==============================================================================
--- httpd/httpd/trunk/test/README	Fri Jul 17 14:42:31 2026	(r1936269)
+++ httpd/httpd/trunk/test/README	Fri Jul 17 15:49:01 2026	(r1936270)
@@ -73,20 +73,24 @@ The runner exits non-zero if either suit
 Running a suite directly
 ------------------------
 
-pytest_suite (from its own directory; it creates its own virtualenv):
-
-  cd pytest_suite
-  uv sync                                   # one-time: create the venv
-  ./runtests.sh --apxs=/path/to/apxs        # all tests
-  ./runtests.sh --php-fpm=/path/to/php-fpm tests/t/php   # PHP tests
-  ./runtests.sh -k rewrite -v               # any pytest args pass through
+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):
 
-  pytest modules/http2                      # all HTTP/2 tests
-  pytest modules/core -k test_001           # a subset
-
+  ./pyhttpd/runtests.sh modules/http2       # all HTTP/2 tests
+  ./pyhttpd/runtests.sh modules/core -k test_001   # a subset
 
 Other contents
 --------------

Modified: httpd/httpd/trunk/test/pyhttpd/runtests.sh
==============================================================================
--- httpd/httpd/trunk/test/pyhttpd/runtests.sh	Fri Jul 17 14:42:31 2026	(r1936269)
+++ httpd/httpd/trunk/test/pyhttpd/runtests.sh	Fri Jul 17 15:49:01 2026	(r1936270)
@@ -18,21 +18,37 @@ 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" ]; 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
+        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 with python3 + pip..." >&2
+        echo "runtests.sh: (re)creating $here/.venv via python3 + pip..." >&2
         python3 -m venv "$here/.venv"
-         # Keep this list in sync with pyproject.toml [project].dependencies
-         "$here/.venv/bin/pip" install --quiet \
-             "pytest>=7.0" cryptography filelock "python-multipart" pyopenssl packaging websockets
+        # 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 on PATH." >&2
+        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
@@ -40,8 +56,40 @@ fi
 # that any shim wrappers earlier on PATH are shadowed.
 export PATH="$here/.venv/bin:$PATH"
 
-targets="${PYHTTPD_TARGETS:-modules}"
+# 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
 
-# shellcheck disable=SC2086
 echo "runtests.sh: $PYTEST $targets $*" >&2
+# shellcheck disable=SC2086  # $targets is an intentional word-split path list
 exec "$PYTEST" $targets "$@"

Modified: httpd/httpd/trunk/test/pytest_suite/runtests.sh
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/runtests.sh	Fri Jul 17 14:42:31 2026	(r1936269)
+++ httpd/httpd/trunk/test/pytest_suite/runtests.sh	Fri Jul 17 15:49:01 2026	(r1936270)
@@ -26,23 +26,37 @@ set -eu
 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 ----------------------------------------
@@ -93,6 +107,6 @@ esac
 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 "$@"

Modified: httpd/httpd/trunk/test/run-all-tests.sh
==============================================================================
--- httpd/httpd/trunk/test/run-all-tests.sh	Fri Jul 17 14:42:31 2026	(r1936269)
+++ httpd/httpd/trunk/test/run-all-tests.sh	Fri Jul 17 15:49:01 2026	(r1936270)
@@ -70,7 +70,14 @@ config_ini="$here/pyhttpd/config.ini"
 #             paths and go ONLY to pytest_suite. The pyhttpd side selects its
 #             tests via PYHTTPD_TARGETS (or its auto-detected default), since a
 #             pytest_suite path is meaningless there.
-# A flag that takes a separate-word value (-k NAME) keeps the value as a flag.
+#
+# The hard part is telling a positional test path from the value of a flag that
+# takes a separate word (e.g. `--tb short`, `--maxfail 3`, `-n 4`). We handle it
+# two ways: (a) the common value-flags -k/-m/-p are known to consume the next
+# word, and (b) any OTHER bare word is treated as a pysuite path only if it
+# actually exists on disk -- a flag value like "short"/"3"/"no" never does, so
+# it stays with `flags` (attached to its preceding flag) instead of being
+# misrouted to pysuite-only paths and stripped from what pyhttpd receives.
 only=""
 apxs_opt=""
 flags=""
@@ -89,7 +96,15 @@ for arg in "$@"; do
         --clean-modules) pysuite_flags="$pysuite_flags $arg" ;;  # pysuite-only; pyhttpd has no C modules
         -k|-m|-p) flags="$flags $arg"; expect_flagval=1 ;;  # take a value next
         -*)       flags="$flags $arg" ;;
-        *)        paths="$paths $arg" ;;
+        # A real pysuite path exists relative to pytest_suite/ (how users type
+        # it, e.g. "tests/t/php") or to our cwd; strip any ::nodeid suffix
+        # first. Anything else is a stray flag value -> keep it with the flags.
+        *)  if [ -e "$suite_dir/${arg%%::*}" ] || [ -e "${arg%%::*}" ]; then
+                paths="$paths $arg"
+            else
+                flags="$flags $arg"
+            fi
+            ;;
     esac
 done
 
@@ -110,6 +125,7 @@ php_args=""
 [ -n "${PHP_FPM:-}" ] && php_args="--php-fpm=$PHP_FPM"
 
 rc=0
+skipped=""   # names of suites that did NOT run (so we never report them "passed")
 
 run_pysuite() {
     echo "=========================================================="
@@ -134,6 +150,7 @@ run_pyhttpd() {
     if [ ! -f "$config_ini" ]; then
         echo "run-all-tests.sh: note: pyhttpd/config.ini not found;" >&2
         echo "  build httpd with its test config (configure) to run these." >&2
+        skipped="$skipped pyhttpd"
         return 0
     fi
     # runtests.sh manages the venv, prepends its bin/ to PATH (so CGI
@@ -159,6 +176,21 @@ case "$only" in
 esac
 
 echo "=========================================================="
-[ "$rc" -eq 0 ] && echo "ALL SUITES PASSED" || echo "SOME TESTS FAILED (rc=$rc)"
+if [ "$rc" -ne 0 ]; then
+    echo "SOME TESTS FAILED (rc=$rc)"
+elif [ -n "$skipped" ]; then
+    # Nothing failed, but at least one suite never ran -- don't claim success
+    # for a suite that was skipped (e.g. pyhttpd with no config.ini).
+    echo "PASSED, BUT SKIPPED:$skipped (not run -- see notes above)"
+else
+    echo "ALL SUITES PASSED"
+fi
 echo "=========================================================="
+
+# If a suite was skipped and the user explicitly asked for ONLY that suite,
+# treat "ran nothing" as a failure -- otherwise --only=pyhttpd could exit 0
+# having executed zero tests.
+if [ -n "$skipped" ] && [ -n "$only" ] && [ "$rc" -eq 0 ]; then
+    exit 3
+fi
 exit "$rc"