From: Jim Jagielski Date: Wed, 3 Jun 2026 17:21:22 +0000 (+0000) Subject: pytest_suite: fix startup failures and add missing Perl helper templates X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=264f12f311010537dce5dbe40fd62e92d444fe23;p=thirdparty%2Fapache%2Fhttpd.git pytest_suite: fix startup failures and add missing Perl helper templates Three bugs fixed in the pytest framework: 1. cmodules.py: compiled C test modules were not rebuilt when apxs changed (e.g. upgrading from Apache 2.4 to 2.5). The staleness check now also compares the .so mtime against apxs itself, so a newer apxs always triggers a rebuild and prevents API-signature mismatches (AP24 vs AP25). Add clean_modules() to remove all apxs build artifacts (make clean equivalent), exposed as --clean-modules pytest option and routed only to pytest_suite (not pyhttpd) in run-all-tests.sh. 2. server.py: httpd was started without -DFOREGROUND, so the MPM parent forked and exited (code 0) before the port opened. The framework misread the fork-exit as a crash and killed the running daemon. Add -DFOREGROUND so the Popen process IS the httpd parent. 3. Missing Perl helper scripts: t/conf/extra.conf.in declares a prg-type RewriteMap (numbers-prg) whose program file did not exist, causing httpd to refuse to start. More broadly, all *.pl CGI scripts, ext_filter programs, rewrite-map programs, and the SSL passphrase helper referenced by the config and tests were absent. Add the full set of *.pl.PL source templates (copied from the httpd-tests framework), covering: - t/conf/ssl/httpd-passphrase.pl.PL - t/htdocs/apache/http_strict/send_hdr.pl.PL (new dir) - t/htdocs/modules/cgi/ (28 CGI scripts) - t/htdocs/modules/ext_filter/ (2 filter programs, new dir) - t/htdocs/modules/negotiation/query/test.pl.PL - t/htdocs/modules/rewrite/ (numbers, numbers2, db) generate_pl_scripts() regenerates the .pl executables from these templates at each test-session start with the correct local perl shebang. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1934936 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/test/pytest_suite/apache_pytest/__init__.py b/test/pytest_suite/apache_pytest/__init__.py index a360c0cee9..9e1860d547 100644 --- a/test/pytest_suite/apache_pytest/__init__.py +++ b/test/pytest_suite/apache_pytest/__init__.py @@ -5,7 +5,7 @@ Phase 2: test-facing API (t_cmp, need_* markers) used by the translated tests. """ from .client import TestClient -from .cmodules import compile_all +from .cmodules import clean_modules, compile_all from .config import TestConfig from .probe import HttpdInfo, probe from .server import HttpdServer @@ -24,6 +24,7 @@ __all__ = [ "HttpdServer", "TestClient", "TestConfig", + "clean_modules", "compile_all", "need_cgi", "need_lwp", diff --git a/test/pytest_suite/apache_pytest/cmodules.py b/test/pytest_suite/apache_pytest/cmodules.py index db0cb42638..a9b277a7a4 100644 --- a/test/pytest_suite/apache_pytest/cmodules.py +++ b/test/pytest_suite/apache_pytest/cmodules.py @@ -21,6 +21,7 @@ actually compiling and loading in the smoke test. from __future__ import annotations import re +import shutil import subprocess from dataclasses import dataclass from pathlib import Path @@ -199,6 +200,25 @@ def _apxs_cmd(apxs: Path, defines: list[str], include_dir: Path, src: Path) -> l return cmd +def clean_modules(cmodules_dir: Path) -> None: + """Remove all apxs build artifacts from ``cmodules_dir`` (emulate ``make clean``). + + Deletes the ``.libs/`` directory and libtool intermediates (``*.o``, ``*.lo``, + ``*.slo``, ``*.la``) from every module subdirectory, and the generated + ``apache_httpd_test.h`` header. Safe to call when nothing has been built yet. + The next ``compile_all`` call will rebuild everything from scratch. + """ + for sub in sorted(p for p in cmodules_dir.iterdir() if p.is_dir()): + libs = sub / _LIB_SUBDIR + if libs.is_dir(): + shutil.rmtree(libs) + for pattern in ("*.o", "*.lo", "*.slo", "*.la"): + for f in sub.glob(pattern): + f.unlink(missing_ok=True) + header = cmodules_dir / "apache_httpd_test.h" + header.unlink(missing_ok=True) + + def compile_all( cmodules_dir: Path, apxs: Path, @@ -216,10 +236,13 @@ def compile_all( generate_header(cmodules_dir / "apache_httpd_test.h") mods, skipped = discover(cmodules_dir, info) + apxs_mtime = apxs.stat().st_mtime loads: list[tuple[str, Path]] = [] for mod in mods: needs = force or not mod.so.exists() or ( mod.so.stat().st_mtime < mod.src.stat().st_mtime + ) or ( + mod.so.stat().st_mtime < apxs_mtime ) if needs: cmd = _apxs_cmd(apxs, defines, cmodules_dir, mod.src) diff --git a/test/pytest_suite/apache_pytest/server.py b/test/pytest_suite/apache_pytest/server.py index f1351af54f..cd111231e3 100644 --- a/test/pytest_suite/apache_pytest/server.py +++ b/test/pytest_suite/apache_pytest/server.py @@ -108,6 +108,7 @@ class HttpdServer: vars_["serverroot"], "-f", vars_["t_conf_file"], + "-DFOREGROUND", ] for d in self.dversion_defines(): argv += ["-D", d] diff --git a/test/pytest_suite/conftest.py b/test/pytest_suite/conftest.py index f1b78302be..5ffb1cbd79 100644 --- a/test/pytest_suite/conftest.py +++ b/test/pytest_suite/conftest.py @@ -24,6 +24,7 @@ from pathlib import Path import pytest from apache_pytest import HttpdServer, TestClient, TestConfig, compile_all, probe +from apache_pytest.cmodules import clean_modules from apache_pytest.probe import HttpdInfo # The suite is self-contained: all assets it needs (t/conf templates, t/htdocs @@ -58,6 +59,12 @@ def pytest_addoption(parser: pytest.Parser) -> None: default=8999, help="TCP port for the managed php-fpm pool (default 8999)", ) + group.addoption( + "--clean-modules", + action="store_true", + default=False, + help="remove all compiled C-module artifacts before building (emulate make clean)", + ) def pytest_configure(config: pytest.Config) -> None: @@ -224,8 +231,11 @@ def framework(request: pytest.FixtureRequest): cmodule_loads: list[tuple[str, Path]] = [] if apxs is not None: + cmodules_dir = REPO_ROOT / "c-modules" + if request.config.getoption("--clean-modules"): + clean_modules(cmodules_dir) cmodule_loads, _skipped = compile_all( - REPO_ROOT / "c-modules", apxs, info, defines=["APACHE2", "APACHE2_4", *defines] + cmodules_dir, apxs, info, defines=["APACHE2", "APACHE2_4", *defines] ) config.generate(cmodule_loads=cmodule_loads) diff --git a/test/pytest_suite/t/conf/ssl/httpd-passphrase.pl.PL b/test/pytest_suite/t/conf/ssl/httpd-passphrase.pl.PL new file mode 100644 index 0000000000..36eba9409e --- /dev/null +++ b/test/pytest_suite/t/conf/ssl/httpd-passphrase.pl.PL @@ -0,0 +1,2 @@ +#for testing SSLPassPhraseDialog exec:@ServerRoot@/conf/ssl/httpd-passphrase.pl +print "httpd\n"; diff --git a/test/pytest_suite/t/htdocs/apache/http_strict/send_hdr.pl.PL b/test/pytest_suite/t/htdocs/apache/http_strict/send_hdr.pl.PL new file mode 100644 index 0000000000..95ccd8518b --- /dev/null +++ b/test/pytest_suite/t/htdocs/apache/http_strict/send_hdr.pl.PL @@ -0,0 +1,10 @@ +use MIME::Base64; +use strict; +use warnings; + +print "Content-type: text/plain\r\n"; +print decode_base64($ENV{QUERY_STRING}), "\r\n"; +print "\r\n"; +print "Hi!\n"; +print "SERVERNAME=$ENV{SERVER_NAME}\n"; +print "HTTP_HOST=$ENV{HTTP_HOST}\n"; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/action.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/action.pl.PL new file mode 100644 index 0000000000..19d65292e9 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/action.pl.PL @@ -0,0 +1,10 @@ +use strict; + +print "Content-type: text/plain\n\n"; + +print $ENV{PATH_INFO} . "\n"; + +if (my $ct = $ENV{CONTENT_LENGTH}) { + read STDIN, my $buffer, $ct; + print $buffer; +} diff --git a/test/pytest_suite/t/htdocs/modules/cgi/big.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/big.pl.PL new file mode 100644 index 0000000000..636fb6638e --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/big.pl.PL @@ -0,0 +1,22 @@ +# This is a regression test for PR 31247. + +# By sleeping, it ensures that the CGI bucket is left in the brigade +# (the first 8K will be morphed into a HEAP bucket), and hence *must* +# be setaside correctly when the byterange filter calls +# ap_save_brigade(). + +# Without the fix for PR 31247, the STDOUT content does not get +# consumed as expected, so the server will deadlock as it tries to +# consume STDERR after script execution in mod_cgi, whilst the script +# tries to write to STDOUT. So close STDERR to avoid that. + +close STDERR; + +print "Content-type: text/plain\n\n"; + +print "x"x8192; + +sleep 1; + +print "x"x8192; + diff --git a/test/pytest_suite/t/htdocs/modules/cgi/bogus-perl.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/bogus-perl.pl.PL new file mode 100755 index 0000000000..8abb7b25d3 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/bogus-perl.pl.PL @@ -0,0 +1,2 @@ + +print "perl cgi"; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/bogus1k.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/bogus1k.pl.PL new file mode 100755 index 0000000000..7c3d2441e1 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/bogus1k.pl.PL @@ -0,0 +1,2 @@ + +print "N"x1024; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/empty.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/empty.pl.PL new file mode 100755 index 0000000000..31e1ae92e2 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/empty.pl.PL @@ -0,0 +1,6 @@ +use strict; + +print "Content-type: text/plain\r\n"; +print "Content-Length: 0\r\n"; +print "\r\n"; + diff --git a/test/pytest_suite/t/htdocs/modules/cgi/env.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/env.pl.PL new file mode 100644 index 0000000000..f776cab522 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/env.pl.PL @@ -0,0 +1,7 @@ +use strict; + +print "Content-type: text/plain\n\n"; + +for (sort keys %ENV) { + print "$_ = $ENV{$_}\n"; +} diff --git a/test/pytest_suite/t/htdocs/modules/cgi/not-modified.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/not-modified.pl.PL new file mode 100644 index 0000000000..6684e48e6f --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/not-modified.pl.PL @@ -0,0 +1,4 @@ +use strict; + +print "Status: 304 Not Modified\r\n\r\n"; + diff --git a/test/pytest_suite/t/htdocs/modules/cgi/nph-102.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/nph-102.pl.PL new file mode 100644 index 0000000000..a49eeaafab --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/nph-102.pl.PL @@ -0,0 +1,9 @@ + +print "HTTP/1.1 102 Please Wait...\r\n"; +print "Host: nph-102\r\n\r\n"; + +print "HTTP/1.1 200 OK\r\n"; +print "Content-Type: text/plain\r\n\r\n"; + +print "this is nph-stdout"; + diff --git a/test/pytest_suite/t/htdocs/modules/cgi/nph-dripfeed.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/nph-dripfeed.pl.PL new file mode 100644 index 0000000000..deaf5f6936 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/nph-dripfeed.pl.PL @@ -0,0 +1,17 @@ +print "HTTP/1.1 200 OK\r\n"; +print "Transfer-Encoding: chunked\r\n"; +print "\r\n"; + +$| = 1; + +sub dripfeed { + my $s = shift; + + while (length($s)) { + select(undef, undef, undef, 0.2); + print substr($s, 0, 1); + $s = substr($s, 1); + } +} + +dripfeed "0005\r\nabcde\r\n1; foo=bar\r\nf\r\n0\r\n\r\n"; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL new file mode 100644 index 0000000000..67d7e9f04e --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL @@ -0,0 +1,12 @@ +# produces output with folded response headers + +print "HTTP/1.0 200 OK\r\n"; + +for (1..50) { + print "X-Foo-Bar-$_:\n " . 'x'x($_*10) . "\n"; + print "X-Bar-$_:\n gamm\r\n beta\n theta\r\n"; +} + +print "Content-type: \n text/plain\n\n"; + +print "hello, world"; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/nph-interim1.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/nph-interim1.pl.PL new file mode 100644 index 0000000000..87c0931809 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/nph-interim1.pl.PL @@ -0,0 +1,16 @@ +foreach $i (1..5) { +print <', "$filein") or die "Could not open file '$filein' for write: $!"; +binmode IN; +print IN ; +close(IN); + +my $cmd = 'openssl ocsp -CA certs/ca.crt'. + ' -index index.txt'. + ' -rsigner certs/server.crt'. + ' -rkey keys/server.pem'. + ' -reqin ' . $filein . + ' -respout ' . $fileout; +system($cmd); + +# Check system result +my $err = ''; +if ($? == -1) { + my $err = "failed to execute '$cmd': $!\n"; +} +elsif ($? & 127) { + my $err = sprintf("child '$cmd' died with signal %d, %s coredump\n", + ($? & 127), ($? & 128) ? 'with' : 'without'); +} +else { + my $rc = $? >> 8; + my $err = "child '$cmd' exited with value $rc\n" if $rc; +} + +unlink($filein); + +if ($err ne '') { + print <; +close(OUT); +unlink($fileout); diff --git a/test/pytest_suite/t/htdocs/modules/cgi/perl.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/perl.pl.PL new file mode 100755 index 0000000000..51969cc96a --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/perl.pl.PL @@ -0,0 +1,3 @@ + +print "Content-type: text/plain\n\n"; +print "perl cgi"; diff --git a/test/pytest_suite/t/htdocs/modules/cgi/perl_echo.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/perl_echo.pl.PL new file mode 100644 index 0000000000..b7591a65a1 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/perl_echo.pl.PL @@ -0,0 +1,14 @@ +#echo some data back to the client + +print "Content-type: text/plain\n\n"; + +if (my $ct = $ENV{CONTENT_LENGTH}) { + read STDIN, my $buffer, $ct; + print $buffer; +} +elsif (my $qs = $ENV{QUERY_STRING}) { + print $qs; +} +else { + print "nada"; +} diff --git a/test/pytest_suite/t/htdocs/modules/cgi/perl_post.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/perl_post.pl.PL new file mode 100755 index 0000000000..e19d204d41 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/perl_post.pl.PL @@ -0,0 +1,23 @@ +local ($buffer, @pairs, $pair, $name, $value); + +print "Content-type: text/plain\n\n"; + +$ENV{'REQUEST_METHOD'} =~ tr/a-z/A-Z/; +print "$ENV{'REQUEST_METHOD'}\n"; + +# Read in text +if ($ENV{'REQUEST_METHOD'} eq "POST") { + read(STDIN, $buffer, $ENV{'CONTENT_LENGTH'}); +} else { + $buffer = $ENV{'QUERY_STRING'}; +} + +# Split information into name/value pairs +@pairs = split(/&/, $buffer); +foreach $pair (@pairs) { + ($name, $value) = split(/=/, $pair); + $value =~ tr/+/ /; + $value =~ s/%(..)/pack("C", hex($1))/eg; + + print "$name: $value\n"; +} diff --git a/test/pytest_suite/t/htdocs/modules/cgi/pr37166.pl.PL b/test/pytest_suite/t/htdocs/modules/cgi/pr37166.pl.PL new file mode 100644 index 0000000000..f565c5c987 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/cgi/pr37166.pl.PL @@ -0,0 +1,8 @@ +print <; diff --git a/test/pytest_suite/t/htdocs/modules/ext_filter/sleepycat.pl.PL b/test/pytest_suite/t/htdocs/modules/ext_filter/sleepycat.pl.PL new file mode 100644 index 0000000000..fc9c399d2e --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/ext_filter/sleepycat.pl.PL @@ -0,0 +1,3 @@ +$| = 1; + +print && select undef,undef,undef,.2 while <>; diff --git a/test/pytest_suite/t/htdocs/modules/negotiation/query/test.pl.PL b/test/pytest_suite/t/htdocs/modules/negotiation/query/test.pl.PL new file mode 100755 index 0000000000..b370163582 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/negotiation/query/test.pl.PL @@ -0,0 +1,8 @@ + +print "Content-type: text/html\n\n"; + +foreach my $key (keys %ENV) { + if ($key eq "QUERY_STRING") { + print "$key --> $ENV{$key}\n"; + } +} diff --git a/test/pytest_suite/t/htdocs/modules/rewrite/db.pl.PL b/test/pytest_suite/t/htdocs/modules/rewrite/db.pl.PL new file mode 100644 index 0000000000..a897a8a2e8 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/rewrite/db.pl.PL @@ -0,0 +1,10 @@ + +($txtmap, $dbmmap) = @ARGV; +open(TXT, "<$txtmap"); +dbmopen(%DB, $dbmmap, 0644); +while () { + next if (m|^s*#.*| or m|^s*$|); + $DB{$1} = $2 if (m|^\s*(\S+)\s+(\S+)$|); +} +dbmclose(%DB); +close(TXT) diff --git a/test/pytest_suite/t/htdocs/modules/rewrite/numbers.pl.PL b/test/pytest_suite/t/htdocs/modules/rewrite/numbers.pl.PL new file mode 100644 index 0000000000..d3d3067891 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/rewrite/numbers.pl.PL @@ -0,0 +1,26 @@ + +# numbers.pl +# program rewrite map for mod_rewrite testing +# +$|=1; +my %map = ( 1 => 'one', + 2 => 'two', + 3 => 'three', + 4 => 'four', + 5 => 'five', + 6 => 'six' ); + +while () { + chomp; + + print STDERR "GOT: ->$_<-\n"; + my $m = $map{$_}; + print STDERR "MAPPED: ->$_<-\n"; + + if ($m) { + print STDOUT "$m\n"; + } else { + print STDOUT "NULL"; + } +} +close (LOG); diff --git a/test/pytest_suite/t/htdocs/modules/rewrite/numbers2.pl.PL b/test/pytest_suite/t/htdocs/modules/rewrite/numbers2.pl.PL new file mode 100755 index 0000000000..d450b683d0 --- /dev/null +++ b/test/pytest_suite/t/htdocs/modules/rewrite/numbers2.pl.PL @@ -0,0 +1,9 @@ + +# numbers.pl +# program rewrite map for mod_rewrite testing +# +funk cold medina. +$|=1; +while () { + print $_; +} diff --git a/test/run-all-tests.sh b/test/run-all-tests.sh index e80655cf4c..3e041ad308 100755 --- a/test/run-all-tests.sh +++ b/test/run-all-tests.sh @@ -41,6 +41,7 @@ config_ini="$here/pyhttpd/config.ini" only="" apxs_opt="" flags="" +pysuite_flags="" paths="" expect_apxs=0 expect_flagval=0 @@ -51,6 +52,7 @@ for arg in "$@"; do --only=*) only="${arg#--only=}" ;; --apxs) expect_apxs=1 ;; --apxs=*) apxs_opt="${arg#--apxs=}" ;; + --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" ;; @@ -86,8 +88,9 @@ run_pysuite() { fi # runtests.sh handles venv + cgisock cleanup + flag assembly. # pytest_suite gets both the shared flags and any positional paths. + # pysuite_flags holds options only meaningful to this suite (e.g. --clean-modules). # shellcheck disable=SC2086 - ( cd "$suite_dir" && ./runtests.sh --apxs="$apxs_opt" $php_args $flags $paths ) || return $? + ( cd "$suite_dir" && ./runtests.sh --apxs="$apxs_opt" $php_args $flags $pysuite_flags $paths ) || return $? } run_pyhttpd() {