svn commit: r1937082 - in httpd/httpd/trunk/test/pytest_suite: . apache_pytest t/conf t/htdocs/modules/cgi tests tests/t/apache tests/t/modules tests/t/ssl

[email protected]
Newsgroups gmane.comp.apache.cvs
Message-ID <178654930240.1263737.14557115731479832820@svn03-he-fi>
Author: jfclere
Date: Wed Aug 12 15:41:41 2026
New Revision: 1937082

Log:
Arrange the testsuite to run it on windows.
used claude ai for the investigation.

Modified:
   httpd/httpd/trunk/test/pytest_suite/apache_pytest/config.py
   httpd/httpd/trunk/test/pytest_suite/apache_pytest/scripts.py
   httpd/httpd/trunk/test/pytest_suite/apache_pytest/server.py
   httpd/httpd/trunk/test/pytest_suite/conftest.py
   httpd/httpd/trunk/test/pytest_suite/t/conf/extra.conf.in
   httpd/httpd/trunk/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL
   httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_byterange2.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr37166.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr49328.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_actions.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_alias.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_cgi.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ext_filter.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_include.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_negotiation.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ratelimit.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_rewrite.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_sed.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_session.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_speling.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_vhost_alias.py
   httpd/httpd/trunk/test/pytest_suite/tests/t/ssl/test_pr43738.py
   httpd/httpd/trunk/test/pytest_suite/tests/test_framework_smoke.py

Modified: httpd/httpd/trunk/test/pytest_suite/apache_pytest/config.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/apache_pytest/config.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/apache_pytest/config.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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'<IfModule mod_mime.c>\n    TypesConfig "{mime}"\n</IfModule>'
             )
@@ -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'<IfModule !mod_{name}.c>\n'
-            f'    LoadModule {name}_module "{so}"\n'
+            f'    LoadModule {name}_module "{so_fwd}"\n'
             f'</IfModule>'
         )
 
@@ -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"<IfModule !{d.cname}>\n"
-                f'    LoadModule {d.symbol} "{d.so}"\n'
+                f'    LoadModule {d.symbol} "{so}"\n'
                 f"</IfModule>"
             )
 
@@ -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 <VirtualHost mod_X>
             # 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(
+                "<IfModule mod_cgi.c>\n"
+                "    ScriptInterpreterSource Script\n"
+                "</IfModule>"
+            )
+
         # 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

Modified: httpd/httpd/trunk/test/pytest_suite/apache_pytest/scripts.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/apache_pytest/scripts.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/apache_pytest/scripts.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -49,7 +49,7 @@ def generate_pl_scripts(root: Path, *, p
             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("\\", "/")

Modified: httpd/httpd/trunk/test/pytest_suite/apache_pytest/server.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/apache_pytest/server.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/apache_pytest/server.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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) -
     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:

Modified: httpd/httpd/trunk/test/pytest_suite/conftest.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/conftest.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/conftest.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -65,6 +65,20 @@ def pytest_addoption(parser: pytest.Pars
         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)
     # 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
-
-    cmods, _skipped = discover(REPO_ROOT / "c-modules", info)
-    for mod in cmods:
-        info.modules.add(f"mod_{mod.name}.c")
+    # 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")
     _probe_cache = info
     return _probe_cache
 

Modified: httpd/httpd/trunk/test/pytest_suite/t/conf/extra.conf.in
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/t/conf/extra.conf.in	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/t/conf/extra.conf.in	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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
 
     <Directory @SERVERROOT@/htdocs/modules/rewrite>
@@ -358,6 +358,7 @@
    </VirtualHost>
 
    # PR60478: pathological rewrite expansion
+   <IfModule mod_test_utilities.c>
    <IfVersion >= 2.4>
    <Location /modules/rewrite/pr60478-rewrite-loop>
       # This pair of RewriteRules will loop but should eventually 500 once we
@@ -368,6 +369,7 @@
       RewriteRule X - [N]
    </Location>
    </IfVersion>
+   </IfModule>
 
 </IfModule>
 

