pytest: fixes and tidy-ups to h3-proxy tests
No binary (build failed)
—
(log yok)
| @@ -55,14 +55,6 @@ MARK_NEEDS_NGHTTPX = pytest.mark.skipif( | |
| condition=not Env.have_nghttpx(), reason="no nghttpx available" | |
| ) | |
| - | H3_PROXY_COMMON_MARKS = [ |
| - | MARK_NEEDS_HTTPS_PROXY, |
| - | MARK_NEEDS_HTTP3, |
| - | MARK_NEEDS_PROXY_HTTP3, |
| - | MARK_NEEDS_NGHTTP3, |
| - | ] |
| - | |
| - | NGTCP2_ONLY_MSG = "only supported with the ngtcp2 quic stack" |
| UNSUPPORTED_OPT_MSG = "does not support this" | |
| H2O_HELLO_MSG = '"message": "Hello from h2o HTTP/3 server"' | |
| @@ -145,11 +137,15 @@ def _h2o_proxy_args( | |
| return xargs | |
| - | class TestH3ProxySuccess: |
| - | """Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP.""" |
| + | @MARK_NEEDS_HTTPS_PROXY |
| + | @MARK_NEEDS_HTTP3 |
| + | @MARK_NEEDS_NGHTTP3 |
| + | class TestH3Proxy: |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| + | # Success matrix for HTTP/3 proxy CONNECT / CONNECT-UDP. |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| @pytest.mark.parametrize( | |
| ["alpn_proto", "proxy_proto"], | |
| [ | |
| @@ -192,12 +188,10 @@ class TestH3ProxySuccess: | |
| r.check_response(count=1, http_status=200) | |
| _check_download_message(curl, H2O_HELLO_MSG) | |
| + | # Failure matrix when proxy side does not support requested mode. |
| - | class TestH3ProxyFailure: |
| - | """Failure matrix when proxy side does not support requested mode.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX] |
| - | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_NGHTTPX |
| @pytest.mark.parametrize( | |
| ["alpn_proto", "proxy_proto", "exp_err"], | |
| [ | |
| @@ -260,12 +254,10 @@ class TestH3ProxyFailure: | |
| f"Expected protocol/proxy error but got: {r.dump_logs()}" | |
| ) | |
| + | # Behavior checks for tunnel vs non-tunnel proxy mode selection. |
| - | class TestH3ProxyModeSelection: |
| - | """Behavior checks for tunnel vs non-tunnel proxy mode selection.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_NGHTTPX] |
| - | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_NGHTTPX |
| @pytest.mark.parametrize( | |
| ["proxy_proto"], | |
| [ | |
| @@ -301,22 +293,8 @@ class TestH3ProxyModeSelection: | |
| f"expected CONNECT-UDP attempt in output, got: {r.dump_logs()}" | |
| ) | |
| - | |
| - | class TestH3ProxyRuntimeGuards: |
| - | """Guard checks for unsupported HTTP/3 proxy options.""" |
| - | |
| - | pytestmark = [ |
| - | MARK_NEEDS_HTTPS_PROXY, |
| - | MARK_NEEDS_PROXY_HTTP3, |
| - | pytest.mark.skipif( |
| - | condition=Env.curl_uses_lib("ngtcp2"), |
| - | reason="guard only applies to non-ngtcp2 builds", |
| - | ), |
| - | ] |
| + | # Guard checks for unsupported HTTP/3 proxy options. |
| - | @pytest.mark.skipif( |
| - | condition=not Env.curl_has_feature("HTTP3"), reason="curl lacks HTTP/3 support" |
| - | ) |
| @pytest.mark.skipif( | |
| condition=Env.curl_has_feature("proxy-HTTP3"), reason="curl has h3 proxy support" | |
| ) | |
| @@ -340,23 +318,11 @@ class TestH3ProxyRuntimeGuards: | |
| f"Expected unsupported option failure but got: {r.stderr}" | |
| ) | |
| + | # Robustness checks for shutdown and proxy loss during transfer. |
| - | class TestH3ProxyRobustness: |
| - | """Robustness checks for shutdown and proxy loss during transfer.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| - | |
| - | @pytest.fixture(autouse=True, scope="class") |
| - | def _class_scope(self, env, h2o_server): |
| - | if not env.have_h2o(): |
| - | pytest.skip("h2o not available") |
| - | env.make_data_file( |
| - | indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024 |
| - | ) |
| - | |
| - | def test_60_05_graceful_shutdown( |
| - | self, env: Env, h2o_server, h2o_proxy |
| - | ): |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| + | def test_60_05_graceful_shutdown(self, env: Env, h2o_server, h2o_proxy): |
| if not env.curl_is_debug(): | |
| pytest.skip("needs debug curl for shutdown trace lines") | |
| if not env.curl_is_verbose(): | |
| @@ -380,9 +346,12 @@ class TestH3ProxyRobustness: | |
| ] | |
| assert shutdown_lines, f"No shutdown trace lines found:\n{r.stderr}" | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_06_proxy_drop_mid_transfer(self, env: Env, h2o_server, h2o_proxy): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| + | env.make_data_file(indir=h2o_server.docs_dir, fname="proxy-drop-20m", fsize=20 * 1024 * 1024) |
| proxy_port = h2o_proxy.port | |
| url = f"https://localhost:{h2o_server.port}/proxy-drop-20m" | |
| out_path = os.path.join(env.gen_dir, "proxy-drop.out") | |
| @@ -423,22 +392,13 @@ class TestH3ProxyRobustness: | |
| proc.wait(timeout=5) | |
| assert h2o_proxy.start(), "failed to restart h2o proxy" | |
| + | # Large file transfers and multiplexing through HTTP/3 proxy. |
| - | class TestH3ProxyDataTransfer: |
| - | """Large file transfers and multiplexing through HTTP/3 proxy.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| - | |
| - | @pytest.fixture(autouse=True, scope="class") |
| - | def _class_scope(self, env, h2o_server): |
| - | if not env.have_h2o(): |
| - | pytest.skip("h2o not available") |
| - | env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024) |
| - | env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024) |
| - | env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024) |
| - | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_07_large_download(self, env: Env, h2o_server, h2o_proxy): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| + | env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024) |
| curl = CurlClient(env=env) | |
| url = f"https://localhost:{h2o_server.port}/download-10m" | |
| proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) | |
| @@ -448,8 +408,11 @@ class TestH3ProxyDataTransfer: | |
| r.check_response(count=1, http_status=200) | |
| _check_download_size(curl, 10 * 1024 * 1024) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_08_large_upload(self, env: Env, httpd, h2o_server, h2o_proxy): | |
| _require_available(h2o_proxy=h2o_proxy) | |
| + | env.make_data_file(indir=env.gen_dir, fname="upload-2m", fsize=2 * 1024 * 1024) |
| fdata = os.path.join(env.gen_dir, "upload-2m") | |
| curl = CurlClient(env=env) | |
| url = f"https://localhost:{httpd.ports['https']}/curltest/echo?id=[0-0]" | |
| @@ -463,8 +426,11 @@ class TestH3ProxyDataTransfer: | |
| ) | |
| r.check_response(count=1, http_status=200) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_09_parallel_downloads(self, env: Env, h2o_server, h2o_proxy): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| + | env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024) |
| count = 5 | |
| curl = CurlClient(env=env) | |
| urln = f"https://localhost:{h2o_server.port}/download-1m?[0-{count - 1}]" | |
| @@ -475,12 +441,8 @@ class TestH3ProxyDataTransfer: | |
| ) | |
| r.check_response(count=count, http_status=200) | |
| - | |
| - | class TestH3ProxyConnectionManagement: |
| - | """Proxy authentication, connection reuse, and session resumption.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| - | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_10_proxy_basic_auth(self, env: Env, h2o_server, h2o_proxy): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| curl = CurlClient(env=env) | |
| @@ -493,6 +455,8 @@ class TestH3ProxyConnectionManagement: | |
| r.check_response(count=1, http_status=200) | |
| _check_download_message(curl, H2O_HELLO_MSG) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| def test_60_11_connection_reuse(self, env: Env, h2o_server, h2o_proxy): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| curl = CurlClient(env=env) | |
| @@ -506,6 +470,8 @@ class TestH3ProxyConnectionManagement: | |
| f"expected proxy connection reuse, got {r.total_connects} connects" | |
| ) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| @pytest.mark.skipif(condition=not Env.curl_has_feature('SSLS-EXPORT'), | |
| reason='curl lacks SSL session export support') | |
| def test_60_12_quic_session_resumption(self, env: Env, h2o_server, h2o_proxy): | |
| @@ -530,20 +496,9 @@ class TestH3ProxyConnectionManagement: | |
| reuses = [line for line in r2.trace_lines if '[SSLS] took session for proxy.http.curl.se' in line] | |
| assert len(reuses), f'{r2.dump_logs()}' | |
| + | # CONNECT-UDP tunnel payload size and capsule-protocol tests. |
| - | class TestH3ProxyUdpTunnel: |
| - | """CONNECT-UDP tunnel payload size and capsule-protocol tests.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS |
| - | |
| - | @pytest.fixture(autouse=True, scope="class") |
| - | def _class_scope(self, env, h2o_server): |
| - | if not env.have_h2o(): |
| - | return |
| - | env.make_data_file(indir=h2o_server.docs_dir, fname="download-1400", fsize=1400) |
| - | env.make_data_file(indir=h2o_server.docs_dir, fname="download-1m", fsize=1 * 1024 * 1024) |
| - | env.make_data_file(indir=h2o_server.docs_dir, fname="download-10m", fsize=10 * 1024 * 1024) |
| - | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| @MARK_NEEDS_H2O | |
| @pytest.mark.parametrize( | |
| "fname,fsize", | |
| @@ -557,6 +512,7 @@ class TestH3ProxyUdpTunnel: | |
| self, env: Env, h2o_server, h2o_proxy, fname, fsize | |
| ): | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| + | env.make_data_file(indir=h2o_server.docs_dir, fname=fname, fsize=fsize) |
| curl = CurlClient(env=env) | |
| url = f"https://localhost:{h2o_server.port}/{fname}" | |
| proxy_args = _h2o_proxy_args(env, h2o_proxy, "h3", tunnel=True) | |
| @@ -566,6 +522,7 @@ class TestH3ProxyUdpTunnel: | |
| r.check_response(count=1, http_status=200) | |
| _check_download_size(curl, fsize) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| @MARK_NEEDS_NGHTTPX | |
| def test_60_14_udp_tunnel_capsule_absent( | |
| self, env: Env, httpd, nghttpx, nghttpx_fwd | |
| @@ -585,12 +542,10 @@ class TestH3ProxyUdpTunnel: | |
| "expected failure: nghttpx does not support CONNECT-UDP / Capsule-Protocol" | |
| ) | |
| + | # Timeout and protocol-mismatch edge cases. |
| - | class TestH3ProxyEdgeCases: |
| - | """Timeout and protocol-mismatch edge cases.""" |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| - | |
| + | #@MARK_NEEDS_PROXY_HTTP3 |
| + | #@MARK_NEEDS_H2O |
| #def test_60_15_connect_timeout(self, env: Env, h2o_proxy): | |
| # _require_available(h2o_proxy=h2o_proxy) | |
| # curl = CurlClient(env=env, timeout=15) | |
| @@ -610,6 +565,8 @@ class TestH3ProxyEdgeCases: | |
| # f"timeout not respected: took {r.duration.total_seconds():.1f}s" | |
| # ) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| @MARK_NEEDS_NGHTTP2 | |
| def test_60_16_h2_uses_connect_tcp_not_udp(self, env: Env, httpd, h2o_proxy): | |
| _require_available(httpd=httpd, h2o_proxy=h2o_proxy) | |
| @@ -624,18 +581,14 @@ class TestH3ProxyEdgeCases: | |
| ) | |
| r.check_response(count=1, http_status=200) | |
| + | # Verify that happy eyeballs is active for HTTP/3 proxy connections. |
| + | # |
| + | # With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address |
| + | # family selection to the proxy is done by happy eyeballs. |
| - | class TestH3ProxyHappyEyeballs: |
| - | """ |
| - | Verify that happy eyeballs is active for HTTP/3 proxy connections. |
| - | |
| - | With the H3-PROXY filter sitting above HAPPY-EYEBALLS -> UDP, address |
| - | family selection to the proxy is done by happy eyeballs. |
| - | """ |
| - | |
| - | pytestmark = H3_PROXY_COMMON_MARKS + [MARK_NEEDS_H2O] |
| - | |
| - | def test_60_17_h3_proxy_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy): |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| + | def test_60_17_happy_eyeballs_filter_present(self, env: Env, h2o_server, h2o_proxy): |
| """Verbose trace confirms HAPPY-EYEBALLS filter is in the H3 proxy chain.""" | |
| if not env.curl_is_debug(): | |
| pytest.skip("needs debug curl for filter trace") | |
| @@ -651,8 +604,10 @@ class TestH3ProxyHappyEyeballs: | |
| f"expected HAPPY-EYEBALLS trace for H3 proxy, got: {r.stderr}" | |
| ) | |
| + | @MARK_NEEDS_PROXY_HTTP3 |
| + | @MARK_NEEDS_H2O |
| @MARK_NEEDS_NGHTTP2 | |
| - | def test_60_18_h3_proxy_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy): |
| + | def test_60_18_happy_eyeballs_ipv4_all_proto(self, env: Env, h2o_server, h2o_proxy): |
| """IPv4-forced H3 proxy works for h1/h2/h3 inner protocols.""" | |
| _require_available(h2o_server=h2o_server, h2o_proxy=h2o_proxy) | |
| for alpn_proto in ["http/1.1", "h2", "h3"]: | |