You are viewing a plain text version of this content. The canonical link for it is here.
Posted to cvs@httpd.apache.org by ic...@apache.org on 2023/01/09 07:35:19 UTC

svn commit: r1906475 [11/11] - in /httpd/httpd/branches/2.4.x: ./ changes-entries/ modules/http2/ test/ test/modules/http2/ test/modules/http2/htdocs/cgi/ test/modules/http2/mod_h2test/ test/pyhttpd/ test/pyhttpd/mod_aptest/

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_201_header_conditional.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_201_header_conditional.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_201_header_conditional.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_201_header_conditional.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestConditionalHeaders:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_202_trailer.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_202_trailer.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_202_trailer.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_202_trailer.py Mon Jan  9 07:35:18 2023
@@ -86,7 +86,7 @@ class TestTrailers:
         url = env.mkurl("https", "cgi", "/h2test/trailer?0")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200
-        assert len(r.response["body"]) == 0
+        assert len(r.response["body"]) == 0, f'{r.response["body"]}'
         assert 'trailer' in r.response
         assert 'trailer-content-length' in r.response['trailer']
         assert r.response['trailer']['trailer-content-length'] == '0'

Added: httpd/httpd/branches/2.4.x/test/modules/http2/test_203_rfc9113.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_203_rfc9113.py?rev=1906475&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_203_rfc9113.py (added)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_203_rfc9113.py Mon Jan  9 07:35:18 2023
@@ -0,0 +1,56 @@
+import pytest
+
+from pyhttpd.env import HttpdTestEnv
+from .env import H2Conf
+
+
+class TestRfc9113:
+
+    @pytest.fixture(autouse=True, scope='class')
+    def _class_scope(self, env):
+        H2Conf(env).add_vhost_test1().install()
+        assert env.apache_restart() == 0
+
+    # by default, we accept leading/trailing ws in request fields
+    def test_h2_203_01_ws_ignore(self, env):
+        url = env.mkurl("https", "test1", "/")
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not  '])
+        assert r.exit_code == 0, f'curl output: {r.stderr}'
+        assert r.response["status"] == 200, f'curl output: {r.stdout}'
+        r = env.curl_get(url, options=['-H', 'trailing-space: must not\t'])
+        assert r.exit_code == 0, f'curl output: {r.stderr}'
+        assert r.response["status"] == 200, f'curl output: {r.stdout}'
+
+    # response header are also handled, but we strip ws before sending
+    @pytest.mark.parametrize(["hvalue", "expvalue", "status"], [
+        ['"123"', '123', 200],
+        ['"123 "', '123', 200],       # trailing space stripped
+        ['"123\t"', '123', 200],     # trailing tab stripped
+        ['" 123"', '123', 200],        # leading space is stripped
+        ['"          123"', '123', 200],  # leading spaces are stripped
+        ['"\t123"', '123', 200],       # leading tab is stripped
+        ['"expr=%{unescape:123%0A 123}"', '', 500],  # illegal char
+        ['" \t "', '', 200],          # just ws
+    ])
+    def test_h2_203_02(self, env, hvalue, expvalue, status):
+        hname = 'ap-test-007'
+        conf = H2Conf(env, extras={
+            f'test1.{env.http_tld}': [
+                '<Location /index.html>',
+                f'Header add {hname} {hvalue}',
+                '</Location>',
+            ]
+        })
+        conf.add_vhost_test1(proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "test1", "/index.html")
+        r = env.curl_get(url, options=['--http2'])
+        if status == 500 and r.exit_code != 0:
+            # in 2.4.x we fail late on control chars in a response
+            # and RST_STREAM. That's also ok
+            return
+        assert r.response["status"] == status
+        if int(status) < 400:
+            assert r.response["header"][hname] == expvalue
+

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_300_interim.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_300_interim.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_300_interim.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_300_interim.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestInterimResponses:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_400_push.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_400_push.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_400_push.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_400_push.py Mon Jan  9 07:35:18 2023
@@ -1,10 +1,11 @@
 import os
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
 # The push tests depend on "nghttp"
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestPush:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_401_early_hints.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_401_early_hints.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_401_early_hints.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_401_early_hints.py Mon Jan  9 07:35:18 2023
@@ -1,9 +1,10 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
 # The push tests depend on "nghttp"
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestEarlyHints:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -25,7 +26,7 @@ class TestEarlyHints:
         assert env.apache_restart() == 0
 
     # H2EarlyHints enabled in general, check that it works for H2PushResource
-    def test_h2_401_31(self, env):
+    def test_h2_401_31(self, env, repeat):
         url = env.mkurl("https", "hints", "/006-hints.html")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200
@@ -37,7 +38,7 @@ class TestEarlyHints:
         assert early["header"]["link"]
 
     # H2EarlyHints enabled in general, but does not trigger on added response headers