Modified: httpd/httpd/trunk/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/t/htdocs/modules/cgi/nph-foldhdr.pl.PL	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -1,5 +1,6 @@
 # produces output with folded response headers
 
+binmode(STDOUT);
 print "HTTP/1.0 200 OK\r\n";
 
 for (1..50) {

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_acceptpathinfo.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -8,6 +8,7 @@ Perl original needed: need_apache(2), mo
 """
 
 import re
+import sys
 
 import pytest
 
@@ -58,6 +59,7 @@ def _cases(http):
 
 @need_module("include")
 @need_lwp()
[email protected](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

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_byterange2.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_byterange2.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_byterange2.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -13,4 +13,4 @@ from apache_pytest import need_cgi, need
 @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"

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr37166.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr37166.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr37166.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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"
     )

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr49328.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr49328.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/apache/test_pr49328.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_actions.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_actions.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_actions.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -4,6 +4,8 @@ Two groups: ``tests_action`` (GET each u
 ``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")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_alias.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_alias.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_alias.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -13,6 +13,7 @@ WINFU (Windows) branches are not reprodu
 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"

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_cgi.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_cgi.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_cgi.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -11,6 +11,7 @@ Perl original: plan tests => ..., \&need
 
 import os
 import re
+import sys
 
 import pytest
 
@@ -47,6 +48,7 @@ def _cgi_log(http):
 
 
 @need_cgi()
[email protected](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):

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ext_filter.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ext_filter.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ext_filter.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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):

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_include.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_include.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_include.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -13,6 +13,7 @@ Perl original: plan tests => ..., need '
 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")
[email protected](sys.platform == "win32", reason="XBitHack relies on Unix file permission bits")
 def test_include_xbithack(http):
     http.scheme("http")
     http.module("mod_include")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_negotiation.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_negotiation.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_negotiation.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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"
 
 

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -13,6 +13,7 @@ Perl original: plan tests => 46, need ne
 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")
[email protected](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")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_proxy_fcgi.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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
 
 
[email protected](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"
 
 
[email protected](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"
 
 
[email protected](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"
 
 
[email protected](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"
 
 
[email protected](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"
 
 
[email protected](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")
[email protected](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",

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ratelimit.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ratelimit.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_ratelimit.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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")
[email protected]("url,code,desc", CASES, ids=[c[2] for c in CASES])
-def test_ratelimit(http, url, code, desc):
[email protected]("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

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_rewrite.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_rewrite.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_rewrite.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -10,6 +10,7 @@ Perl original: plan tests => ..., todo =
 """
 
 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")
[email protected](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")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_sed.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_sed.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_sed.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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:

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_session.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_session.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_session.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -105,7 +105,7 @@ CREATE_SESSION = "action=set&name=test&v
 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

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_speling.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_speling.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_speling.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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)

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_vhost_alias.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_vhost_alias.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/modules/test_vhost_alias.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -11,6 +11,7 @@ selected the mod_vhost_alias vhost port;
 
 import os
 import stat
+import sys
 
 import pytest
 
@@ -83,6 +84,7 @@ def _setup(root):
 
 @need_module("vhost_alias")
 @need_cgi()
[email protected](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")

Modified: httpd/httpd/trunk/test/pytest_suite/tests/t/ssl/test_pr43738.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/t/ssl/test_pr43738.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/t/ssl/test_pr43738.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -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"

Modified: httpd/httpd/trunk/test/pytest_suite/tests/test_framework_smoke.py
==============================================================================
--- httpd/httpd/trunk/test/pytest_suite/tests/test_framework_smoke.py	Wed Aug 12 12:45:04 2026	(r1937081)
+++ httpd/httpd/trunk/test/pytest_suite/tests/test_framework_smoke.py	Wed Aug 12 15:41:41 2026	(r1937082)
@@ -39,8 +39,8 @@ def test_cmodule_compiled_and_loaded(con
         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
lmpx.com only provides a reader for public news (NNTP) servers. It is not affiliated with the servers or forums shown here and is not responsible for the content of articles, which is written by their respective authors.