]> git.ipfire.org Git - thirdparty/apache/httpd.git/commitdiff
Arrange the testsuite to run it on windows.
authorJean-Frederic Clere <jfclere@apache.org>
Wed, 12 Aug 2026 15:41:41 +0000 (15:41 +0000)
committerJean-Frederic Clere <jfclere@apache.org>
Wed, 12 Aug 2026 15:41:41 +0000 (15:41 +0000)
used claude ai for the investigation.

git-svn-id: https://svn.apache.org/repos/asf/httpd/httpd/trunk@1937082 13f79535-47bb-0310-9956-ffa450edef68

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

index 3e362be077f2ab091c4b3db4431d5374ca986caa..a0639fcfa329764b6fc202ad0b601e1ff8083c2a 100644 (file)
@@ -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
index fbceb3dceff058924a42d352c9b13ec0547e4f15..ccae4322ed59ada6c5004a9d4f368ef07a9847e5 100644 (file)
@@ -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("\\", "/")
index cd111231e3d57840507210e50bfc6adf8777542e..ac350877d4a229143ff7fc97b429725d38084a1b 100644 (file)
@@ -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:
index 9fbaa80efe5518f2d280ac8160df257b9acf2d5a..8d3fd2111c6f6875f621f318d88338d11b349c81 100644 (file)
@@ -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
 
index b327cccfca9b55a03221b52588460a52d765a8e2..745db3ba2807e8249e75d500591cc2e8768c6078 100644 (file)
     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>
    </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
       RewriteRule X - [N]
    </Location>
    </IfVersion>
+   </IfModule>
 
 </IfModule>
 
index 67d7e9f04e9d7e72313b8c4269d8b7bba2e975d2..41dafce5b1a22a89cfc95f48674734b7926813a6 100644 (file)
@@ -1,5 +1,6 @@
 # produces output with folded response headers
 
+binmode(STDOUT);
 print "HTTP/1.0 200 OK\r\n";
 
 for (1..50) {
index a55390c85a801a4dfba9633d68a549a502d811ef..a4a07ee06ae4f29800295bf25f9ed3b13f397263 100644 (file)
@@ -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
index c8f23ca09e7cc74b366e09df1e65ef2d8585bcf5..7b30be2683ea037ca55236457420b8c9539425bc 100644 (file)
@@ -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"
index ba2bf73378757a9c62e9fc0bac648a7612fb0bd3..eb8aea40412a74e3c06c920d9989682d50ee0f36 100644 (file)
@@ -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"
     )
index 5d1a665150cb4921096dae4b86a5df5cc49b5135..f595f1c9c2864497d96d156787c4f34fb9815e87 100644 (file)
@@ -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")
index 649e7b0c4877cd5c15a8add9b1e40650b62ab74b..79f0d773b3387aa8408ab258cbf62f18d4769789 100644 (file)
@@ -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")
index f6e8d4425e37e2acc737ade0b714f27297a69859..3ff21e89fa728799f67befda76cc77d256ab1fc7 100644 (file)
@@ -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"
index 8954dd2ebea7fa1c2088d1f798b7912e790387f3..28604dd39b7b926402fb4fccfdf234e3bda5d48f 100644 (file)
@@ -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):
index 652aab267d92a0098fa0a94720a1de775bf5550e..8ea3de5729df67ea13dad23e8a373a1574c4c6b2 100644 (file)
@@ -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):
index 82a4c0589bc15e841e0d69fe715f4282ae84dd36..1b849c8bf548c963c4ddd1e846aef6bce43b0c5b 100644 (file)
@@ -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")
index c8dc4cbc03380686ecab007a16ec7b24ed64fbe2..b56b6218083c63de69bac87398837c985bf894e3 100644 (file)
@@ -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"
 
 
index 895cff10a10c22a734939ecce9d8d0d293d499ec..b06cced58001a46140fa960733865dde1bed6878 100644 (file)
@@ -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")
index c445e0c7698b277fc2d00626cff38b8b0a13fd54..146a8ad4436e9af3b5c00d54515a4719ac093b72 100644 (file)
@@ -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",
index bcee559afe704dca58ca2db0e6d3594d639941aa..737d5f49270360483fcb0cd494a372dd25050953 100644 (file)
@@ -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
index 438ae421bd354e2afb4e723241ed9bcc6f10af55..a538db861dc42d5e2b7bb2254554ef36a07b3727 100644 (file)
@@ -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")
index 5338161bde023db92456c02a4c36c8103daad6ce..8fa06bfb456a73d2c4f8813024bfa73bad7d0684 100644 (file)
@@ -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:
index 7b10d30b1a3f14a306d974242c4c3786b75a0ab8..c178b9bf9efaa6170502137bb2c6e16986bb5adb 100644 (file)
@@ -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
index d459fc75ee6e9ba1d1fa84143d2f512614d76e5f..ce39ce16915c27bcc9ebae07f001f46725c06cba 100644 (file)
@@ -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)
index 50d839fde0a2766b1f5f9912e7a43c34be3146aa..4ecf28e6ac637e3217c8c75e002e62e66f8e9562 100644 (file)
@@ -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")
index 1cc4e0d8cbc6312f4637f005fdc3fab4a2f3a37f..61ab134abb5ce701e3539e29a6fec60caf22416e 100644 (file)
@@ -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"
index 2488a4d3e4025de018a318e1f1d925b9956dfbde..d9b0990b0a2c74ce60c6b7c754e09da9e8b24edc 100644 (file)
@@ -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