-    def test_h2_401_32(self, env):
+    def test_h2_401_32(self, env, repeat):
         url = env.mkurl("https", "hints", "/006-nohints.html")
         r = env.nghttp().get(url)
         assert r.response["status"] == 200

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_500_proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_500_proxy.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_500_proxy.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_500_proxy.py Mon Jan  9 07:35:18 2023
@@ -3,9 +3,10 @@ import os
 import re
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestProxy:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -65,7 +66,7 @@ class TestProxy:
             src = file.read()
         if r.response["body"] != src:
             with open(os.path.join(env.gen_dir, "nghttp.out"), 'w') as fd:
-                fd.write(r.outraw)
+                fd.write(r.outraw.decode())
                 fd.write("\nstderr:\n")
                 fd.write(r.stderr)
             assert r.response["body"] == src
@@ -125,3 +126,26 @@ class TestProxy:
     def test_h2_500_24(self, env):
         for i in range(100):
             self.nghttp_upload_stat(env, "data-1k", ["--no-content-length"])
+
+    # lets do some error tests
+    def test_h2_500_30(self, env):
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_500_31(self, env, repeat):
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, r
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_500_32(self, env, repeat):
+        url = env.mkurl("https", "cgi", "/proxy/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        assert r.exit_code != 0, r

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_501_proxy_serverheader.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_501_proxy_serverheader.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_501_proxy_serverheader.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_501_proxy_serverheader.py Mon Jan  9 07:35:18 2023
@@ -1,9 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
-@pytest.mark.skip(reason="only in 2.5.x")
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestProxyServerHeader:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_502_proxy_port.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_502_proxy_port.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_502_proxy_port.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_502_proxy_port.py Mon Jan  9 07:35:18 2023
@@ -1,8 +1,9 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestProxyPort:
 
     @pytest.fixture(autouse=True, scope='class')

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_600_h2proxy.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_600_h2proxy.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_600_h2proxy.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_600_h2proxy.py Mon Jan  9 07:35:18 2023
@@ -1,20 +1,20 @@
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestH2Proxy:
 
-    @pytest.fixture(autouse=True, scope='class')
-    def _class_scope(self, env):
-        conf = H2Conf(env)
+    def test_h2_600_01(self, env):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                "SetEnvIf Host (.+) X_HOST=$1",
+            ]
+        })
         conf.add_vhost_cgi(h2proxy_self=True)
-        if env.verbosity > 1:
-            conf.add("LogLevel proxy:trace2 proxy_http2:trace2")
         conf.install()
         assert env.apache_restart() == 0
-
-    def test_h2_600_01(self, env):
         url = env.mkurl("https", "cgi", "/h2proxy/hello.py")
         r = env.curl_get(url, 5)
         assert r.response["status"] == 200
@@ -23,4 +23,153 @@ class TestH2Proxy:
         assert r.response["json"]["ssl_protocol"] != ""
         assert r.response["json"]["h2"] == "on"
         assert r.response["json"]["h2push"] == "off"
