From: Jean-Frederic Clere Date: Wed, 12 Aug 2026 15:41:41 +0000 (+0000) Subject: Arrange the testsuite to run it on windows. X-Git-Url: http://git.ipfire.org/gitweb/index.cgi?a=commitdiff_plain;h=3417e9d0a8f85ef92d3f600e758d97fda2c7c19a;p=thirdparty%2Fapache%2Fhttpd.git Arrange the testsuite to run it on windows. used claude ai for the investigation. git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1937082 13f79535-47bb-0310-9956-ffa450edef68 --- diff --git a/test/pytest_suite/apache_pytest/config.py b/test/pytest_suite/apache_pytest/config.py index 3e362be077..a0639fcfa3 100644 --- a/test/pytest_suite/apache_pytest/config.py +++ b/test/pytest_suite/apache_pytest/config.py @@ -22,6 +22,7 @@ from __future__ import annotations import re import socket +import sys from collections.abc import Iterator from dataclasses import dataclass, field from pathlib import Path @@ -290,15 +291,15 @@ class TestConfig: v: dict[str, str] = {} v["top_dir"] = str(top_dir) v["t_dir"] = str(t_dir) - v["serverroot"] = str(serverroot) - v["documentroot"] = str(serverroot / "htdocs") - v["t_conf"] = str(serverroot / "conf") - v["t_logs"] = str(serverroot / "logs") - v["t_state"] = str(serverroot / "state") + v["serverroot"] = str(serverroot).replace("\\", "/") + v["documentroot"] = v["serverroot"] + "/htdocs" + v["t_conf"] = v["serverroot"] + "/conf" + v["t_logs"] = v["serverroot"] + "/logs" + v["t_state"] = v["serverroot"] + "/state" v["statedir"] = v["t_state"] - v["t_conf_file"] = str(serverroot / "conf" / "httpd.conf") - v["t_pid_file"] = str(serverroot / "logs" / "httpd.pid") - v["sslca"] = str(serverroot / "conf" / "ssl" / "ca") + v["t_conf_file"] = v["t_conf"] + "/httpd.conf" + v["t_pid_file"] = v["t_logs"] + "/httpd.pid" + v["sslca"] = v["t_conf"] + "/ssl/ca" v["sslcaorg"] = "asf" v["sslproto"] = "all" v["scheme"] = "http" @@ -309,9 +310,9 @@ class TestConfig: # getfiles-* download aliases (see generate_httpd_conf %aliases). httpd # is the probed binary; perl is whatever runs the helper scripts. v["httpd"] = str(self.info.httpd) - from shutil import which + from .scripts import default_perl - v["perl"] = which("perl") or "" + v["perl"] = default_perl() # perlpod: a 'pods' dir under @INC, like Perl's find_in_inc('pods') # (TestConfig.pm:297). Drives the /getfiles-perl-pod alias that # t/filter/case.t (mod_alias case) downloads from. Left "" if not found, @@ -497,7 +498,7 @@ class TestConfig: rewritten = self._maybe_rewrite_vhost(expanded) out_lines.append(rewritten if rewritten is not None else expanded) out_path = conf_in.with_suffix("") # strip ".in" -> ".conf" - out_path.write_text("\n".join(out_lines) + "\n") + out_path.write_text("\n".join(out_lines) + "\n", newline="\n") return out_path def conf_in_files(self) -> list[Path]: @@ -537,7 +538,7 @@ class TestConfig: else: mime = Path(self.vars["t_conf"]) / "mime.types" if not mime.exists(): - mime.write_text(self.MIME_TYPES) + mime.write_text(self.MIME_TYPES, newline="\n") self.postamble.append( f'\n TypesConfig "{mime}"\n' ) @@ -546,14 +547,16 @@ class TestConfig: index = Path(self.vars["documentroot"]) / "index.html" if not index.exists(): index.write_text( - f"welcome to {self.vars['servername']}:{self.vars['port']}\n" + f"welcome to {self.vars['servername']}:{self.vars['port']}\n", + newline="\n", ) def _load_module_preamble(self, name: str, so: Path) -> None: """Append a guarded LoadModule (find_and_load_module, TestConfig.pm:1329).""" + so_fwd = str(so).replace("\\", "/") self.preamble.append( f'\n' - f' LoadModule {name}_module "{so}"\n' + f' LoadModule {name}_module "{so_fwd}"\n' f'' ) @@ -674,9 +677,10 @@ class TestConfig: for d in self.info.load_directives: if not _Path(d.so).exists(): continue + so = d.so.replace("\\", "/") self.preamble.append( f"\n" - f' LoadModule {d.symbol} "{d.so}"\n' + f' LoadModule {d.symbol} "{so}"\n' f"" ) @@ -836,7 +840,8 @@ class TestConfig: # accumulate across modules and are flushed once at the end. cmodule_args: list[str] = [] for sym, so in cmodule_loads or []: - self.preamble.append(f'LoadModule {sym}_module "{so}"') + so_fwd = str(so).replace("\\", "/") + self.preamble.append(f'LoadModule {sym}_module "{so_fwd}"') # Register the module in the modules set so # rewriting recognizes it (TestConfigC.pm:308 $self->{modules}{$cname}=1). self.info.modules.add(f"mod_{sym}.c") @@ -859,7 +864,8 @@ class TestConfig: generated.append(self.process_conf_in(f)) self._check_vars() for g in sorted(generated): - self.postamble.append(f'Include "{g}"') + g_fwd = str(g).replace("\\", "/") + self.postamble.append(f'Include "{g_fwd}"') # mod_mime/mod_alias may be shared and absent from the system conf; load # them defensively. Order matches Perl: generate_types_config loads @@ -867,6 +873,15 @@ class TestConfig: self._find_and_load_fallback("mod_mime") self._find_and_load_fallback("mod_alias") + # On Windows, tell mod_cgi to read the #! shebang line instead of + # using the Registry file association to find the script interpreter. + if sys.platform == "win32": + self.postamble.append( + "\n" + " ScriptInterpreterSource Script\n" + "" + ) + # Assemble httpd.conf in generate_httpd_conf order (TestConfig.pm:1609-1690). parts: list[str] = [] parts.extend(self.preamble) @@ -880,5 +895,5 @@ class TestConfig: parts.extend(self.postamble) conf = Path(self.vars["t_conf_file"]) - conf.write_text("\n".join(parts) + "\n") + conf.write_text("\n".join(parts) + "\n", newline="\n") return conf diff --git a/test/pytest_suite/apache_pytest/scripts.py b/test/pytest_suite/apache_pytest/scripts.py index fbceb3dcef..ccae4322ed 100644 --- a/test/pytest_suite/apache_pytest/scripts.py +++ b/test/pytest_suite/apache_pytest/scripts.py @@ -49,7 +49,7 @@ def generate_pl_scripts(root: Path, *, perl: str | None = None) -> list[Path]: continue target = pl.with_suffix("") # strip ".PL" -> "...pl" body = pl.read_text() - target.write_text(_shebang() + body) + target.write_text(_shebang() + body, newline="\n") target.chmod(target.stat().st_mode | stat.S_IRWXU | stat.S_IRGRP | stat.S_IXGRP) generated.append(target) return generated @@ -59,4 +59,5 @@ def default_perl() -> str: """Best-effort path to a perl interpreter for the generated shebangs.""" from shutil import which - return which("perl") or PERL or sys.executable + path = PERL or which("perl") or sys.executable + return path.replace("\\", "/") diff --git a/test/pytest_suite/apache_pytest/server.py b/test/pytest_suite/apache_pytest/server.py index cd111231e3..ac350877d4 100644 --- a/test/pytest_suite/apache_pytest/server.py +++ b/test/pytest_suite/apache_pytest/server.py @@ -18,6 +18,7 @@ from __future__ import annotations import contextlib import errno import os +import sys import signal import socket import subprocess @@ -66,9 +67,15 @@ def _killpg_or_pid(pid: int, sig: int) -> None: the parent's process group id (== parent pid), so signalling the group with ``os.killpg`` reaches the parent and all its workers. If the process is not a group leader (no such pgid) we fall back to signalling the bare pid. + + On Windows there are no process groups; use ``os.kill`` directly. """ if pid <= 0: return + if sys.platform == "win32": + with contextlib.suppress(OSError): + os.kill(pid, sig) + return try: os.killpg(pid, sig) except OSError as exc: @@ -159,9 +166,11 @@ class HttpdServer: break time.sleep(0.1) if _pid_alive(pid): - _killpg_or_pid(pid, signal.SIGKILL) - with contextlib.suppress(OSError): - os.waitpid(pid, 0) # reap if it happens to be our child + kill_sig = signal.SIGTERM if sys.platform == "win32" else signal.SIGKILL + _killpg_or_pid(pid, kill_sig) + if sys.platform != "win32": + with contextlib.suppress(OSError): + os.waitpid(pid, 0) # reap if it happens to be our child if pid_file.exists(): with contextlib.suppress(OSError): pid_file.unlink() @@ -180,8 +189,11 @@ class HttpdServer: # start_new_session=True (setsid) puts httpd in its own process group so # the parent and all forked MPM children can be signalled together via # os.killpg, guaranteeing no orphaned children survive a failed start. + popen_kwargs = {} + if sys.platform != "win32": + popen_kwargs["start_new_session"] = True self.proc = subprocess.Popen( # noqa: S603 - trusted paths - self.args(), start_new_session=True + self.args(), **popen_kwargs ) deadline = time.monotonic() + timeout while time.monotonic() < deadline: @@ -214,7 +226,8 @@ class HttpdServer: try: proc.wait(timeout=timeout) except subprocess.TimeoutExpired: - _killpg_or_pid(pgid_pid, signal.SIGKILL) + kill_sig = signal.SIGTERM if sys.platform == "win32" else signal.SIGKILL + _killpg_or_pid(pgid_pid, kill_sig) with contextlib.suppress(subprocess.TimeoutExpired): proc.wait(timeout=timeout) elif proc is not None: diff --git a/test/pytest_suite/conftest.py b/test/pytest_suite/conftest.py index 9fbaa80efe..8d3fd2111c 100644 --- a/test/pytest_suite/conftest.py +++ b/test/pytest_suite/conftest.py @@ -65,6 +65,20 @@ def pytest_addoption(parser: pytest.Parser) -> None: default=False, help="remove all compiled C-module artifacts before building (emulate make clean)", ) + group.addoption( + "--conf", + action="store", + default=None, + help="path to the installed httpd.conf (for LoadModule discovery " + "when --apxs is not available, e.g. on Windows)", + ) + group.addoption( + "--prefix", + action="store", + default=None, + help="server install prefix for resolving relative module paths " + "(default: derived from --conf path)", + ) def pytest_configure(config: pytest.Config) -> None: @@ -114,6 +128,16 @@ def _resolve_paths( inherited_conf = sysconfdir / "httpd.conf" if httpd_opt is None: httpd_opt = str(sbindir / "httpd") + + conf_opt = config.getoption("--conf") + prefix_opt = config.getoption("--prefix") + if conf_opt is not None and inherited_conf is None: + inherited_conf = Path(conf_opt) + if prefix_opt is not None: + install_prefix = Path(prefix_opt) + elif inherited_conf is not None and install_prefix is None: + install_prefix = inherited_conf.parent.parent + if httpd_opt is None: raise _NoServerError("must pass --httpd or --apxs") return Path(httpd_opt), apxs, inherited_conf, install_prefix, defines @@ -144,11 +168,13 @@ def _probed_info(config: pytest.Config) -> HttpdInfo | None: # fixture, so need_module("authany") etc. should be satisfied at collection # time too. Augment the probed set with the C modules that WILL be built # (honoring the same HTTPD_TEST_REQUIRE_APACHE gating discover() applies). - from apache_pytest.cmodules import discover + # Without apxs the modules can't be compiled, so don't promise them. + if _apxs is not None: + from apache_pytest.cmodules import discover - cmods, _skipped = discover(REPO_ROOT / "c-modules", info) - for mod in cmods: - info.modules.add(f"mod_{mod.name}.c") + cmods, _skipped = discover(REPO_ROOT / "c-modules", info) + for mod in cmods: + info.modules.add(f"mod_{mod.name}.c") _probe_cache = info return _probe_cache diff --git a/test/pytest_suite/t/conf/extra.conf.in b/test/pytest_suite/t/conf/extra.conf.in index b327cccfca..745db3ba28 100644 --- a/test/pytest_suite/t/conf/extra.conf.in +++ b/test/pytest_suite/t/conf/extra.conf.in @@ -178,7 +178,7 @@ RewriteMap numbers-txt txt:@SERVERROOT@/htdocs/modules/rewrite/numbers.txt RewriteMap numbers-rnd rnd:@SERVERROOT@/htdocs/modules/rewrite/numbers.rnd #RewriteMap numbers-dbm dbm:@SERVERROOT@/htdocs/modules/rewrite/numbers.dbm - RewriteMap numbers-prg prg:@SERVERROOT@/htdocs/modules/rewrite/numbers.pl + RewriteMap numbers-prg "prg:@PERL@ @SERVERROOT@/htdocs/modules/rewrite/numbers.pl" RewriteMap lower int:tolower @@ -358,6 +358,7 @@ # PR60478: pathological rewrite expansion + = 2.4> # This pair of RewriteRules will loop but should eventually 500 once we @@ -368,6 +369,7 @@ RewriteRule X - [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 index 67d7e9f04e..41dafce5b1 100644 --- a/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL +++ b/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL @@ -1,5 +1,6 @@ # produces output with folded response headers +binmode(STDOUT); print "HTTP/1.0 200 OK\r\n"; for (1..50) { diff --git a/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py b/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py index a55390c85a..a4a07ee06a 100644 --- a/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py +++ b/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py @@ -8,6 +8,7 @@ Perl original needed: need_apache(2), mod_include, need_lwp. """ import re +import sys import pytest @@ -58,6 +59,7 @@ def _cases(http): @need_module("include") @need_lwp() +@pytest.mark.skipif(sys.platform == "win32", reason="uses shell CGI scripts") def test_acceptpathinfo(http): for mode, req, exp_rc, exp_body in _cases(http): # Apache::TestRequest's GET follows redirects by default; the bare diff --git a/test/pytest_suite/tests/t/apache/test_byterange2.py b/test/pytest_suite/tests/t/apache/test_byterange2.py index c8f23ca09e..7b30be2683 100644 --- a/test/pytest_suite/tests/t/apache/test_byterange2.py +++ b/test/pytest_suite/tests/t/apache/test_byterange2.py @@ -13,4 +13,4 @@ from apache_pytest import need_cgi, need_min_apache_version, t_cmp @need_cgi() def test_byterange2(http): resp = http.GET_BODY("/modules/cgi/ranged.pl", headers={"Range": "bytes=5-10/10"}) - assert t_cmp(resp, "hello\n"), "return correct content" + assert t_cmp(resp.replace("\r\n", "\n"), "hello\n"), "return correct content" diff --git a/test/pytest_suite/tests/t/apache/test_pr37166.py b/test/pytest_suite/tests/t/apache/test_pr37166.py index ba2bf73378..eb8aea4041 100644 --- a/test/pytest_suite/tests/t/apache/test_pr37166.py +++ b/test/pytest_suite/tests/t/apache/test_pr37166.py @@ -15,10 +15,11 @@ URI = "/modules/cgi/pr37166.pl" def test_pr37166(http): r = http.GET(URI) assert t_cmp(r.status_code, 200), "SSI was allowed for location" - assert t_cmp(r.text, "Hello world\n"), "file was served with correct content" + assert t_cmp(r.text.replace("\r\n", "\n"), "Hello world\n"), \ + "file was served with correct content" r = http.GET(URI, headers={"If-Modified-Since": "Tue, 15 Feb 2005 15:00:00 GMT"}) assert t_cmp(r.status_code, 200), "explicit 200 response" - assert t_cmp(r.text, "Hello world\n"), ( + assert t_cmp(r.text.replace("\r\n", "\n"), "Hello world\n"), ( "file was again served with correct content" ) diff --git a/test/pytest_suite/tests/t/apache/test_pr49328.py b/test/pytest_suite/tests/t/apache/test_pr49328.py index 5d1a665150..f595f1c9c2 100644 --- a/test/pytest_suite/tests/t/apache/test_pr49328.py +++ b/test/pytest_suite/tests/t/apache/test_pr49328.py @@ -12,7 +12,7 @@ INFLATOR = "/modules/deflate/echo_post" URI = "/modules/filter/pr49328/pr49328.shtml" -@need_module("filter", "include", "deflate") +@need_module("filter", "include", "deflate", "echo_post") def test_pr49328(http): # GET_RAW: keep the gzip stream undecoded so we can re-POST it through the # inflate input filter (httpx would otherwise auto-decompress .content). @@ -20,4 +20,4 @@ def test_pr49328(http): deflated = http.POST_BODY( INFLATOR, content=content, headers={"Content-Encoding": "gzip"} ) - assert t_cmp(deflated, "before\nincluded\nafter\n") + assert t_cmp(deflated.replace("\r\n", "\n"), "before\nincluded\nafter\n") diff --git a/test/pytest_suite/tests/t/modules/test_actions.py b/test/pytest_suite/tests/t/modules/test_actions.py index 649e7b0c48..79f0d773b3 100644 --- a/test/pytest_suite/tests/t/modules/test_actions.py +++ b/test/pytest_suite/tests/t/modules/test_actions.py @@ -4,6 +4,8 @@ Two groups: ``tests_action`` (GET each url, check code; if 200 check body) and ``tests_script`` (GET, POST and PUT against script locations). """ +import sys + import pytest from apache_pytest import need_module, t_cmp @@ -35,6 +37,8 @@ TESTS_SCRIPT = [ def test_actions_action(http, case): if case in TESTS_ACTION_2460 and not http.have_min_apache_version("2.4.60"): pytest.skip("requires httpd >= 2.4.60") + if sys.platform == "win32" and (".sh?" in case[0] or case[0].endswith(".sh")): + pytest.skip("shell scripts not available on Windows") url, code = case[0], case[1] r = http.GET(url) assert t_cmp(r.status_code, code), f"Check {url} for {code}" @@ -53,7 +57,7 @@ def test_actions_script(http, case): r = http.POST(url, content="foo2=bar2") assert t_cmp(r.status_code, 200) - assert t_cmp(r.text, "POST\nfoo2: bar2\n") + assert t_cmp(r.text.replace("\r\n", "\n"), "POST\nfoo2: bar2\n") # Method not allowed r = http.PUT(url, content="foo2=bar2") diff --git a/test/pytest_suite/tests/t/modules/test_alias.py b/test/pytest_suite/tests/t/modules/test_alias.py index f6e8d4425e..3ff21e89fa 100644 --- a/test/pytest_suite/tests/t/modules/test_alias.py +++ b/test/pytest_suite/tests/t/modules/test_alias.py @@ -13,6 +13,7 @@ WINFU (Windows) branches are not reproduced (POSIX shell CGI assumed). import os import re import stat +import sys import pytest @@ -133,19 +134,22 @@ def test_scriptalias(http): _write_cgi(http) # Served as plain text at /modules/alias/script. - assert t_cmp(http.GET_BODY("/modules/alias/script"), CGI), \ - "/modules/alias/script" + body = http.GET_BODY("/modules/alias/script").replace("\r\n", "\n") + assert t_cmp(body, CGI), "/modules/alias/script" if http.have_module("mod_cgi") or http.have_module("mod_cgid"): + if sys.platform == "win32": + pytest.skip("shell CGI scripts not available on Windows") # Executed as CGI at /cgi/script. - assert t_cmp(http.GET_BODY("/cgi/script"), f"{CGI_STRING}\n"), "/cgi/script" + body = http.GET_BODY("/cgi/script").replace("\r\n", "\n") + assert t_cmp(body, f"{CGI_STRING}\n"), "/cgi/script" # ScriptAliasMatch. - assert t_cmp(http.GET_BODY("/aliascgi-script"), f"{CGI_STRING}\n"), \ - "/aliascgi-script" + body = http.GET_BODY("/aliascgi-script").replace("\r\n", "\n") + assert t_cmp(body, f"{CGI_STRING}\n"), "/aliascgi-script" if http.have_min_apache_version("2.4.19"): # ScriptAlias inside LocationMatch. - assert t_cmp(http.GET_BODY("/expr/aliascgi-script"), - f"{CGI_STRING}\n"), "/aliascgi-script" + body = http.GET_BODY("/expr/aliascgi-script").replace("\r\n", "\n") + assert t_cmp(body, f"{CGI_STRING}\n"), "/aliascgi-script" # Bad ScriptAliasMatch. assert t_cmp(http.GET_RC("/aliascgi-nada"), 404), "/aliascgi-nada" diff --git a/test/pytest_suite/tests/t/modules/test_cgi.py b/test/pytest_suite/tests/t/modules/test_cgi.py index 8954dd2ebe..28604dd39b 100644 --- a/test/pytest_suite/tests/t/modules/test_cgi.py +++ b/test/pytest_suite/tests/t/modules/test_cgi.py @@ -11,6 +11,7 @@ Perl original: plan tests => ..., \&need_cgi; (mod_cgid present locally) import os import re +import sys import pytest @@ -47,6 +48,7 @@ def _cgi_log(http): @need_cgi() +@pytest.mark.skipif(sys.platform == "win32", reason="shell CGI scripts not available on Windows") def test_cgi(http): cgi_log = _cgi_log(http) if os.path.exists(cgi_log): diff --git a/test/pytest_suite/tests/t/modules/test_ext_filter.py b/test/pytest_suite/tests/t/modules/test_ext_filter.py index 652aab267d..8ea3de5729 100644 --- a/test/pytest_suite/tests/t/modules/test_ext_filter.py +++ b/test/pytest_suite/tests/t/modules/test_ext_filter.py @@ -9,12 +9,19 @@ keep-alive UA. """ import re +import sys import pytest from apache_pytest import need_cgi, need_module, t_cmp +_skip_win32 = pytest.mark.skipif( + sys.platform == "win32", + reason="ext_filter cmd cannot execute .pl scripts directly on Windows", +) + +@_skip_win32 @need_module("ext_filter") @need_cgi() def test_ext_filter_output(http): @@ -22,6 +29,7 @@ def test_ext_filter_output(http): assert t_cmp(content, "barbar"), "sed output filter" +@_skip_win32 @need_module("ext_filter") @need_cgi() def test_ext_filter_slow(http): @@ -29,6 +37,7 @@ def test_ext_filter_slow(http): assert t_cmp(content, "foobar"), "slow filter process" +@_skip_win32 @need_module("ext_filter") @need_cgi() def test_ext_filter_input(http): diff --git a/test/pytest_suite/tests/t/modules/test_include.py b/test/pytest_suite/tests/t/modules/test_include.py index 82a4c0589b..1b849c8bf5 100644 --- a/test/pytest_suite/tests/t/modules/test_include.py +++ b/test/pytest_suite/tests/t/modules/test_include.py @@ -13,6 +13,7 @@ Perl original: plan tests => ..., need 'DateTime', need_lwp, need_module 'includ import os import re import stat +import sys import pytest @@ -149,6 +150,8 @@ def test_include_pages(http): for doc in sorted(tests): expected = tests[doc] + if sys.platform == "win32" and doc.startswith("exec/on/cmd"): + continue if isinstance(expected, tuple): body, host = expected got = super_chomp(http.GET_BODY(f"{DIR}{doc}", headers={"Host": host})) @@ -234,6 +237,7 @@ def _check_xbithack_etag(resp): @need_module("include") +@pytest.mark.skipif(sys.platform == "win32", reason="XBitHack relies on Unix file permission bits") def test_include_xbithack(http): http.scheme("http") http.module("mod_include") diff --git a/test/pytest_suite/tests/t/modules/test_negotiation.py b/test/pytest_suite/tests/t/modules/test_negotiation.py index c8dc4cbc03..b56b621808 100644 --- a/test/pytest_suite/tests/t/modules/test_negotiation.py +++ b/test/pytest_suite/tests/t/modules/test_negotiation.py @@ -137,7 +137,7 @@ def test_quality_preferences(http): @need_cgi() def test_query_typemap(http): actual = _chomp(http.GET_BODY("/modules/negotiation/query/test?foo")) - assert t_cmp(actual, "QUERY_STRING --> foo"), \ + assert t_cmp(actual.replace("\r", ""), "QUERY_STRING --> foo"), \ "The type map gives the script the highest quality; query string included" diff --git a/test/pytest_suite/tests/t/modules/test_proxy.py b/test/pytest_suite/tests/t/modules/test_proxy.py index 895cff10a1..b06cced580 100644 --- a/test/pytest_suite/tests/t/modules/test_proxy.py +++ b/test/pytest_suite/tests/t/modules/test_proxy.py @@ -13,6 +13,7 @@ Perl original: plan tests => 46, need need_module 'proxy', need_module 'setenvif import os import re import socket +import sys import threading import time @@ -87,7 +88,7 @@ def test_proxy_cgi(http): r = http.GET("/reverse/modules/cgi/env.pl?reverse-proxy") assert t_cmp(r.status_code, 200), "reverse proxy with query string" - assert t_cmp(r.text, re.compile(r"QUERY_STRING = reverse-proxy\n", re.S)), \ + assert t_cmp(r.text, re.compile(r"QUERY_STRING = reverse-proxy\r?\n", re.S)), \ "reverse proxied query string OK" r = http.GET("/reverse/modules/cgi/nph-dripfeed.pl") @@ -187,6 +188,7 @@ def test_proxy_redirect_rewrite(http): @need_module("proxy", "setenvif") +@pytest.mark.skipif(sys.platform == "win32", reason="AF_UNIX not available on Windows") def test_proxy_uds(http): if not http.have_min_apache_version("2.4.7"): pytest.skip("UDS requires httpd >= 2.4.7") diff --git a/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py b/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py index c445e0c769..146a8ad443 100644 --- a/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py +++ b/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py @@ -22,6 +22,7 @@ import os import re import socket import struct +import sys import threading import pytest @@ -181,6 +182,8 @@ def _run_echo_request(http, address, uri): return r, envs +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_setenvif(http): if not http.have_min_apache_version("2.4.26"): @@ -203,6 +206,8 @@ def test_fcgi_setenvif(http): assert t_cmp(envs.get("REMOTE_ADDR"), None), "ProxyFCGISetEnvIf can unset var" +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_generic(http): if not http.have_min_apache_version("2.4.26"): @@ -217,6 +222,8 @@ def test_fcgi_generic(http): "GENERIC SCRIPT_FILENAME has neither query string nor proxy: prefix" +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_generic_rewrite(http): if not (http.have_min_apache_version("2.4.26") and http.have_module("rewrite")): @@ -232,6 +239,8 @@ def test_fcgi_generic_rewrite(http): "GENERIC SCRIPT_FILENAME (rewrite) is correct" +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_rewrite_path_info(http): if not http.have_module("rewrite"): @@ -258,6 +267,8 @@ def test_fcgi_rewrite_path_info(http): "Default REDIRECT_URL uses original client URL" +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_action(http): if not http.have_module("actions"): @@ -286,6 +297,8 @@ def test_fcgi_action(http): "Action REDIRECT_URL uses original client URL" +@pytest.mark.skipif(sys.platform == "win32", + reason="mod_proxy_fcgi misparses drive-letter paths as port") @need_module("proxy_fcgi") def test_fcgi_default(http): http.module("proxy_fcgi") @@ -297,6 +310,7 @@ def test_fcgi_default(http): @need_module("proxy_fcgi") +@pytest.mark.skipif(sys.platform == "win32", reason="AF_UNIX not available on Windows") @pytest.mark.parametrize("url", [ "/modules/proxy/fcgi-uds/index.php", "/modules/proxy/fcgi-uds-sethandler/index.php", diff --git a/test/pytest_suite/tests/t/modules/test_ratelimit.py b/test/pytest_suite/tests/t/modules/test_ratelimit.py index bcee559afe..737d5f4927 100644 --- a/test/pytest_suite/tests/t/modules/test_ratelimit.py +++ b/test/pytest_suite/tests/t/modules/test_ratelimit.py @@ -17,15 +17,17 @@ import pytest from apache_pytest import need_min_apache_version, need_module, t_cmp CASES = [ - ("/apache/ratelimit/", 200, "ratelimited small file"), - ("/apache/ratelimit/autoindex/", 200, "ratelimited small autoindex output"), - ("/apache/ratelimit/chunk?0,8192", 200, "ratelimited chunked response"), + ("/apache/ratelimit/", 200, "ratelimited small file", False), + ("/apache/ratelimit/autoindex/", 200, "ratelimited small autoindex output", False), + ("/apache/ratelimit/chunk?0,8192", 200, "ratelimited chunked response", True), ] @need_module("ratelimit", "autoindex") @need_min_apache_version("2.4.35") -@pytest.mark.parametrize("url,code,desc", CASES, ids=[c[2] for c in CASES]) -def test_ratelimit(http, url, code, desc): +@pytest.mark.parametrize("url,code,desc,needs_cmod", CASES, ids=[c[2] for c in CASES]) +def test_ratelimit(http, url, code, desc, needs_cmod): + if needs_cmod and not http.have_module("random_chunk"): + pytest.skip("random_chunk C test module not available") r = http.GET(url) assert t_cmp(r.status_code, code), desc diff --git a/test/pytest_suite/tests/t/modules/test_rewrite.py b/test/pytest_suite/tests/t/modules/test_rewrite.py index 438ae421bd..a538db861d 100644 --- a/test/pytest_suite/tests/t/modules/test_rewrite.py +++ b/test/pytest_suite/tests/t/modules/test_rewrite.py @@ -10,6 +10,7 @@ Perl original: plan tests => ..., todo => \@todo, need_module 'rewrite'. """ import re +import sys import pytest @@ -53,8 +54,8 @@ def test_rewrite_special_accepts(http): @need_module("rewrite") def test_rewrite_qsa(http): - r = http.GET_BODY("/modules/rewrite/qsa.html?baz=bee").rstrip("\n") - assert t_cmp(r, re.compile(r"\nQUERY_STRING = foo=bar&baz=bee\n", re.S)), \ + r = http.GET_BODY("/modules/rewrite/qsa.html?baz=bee").rstrip("\r\n") + assert t_cmp(r, re.compile(r"\r?\nQUERY_STRING = foo=bar&baz=bee\r?\n", re.S)), \ "query-string append test" @@ -88,24 +89,24 @@ def test_rewrite_to_proxy(http): def test_rewrite_proxy_query_string(http): if not (_have_proxy(http) and _have_cgi(http)): pytest.skip("missing proxy or CGI module") - r = http.GET_BODY("/modules/rewrite/proxy2/env.pl?fish=fowl").rstrip("\n") - assert t_cmp(r, re.compile(r"QUERY_STRING = fish=fowl\n", re.S)), \ + r = http.GET_BODY("/modules/rewrite/proxy2/env.pl?fish=fowl").rstrip("\r\n") + assert t_cmp(r, re.compile(r"QUERY_STRING = fish=fowl\r?\n", re.S)), \ "QUERY_STRING passed OK" assert t_cmp(http.GET_RC("/modules/rewrite/proxy3/env.pl?horse=norman"), 404), \ "RewriteCond QUERY_STRING test" - r = http.GET_BODY("/modules/rewrite/proxy3/env.pl?horse=trigger").rstrip("\n") - assert t_cmp(r, re.compile(r"QUERY_STRING = horse=trigger\n", re.S)), \ + r = http.GET_BODY("/modules/rewrite/proxy3/env.pl?horse=trigger").rstrip("\r\n") + assert t_cmp(r, re.compile(r"QUERY_STRING = horse=trigger\r?\n", re.S)), \ "QUERY_STRING passed OK" r = http.GET("/modules/rewrite/proxy-qsa.html?bloo=blar") assert t_cmp(r.status_code, 200), "proxy/QSA test success" - assert t_cmp(r.text, re.compile(r"QUERY_STRING = foo=bar&bloo=blar\n", re.S)), \ + assert t_cmp(r.text, re.compile(r"QUERY_STRING = foo=bar&bloo=blar\r?\n", re.S)), \ "proxy/QSA test appended args correctly" -@need_module("rewrite") +@need_module("rewrite", "test_utilities") def test_rewrite_pr60478(http): if not http.have_min_apache_version("2.4"): pytest.skip("PR 60478 requires ap_expr in version 2.4") @@ -301,6 +302,8 @@ def _prefixstats(http): @need_module("rewrite") +@pytest.mark.skipif(sys.platform == "win32", + reason="Windows drive-letter colons in paths cause 400 Bad Request") def test_rewrite_prefixstat(http): # Uses the rewrite_prefix_stat vhost (larger LimitRequestLine). http.module("rewrite_prefix_stat") diff --git a/test/pytest_suite/tests/t/modules/test_sed.py b/test/pytest_suite/tests/t/modules/test_sed.py index 5338161bde..8fa06bfb45 100644 --- a/test/pytest_suite/tests/t/modules/test_sed.py +++ b/test/pytest_suite/tests/t/modules/test_sed.py @@ -28,7 +28,7 @@ CASES = [ ] -@need_module("sed") +@need_module("sed", "echo_post") @pytest.mark.parametrize("case", CASES, ids=[c["url"] for c in CASES]) def test_sed(http, case): if case["body"] is not None: diff --git a/test/pytest_suite/tests/t/modules/test_session.py b/test/pytest_suite/tests/t/modules/test_session.py index 7b10d30b1a..c178b9bf9e 100644 --- a/test/pytest_suite/tests/t/modules/test_session.py +++ b/test/pytest_suite/tests/t/modules/test_session.py @@ -105,7 +105,7 @@ CREATE_SESSION = "action=set&name=test&value=value" READ_SESSION = "action=get&name=test" -@need_module("session") +@need_module("session", "test_session") @need_min_apache_version("2.3.0") def test_session(http): # Session directive diff --git a/test/pytest_suite/tests/t/modules/test_speling.py b/test/pytest_suite/tests/t/modules/test_speling.py index d459fc75ee..ce39ce1691 100644 --- a/test/pytest_suite/tests/t/modules/test_speling.py +++ b/test/pytest_suite/tests/t/modules/test_speling.py @@ -32,8 +32,8 @@ TESTCASES = [ ("several0.html", "multiple choice", 300, 404), ] -# macOS HFS is case-insensitive but case-preserving, so this would mislead. -if sys.platform != "darwin": +# macOS HFS and Windows NTFS are case-insensitive, so this would mislead. +if sys.platform not in ("darwin", "win32"): TESTCASES.append(("GOOD.html", "case", 301, 301)) # (path-prefix, index into the case tuple for the expected status) diff --git a/test/pytest_suite/tests/t/modules/test_vhost_alias.py b/test/pytest_suite/tests/t/modules/test_vhost_alias.py index 50d839fde0..4ecf28e6ac 100644 --- a/test/pytest_suite/tests/t/modules/test_vhost_alias.py +++ b/test/pytest_suite/tests/t/modules/test_vhost_alias.py @@ -11,6 +11,7 @@ selected the mod_vhost_alias vhost port; SSL is not listening on this vhost. import os import stat +import sys import pytest @@ -83,6 +84,7 @@ def _setup(root): @need_module("vhost_alias") @need_cgi() +@pytest.mark.skipif(sys.platform == "win32", reason="uses shell CGI scripts") @pytest.mark.parametrize("vh", VHOSTS) def test_vhost_alias(http, vh): root = os.path.join(http.vars("documentroot"), "modules", "vhost_alias") diff --git a/test/pytest_suite/tests/t/ssl/test_pr43738.py b/test/pytest_suite/tests/t/ssl/test_pr43738.py index 1cc4e0d8cb..61ab134abb 100644 --- a/test/pytest_suite/tests/t/ssl/test_pr43738.py +++ b/test/pytest_suite/tests/t/ssl/test_pr43738.py @@ -22,4 +22,5 @@ def test_pr43738(http): for path in ("/modules/ssl/aes128/empty.pfa", "/modules/ssl/aes256/empty.pfa"): r = http.POST(path, content="hello world") assert t_cmp(r.status_code, 200), "renegotiation on POST works" - assert t_cmp(r.text, f"{path}\nhello world"), "request body matches response" + assert t_cmp(r.text.replace("\r\n", "\n"), f"{path}\nhello world"), \ + "request body matches response" diff --git a/test/pytest_suite/tests/test_framework_smoke.py b/test/pytest_suite/tests/test_framework_smoke.py index 2488a4d3e4..d9b0990b0a 100644 --- a/test/pytest_suite/tests/test_framework_smoke.py +++ b/test/pytest_suite/tests/test_framework_smoke.py @@ -39,8 +39,8 @@ def test_cmodule_compiled_and_loaded(config) -> None: config.vars["t_conf_file"] and open(config.vars["t_conf_file"]).read() # noqa: SIM115 ) - assert "LoadModule echo_post_module" in conf_text - # echo_post.c registers the echo_post handler; the module is now in scope. + if "LoadModule echo_post_module" not in conf_text: + pytest.skip("C test modules not compiled (no --apxs)") assert config.info.has_module("mod_echo_post") or "echo_post" in conf_text