-        assert r.response["json"]["host"] == f"cgi.{env.http_tld}"
+        assert r.response["json"]["host"] == f"cgi.{env.http_tld}:{env.https_port}"
+
+    def test_h2_600_02(self, env):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                "SetEnvIf Host (.+) X_HOST=$1",
+                f"ProxyPreserveHost on",
+                f"ProxyPass /h2c/ h2c://127.0.0.1:{env.http_port}/",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2c/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert r.response["json"]["protocol"] == "HTTP/2.0"
+        assert r.response["json"]["https"] == ""
+        # the proxied backend sees Host header as passed on front
+        assert r.response["json"]["host"] == f"cgi.{env.http_tld}:{env.https_port}"
+        assert r.response["json"]["h2_original_host"] == ""
+
+    def test_h2_600_03(self, env):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                "SetEnvIf Host (.+) X_HOST=$1",
+                f"ProxyPreserveHost off",
+                f"ProxyPass /h2c/ h2c://127.0.0.1:{env.http_port}/",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2c/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert r.response["json"]["protocol"] == "HTTP/2.0"
+        assert r.response["json"]["https"] == ""
+        # the proxied backend sees Host as using in connecting to it
+        assert r.response["json"]["host"] == f"127.0.0.1:{env.http_port}"
+        assert r.response["json"]["h2_original_host"] == ""
+
+    # check that connection reuse actually happens as configured
+    @pytest.mark.parametrize("enable_reuse", [ "on", "off" ])
+    def test_h2_600_04(self, env, enable_reuse):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ "
+                f"  h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert r.json["h2_stream_id"] == "1"
+        # httpd 2.5.0 disables reuse, not matter the config
+        if enable_reuse == "on" and not env.httpd_is_at_least("2.5.0"):
+            # reuse is not guarantueed for each request, but we expect some
+            # to do it and run on a h2 stream id > 1
+            reused = False
+            for _ in range(10):
+                r = env.curl_get(url, 5)
+                assert r.response["status"] == 200
+                if int(r.json["h2_stream_id"]) > 1:
+                    reused = True
+                    break
+            assert reused
+        else:
+            r = env.curl_get(url, 5)
+            assert r.response["status"] == 200
+            assert r.json["h2_stream_id"] == "1"
+
+    # do some flexible setup from #235 to proper connection selection
+    @pytest.mark.parametrize("enable_reuse", [ "on", "off" ])
+    def test_h2_600_05(self, env, enable_reuse):
+        conf = H2Conf(env, extras={
+            f'cgi.{env.http_tld}': [
+                f"ProxyPassMatch ^/h2proxy/([0-9]+)/(.*)$ "
+                f"  h2c://127.0.0.1:$1/$2 enablereuse={enable_reuse} keepalive=on",
+            ]
+        })
+        conf.add_vhost_cgi()
+        conf.add([
+            f'Listen {env.http_port2}',
+            'UseCanonicalName On',
+            'UseCanonicalPhysicalPort On'
+        ])
+        conf.start_vhost(domains=[f'cgi.{env.http_tld}'],
+                         port=5004, doc_root="htdocs/cgi")
+        conf.add("AddHandler cgi-script .py")
+        conf.end_vhost()
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        assert int(r.json["port"]) == env.http_port
+        # going to another backend port must create a new connection and
+        # we should see stream id one again
+        url = env.mkurl("https", "cgi", f"/h2proxy/{env.http_port2}/hello.py")
+        r = env.curl_get(url, 5)
+        assert r.response["status"] == 200
+        exp_port = env.http_port if enable_reuse == "on" \
+                                    and not env.httpd_is_at_least("2.5.0")\
+            else env.http_port2
+        assert int(r.json["port"]) == exp_port
+
+    # lets do some error tests
+    def test_h2_600_30(self, env):
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?status=500")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 500
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?error=timeout")
+        r = env.curl_get(url)
+        assert r.exit_code == 0, r
+        assert r.response['status'] == 408
+
+    # produce an error during response body
+    def test_h2_600_31(self, env, repeat):
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout")
+        r = env.curl_get(url)
+        # depending on when the error is detect in proxying, if may RST the
+        # stream (exit_code != 0) or give a 503 response.
+        if r.exit_code == 0:
+            assert r.response['status'] == 503
+
+    # produce an error, fail to generate an error bucket
+    def test_h2_600_32(self, env, repeat):
+        conf = H2Conf(env)
+        conf.add_vhost_cgi(h2proxy_self=True)
+        conf.install()
+        assert env.apache_restart() == 0
+        url = env.mkurl("https", "cgi", "/h2proxy/h2test/error?body_error=timeout&error_bucket=0")
+        r = env.curl_get(url)
+        # depending on when the error is detect in proxying, if may RST the
+        # stream (exit_code != 0) or give a 503 response.
+        if r.exit_code == 0:
+            assert r.response['status'] == 503

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_700_load_get.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_700_load_get.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_700_load_get.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_700_load_get.py Mon Jan  9 07:35:18 2023
@@ -3,6 +3,7 @@ import pytest
 from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 @pytest.mark.skipif(not H2TestEnv().h2load_is_at_least('1.41.0'),
                     reason="h2load misses --connect-to option")
 class TestLoadGet:
@@ -29,6 +30,7 @@ class TestLoadGet:
         1000, 80000
     ])
     def test_h2_700_10(self, env, start):
+        assert env.is_live()
         text = "X"
         chunk = 32
         for n in range(0, 5):
@@ -46,6 +48,7 @@ class TestLoadGet:
         1, 2, 16, 32
     ])
     def test_h2_700_11(self, env, conns):
+        assert env.is_live()
         text = "X"
         start = 1200
         chunk = 64

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_710_load_post_static.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_710_load_post_static.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_710_load_post_static.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_710_load_post_static.py Mon Jan  9 07:35:18 2023
@@ -1,9 +1,10 @@
 import pytest
 import os
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestLoadPostStatic:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -24,7 +25,8 @@ class TestLoadPostStatic:
         assert 0 == r.results["h2load"]["status"]["5xx"]
     
     # test POST on static file, slurped in by server
-    def test_h2_710_00(self, env):
+    def test_h2_710_00(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/index.html")
         n = 10
         m = 1
@@ -36,7 +38,8 @@ class TestLoadPostStatic:
         r = env.run(args)
         self.check_h2load_ok(env, r, n)
 
-    def test_h2_710_01(self, env):
+    def test_h2_710_01(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/index.html")
         n = 1000
         m = 100
@@ -48,7 +51,8 @@ class TestLoadPostStatic:
         r = env.run(args)
         self.check_h2load_ok(env, r, n)
 
-    def test_h2_710_02(self, env):
+    def test_h2_710_02(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/index.html")
         n = 100
         m = 50

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_711_load_post_cgi.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_711_load_post_cgi.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_711_load_post_cgi.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_711_load_post_cgi.py Mon Jan  9 07:35:18 2023
@@ -1,9 +1,10 @@
 import pytest
 import os
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestLoadCgi:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -24,7 +25,8 @@ class TestLoadCgi:
         assert 0 == r.results["h2load"]["status"]["5xx"]
     
     # test POST on cgi, where input is read
-    def test_h2_711_10(self, env):
+    def test_h2_711_10(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/echo.py")
         n = 100
         m = 5
@@ -39,7 +41,8 @@ class TestLoadCgi:
         self.check_h2load_ok(env, r, n)
 
     # test POST on cgi via http/1.1 proxy, where input is read
-    def test_h2_711_11(self, env):
+    def test_h2_711_11(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/proxy/echo.py")
         n = 100
         m = 5
@@ -54,7 +57,8 @@ class TestLoadCgi:
         self.check_h2load_ok(env, r, n)
 
     # test POST on cgi via h2proxy, where input is read
-    def test_h2_711_12(self, env):
+    def test_h2_711_12(self, env, repeat):
+        assert env.is_live()
         url = env.mkurl("https", "test1", "/h2proxy/echo.py")
         n = 100
         m = 5

Modified: httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py (original)
+++ httpd/httpd/branches/2.4.x/test/modules/http2/test_712_buffering.py Mon Jan  9 07:35:18 2023
@@ -2,10 +2,11 @@ from datetime import timedelta
 
 import pytest
 
-from .env import H2Conf
+from .env import H2Conf, H2TestEnv
 from pyhttpd.curl import CurlPiper
 
 
+@pytest.mark.skipif(condition=H2TestEnv.is_unsupported, reason="mod_http2 not supported here")
 class TestBuffering:
 
     @pytest.fixture(autouse=True, scope='class')
@@ -36,7 +37,6 @@ class TestBuffering:
         piper = CurlPiper(env=env, url=url)
         piper.stutter_check(chunks, stutter)
 
-    @pytest.mark.skip(reason="only in 2.5.x")
     def test_h2_712_02(self, env):
         # same as 712_01 but via mod_proxy_http2
         #
@@ -47,7 +47,6 @@ class TestBuffering:
         piper = CurlPiper(env=env, url=url)
         piper.stutter_check(chunks, stutter)
 
-    @pytest.mark.skip(reason="only in 2.5.x")
     def test_h2_712_03(self, env):
         # same as 712_02 but with smaller chunks
         #

Modified: httpd/httpd/branches/2.4.x/test/pyhttpd/conf.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/conf.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/conf.py (original)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/conf.py Mon Jan  9 07:35:18 2023
@@ -157,22 +157,12 @@ class HttpdConf(object):
         self.start_vhost(domains=[domain, f"cgi-alias.{self.env.http_tld}"],
                          port=self.env.https_port, doc_root="htdocs/cgi")
         self.add_proxies("cgi", proxy_self=proxy_self, h2proxy_self=h2proxy_self)
-        self.add("<Location \"/h2test/echo\">")
-        self.add("    SetHandler h2test-echo")
-        self.add("</Location>")
-        self.add("<Location \"/h2test/delay\">")
-        self.add("    SetHandler h2test-delay")
-        self.add("</Location>")
-        if domain in self._extras:
-            self.add(self._extras[domain])
         self.end_vhost()
         self.start_vhost(domains=[domain, f"cgi-alias.{self.env.http_tld}"],
                          port=self.env.http_port, doc_root="htdocs/cgi")
         self.add("AddHandler cgi-script .py")
         self.add_proxies("cgi", proxy_self=proxy_self, h2proxy_self=h2proxy_self)
         self.end_vhost()
-        self.add("LogLevel proxy:info")
-        self.add("LogLevel proxy_http:info")
         return self
 
     @staticmethod

Modified: httpd/httpd/branches/2.4.x/test/pyhttpd/config.ini.in
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/config.ini.in?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/config.ini.in (original)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/config.ini.in Mon Jan  9 07:35:18 2023
@@ -18,13 +18,14 @@ apachectl = @sbindir@/apachectl
 version = @HTTPD_VERSION@
 name = @progname@
 dso_modules = @DSO_MODULES@
-static_modules = @STATIC_MODULES@
+mpm_modules = @MPM_MODULES@
 
 [test]
 gen_dir = @abs_srcdir@/../gen
 http_port = 5002
 https_port = 5001
 proxy_port = 5003
+http_port2 = 5004
 http_tld = tests.httpd.apache.org
 test_dir = @abs_srcdir@
 test_src_dir = @abs_srcdir@

Modified: httpd/httpd/branches/2.4.x/test/pyhttpd/env.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/env.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/env.py (original)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/env.py Mon Jan  9 07:35:18 2023
@@ -1,3 +1,4 @@
+import importlib
 import inspect
 import logging
 import re
@@ -35,6 +36,7 @@ class HttpdTestSetup:
         "logio",
         "unixd",
         "version",
+        "watchdog",
         "authn_core",
         "authz_host",
         "authz_groupfile",
@@ -67,6 +69,7 @@ class HttpdTestSetup:
         self.env = env
         self._source_dirs = [os.path.dirname(inspect.getfile(HttpdTestSetup))]
         self._modules = HttpdTestSetup.MODULES.copy()
+        self._optional_modules = []
 
     def add_source_dir(self, source_dir):
         self._source_dirs.append(source_dir)
@@ -74,15 +77,20 @@ class HttpdTestSetup:
     def add_modules(self, modules: List[str]):
         self._modules.extend(modules)
 
+    def add_optional_modules(self, modules: List[str]):
+        self._optional_modules.extend(modules)
+
     def make(self):
         self._make_dirs()
         self._make_conf()
-        if self.env.mpm_module is not None:
+        if self.env.mpm_module is not None \
+                and self.env.mpm_module in self.env.mpm_modules:
             self.add_modules([self.env.mpm_module])
         if self.env.ssl_module is not None:
             self.add_modules([self.env.ssl_module])
         self._make_modules_conf()
         self._make_htdocs()
+        self._add_aptest()
         self.env.clear_curl_headerfiles()
 
     def _make_dirs(self):
@@ -134,11 +142,21 @@ class HttpdTestSetup:
                 mod_path = os.path.join(self.env.libexec_dir, f"mod_{m}.so")
                 if os.path.isfile(mod_path):
                     fd.write(f"LoadModule {m}_module   \"{mod_path}\"\n")
-                elif m in self.env.static_modules:
-                    fd.write(f"#built static: LoadModule {m}_module   \"{mod_path}\"\n")
-                else:
+                elif m in self.env.dso_modules:
                     missing_mods.append(m)
+                else:
+                    fd.write(f"#built static: LoadModule {m}_module   \"{mod_path}\"\n")
                 loaded.add(m)
+            for m in self._optional_modules:
+                match = re.match(r'^mod_(.+)$', m)
+                if match:
+                    m = match.group(1)
+                if m in loaded:
+                    continue
+                mod_path = os.path.join(self.env.libexec_dir, f"mod_{m}.so")
+                if os.path.isfile(mod_path):
+                    fd.write(f"LoadModule {m}_module   \"{mod_path}\"\n")
+                    loaded.add(m)
         if len(missing_mods) > 0:
             raise Exception(f"Unable to find modules: {missing_mods} "
                             f"DSOs: {self.env.dso_modules}")
@@ -162,13 +180,50 @@ class HttpdTestSetup:
                     st = os.stat(py_file)
                     os.chmod(py_file, st.st_mode | stat.S_IEXEC)
 
+    def _add_aptest(self):
+        local_dir = os.path.dirname(inspect.getfile(HttpdTestSetup))
+        p = subprocess.run([self.env.apxs, '-c', 'mod_aptest.c'],
+                           capture_output=True,
+                           cwd=os.path.join(local_dir, 'mod_aptest'))
+        rv = p.returncode
+        if rv != 0:
+            log.error(f"compiling mod_aptest failed: {p.stderr}")
+            raise Exception(f"compiling mod_aptest failed: {p.stderr}")
+
+        modules_conf = os.path.join(self.env.server_dir, 'conf/modules.conf')
+        with open(modules_conf, 'a') as fd:
+            # load our test module which is not installed
+            fd.write(f"LoadModule aptest_module   \"{local_dir}/mod_aptest/.libs/mod_aptest.so\"\n")
+
 
 class HttpdTestEnv:
 
+    LIBEXEC_DIR = None
+
+    @classmethod
+    def has_python_package(cls, name: str) -> bool:
+        if name in sys.modules:
+            # already loaded
+            return True
+        elif (spec := importlib.util.find_spec(name)) is not None:
+            module = importlib.util.module_from_spec(spec)
+            sys.modules[name] = module
+            spec.loader.exec_module(module)
+            return True
+        else:
+            return False
+
     @classmethod
     def get_ssl_module(cls):
         return os.environ['SSL'] if 'SSL' in os.environ else 'mod_ssl'
 
+    @classmethod
+    def has_shared_module(cls, name):
+        if cls.LIBEXEC_DIR is None:
+            env = HttpdTestEnv()  # will initialized it
+        path = os.path.join(cls.LIBEXEC_DIR, f"mod_{name}.so")
+        return os.path.isfile(path)
+
     def __init__(self, pytestconfig=None):
         self._our_dir = os.path.dirname(inspect.getfile(Dummy))
         self.config = ConfigParser(interpolation=ExtendedInterpolation())
@@ -178,8 +233,8 @@ class HttpdTestEnv:
         self._apxs = self.config.get('global', 'apxs')
         self._prefix = self.config.get('global', 'prefix')
         self._apachectl = self.config.get('global', 'apachectl')
-        self._libexec_dir = self.get_apxs_var('LIBEXECDIR')
-
+        if HttpdTestEnv.LIBEXEC_DIR is None:
+            HttpdTestEnv.LIBEXEC_DIR = self._libexec_dir = self.get_apxs_var('LIBEXECDIR')
         self._curl = self.config.get('global', 'curl_bin')
         self._nghttp = self.config.get('global', 'nghttp')
         if self._nghttp is None:
@@ -189,6 +244,7 @@ class HttpdTestEnv:
             self._h2load = 'h2load'
 
         self._http_port = int(self.config.get('test', 'http_port'))
+        self._http_port2 = int(self.config.get('test', 'http_port2'))
         self._https_port = int(self.config.get('test', 'https_port'))
         self._proxy_port = int(self.config.get('test', 'proxy_port'))
         self._http_tld = self.config.get('test', 'http_tld')
@@ -203,7 +259,7 @@ class HttpdTestEnv:
         self._apachectl_stderr = None
 
         self._dso_modules = self.config.get('httpd', 'dso_modules').split(' ')
-        self._static_modules = self.config.get('httpd', 'static_modules').split(' ')
+        self._mpm_modules = self.config.get('httpd', 'mpm_modules').split(' ')
         self._mpm_module = f"mpm_{os.environ['MPM']}" if 'MPM' in os.environ else 'mpm_event'
         self._ssl_module = self.get_ssl_module()
         if len(self._ssl_module.strip()) == 0:
@@ -216,7 +272,7 @@ class HttpdTestEnv:
         self._verbosity = pytestconfig.option.verbose if pytestconfig is not None else 0
         self._test_conf = os.path.join(self._server_conf_dir, "test.conf")
         self._httpd_base_conf = []
-        self._httpd_log_modules = []
+        self._httpd_log_modules = ['aptest']
         self._log_interesting = None
         self._setup = None
 
@@ -230,6 +286,8 @@ class HttpdTestEnv:
 
         self._verify_certs = False
         self._curl_headerfiles_n = 0
+        self._h2load_version = None
+        self._current_test = None
 
     def add_httpd_conf(self, lines: List[str]):
         self._httpd_base_conf.extend(lines)
@@ -289,6 +347,10 @@ class HttpdTestEnv:
         return self._http_port
 
     @property
+    def http_port2(self) -> int:
+        return self._http_port2
+
+    @property
     def https_port(self) -> int:
         return self._https_port
 
@@ -330,15 +392,15 @@ class HttpdTestEnv:
 
     @property
     def libexec_dir(self) -> str:
-        return self._libexec_dir
+        return HttpdTestEnv.LIBEXEC_DIR
 
     @property
     def dso_modules(self) -> List[str]:
         return self._dso_modules
 
     @property
-    def static_modules(self) -> List[str]:
-        return self._static_modules
+    def mpm_modules(self) -> List[str]:
+        return self._mpm_modules
 
     @property
     def server_conf_dir(self) -> str:
@@ -364,6 +426,13 @@ class HttpdTestEnv:
         return self._ca
 
     @property
+    def current_test_name(self) -> str:
+        return self._current_test
+
+    def set_current_test_name(self, val) -> None:
+        self._current_test = val
+
+    @property
     def apachectl_stderr(self):
         return self._apachectl_stderr
 
@@ -377,6 +446,7 @@ class HttpdTestEnv:
         return []
 
     def _versiontuple(self, v):
+        v = re.sub(r'(\d+\.\d+(\.\d+)?)(-\S+)?', r'\1', v)
         return tuple(map(int, v.split('.')))
 
     def httpd_is_at_least(self, minv):
@@ -389,14 +459,16 @@ class HttpdTestEnv:
     def h2load_is_at_least(self, minv):
         if not self.has_h2load():
             return False
-        p = subprocess.run([self._h2load, '--version'], capture_output=True, text=True)
-        if p.returncode != 0:
-            return False
-        s = p.stdout.strip()
-        m = re.match(r'h2load nghttp2/(\S+)', s)
-        if m:
-            hv = self._versiontuple(m.group(1))
-            return hv >= self._versiontuple(minv)
+        if self._h2load_version is None:
+            p = subprocess.run([self._h2load, '--version'], capture_output=True, text=True)
+            if p.returncode != 0:
+                return False
+            s = p.stdout.strip()
+            m = re.match(r'h2load nghttp2/(\S+)', s)
+            if m:
+                self._h2load_version = self._versiontuple(m.group(1))
+        if self._h2load_version is not None:
+            return self._h2load_version >= self._versiontuple(minv)
         return False
 
     def has_nghttp(self):
@@ -570,7 +642,7 @@ class HttpdTestEnv:
         if not isinstance(urls, list):
             urls = [urls]
         u = urlparse(urls[0])
-        assert u.hostname, f"hostname not in url: {urls[0]}"
+        #assert u.hostname, f"hostname not in url: {urls[0]}"
         headerfile = f"{self.gen_dir}/curl.headers.{self._curl_headerfiles_n}"
         self._curl_headerfiles_n += 1
 
@@ -583,12 +655,15 @@ class HttpdTestEnv:
             args.append('--insecure')
         elif options and "--cacert" in options:
             pass
-        else:
+        elif u.hostname:
             ca_pem = self.get_ca_pem_file(u.hostname)
             if ca_pem:
                 args.extend(["--cacert", ca_pem])
 
-        if force_resolve and u.hostname != 'localhost' \
+        if self._current_test is not None:
+            args.extend(["-H", f'AP-Test-Name: {self._current_test}'])
+
+        if force_resolve and u.hostname and u.hostname != 'localhost' \
                 and u.hostname != self._httpd_addr \
                 and not re.match(r'^(\d+|\[|:).*', u.hostname):
             assert u.port, f"port not in url: {urls[0]}"
@@ -602,39 +677,60 @@ class HttpdTestEnv:
 
     def curl_parse_headerfile(self, headerfile: str, r: ExecResult = None) -> ExecResult:
         lines = open(headerfile).readlines()
-        exp_stat = True
         if r is None:
             r = ExecResult(args=[], exit_code=0, stdout=b'', stderr=b'')
-        header = {}
+
+        response = None
+        def fin_response(response):
+            if response:
+                r.add_response(response)
+
+        expected = ['status']
         for line in lines:
-            if exp_stat:
+            if re.match(r'^$', line):
+                if 'trailer' in expected:
+                    # end of trailers
+                    fin_response(response)
+                    response = None
+                    expected = ['status']
+                elif 'header' in expected:
+                    # end of header, another status or trailers might follow
+                    expected = ['status', 'trailer']
+                else:
+                    assert False, f"unexpected line: {line}"
+                continue
+            if 'status' in expected:
                 log.debug("reading 1st response line: %s", line)
                 m = re.match(r'^(\S+) (\d+) (.*)$', line)
-                assert m
-                r.add_response({
-                    "protocol": m.group(1),
-                    "status": int(m.group(2)),
-                    "description": m.group(3),
-                    "body": r.outraw
-                })
-                exp_stat = False
-                header = {}
-            elif re.match(r'^$', line):
-                exp_stat = True
-            else:
-                log.debug("reading header line: %s", line)
+                if m:
+                    fin_response(response)
+                    response = {
+                        "protocol": m.group(1),
+                        "status": int(m.group(2)),
+                        "description": m.group(3),
+                        "header": {},
+                        "trailer": {},
+                        "body": r.outraw
+                    }
+                    expected = ['header']
+                    continue
+            if 'trailer' in expected:
                 m = re.match(r'^([^:]+):\s*(.*)$', line)
-                assert m
-                header[m.group(1).lower()] = m.group(2)
-        if r.response:
-            r.response["header"] = header
+                if m:
+                    response['trailer'][m.group(1).lower()] = m.group(2)
+                    continue
+            if 'header' in expected:
+                m = re.match(r'^([^:]+):\s*(.*)$', line)
+                if m:
+                    response['header'][m.group(1).lower()] = m.group(2)
+                    continue
+            assert False, f"unexpected line: {line}"
+
+        fin_response(response)
         return r
 
     def curl_raw(self, urls, timeout=10, options=None, insecure=False,
                  force_resolve=True):
-        xopt = ['-vvvv']
-        if options:
-            xopt.extend(options)
         args, headerfile = self.curl_complete_args(
             urls=urls, timeout=timeout, options=options, insecure=insecure,
             force_resolve=force_resolve)
@@ -679,7 +775,8 @@ class HttpdTestEnv:
         return -1
         
     def nghttp(self):
-        return Nghttp(self._nghttp, connect_addr=self._httpd_addr, tmp_dir=self.gen_dir)
+        return Nghttp(self._nghttp, connect_addr=self._httpd_addr,
+                      tmp_dir=self.gen_dir, test_name=self._current_test)
 
     def h2load_status(self, run: ExecResult):
         stats = {}

Added: httpd/httpd/branches/2.4.x/test/pyhttpd/mod_aptest/mod_aptest.c
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/mod_aptest/mod_aptest.c?rev=1906475&view=auto
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/mod_aptest/mod_aptest.c (added)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/mod_aptest/mod_aptest.c Mon Jan  9 07:35:18 2023
@@ -0,0 +1,66 @@
+/* Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *     http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+#include <apr_optional.h>
+#include <apr_optional_hooks.h>
+#include <apr_strings.h>
+#include <apr_cstr.h>
+#include <apr_want.h>
+
+#include <httpd.h>
+#include <http_protocol.h>
+#include <http_request.h>
+#include <http_log.h>
+
+static void aptest_hooks(apr_pool_t *pool);
+
+AP_DECLARE_MODULE(aptest) = {
+    STANDARD20_MODULE_STUFF,
+    NULL, /* func to create per dir config */
+    NULL,  /* func to merge per dir config */
+    NULL, /* func to create per server config */
+    NULL,  /* func to merge per server config */
+    NULL,              /* command handlers */
+    aptest_hooks,
+#if defined(AP_MODULE_FLAG_NONE)
+    AP_MODULE_FLAG_ALWAYS_MERGE
+#endif
+};
+
+
+static int aptest_post_read_request(request_rec *r)
+{
+    const char *test_name = apr_table_get(r->headers_in, "AP-Test-Name");
+    if (test_name) {
+        ap_log_rerror(APLOG_MARK, APLOG_INFO, 0, r, "test[%s]: %s",
+                      test_name, r->the_request);
+    }
+    return DECLINED;
+}
+
+/* Install this module into the apache2 infrastructure.
+ */
+static void aptest_hooks(apr_pool_t *pool)
+{
+    ap_log_perror(APLOG_MARK, APLOG_TRACE1, 0, pool,
+                  "installing hooks and handlers");
+
+    /* test case monitoring */
+    ap_hook_post_read_request(aptest_post_read_request, NULL,
+                              NULL, APR_HOOK_MIDDLE);
+
+}
+

Modified: httpd/httpd/branches/2.4.x/test/pyhttpd/nghttp.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/nghttp.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/nghttp.py (original)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/nghttp.py Mon Jan  9 07:35:18 2023
@@ -15,10 +15,12 @@ def _get_path(x):
 
 class Nghttp:
 
-    def __init__(self, path, connect_addr=None, tmp_dir="/tmp"):
+    def __init__(self, path, connect_addr=None, tmp_dir="/tmp",
+                 test_name: str = None):
         self.NGHTTP = path
         self.CONNECT_ADDR = connect_addr
         self.TMP_DIR = tmp_dir
+        self._test_name = test_name
 
     @staticmethod
     def get_stream(streams, sid):
@@ -104,7 +106,7 @@ class Nghttp:
                 body += m.group(1)
                 s = self.get_stream(streams, m.group(2))
                 if s:
-                    print("stream %d: recv %d header" % (s["id"], len(s["header"]))) 
+                    print("stream %d: recv %d header" % (s["id"], len(s["header"])))
                     response = s["response"]
                     hkey = "header"
                     if "header" in response:
@@ -119,7 +121,8 @@ class Nghttp:
                                 prev["previous"] = response["previous"]
                             response["previous"] = prev
                     response[hkey] = s["header"]
-                    s["header"] = {} 
+                    s["header"] = {}
+                    body = ''
                 continue
             
             m = re.match(r'(.*)\[.*] recv DATA frame <length=(\d+), .*stream_id=(\d+)>', l)
@@ -194,9 +197,11 @@ class Nghttp:
             output["response"] = streams[main_stream]["response"]
             output["paddings"] = streams[main_stream]["paddings"]
         return output
-    
+
     def _raw(self, url, timeout, options):
         args = ["-v"]
+        if self._test_name is not None:
+            args.append(f'--header=AP-Test-Name: {self._test_name}')
         if options:
             args.extend(options)
         r = self._baserun(url, timeout, args)

Modified: httpd/httpd/branches/2.4.x/test/pyhttpd/result.py
URL: http://svn.apache.org/viewvc/httpd/httpd/branches/2.4.x/test/pyhttpd/result.py?rev=1906475&r1=1906474&r2=1906475&view=diff
==============================================================================
--- httpd/httpd/branches/2.4.x/test/pyhttpd/result.py (original)
+++ httpd/httpd/branches/2.4.x/test/pyhttpd/result.py Mon Jan  9 07:35:18 2023
@@ -9,21 +9,21 @@ class ExecResult:
                  stdout: bytes, stderr: bytes = None, duration: timedelta = None):
         self._args = args
         self._exit_code = exit_code
-        self._raw = stdout if stdout else b''
-        self._stdout = stdout.decode() if stdout is not None else ""
-        self._stderr = stderr.decode() if stderr is not None else ""
+        self._stdout = stdout if stdout is not None else b''
+        self._stderr = stderr if stderr is not None else b''
         self._duration = duration if duration is not None else timedelta()
         self._response = None
         self._results = {}
         self._assets = []
         # noinspection PyBroadException
         try:
-            self._json_out = json.loads(self._stdout)
+            out = self._stdout.decode()
+            self._json_out = json.loads(out)
         except:
             self._json_out = None
 
     def __repr__(self):
-        return f"ExecResult[code={self.exit_code}, args={self._args}, stdout={self.stdout}, stderr={self.stderr}]"
+        return f"ExecResult[code={self.exit_code}, args={self._args}, stdout={self._stdout}, stderr={self._stderr}]"
 
     @property
     def exit_code(self) -> int:
@@ -35,11 +35,11 @@ class ExecResult:
 
     @property
     def outraw(self) -> bytes:
-        return self._raw
+        return self._stdout
 
     @property
     def stdout(self) -> str:
-        return self._stdout
+        return self._stdout.decode()
 
     @property
     def json(self) -> Optional[Dict]:
@@ -48,7 +48,7 @@ class ExecResult:
 
     @property
     def stderr(self) -> str:
-        return self._stderr
+        return self._stderr.decode()
 
     @property
     def duration(self) -> timedelta: