From 72c721a933ea2d1d86ae2e468fc0f2ab097fd69a Mon Sep 17 00:00:00 2001 From: bneradt Date: Fri, 21 Aug 2026 17:57:59 -0500 Subject: [PATCH] Migrate end-to-end tests to pytest Uranium Migrate every end-to-end scenario to ordinary test_*.py modules backed by a lightweight Uranium runtime and pytest-xdist. Generate the build-tree urtest.sh with isolated, failure-preserving sandboxes and CI sharding. Use explicit stream expectation and return-code methods so accidental assignment fails during test declaration and regex failures explain the behavior under test. Remove the legacy harness and update CI, docs, and contributor guidance to use the new workflow. --- .agents/skills/apply-pr-comments/SKILL.md | 2 +- .agents/skills/plan-issue/SKILL.md | 4 +- .agents/skills/run-autests/SKILL.md | 57 -- .agents/skills/run-urtests/SKILL.md | 46 ++ .agents/skills/writing-autests/SKILL.md | 14 - .agents/skills/writing-urtests/SKILL.md | 45 ++ .dockerignore | 2 +- .github/workflows/ci.yml | 26 +- .gitignore | 2 +- AGENTS.md | 26 +- CMakeLists.txt | 12 +- CONTRIBUTING.md | 2 +- NOTICES | 4 +- README.md | 101 ++- tests/CMakeLists.txt | 33 + tests/autests/CMakeLists.txt | 25 - tests/autests/autest-parallel.py.in | 688 ------------------ tests/autests/autest.sh.in | 55 -- .../format_argument/format_argument.test.py | 175 ----- .../arguments/keys_argument/keys_arg.test.py | 66 -- .../port_argument/no_port_argument.test.py | 34 - .../repeat_argument/repeat_argument.test.py | 101 --- .../version_argument/version_argument.test.py | 60 -- .../gold_tests/autest-site/client.test.ext | 252 ------- .../gold_tests/autest-site/init.cli.ext | 23 - tests/autests/gold_tests/autest-site/ports.py | 271 ------- .../gold_tests/autest-site/proxy.test.ext | 169 ----- .../autest-site/replay_gen.test.ext | 72 -- .../gold_tests/autest-site/server.test.ext | 264 ------- .../gold_tests/autest-site/setup.cli.ext | 61 -- .../gold_tests/autest-site/socket_util.py | 31 - tests/autests/gold_tests/body/body.test.py | 28 - .../body_verification.test.py | 84 --- .../connect_tunnel/connect_tunnel.test.py | 77 -- tests/autests/gold_tests/delay/delay.test.py | 97 --- .../gold_tests/doctest/doctest.test.py | 57 -- .../field_parsing/duplicate_fields.test.py | 25 - .../field_parsing/empty_field.test.py | 35 - .../field_parsing/empty_proxy.test.py | 25 - .../field_parsing/parse_yaml.test.py | 42 -- .../field_verification.test.py | 252 ------- tests/autests/gold_tests/http/http.test.py | 69 -- tests/autests/gold_tests/http2/http2.test.py | 180 ----- .../http2_abort/http2_abort.test.py | 184 ----- .../http2_close_on_goaway.test.py | 32 - .../http2_frames/http2_frames.test.py | 40 - .../http2_multi_data_frames.test.py | 63 -- tests/autests/gold_tests/http3/http3.test.py | 87 --- tests/autests/gold_tests/https/https.test.py | 31 - tests/autests/gold_tests/https/mtls.test.py | 33 - tests/autests/gold_tests/ipv6/ipv6.test.py | 31 - .../method_verification.test.py | 87 --- .../gold_tests/no_proxy/no_proxy.test.py | 75 -- .../gold_tests/on_connect/on_connect.test.py | 124 ---- .../protocol_syntax/protocol_syntax.test.py | 29 - .../proxy-protocol/proxy_protocol.test.py | 219 ------ .../gold_tests/replay_gen/replay_gen.test.py | 37 - .../request_absence/request_absence.test.py | 183 ----- .../sigint_shutdown/sigint_shutdown.test.py | 37 - .../strict_mode/strict_mode.test.py | 51 -- .../url_verification/url_verification.test.py | 67 -- tests/autests/serial_tests.txt | 5 - tests/autests/test-env-check.sh | 89 --- tests/{autests => }/pyproject.toml | 10 +- tests/tools/uranium/__init__.py | 16 + tests/tools/uranium/assertions.py | 49 ++ .../uranium}/default_url_file | 0 tests/tools/uranium/model.py | 332 +++++++++ tests/tools/uranium/plugin.py | 107 +++ tests/tools/uranium/process.py | 152 ++++ .../uranium/proxy}/directive_engine.py | 0 .../uranium/proxy}/proxy_http1.py | 0 .../uranium/proxy}/proxy_http2.py | 1 - .../uranium/proxy}/proxy_http3.py | 4 +- .../uranium/proxy}/proxy_protocol_context.py | 0 .../uranium/proxy}/test_proxy.py | 0 tests/tools/uranium/runtime.py | 427 +++++++++++ tests/tools/uranium/tests/test_assertions.py | 65 ++ tests/tools/uranium/tests/test_model.py | 133 ++++ tests/tools/uranium/tests/test_process.py | 41 ++ .../arguments/format_argument/no_uuid.yaml | 0 .../format_argument/test_format_argument.py | 166 +++++ .../format_argument/unique_by_host.yaml | 0 .../format_argument/unique_by_url.yaml | 0 .../arguments/format_argument/uuid1.yaml | 0 .../arguments/format_argument/uuid2.yaml | 0 .../keys_argument/five_transactions.yaml | 0 .../arguments/keys_argument/test_keys_arg.py | 65 ++ .../arguments/port_argument/not_used.yaml | 0 .../port_argument/test_no_port_argument.py | 38 + .../two_files/five_transactions.yaml | 0 .../two_files/three_transactions.yaml | 0 .../repeat_argument/test_repeat_argument.py | 92 +++ .../replay_files/client_subset_http1.yaml | 0 .../replay_files/client_subset_http2.yaml | 0 .../replay_files/server_superset_http1.yaml | 0 .../replay_files/server_superset_http2.yaml | 0 .../verify_unprocessed_response_http1.yaml | 0 .../verify_unprocessed_response_http2.yaml | 0 .../verify_unprocessed_response_http3.yaml | 0 .../test_unprocessed_verifications.py} | 99 +-- .../version_argument/test_version_argument.py | 58 ++ .../await/await.replay.yaml | 0 .../await/test_await.py} | 29 +- .../body/body.yaml | 0 .../body/gold/body_client.gold | 0 .../body/gold/body_proxy.gold | 0 .../body/gold/body_server.gold | 0 tests/uranium_tests/body/test_body.py | 32 + .../body_verification/http.yaml | 0 .../body_verification/http2.yaml | 0 .../body_verification/http3.yaml | 0 .../test_body_verification.py | 77 ++ .../replay_files/client/connect_tunnel.yaml | 0 .../replay_files/server/connect_tunnel.yaml | 0 .../connect_tunnel/test_connect_tunnel.py | 74 ++ .../delay/client-side-delay.yaml | 0 .../delay/server-side-delay.yaml | 0 tests/uranium_tests/delay/test_delay.py | 99 +++ .../delay/verify_duration.py | 0 .../doctest/example_replay.yaml | 0 .../doctest/gold/doctest_client.gold | 0 .../doctest/gold/doctest_server.gold | 0 tests/uranium_tests/doctest/test_doctest.py | 54 ++ .../doctest/verify_example_replay_contents.py | 0 .../gold/duplicate_fields_client.gold | 0 .../gold/duplicate_fields_proxy.gold | 0 .../gold/duplicate_fields_server.gold | 0 .../gold/empty_proxy_client.gold | 0 .../field_parsing/gold/empty_proxy_proxy.gold | 0 .../gold/empty_proxy_server.gold | 0 .../gold/transaction_fields_client.gold | 0 .../gold/transaction_fields_proxy.gold | 0 .../gold/transaction_fields_server.gold | 0 .../gold/yaml_specified_client.gold | 0 .../gold/yaml_specified_proxy.gold | 0 .../gold/yaml_specified_server.gold | 0 .../replay_files/duplicate_fields.yaml | 0 .../replay_files/empty_field.yaml | 0 .../replay_files/empty_proxy.yaml | 0 .../replay_files/transaction_fields.yaml | 0 .../replay_files/yaml_specified.yaml | 0 .../field_parsing/test_duplicate_fields.py | 28 + .../field_parsing/test_empty_field.py | 36 + .../field_parsing/test_empty_proxy.py | 28 + .../field_parsing/test_parse_yaml.py | 44 ++ .../replay_files/cookie_equal.yaml | 0 .../replay_files/duplicate_fields.yaml | 0 .../replay_files/map_specification.yaml | 0 .../replay_files/multi_value_equal.yaml | 0 .../replay_files/multi_value_includes.yaml | 0 .../replay_files/not_nocase.yaml | 0 .../replay_files/set_cookie_absent.yaml | 0 .../replay_files/set_cookie_negative.yaml | 0 .../set_cookie_negative_failure.yaml | 0 .../replay_files/substr_rules.yaml | 0 .../replay_files/substr_rules_duplicate.yaml | 0 .../replay_files/various_verification.json | 0 .../test_field_verification.py | 233 ++++++ .../field_verification/test_not_nocase.py} | 118 ++- .../field_verification/test_substr_rules.py} | 114 +-- .../http/gold/single_transaction_client.gold | 0 .../http/gold/single_transaction_proxy.gold | 0 .../http/gold/single_transaction_server.gold | 0 .../multiple_transactions/file1.json | 0 .../multiple_transactions/file2.json | 0 .../multiple_transactions/file3.yaml | 0 .../http/replay_files/single_transaction.yaml | 0 tests/uranium_tests/http/test_http.py | 63 ++ .../http2/gold/http2_to_http1_client.gold | 0 .../http2/gold/http2_to_http1_proxy.gold | 0 .../http2/gold/http2_to_http1_server.gold | 0 .../http2/gold/http2_to_http2_client.gold | 0 .../http2/gold/http2_to_http2_proxy.gold | 0 .../http2/gold/http2_to_http2_server.gold | 2 +- .../http2/gold/set_alpn_proxy.gold | 0 .../http2/replay_files/http2_hop_by_hop.yaml | 0 .../http2/replay_files/http2_to_http1.yaml | 0 .../http2/replay_files/http2_to_http2.yaml | 0 .../http2_to_http2_verification_failures.yaml | 0 .../http2/replay_files/set_alpn.yaml | 0 tests/uranium_tests/http2/test_http2.py | 158 ++++ .../client_goaway_after_headers.yaml | 2 +- .../client_rst_stream_after_data.yaml | 2 +- .../client_rst_stream_after_headers.yaml | 2 +- .../client_rst_stream_mixed_data.yaml | 2 +- .../server_goaway_after_headers.yaml | 0 .../server_rst_stream_after_headers.yaml | 0 .../http2_abort/test_http2_abort.py | 169 +++++ .../http2_close_on_goaway.yaml | 0 .../test_http2_close_on_goaway.py | 36 + .../http2_frames/gold/client.gold | 0 .../gold/multi_data_frame_client.gold | 0 .../gold/multi_data_frame_proxy.gold | 0 .../gold/multi_data_frame_server.gold | 0 .../http2_frames/gold/proxy.gold | 0 .../http2_frames/gold/server.gold | 0 .../http2_frames/http2_frames.yaml | 0 .../http2_frames/http2_multi_data_frames.yaml | 0 .../http2_frames/test_http2_frames.py | 43 ++ .../test_http2_multi_data_frames.py | 63 ++ .../http2_frames/verify_duration.py | 0 .../http3/filter_http3_output.py | 0 .../gold/http3_to_http1_client_server.gold | 0 .../http3/gold/http3_to_http1_proxy.gold | 0 .../http3/replay_files/http3_to_http1.yaml | 0 .../replay_files/http3_to_http1_failures.yaml | 0 tests/uranium_tests/http3/test_http3.py | 95 +++ .../https/gold/single_transaction_client.gold | 0 .../https/gold/single_transaction_proxy.gold | 0 .../https/gold/single_transaction_server.gold | 0 .../https/replay_files/mtls.yaml | 0 .../replay_files/single_transaction.yaml | 0 tests/uranium_tests/https/test_https.py | 35 + tests/uranium_tests/https/test_mtls.py | 34 + .../ipv6/gold/single_transaction_client.gold | 0 .../ipv6/gold/single_transaction_server.gold | 0 .../ipv6/replay_files/single_transaction.yaml | 0 tests/uranium_tests/ipv6/test_ipv6.py | 35 + .../http1_method_verification.yaml | 0 .../http2_method_verification.yaml | 0 .../http3_method_verification.yaml | 0 .../test_method_verification.py | 90 +++ .../no_proxy/gold/h2_client.gold | 0 .../no_proxy/gold/h2_server.gold | 0 .../no_proxy/replay/h2.yaml | 0 .../no_proxy/replay/single_transaction.json | 0 tests/uranium_tests/no_proxy/test_no_proxy.py | 73 ++ .../on_connect/replay_files/on_connect.yaml | 0 .../replay_files/on_connect_http2.yaml | 0 .../on_connect/test_on_connect.py | 113 +++ .../gold/single_transaction_client.gold | 0 .../gold/single_transaction_proxy.gold | 0 .../gold/single_transaction_server.gold | 0 .../replay_files/single_transaction.yaml | 0 .../protocol_syntax/test_protocol_syntax.py | 33 + .../gold/http_single_transaction_client.gold | 0 .../gold/http_single_transaction_proxy.gold | 0 .../gold/http_single_transaction_server.gold | 0 .../gold/https_single_transaction_client.gold | 0 .../gold/https_single_transaction_proxy.gold | 0 .../gold/https_single_transaction_server.gold | 0 .../proxy-protocol/gold/ipv6_client.gold | 0 .../proxy-protocol/gold/ipv6_server.gold | 0 .../http_pp_v1_with_address.replay.yaml | 0 .../http_single_transaction_no_pp.replay.yaml | 0 .../http_single_transaction_pp_v1.replay.yaml | 0 .../http_single_transaction_pp_v2.replay.yaml | 0 ...https_single_transaction_pp_v1.replay.yaml | 0 ...https_single_transaction_pp_v2.replay.yaml | 0 .../single_transaction_ipv6_pp_v1.replay.yaml | 0 .../single_transaction_ipv6_pp_v2.replay.yaml | 0 .../proxy-protocol/test_proxy_protocol.py | 218 ++++++ .../replay_gen/test_replay_gen.py | 40 + .../replay_files/http1_cache_hit.yaml | 0 .../replay_files/http1_explicit_response.yaml | 0 .../replay_files/http1_present_cache_hit.yaml | 0 .../replay_files/http1_present_success.yaml | 0 .../http1_unspecified_cache_hit.yaml | 0 .../replay_files/http1_violation.yaml | 0 .../replay_files/http2_violation.yaml | 0 .../replay_files/missing_server_response.yaml | 0 .../request_absence/test_request_absence.py | 169 +++++ .../sigint_shutdown/test_sigint_shutdown.py | 38 + .../sigint_shutdown/verify_sigint_shutdown.py | 1 - .../replay_files/status_verification.yaml | 0 .../test_status_verification.py} | 33 +- .../strict_mode/gold/fields_differ_proxy.gold | 0 .../strict_mode/gold/fields_match_proxy.gold | 0 .../replay_files/fields_differ.json | 0 .../replay_files/fields_match.json | 0 .../strict_mode/test_strict_mode.py | 53 ++ .../url_verification/test_url_verification.py | 31 + .../url_verification/url_verification.yaml | 0 tests/urtest.sh.in | 35 + tests/{autests => }/uv.lock | 191 ++++- 276 files changed, 4653 insertions(+), 5225 deletions(-) delete mode 100644 .agents/skills/run-autests/SKILL.md create mode 100644 .agents/skills/run-urtests/SKILL.md delete mode 100644 .agents/skills/writing-autests/SKILL.md create mode 100644 .agents/skills/writing-urtests/SKILL.md create mode 100644 tests/CMakeLists.txt delete mode 100644 tests/autests/CMakeLists.txt delete mode 100644 tests/autests/autest-parallel.py.in delete mode 100644 tests/autests/autest.sh.in delete mode 100644 tests/autests/gold_tests/arguments/format_argument/format_argument.test.py delete mode 100644 tests/autests/gold_tests/arguments/keys_argument/keys_arg.test.py delete mode 100644 tests/autests/gold_tests/arguments/port_argument/no_port_argument.test.py delete mode 100644 tests/autests/gold_tests/arguments/repeat_argument/repeat_argument.test.py delete mode 100644 tests/autests/gold_tests/arguments/version_argument/version_argument.test.py delete mode 100755 tests/autests/gold_tests/autest-site/client.test.ext delete mode 100644 tests/autests/gold_tests/autest-site/init.cli.ext delete mode 100644 tests/autests/gold_tests/autest-site/ports.py delete mode 100755 tests/autests/gold_tests/autest-site/proxy.test.ext delete mode 100755 tests/autests/gold_tests/autest-site/replay_gen.test.ext delete mode 100755 tests/autests/gold_tests/autest-site/server.test.ext delete mode 100644 tests/autests/gold_tests/autest-site/setup.cli.ext delete mode 100755 tests/autests/gold_tests/autest-site/socket_util.py delete mode 100644 tests/autests/gold_tests/body/body.test.py delete mode 100644 tests/autests/gold_tests/body_verification/body_verification.test.py delete mode 100644 tests/autests/gold_tests/connect_tunnel/connect_tunnel.test.py delete mode 100644 tests/autests/gold_tests/delay/delay.test.py delete mode 100644 tests/autests/gold_tests/doctest/doctest.test.py delete mode 100644 tests/autests/gold_tests/field_parsing/duplicate_fields.test.py delete mode 100644 tests/autests/gold_tests/field_parsing/empty_field.test.py delete mode 100644 tests/autests/gold_tests/field_parsing/empty_proxy.test.py delete mode 100644 tests/autests/gold_tests/field_parsing/parse_yaml.test.py delete mode 100644 tests/autests/gold_tests/field_verification/field_verification.test.py delete mode 100644 tests/autests/gold_tests/http/http.test.py delete mode 100644 tests/autests/gold_tests/http2/http2.test.py delete mode 100644 tests/autests/gold_tests/http2_abort/http2_abort.test.py delete mode 100644 tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.test.py delete mode 100644 tests/autests/gold_tests/http2_frames/http2_frames.test.py delete mode 100644 tests/autests/gold_tests/http2_frames/http2_multi_data_frames.test.py delete mode 100644 tests/autests/gold_tests/http3/http3.test.py delete mode 100644 tests/autests/gold_tests/https/https.test.py delete mode 100644 tests/autests/gold_tests/https/mtls.test.py delete mode 100644 tests/autests/gold_tests/ipv6/ipv6.test.py delete mode 100644 tests/autests/gold_tests/method_verification/method_verification.test.py delete mode 100644 tests/autests/gold_tests/no_proxy/no_proxy.test.py delete mode 100644 tests/autests/gold_tests/on_connect/on_connect.test.py delete mode 100644 tests/autests/gold_tests/protocol_syntax/protocol_syntax.test.py delete mode 100644 tests/autests/gold_tests/proxy-protocol/proxy_protocol.test.py delete mode 100644 tests/autests/gold_tests/replay_gen/replay_gen.test.py delete mode 100644 tests/autests/gold_tests/request_absence/request_absence.test.py delete mode 100644 tests/autests/gold_tests/sigint_shutdown/sigint_shutdown.test.py delete mode 100644 tests/autests/gold_tests/strict_mode/strict_mode.test.py delete mode 100644 tests/autests/gold_tests/url_verification/url_verification.test.py delete mode 100644 tests/autests/serial_tests.txt delete mode 100755 tests/autests/test-env-check.sh rename tests/{autests => }/pyproject.toml (62%) create mode 100644 tests/tools/uranium/__init__.py create mode 100644 tests/tools/uranium/assertions.py rename tests/{autests/gold_tests/autest-site => tools/uranium}/default_url_file (100%) create mode 100644 tests/tools/uranium/model.py create mode 100644 tests/tools/uranium/plugin.py create mode 100644 tests/tools/uranium/process.py rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/directive_engine.py (100%) rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/proxy_http1.py (100%) rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/proxy_http2.py (99%) rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/proxy_http3.py (99%) rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/proxy_protocol_context.py (100%) rename tests/{autests/gold_tests/autest-site => tools/uranium/proxy}/test_proxy.py (100%) create mode 100644 tests/tools/uranium/runtime.py create mode 100644 tests/tools/uranium/tests/test_assertions.py create mode 100644 tests/tools/uranium/tests/test_model.py create mode 100644 tests/tools/uranium/tests/test_process.py rename tests/{autests/gold_tests => uranium_tests}/arguments/format_argument/no_uuid.yaml (100%) create mode 100644 tests/uranium_tests/arguments/format_argument/test_format_argument.py rename tests/{autests/gold_tests => uranium_tests}/arguments/format_argument/unique_by_host.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/format_argument/unique_by_url.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/format_argument/uuid1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/format_argument/uuid2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/keys_argument/five_transactions.yaml (100%) create mode 100644 tests/uranium_tests/arguments/keys_argument/test_keys_arg.py rename tests/{autests/gold_tests => uranium_tests}/arguments/port_argument/not_used.yaml (100%) create mode 100644 tests/uranium_tests/arguments/port_argument/test_no_port_argument.py rename tests/{autests/gold_tests => uranium_tests}/arguments/repeat_argument/replay_files/two_files/five_transactions.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/repeat_argument/replay_files/two_files/three_transactions.yaml (100%) create mode 100644 tests/uranium_tests/arguments/repeat_argument/test_repeat_argument.py rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/client_subset_http1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/client_subset_http2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/server_superset_http1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/server_superset_http2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http3.yaml (100%) rename tests/{autests/gold_tests/arguments/unprocessed_verifications/unprocessed_verifications.test.py => uranium_tests/arguments/unprocessed_verifications/test_unprocessed_verifications.py} (56%) create mode 100644 tests/uranium_tests/arguments/version_argument/test_version_argument.py rename tests/{autests/gold_tests => uranium_tests}/await/await.replay.yaml (100%) rename tests/{autests/gold_tests/await/await.test.py => uranium_tests/await/test_await.py} (60%) rename tests/{autests/gold_tests => uranium_tests}/body/body.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/body/gold/body_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/body/gold/body_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/body/gold/body_server.gold (100%) create mode 100644 tests/uranium_tests/body/test_body.py rename tests/{autests/gold_tests => uranium_tests}/body_verification/http.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/body_verification/http2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/body_verification/http3.yaml (100%) create mode 100644 tests/uranium_tests/body_verification/test_body_verification.py rename tests/{autests/gold_tests => uranium_tests}/connect_tunnel/replay_files/client/connect_tunnel.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/connect_tunnel/replay_files/server/connect_tunnel.yaml (100%) create mode 100644 tests/uranium_tests/connect_tunnel/test_connect_tunnel.py rename tests/{autests/gold_tests => uranium_tests}/delay/client-side-delay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/delay/server-side-delay.yaml (100%) create mode 100644 tests/uranium_tests/delay/test_delay.py rename tests/{autests/gold_tests => uranium_tests}/delay/verify_duration.py (100%) rename tests/{autests/gold_tests => uranium_tests}/doctest/example_replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/doctest/gold/doctest_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/doctest/gold/doctest_server.gold (100%) create mode 100644 tests/uranium_tests/doctest/test_doctest.py rename tests/{autests/gold_tests => uranium_tests}/doctest/verify_example_replay_contents.py (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/duplicate_fields_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/duplicate_fields_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/duplicate_fields_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/empty_proxy_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/empty_proxy_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/empty_proxy_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/transaction_fields_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/transaction_fields_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/transaction_fields_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/yaml_specified_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/yaml_specified_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/gold/yaml_specified_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/replay_files/duplicate_fields.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/replay_files/empty_field.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/replay_files/empty_proxy.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/replay_files/transaction_fields.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_parsing/replay_files/yaml_specified.yaml (100%) create mode 100644 tests/uranium_tests/field_parsing/test_duplicate_fields.py create mode 100644 tests/uranium_tests/field_parsing/test_empty_field.py create mode 100644 tests/uranium_tests/field_parsing/test_empty_proxy.py create mode 100644 tests/uranium_tests/field_parsing/test_parse_yaml.py rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/cookie_equal.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/duplicate_fields.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/map_specification.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/multi_value_equal.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/multi_value_includes.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/not_nocase.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/set_cookie_absent.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/set_cookie_negative.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/set_cookie_negative_failure.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/substr_rules.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/substr_rules_duplicate.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/field_verification/replay_files/various_verification.json (100%) create mode 100644 tests/uranium_tests/field_verification/test_field_verification.py rename tests/{autests/gold_tests/field_verification/not_nocase.test.py => uranium_tests/field_verification/test_not_nocase.py} (68%) rename tests/{autests/gold_tests/field_verification/substr_rules.test.py => uranium_tests/field_verification/test_substr_rules.py} (65%) rename tests/{autests/gold_tests => uranium_tests}/http/gold/single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http/gold/single_transaction_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http/gold/single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http/replay_files/multiple_transactions/file1.json (100%) rename tests/{autests/gold_tests => uranium_tests}/http/replay_files/multiple_transactions/file2.json (100%) rename tests/{autests/gold_tests => uranium_tests}/http/replay_files/multiple_transactions/file3.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http/replay_files/single_transaction.yaml (100%) create mode 100644 tests/uranium_tests/http/test_http.py rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http1_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http1_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http1_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http2_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http2_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/http2_to_http2_server.gold (98%) rename tests/{autests/gold_tests => uranium_tests}/http2/gold/set_alpn_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/replay_files/http2_hop_by_hop.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/replay_files/http2_to_http1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/replay_files/http2_to_http2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/replay_files/http2_to_http2_verification_failures.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2/replay_files/set_alpn.yaml (100%) create mode 100644 tests/uranium_tests/http2/test_http2.py rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/client_goaway_after_headers.yaml (94%) rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/client_rst_stream_after_data.yaml (92%) rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/client_rst_stream_after_headers.yaml (91%) rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/client_rst_stream_mixed_data.yaml (92%) rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/server_goaway_after_headers.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_abort/replay_files/server_rst_stream_after_headers.yaml (100%) create mode 100644 tests/uranium_tests/http2_abort/test_http2_abort.py rename tests/{autests/gold_tests => uranium_tests}/http2_close_on_goaway/http2_close_on_goaway.yaml (100%) create mode 100644 tests/uranium_tests/http2_close_on_goaway/test_http2_close_on_goaway.py rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/multi_data_frame_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/multi_data_frame_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/multi_data_frame_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/gold/server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/http2_frames.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http2_frames/http2_multi_data_frames.yaml (100%) create mode 100644 tests/uranium_tests/http2_frames/test_http2_frames.py create mode 100644 tests/uranium_tests/http2_frames/test_http2_multi_data_frames.py rename tests/{autests/gold_tests => uranium_tests}/http2_frames/verify_duration.py (100%) rename tests/{autests/gold_tests => uranium_tests}/http3/filter_http3_output.py (100%) rename tests/{autests/gold_tests => uranium_tests}/http3/gold/http3_to_http1_client_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http3/gold/http3_to_http1_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/http3/replay_files/http3_to_http1.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/http3/replay_files/http3_to_http1_failures.yaml (100%) create mode 100644 tests/uranium_tests/http3/test_http3.py rename tests/{autests/gold_tests => uranium_tests}/https/gold/single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/https/gold/single_transaction_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/https/gold/single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/https/replay_files/mtls.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/https/replay_files/single_transaction.yaml (100%) create mode 100644 tests/uranium_tests/https/test_https.py create mode 100644 tests/uranium_tests/https/test_mtls.py rename tests/{autests/gold_tests => uranium_tests}/ipv6/gold/single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/ipv6/gold/single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/ipv6/replay_files/single_transaction.yaml (100%) create mode 100644 tests/uranium_tests/ipv6/test_ipv6.py rename tests/{autests/gold_tests => uranium_tests}/method_verification/replay_files/http1_method_verification.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/method_verification/replay_files/http2_method_verification.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/method_verification/replay_files/http3_method_verification.yaml (100%) create mode 100644 tests/uranium_tests/method_verification/test_method_verification.py rename tests/{autests/gold_tests => uranium_tests}/no_proxy/gold/h2_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/no_proxy/gold/h2_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/no_proxy/replay/h2.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/no_proxy/replay/single_transaction.json (100%) create mode 100644 tests/uranium_tests/no_proxy/test_no_proxy.py rename tests/{autests/gold_tests => uranium_tests}/on_connect/replay_files/on_connect.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/on_connect/replay_files/on_connect_http2.yaml (100%) create mode 100644 tests/uranium_tests/on_connect/test_on_connect.py rename tests/{autests/gold_tests => uranium_tests}/protocol_syntax/gold/single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/protocol_syntax/gold/single_transaction_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/protocol_syntax/gold/single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/protocol_syntax/replay_files/single_transaction.yaml (100%) create mode 100644 tests/uranium_tests/protocol_syntax/test_protocol_syntax.py rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/http_single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/http_single_transaction_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/http_single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/https_single_transaction_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/https_single_transaction_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/https_single_transaction_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/ipv6_client.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/gold/ipv6_server.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/http_pp_v1_with_address.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/http_single_transaction_no_pp.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/http_single_transaction_pp_v1.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/http_single_transaction_pp_v2.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/https_single_transaction_pp_v1.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/https_single_transaction_pp_v2.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/single_transaction_ipv6_pp_v1.replay.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/proxy-protocol/replay_files/single_transaction_ipv6_pp_v2.replay.yaml (100%) create mode 100644 tests/uranium_tests/proxy-protocol/test_proxy_protocol.py create mode 100644 tests/uranium_tests/replay_gen/test_replay_gen.py rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_cache_hit.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_explicit_response.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_present_cache_hit.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_present_success.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_unspecified_cache_hit.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http1_violation.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/http2_violation.yaml (100%) rename tests/{autests/gold_tests => uranium_tests}/request_absence/replay_files/missing_server_response.yaml (100%) create mode 100644 tests/uranium_tests/request_absence/test_request_absence.py create mode 100644 tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py rename tests/{autests/gold_tests => uranium_tests}/sigint_shutdown/verify_sigint_shutdown.py (99%) rename tests/{autests/gold_tests => uranium_tests}/status_verification/replay_files/status_verification.yaml (100%) rename tests/{autests/gold_tests/status_verification/status_verification.test.py => uranium_tests/status_verification/test_status_verification.py} (53%) rename tests/{autests/gold_tests => uranium_tests}/strict_mode/gold/fields_differ_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/strict_mode/gold/fields_match_proxy.gold (100%) rename tests/{autests/gold_tests => uranium_tests}/strict_mode/replay_files/fields_differ.json (100%) rename tests/{autests/gold_tests => uranium_tests}/strict_mode/replay_files/fields_match.json (100%) create mode 100644 tests/uranium_tests/strict_mode/test_strict_mode.py create mode 100644 tests/uranium_tests/url_verification/test_url_verification.py rename tests/{autests/gold_tests => uranium_tests}/url_verification/url_verification.yaml (100%) create mode 100644 tests/urtest.sh.in rename tests/{autests => }/uv.lock (79%) diff --git a/.agents/skills/apply-pr-comments/SKILL.md b/.agents/skills/apply-pr-comments/SKILL.md index 641a64860..c83100370 100644 --- a/.agents/skills/apply-pr-comments/SKILL.md +++ b/.agents/skills/apply-pr-comments/SKILL.md @@ -11,5 +11,5 @@ should drive code changes and which should be answered without changes. - Do not post replies on GitHub. Propose suggested replies in your report. - Apply code changes for valid review feedback. - Build using `../build-pv/SKILL.md`. -- Run relevant tests via `../run-unit-tests/SKILL.md` and `../run-autests/SKILL.md` when applicable. +- Run relevant tests via `../run-unit-tests/SKILL.md` and `../run-urtests/SKILL.md` when applicable. - Do not commit or push. Leave changes unstaged for manual review. diff --git a/.agents/skills/plan-issue/SKILL.md b/.agents/skills/plan-issue/SKILL.md index 0f26884cf..02772757b 100644 --- a/.agents/skills/plan-issue/SKILL.md +++ b/.agents/skills/plan-issue/SKILL.md @@ -8,8 +8,8 @@ description: Read a GitHub issue with gh and produce an implementation and verif Using `gh`, read the issue and produce an implementation plan. - Understand the issue description and inspect relevant code paths. -- Plan tests for the fix (Catch2 and/or autest as appropriate). See `../writing-autests/SKILL.md`. +- Plan tests for the fix (Catch2 and/or Uranium as appropriate). See `../writing-urtests/SKILL.md`. - Plan the production code changes. -- Include build and verification steps using `../build-pv/SKILL.md` and `../run-autests/SKILL.md`. +- Include build and verification steps using `../build-pv/SKILL.md` and `../run-urtests/SKILL.md`. Unless asked otherwise, do not post comments to the issue. diff --git a/.agents/skills/run-autests/SKILL.md b/.agents/skills/run-autests/SKILL.md deleted file mode 100644 index 497161ebc..000000000 --- a/.agents/skills/run-autests/SKILL.md +++ /dev/null @@ -1,57 +0,0 @@ ---- -name: run-autests -description: Run Apache Traffic Server AuTests correctly via autest.sh from tests/autests. ---- - -# AuTests - -AuTests are end-to-end tests. They are Python-based but must be run through -the build-generated `autest.sh`, not by invoking Python directly. The test -descriptions are in the `tests/autests/gold_tests` directory and they have -`.test.py` extensions. - -# Run AuTests - -After building (see `../build-pv/SKILL.md`), run tests from the build tree: - -```bash - -if [ "`uname`" = "Linux" ] -then - num_threads=$(nproc) -else - num_threads=$(sysctl -n hw.logicalcpu) -fi -./build/dev/autest.sh --clean=none -j${num_threads} -``` - -The `--clean=none` option is used to prevent the sandbox from being cleaned up -after tests pass, which is helpful for verifying expected output from a passed -test. - -An optional `--sandbox /tmp/sbpv` can be used to specify a custom sandbox -location rather than the default. The sandbox contains process logging output -which can be helpful to or otherwise diagnose test process behavior. Changing -the default location via `--sandbox` is often not necessary. If you omit -`--sandbox`, sequential runs default to `./tests/autests/_sandbox`, while -parallel runs default to `/tmp/proxy-verifier-autest`. - -The `-j` option runs AuTests in parallel. Not passing `-j` will run the tests -sequentially, which is significantly slower of course. Use `-j` to speed up -running all the tests to verify that the patch didn't introduce a regression. - -Individual tests can be run via the `-f` option, which takes a set of AuTests -to run, excluding their `.test.py` extension. For example, to run the -`tests/autests/gold_tests/https/https.test.py` and the -`tests/autests/gold_tests/http2/http2.test.py` tests, you would: - -```bash -./build/dev/autest.sh -f https http2 -``` - -The `-f` and `-j` options can be combined when you want a smaller parallel -smoke test: - -```bash -./build/dev/autest.sh -j4 -f http https -``` diff --git a/.agents/skills/run-urtests/SKILL.md b/.agents/skills/run-urtests/SKILL.md new file mode 100644 index 000000000..b210aad05 --- /dev/null +++ b/.agents/skills/run-urtests/SKILL.md @@ -0,0 +1,46 @@ +--- +name: run-urtests +description: Run Proxy Verifier end-to-end tests with the build-generated pytest wrapper. +--- + +# Uranium Tests + +Uranium tests are pytest-based end-to-end tests. Run them through the +build-generated `urtest.sh` so the correct binaries, plugins, and sandbox are +selected. Tests are ordinary `test_*.py` modules under `tests/uranium_tests`. + +# Run Uranium Tests + +After building (see `../build-pv/SKILL.md`), run tests from the build tree: + +```bash + +if [ "`uname`" = "Linux" ] +then + num_threads=$(nproc) +else + num_threads=$(sysctl -n hw.logicalcpu) +fi +./build/dev/urtest.sh -n "${num_threads}" +``` + +An optional `--sandbox /tmp/sbpv` can be used to specify a custom sandbox +location rather than the default. The sandbox contains process logging output +which can help diagnose process behavior. Passed-case sandboxes are removed; +failed-case sandboxes are retained. + +The `-n` option runs tests in parallel. Omit it for a sequential run. + +Use pytest's `-k` option to select tests by name. For example, to run the HTTPS +and HTTP/2 modules: + +```bash +./build/dev/urtest.sh -k 'https or http2' +``` + +The `-k` and `-n` options can be combined when you want a smaller parallel +smoke test: + +```bash +./build/dev/urtest.sh -n 4 -k 'http or https' +``` diff --git a/.agents/skills/writing-autests/SKILL.md b/.agents/skills/writing-autests/SKILL.md deleted file mode 100644 index a59a0b16f..000000000 --- a/.agents/skills/writing-autests/SKILL.md +++ /dev/null @@ -1,14 +0,0 @@ ---- -name: writing-autests -description: Guidance for adding Proxy Verifier autests, the end to end tests. ---- - -# Write ATS AuTests - -AuTest is an end to end testing framework documented here: -https://autestsuite.bitbucket.io/ - -- Tests exis in the `test/autests/gold_tests` directory. -- The extensions to AuTest specific to Proxy Verifier are in the `test/autests/gold_tests/autest-site` directory. -- Tests end with the `.test.py` extension. -- When writing new tests, view the existing `test/autests/gold_tests` `.test.py` files for inspiration. diff --git a/.agents/skills/writing-urtests/SKILL.md b/.agents/skills/writing-urtests/SKILL.md new file mode 100644 index 000000000..2d652808c --- /dev/null +++ b/.agents/skills/writing-urtests/SKILL.md @@ -0,0 +1,45 @@ +--- +name: writing-urtests +description: Guidance for adding Proxy Verifier pytest-based end-to-end tests. +--- + +# Write Uranium Tests + +Uranium is Proxy Verifier's pytest-based end-to-end test framework. + +- Put tests under `tests/uranium_tests` in ordinary `test_*.py` modules. +- Use `CaseSuite` and the process helpers from `tools.uranium`. +- Treat replay YAML, JSON, and gold files as data referenced by Python tests; + they are not collected directly. +- Use existing modules under `tests/uranium_tests` for examples. +- Run tests through the build-generated `urtest.sh` wrapper described in + `../run-urtests/SKILL.md`. + +## Process Expectations + +Register output expectations through the explicit stream API. Both regex +methods require an explanation that describes the intended behavior and is +reported when the expectation fails. + +```python +server.stdout.contains( + "Ready with 3 transactions", + "The server should parse all three transactions.", +) +server.stdout.excludes( + "Violation:", + "The server should not report verification errors.", +) +server.stdout.matches_gold("gold/server.gold") +``` + +Use `server.stdout.reset()` to intentionally discard all expectations already +registered for that stream. The reset affects expectations only; it does not +change the captured output path. + +The `stdout` and `stderr` properties are read-only. Do not use `=` or `+=` to +register expectations. Configure acceptable process exit statuses explicitly: + +```python +client.expect_return_codes(0, 1) +``` diff --git a/.dockerignore b/.dockerignore index bd7b6a912..942f4b348 100644 --- a/.dockerignore +++ b/.dockerignore @@ -2,5 +2,5 @@ .github .venv build -tests/autests/.venv +tests/.venv **/__pycache__ diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 37b151252..f83888ff4 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -52,7 +52,7 @@ jobs: --input-exclude "**/*.lock" \ --input-exclude "**/*.md" "**/*.json" "**/*.yaml" "**/*.gold" "**/*.pem" \ --input-exclude "tests/unit_tests/catch.hpp" \ - --input-exclude "tests/autests/gold_tests/autest-site/default_url_file" \ + --input-exclude "tests/tools/uranium/default_url_file" \ --input-exclude-std GIT HIDDEN_DIR HIDDEN_FILE -- \ "${GITHUB_WORKSPACE}" | tee "${RUNNER_TEMP}/RAT.txt" @@ -113,8 +113,8 @@ jobs: path: build/dev/Testing if-no-files-found: ignore - autests: - name: AuTests (Ubuntu 26.04, GCC) + urtests: + name: Uranium tests (Ubuntu 26.04, GCC) runs-on: ubuntu-24.04 timeout-minutes: 60 container: @@ -127,28 +127,28 @@ jobs: uses: actions/checkout@v6 - name: Configure - run: cmake --preset dev + run: cmake --preset dev -DURTEST_SANDBOX=/tmp/proxy-verifier-urtest - name: Build run: cmake --build --preset dev --parallel - - name: Run AuTests - run: ./build/dev/autest.sh -v --clean=none -j"$(nproc)" + - name: Run Uranium tests + run: ./build/dev/urtest.sh -v -n "$(nproc)" - - name: Collect AuTest sandbox + - name: Collect Uranium sandbox if: failure() run: | mkdir -p artifacts - if [[ -d /tmp/proxy-verifier-autest ]]; then + if [[ -d /tmp/proxy-verifier-urtest ]]; then tar --create --gzip \ - --file="${GITHUB_WORKSPACE}/artifacts/autest-sandbox.tar.gz" \ - --directory=/tmp proxy-verifier-autest + --file="${GITHUB_WORKSPACE}/artifacts/urtest-sandbox.tar.gz" \ + --directory=/tmp proxy-verifier-urtest fi - - name: Upload AuTest sandbox + - name: Upload Uranium sandbox if: failure() uses: actions/upload-artifact@v6 with: - name: autest-sandbox - path: artifacts/autest-sandbox.tar.gz + name: urtest-sandbox + path: artifacts/urtest-sandbox.tar.gz if-no-files-found: ignore diff --git a/.gitignore b/.gitignore index a232b918a..9a9cbbe71 100644 --- a/.gitignore +++ b/.gitignore @@ -67,7 +67,7 @@ share/ *.lock !uv.lock -!tests/autests/uv.lock +!tests/uv.lock # vim temporary files *.sw[a-p] diff --git a/AGENTS.md b/AGENTS.md index 968ebd4bc..62f000105 100644 --- a/AGENTS.md +++ b/AGENTS.md @@ -16,9 +16,9 @@ existing project style and preserve current CLI and YAML behavior. as compatibility-sensitive. - Update `README.md` when behavior, flags, workflows, or replay-file semantics change. -- For new features and bug fixes, always consider whether unit tests and AuTests - should be added or updated. See .agents/skills/write-autests/SKILL.md for how to - write AuTests. +- For new features and bug fixes, always consider whether unit tests and + Uranium tests should be added or updated. See + `.agents/skills/writing-urtests/SKILL.md` for how to write Uranium tests. ## Build And Test @@ -32,14 +32,14 @@ cmake --build --preset dev --parallel ctest --preset dev ``` -For end-to-end coverage, use the generated AuTest wrapper from the build tree: +For end-to-end coverage, use the generated Uranium wrapper from the build tree: ```bash -./build/dev/autest.sh +./build/dev/urtest.sh ``` -When a change is localized, prefer running the relevant AuTest subset with -`-f ...` instead of the full suite. +When a change is localized, prefer running the relevant Uranium subset with +`-k ...` instead of the full suite. ## Formatting And File Structure @@ -55,10 +55,10 @@ When a change is localized, prefer running the relevant AuTest subset with it. - Almost all new files should contain the Apache license preface with this year referenced as the copyright, except of course files that cannot contain such - comments, such as `.json` files and autest `.gold` files. -- Replay YAML files, including AuTest replay files under `tests/autests`, also - need the same commented file prologue (`# @file`, copyright, SPDX). Do not - add new replay YAMLs without that header. + comments, such as `.json` and `.gold` files. +- Replay YAML files, including Uranium replay files under + `tests/uranium_tests`, also need the same commented file prologue (`# @file`, + copyright, SPDX). Do not add new replay YAMLs without that header. ## C++ Style @@ -130,7 +130,7 @@ do_something(!IGNORE_CASE); naming with descriptive behavior-oriented titles. - Table-driven tests and designated initializers are already used and are good patterns to continue when they improve clarity. -- End-to-end behavior belongs in AuTests under `tests/autests/gold_tests`. +- End-to-end behavior belongs in Uranium tests under `tests/uranium_tests`. - For parser, protocol, replay, CLI, or compatibility fixes, prefer adding a regression test alongside the code change. @@ -142,4 +142,4 @@ Before finishing a change, sanity-check: - Does it preserve existing YAML schema and replay semantics? - Does it keep current diagnostics and docs accurate enough? - Should `README.md` change? -- Should a unit test or AuTest be added? +- Should a unit test or Uranium test be added? diff --git a/CMakeLists.txt b/CMakeLists.txt index 8b3fe6d9a..3c6d08f45 100644 --- a/CMakeLists.txt +++ b/CMakeLists.txt @@ -154,5 +154,15 @@ include(HttpDependencies) pv_find_http_dependencies(PV_HTTP_DEPENDENCY_TARGETS) add_subdirectory(src) -add_subdirectory(tests/autests) +string(SHA256 _pv_source_hash "${CMAKE_SOURCE_DIR}") +string(SUBSTRING "${_pv_source_hash}" 0 8 _pv_source_hash) +set(URTEST_SANDBOX + "/tmp/proxy-verifier-urtest-${_pv_source_hash}" + CACHE PATH "Location for Uranium test output") +set(URTEST_OPTIONS + "" + CACHE STRING + "Additional pytest options supplied by the Uranium test target") + +add_subdirectory(tests) add_subdirectory(tests/unit_tests) diff --git a/CONTRIBUTING.md b/CONTRIBUTING.md index 6d4fcc48a..f9976fab9 100644 --- a/CONTRIBUTING.md +++ b/CONTRIBUTING.md @@ -20,7 +20,7 @@ When your code is ready to be submitted, read github's article about how to [sub 1. Run `tools/pr-prep.sh` to format the source code and update copyright years according to the repository's standards. -1. Verify that all AuTests pass (see the [README](https://github.com/yahoo/proxy-verifier/blob/master/README.md) for instructions on how to run the AuTests). +1. Verify that all Uranium tests pass (see the [README](https://github.com/yahoo/proxy-verifier/blob/master/README.md) for instructions). 1. Verify that all unit tests pass (see the [README](https://github.com/yahoo/proxy-verifier/blob/master/README.md) for instructions on how to run the unit tests). We only seek to accept code that you are authorized to contribute to the project. We have added a pull request template on our projects so that your contributions are made with the following confirmation: diff --git a/NOTICES b/NOTICES index aef78d066..94cd3f6bc 100644 --- a/NOTICES +++ b/NOTICES @@ -7,7 +7,7 @@ SPDX-License-Identifier: Apache-2.0 proxy_http1.py -The test HTTP/1.x proxy named tests/autests/gold_tests/autest-site/proxy_http1.py +The test HTTP/1.x proxy named tests/tools/uranium/proxy/proxy_http1.py was initially copied from: https://github.com/inaz2/proxy2/tree/50ae4b9ef7fbc51799f95e405d4d7adf1af239f7 @@ -54,7 +54,7 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. proxy_http2.py -The test HTTP/2 proxy named tests/autests/gold_tests/autest-site/proxy_http2.py +The test HTTP/2 proxy named tests/tools/uranium/proxy/proxy_http2.py was initially copied from: https://github.com/python-hyper/hyper-h2/blob/e0a3e2eb5449b78ab811f32b7672bfd4e1033cf0/examples/eventlet/eventlet-server.py diff --git a/README.md b/README.md index 580d4a9df..40eaa53d3 100644 --- a/README.md +++ b/README.md @@ -41,8 +41,6 @@ Table of Contents * [Building from Source](#building-from-source) * [Prerequisites](#prerequisites) * [Build](#build) - * [Bootstrapping Dependencies Under CMake](#bootstrapping-dependencies-under-cmake) - * [Using Prebuilt Libraries](#using-prebuilt-libraries) * [Development Docker Images](#development-docker-images) * [Install](#install) * [ASan Instrumentation](#asan-instrumentation) @@ -51,7 +49,7 @@ Table of Contents * [Native Build](#native-build) * [Running the Tests](#running-the-tests) * [Unit Tests](#unit-tests) - * [Gold Tests](#gold-tests) + * [Uranium Tests](#uranium-tests) * [Usage](#usage) * [Required Arguments](#required-arguments) * [Optional Arguments](#optional-arguments) @@ -111,9 +109,7 @@ needs: in manual or automated end to end tests to verify correct behavior of the details of generally a small number of transactions. [Transaction Box](https://github.com/SolidWallOfCode/txn_box) is an example of a tool that - relies entirely on Proxy Verifier for its automated end to end tests (see its - [autest](https://github.com/SolidWallOfCode/txn_box/tree/master/test/autest) - directory). + relies entirely on Proxy Verifier for its automated end to end tests. * **Production simulation testing**: In this context, Proxy Verifier is used in an isolated lab environment configured as much like a production environment as possible. The Verifier client and server are provided replay files as @@ -145,9 +141,8 @@ configure the proxy under test to direct these requests not to the actual production servers but to the Proxy Verifier server or servers. The way this is achieved will depend upon the proxy. One way to accomplish this is to configure the production environment with a test DNS server such as -[MicroDNS](https://bitbucket.org/autestsuite/microdns/src/master/) configured -to resolve all host names to the Proxy Verifier server or servers in the -production simulation environment. +MicroDNS configured to resolve all host names to the Proxy Verifier server or +servers in the production simulation environment. ## Traffic Replay Specification @@ -1874,7 +1869,8 @@ This places the build-tree binaries under `build/dev/bin`. The Dockerfiles under `docker/alpine_3.24`, `docker/fedora_44`, and `docker/ubuntu_26.04` are build and development environments, not deployment images. Each installs OpenSSL, nghttp2, and nghttp3 from its distribution along -with the Proxy Verifier build, formatting, license-audit, and AuTest toolchains. +with the Proxy Verifier build, formatting, license-audit, and Uranium/pytest +toolchains. GitHub Actions uses only the Ubuntu 26.04 image. Alpine 3.24 is used to build the portable Linux release binaries, while Fedora 44 provides an additional current-distribution development environment. Users who prefer Alpine or @@ -1901,7 +1897,7 @@ cd /workspace cmake --preset dev cmake --build --preset dev --parallel ctest --preset dev -./build/dev/autest.sh --sandbox /tmp/pv-autest --clean=none +./build/dev/urtest.sh --sandbox /tmp/pv-urtest -n "$(nproc)" ``` #### Install @@ -2011,67 +2007,60 @@ cmake --build --preset dev --parallel ctest --preset dev ``` -#### Gold Tests -Proxy Verifier ships with a set of automated end to end tests written using the -[AuTest](https://bitbucket.org/autestsuite/reusable-gold-testing-system/src/master/) -framework. After configuring a preset, CMake generates an `autest.sh` wrapper -in that build directory. To run the tests for `dev`, use: +#### Uranium Tests + +Proxy Verifier's end-to-end tests use the pytest-based Uranium framework. +Tests live under `tests/uranium_tests` as ordinary `test_*.py` modules; replay +YAML, JSON, and gold files in those directories are test data. After +configuring a preset, CMake generates an `urtest.sh` wrapper in that build +directory. To run the tests for `dev`, use: ``` -./build/dev/autest.sh +./build/dev/urtest.sh ``` -That wrapper defaults `VERIFIER_BIN` to its sibling `bin/` directory, so each -build tree uses its own `verifier-client` and `verifier-server` binaries. If -you bypass the wrapper and invoke AuTest directly, binary lookup still follows -this order: explicit `--verifier-bin`, then `VERIFIER_BIN`, then -`/usr/local/bin`. +That wrapper selects the `verifier-client` and `verifier-server` binaries from +its build tree and creates an isolated sandbox for each test case. Passed-case +sandboxes are removed, while failed-case sandboxes are retained for diagnosis. +It also enables `cdifflib` to accelerate unified-diff diagnostics for failed +gold-file comparisons; the framework falls back to Python's standard-library +matcher if the optional extension is unavailable. -When doing development for which a particular AuTest is relevant, the `-f` -option can be used to run just a particular test (or set of tests, specified -in a space-separated list). For instance, the following invocation runs -just the http and https tests: +Use pytest's `-k` option to run a matching subset. For example, this runs the +HTTP and HTTPS tests: ``` -./build/dev/autest.sh -f http https +./build/dev/urtest.sh -k 'http or https' ``` -The build-generated wrapper also accepts `-j` or `--jobs` for parallel runs. -That path uses worker-specific sandboxes and port offsets so the top-level -AuTests do not stomp on each other: +Use pytest-xdist's `-n` option for parallel runs. Worker processes use separate +sandboxes and a shared port allocator: ``` -./build/dev/autest.sh -j8 -f http https +./build/dev/urtest.sh -n 8 -k 'http or https' ``` -AuTest supports a variety of other options. Run -`./build/dev/autest.sh --help` to get a -quick description of the various command-line options. See the [AuTest -Documentation](https://autestsuite.bitbucket.io) for further details about the -framework. - -**A note for macOS**: The Python virtual environment for these gold tests -requires the [cryptograpy](https://github.com/pyca/cryptography) package as a -dependency of the [pyOpenSSL](https://www.pyopenssl.org/en/stable/) package. -The build-generated `autest.sh` wrapper installs the test environment with -`uv sync --locked`, but the installation of the -`cryptography` package will require compiling certain c files against OpenSSL. -macOS has its own SSL libraries which brew's version of OpenSSL does not -replace, for understandable reasons. The building of `cryptography` will -fail against the system's SSL libraries. To point the build to brew's OpenSSL -libraries, the `autest.sh` script exports the following variables before -running `uv sync --locked`: +Register process-output expectations with the explicit stream API. Regex +expectations require an explanation so failures report the intended behavior: +```python +server.stdout.contains("Ready with 3 transactions", + "The server should parse all three transactions.") +server.stdout.excludes("Violation:", + "The server should not report verification errors.") +server.stdout.matches_gold("gold/server.gold") +client.expect_return_codes(0, 1) ``` -export LDFLAGS="-L/usr/local/opt/openssl/lib" -export CPPFLAGS="-I/usr/local/opt/openssl/include" -export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig" -``` -Thus if you stick with using the `autest.sh` script you do not need to worry -about this. But if you install the test environment by hand rather than through the -`autest.sh` script on macOS, then keep this in mind and export those variables -before running `uv sync --locked`. +The `stdout`, `stderr`, and `return_codes` properties are read-only; assignment +and augmented assignment are not expectation-registration APIs. Use +`server.stdout.reset()` when earlier stream expectations must be discarded +explicitly. + +The wrapper accepts normal pytest options; run `./build/dev/urtest.sh --help` +for the complete list. Use `--sandbox PATH` to select the sandbox root. CI +sharding is also supported with the zero-based `SHARD` and `SHARDCNT` +environment variables. ## Usage diff --git a/tests/CMakeLists.txt b/tests/CMakeLists.txt new file mode 100644 index 000000000..8113e150c --- /dev/null +++ b/tests/CMakeLists.txt @@ -0,0 +1,33 @@ +# @file +# +# Copyright 2026, Verizon Media SPDX-License-Identifier: Apache-2.0 +# + +set(PV_URTEST_WRAPPER "${CMAKE_BINARY_DIR}/urtest.sh") +set(PV_URTEST_PROJECT_DIRECTORY "${CMAKE_BINARY_DIR}/tests") + +file(MAKE_DIRECTORY "${PV_URTEST_PROJECT_DIRECTORY}") +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/pyproject.toml" + "${PV_URTEST_PROJECT_DIRECTORY}/pyproject.toml" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/uv.lock" + "${PV_URTEST_PROJECT_DIRECTORY}/uv.lock" COPYONLY) +configure_file("${CMAKE_CURRENT_SOURCE_DIR}/urtest.sh.in" + "${PV_URTEST_WRAPPER}" @ONLY) + +file( + CHMOD + "${PV_URTEST_WRAPPER}" + PERMISSIONS + OWNER_READ + OWNER_WRITE + OWNER_EXECUTE + GROUP_READ + GROUP_EXECUTE + WORLD_READ + WORLD_EXECUTE) + +add_custom_target( + urtest + COMMAND "${PV_URTEST_WRAPPER}" + DEPENDS verifier-client verifier-server + USES_TERMINAL) diff --git a/tests/autests/CMakeLists.txt b/tests/autests/CMakeLists.txt deleted file mode 100644 index 92f26eb49..000000000 --- a/tests/autests/CMakeLists.txt +++ /dev/null @@ -1,25 +0,0 @@ -# @file -# -# Copyright 2026, Verizon Media SPDX-License-Identifier: Apache-2.0 -# - -set(PV_AUTEST_WRAPPER "${CMAKE_BINARY_DIR}/autest.sh") -set(PV_AUTEST_PARALLEL_RUNNER "${CMAKE_BINARY_DIR}/autest-parallel.py") - -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/autest.sh.in" - "${PV_AUTEST_WRAPPER}" @ONLY) -configure_file("${CMAKE_CURRENT_SOURCE_DIR}/autest-parallel.py.in" - "${PV_AUTEST_PARALLEL_RUNNER}" @ONLY) - -file( - CHMOD - "${PV_AUTEST_WRAPPER}" - "${PV_AUTEST_PARALLEL_RUNNER}" - PERMISSIONS - OWNER_READ - OWNER_WRITE - OWNER_EXECUTE - GROUP_READ - GROUP_EXECUTE - WORLD_READ - WORLD_EXECUTE) diff --git a/tests/autests/autest-parallel.py.in b/tests/autests/autest-parallel.py.in deleted file mode 100644 index 52c353918..000000000 --- a/tests/autests/autest-parallel.py.in +++ /dev/null @@ -1,688 +0,0 @@ -#!/usr/bin/env python3 -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -""" -Parallel AuTest runner for Proxy Verifier. - -This wrapper partitions top-level tests across workers, gives each worker an -isolated sandbox, and offsets the per-test port allocator so `autest.sh -jN` -does not collide on localhost ports. -""" - -import argparse -import fnmatch -import json -import os -import re -import signal -import subprocess -import sys -import time -from concurrent.futures import ProcessPoolExecutor, as_completed -from dataclasses import dataclass, field -from pathlib import Path -from typing import Dict, List, Optional, Tuple - -SOURCE_AUTEST_DIR = Path("@CMAKE_CURRENT_SOURCE_DIR@").resolve() -DEFAULT_TEST_DIR = SOURCE_AUTEST_DIR / "gold_tests" -DEFAULT_SERIAL_TESTS_FILE = SOURCE_AUTEST_DIR / "serial_tests.txt" -DEFAULT_SANDBOX = Path("/tmp/proxy-verifier-autest") -DEFAULT_TEST_TIME = 15.0 - - -@dataclass -class TestResult: - worker_id: int - tests: List[str] - passed: int = 0 - failed: int = 0 - skipped: int = 0 - warnings: int = 0 - exceptions: int = 0 - unknown: int = 0 - duration: float = 0.0 - failed_tests: List[str] = field(default_factory=list) - test_timings: Dict[str, float] = field(default_factory=dict) - output: str = "" - return_code: int = 0 - is_serial: bool = False - - -def resolve_test_dir(test_dir: str) -> Path: - path = Path(test_dir) - if not path.is_absolute(): - path = SOURCE_AUTEST_DIR / path - return path.resolve() - - -def discover_tests(test_dir: Path, filter_patterns: Optional[List[str]] = None) -> List[str]: - tests = [] - for test_file in test_dir.rglob("*.test.py"): - test_name = test_file.stem.replace(".test", "") - if filter_patterns and not any(fnmatch.fnmatch(test_name, pattern) for pattern in filter_patterns): - continue - tests.append(test_name) - return sorted(tests) - - -def load_serial_tests(serial_file: Path) -> set[str]: - serial_tests: set[str] = set() - if not serial_file.exists(): - return serial_tests - - try: - with serial_file.open() as handle: - for line in handle: - line = line.strip() - if not line or line.startswith("#"): - continue - if line.endswith(".test.py"): - line = line[:-8] - serial_tests.add(Path(line).stem.replace(".test", "")) - except IOError: - pass - return serial_tests - - -def load_timings(timing_file: Path) -> Dict[str, float]: - if timing_file.exists(): - try: - with timing_file.open() as handle: - return json.load(handle) - except (IOError, json.JSONDecodeError): - pass - return {} - - -def save_timings(timing_file: Path, timings: Dict[str, float]) -> None: - try: - with timing_file.open("w") as handle: - json.dump(timings, handle, indent=2, sort_keys=True) - except IOError as exc: - print(f"Warning: could not save timings to {timing_file}: {exc}", file=sys.stderr) - - -def partition_tests(tests: List[str], num_jobs: int) -> List[List[str]]: - if not tests: - return [] - worker_count = max(1, min(num_jobs, len(tests))) - partitions = [[] for _ in range(worker_count)] - for index, test in enumerate(tests): - partitions[index % worker_count].append(test) - return [partition for partition in partitions if partition] - - -def partition_tests_by_time( - tests: List[str], num_jobs: int, timings: Dict[str, float] -) -> Tuple[List[List[str]], List[float]]: - if not tests: - return [], [] - - worker_count = max(1, min(num_jobs, len(tests))) - partitions = [[] for _ in range(worker_count)] - worker_loads = [0.0] * worker_count - unknown_tests = [] - known_tests = [] - - for test in tests: - if test in timings: - known_tests.append((test, timings[test])) - else: - unknown_tests.append(test) - - known_tests.sort(key=lambda item: item[1], reverse=True) - - for test, duration in known_tests: - worker_id = min(range(worker_count), key=lambda idx: worker_loads[idx]) - partitions[worker_id].append(test) - worker_loads[worker_id] += duration - - for test in unknown_tests: - worker_id = min(range(worker_count), key=lambda idx: worker_loads[idx]) - partitions[worker_id].append(test) - worker_loads[worker_id] += DEFAULT_TEST_TIME - - return partitions, worker_loads - - -def strip_ansi(text: str) -> str: - ansi_escape = re.compile(r"\x1B(?:[@-Z\\-_]|\[[0-?]*[ -/]*[@-~])") - return ansi_escape.sub("", text) - - -def parse_autest_output(output: str) -> dict: - result = { - "passed": 0, - "failed": 0, - "skipped": 0, - "warnings": 0, - "exceptions": 0, - "unknown": 0, - "failed_tests": [], - } - - clean_output = strip_ansi(output) - lines = clean_output.splitlines() - - for line in lines: - stripped = line.strip() - if "Passed:" in stripped: - try: - result["passed"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - elif "Failed:" in stripped and "test" not in stripped.lower(): - try: - result["failed"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - elif "Skipped:" in stripped: - try: - result["skipped"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - elif "Warning:" in stripped: - try: - result["warnings"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - elif "Exception:" in stripped: - try: - result["exceptions"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - elif "Unknown:" in stripped: - try: - result["unknown"] = int(stripped.split(":")[-1].strip()) - except ValueError: - pass - - failed_pattern = re.compile(r"Test:\s+(\S+):\s+Failed", re.IGNORECASE) - for match in failed_pattern.finditer(clean_output): - result["failed_tests"].append(match.group(1)) - - return result - - -def extract_failure_output(output: str, failed_tests: List[str]) -> Dict[str, str]: - if not failed_tests: - return {} - - clean_output = strip_ansi(output) - lines = clean_output.splitlines() - block_starts = [] - block_pattern = re.compile(r"^Test:\s+(\S+):\s+(Passed|Failed|Skipped)", re.IGNORECASE) - - for index, line in enumerate(lines): - match = block_pattern.match(line.strip()) - if match: - block_starts.append((index, match.group(1))) - - details: Dict[str, str] = {} - wanted = set(failed_tests) - for index, (start_line, test_name) in enumerate(block_starts): - if test_name not in wanted: - continue - end_line = block_starts[index + 1][0] if index + 1 < len(block_starts) else len(lines) - block = "\n".join(lines[start_line:end_line]).rstrip() - if block: - details[test_name] = block - return details - - -def build_autest_command(test_dir: Path, sandbox: Path, extra_args: List[str], tests: Optional[List[str]] = None) -> List[str]: - cmd = [ - "uv", - "run", - "autest", - "run", - "-D", - str(test_dir), - "--sandbox", - str(sandbox), - ] - if tests: - cmd.extend(["-f", *tests]) - cmd.extend(extra_args) - return cmd - - -def run_single_test(test: str, test_dir: Path, sandbox: Path, extra_args: List[str], env: dict) -> Tuple[str, float, str, str]: - cmd = build_autest_command(test_dir, sandbox / test, extra_args, [test]) - start = time.time() - try: - proc = subprocess.run( - cmd, - cwd=SOURCE_AUTEST_DIR, - capture_output=True, - text=True, - env=env, - timeout=900, - ) - except subprocess.TimeoutExpired: - return test, 900.0, "FAIL", "TIMEOUT" - except Exception as exc: - return test, time.time() - start, "FAIL", str(exc) - - duration = time.time() - start - output = proc.stdout + proc.stderr - parsed = parse_autest_output(output) - if parsed["skipped"] > 0 and parsed["passed"] == 0 and parsed["failed"] == 0: - status = "SKIP" - elif parsed["failed"] == 0 and parsed["exceptions"] == 0 and proc.returncode == 0 and ( - parsed["passed"] > 0 or parsed["skipped"] > 0 - ): - status = "PASS" - else: - status = "FAIL" - return test, duration, status, output - - -def run_worker( - worker_id: int, - tests: List[str], - test_dir: Path, - sandbox_base: Path, - extra_args: List[str], - port_offset_step: int, - verbose: bool, - collect_timings: bool, -) -> TestResult: - start_time = time.time() - result = TestResult(worker_id=worker_id, tests=tests) - sandbox = sandbox_base / f"worker-{worker_id}" - sandbox.mkdir(parents=True, exist_ok=True) - - env = os.environ.copy() - env["AUTEST_PORT_OFFSET"] = str(worker_id * port_offset_step) - - if verbose: - timestamp = time.strftime("%H:%M:%S") - print(f"[{timestamp}] Worker:{worker_id:2d} Starting {len(tests)} tests", flush=True) - - try: - if collect_timings: - output_chunks = [] - for test in tests: - test_name, duration, status, output = run_single_test(test, test_dir, sandbox, extra_args, env) - result.test_timings[test_name] = duration - output_chunks.append(output) - if status == "PASS": - result.passed += 1 - elif status == "SKIP": - result.skipped += 1 - else: - result.failed += 1 - result.failed_tests.append(test_name) - if verbose: - timestamp = time.strftime("%H:%M:%S") - print(f"[{timestamp}] Worker:{worker_id:2d} {status:4s} {duration:6.1f}s {test_name}", flush=True) - result.output = "\n".join(output_chunks) - result.return_code = 0 if result.failed == 0 else 1 - else: - cmd = build_autest_command(test_dir, sandbox, extra_args, tests) - if verbose: - proc = subprocess.Popen( - cmd, - cwd=SOURCE_AUTEST_DIR, - stdout=subprocess.PIPE, - stderr=subprocess.STDOUT, - text=True, - env=env, - ) - output_lines = [] - try: - if proc.stdout is not None: - for line in proc.stdout: - output_lines.append(line) - clean = strip_ansi(line).strip() - if clean.startswith("Running Test") or "Passed" in clean or "Failed" in clean: - timestamp = time.strftime("%H:%M:%S") - print(f"[{timestamp}] Worker:{worker_id:2d} {clean}", flush=True) - proc.wait(timeout=60) - finally: - if proc.poll() is None: - proc.terminate() - try: - proc.wait(timeout=5) - except subprocess.TimeoutExpired: - proc.kill() - proc.wait() - result.output = "".join(output_lines) - result.return_code = proc.returncode - else: - proc = subprocess.run( - cmd, - cwd=SOURCE_AUTEST_DIR, - capture_output=True, - text=True, - env=env, - timeout=3600, - ) - result.output = proc.stdout + proc.stderr - result.return_code = proc.returncode - parsed = parse_autest_output(result.output) - result.passed = parsed["passed"] - result.failed = parsed["failed"] - result.skipped = parsed["skipped"] - result.warnings = parsed["warnings"] - result.exceptions = parsed["exceptions"] - result.unknown = parsed["unknown"] - result.failed_tests = parsed["failed_tests"] - - total_ran = result.passed + result.failed + result.skipped - if total_ran == 0 and proc.returncode != 0: - result.failed = len(tests) - result.failed_tests = list(tests) - except subprocess.TimeoutExpired: - result.output = "TIMEOUT: worker exceeded 1 hour timeout" - result.return_code = -1 - result.failed = len(tests) - result.failed_tests = list(tests) - except KeyboardInterrupt: - result.output = "INTERRUPTED by user" - result.return_code = 130 - result.failed = len(tests) - result.failed_tests = list(tests) - except Exception as exc: - result.output = f"ERROR: {exc}" - result.return_code = -1 - result.failed = len(tests) - result.failed_tests = list(tests) - - result.duration = time.time() - start_time - return result - - -def print_summary(results: List[TestResult], total_duration: float) -> None: - total_passed = sum(result.passed for result in results) - total_failed = sum(result.failed for result in results) - total_skipped = sum(result.skipped for result in results) - total_warnings = sum(result.warnings for result in results) - total_exceptions = sum(result.exceptions for result in results) - total_unknown = sum(result.unknown for result in results) - total_tests = sum(len(result.tests) for result in results) - - seen = set() - failed_tests = [] - for result in results: - for test_name in result.failed_tests: - if test_name not in seen: - seen.add(test_name) - failed_tests.append(test_name) - - print("\n" + "=" * 70) - print("PARALLEL AUTEST SUMMARY") - print("=" * 70) - print(f"Workers: {len(results)}") - print(f"Total tests: {total_tests}") - print(f"Duration: {total_duration:.1f}s") - print("-" * 70) - print(f" Passed: {total_passed}") - print(f" Failed: {total_failed}") - print(f" Skipped: {total_skipped}") - print(f" Warnings: {total_warnings}") - print(f" Exceptions: {total_exceptions}") - print(f" Unknown: {total_unknown}") - - if failed_tests: - print("-" * 70) - print("FAILED TESTS:") - for test_name in sorted(failed_tests): - print(f" - {test_name}") - print("-" * 70) - print("FAILED TEST OUTPUT:") - print("-" * 70) - for result in results: - for test_name, detail in sorted(extract_failure_output(result.output, result.failed_tests).items()): - print(f"\n--- {test_name} ---") - print(detail) - print("-" * 70) - - print("=" * 70) - print("\nPer-worker breakdown:") - for result in results: - status = "OK" if result.failed == 0 and result.exceptions == 0 else "FAIL" - worker_label = " Serial: " if result.is_serial else f" Worker:{result.worker_id:2d}" - print( - f"{worker_label} {result.passed:3d} passed, {result.failed:3d} failed, " - f"{result.skipped:3d} skipped ({result.duration:6.1f}s) [{status}]" - ) - print("-" * 70) - print( - f" TOTAL: {total_passed:3d} passed, {total_failed:3d} failed, " - f"{total_skipped:3d} skipped ({total_duration:6.1f}s)" - ) - - -def format_eta(elapsed: float, done: int, total: int) -> str: - if done <= 0 or done >= total: - return "--:--" - remaining = (elapsed / done) * (total - done) - minutes, seconds = divmod(int(remaining), 60) - if minutes >= 60: - hours, minutes = divmod(minutes, 60) - return f"{hours}h {minutes:02d}m" - return f"{minutes}m {seconds:02d}s" - - -def print_progress(phase: str, tests_done: int, total_tests: int, workers_done: int, total_workers: int, tests_failed: int, tests_skipped: int, start_time: float) -> None: - elapsed = time.time() - start_time - fail_text = f" | {tests_failed} FAILED" if tests_failed else "" - skip_text = f" | {tests_skipped} skipped" if tests_skipped else "" - percent = (tests_done * 100 // total_tests) if total_tests else 0 - line = ( - f"\r[{phase}] {tests_done}/{total_tests} tests ({percent}%) " - f"| {workers_done}/{total_workers} workers done" - f"{fail_text}{skip_text} | {int(elapsed)}s elapsed | ETA: {format_eta(elapsed, tests_done, total_tests)} " - ) - print(line, end="", flush=True) - - -def sigterm_handler(_signum, _frame) -> None: - raise KeyboardInterrupt - - -def main() -> None: - signal.signal(signal.SIGTERM, sigterm_handler) - - parser = argparse.ArgumentParser(description="Run Proxy Verifier AuTests in parallel") - parser.add_argument("-j", "--jobs", type=int, default=os.cpu_count() or 4, help="Number of parallel workers") - parser.add_argument("--sandbox", default=str(DEFAULT_SANDBOX), help="Base sandbox directory") - parser.add_argument("-f", "--filters", nargs="+", action="extend", dest="filters", help="Filter tests by name or glob") - parser.add_argument("-D", "--directory", default="gold_tests", help="AuTest test directory") - parser.add_argument("--port-offset-step", type=int, default=1000, help="Port offset between workers") - parser.add_argument("-v", "--verbose", action="store_true", help="Stream test progress") - parser.add_argument("--collect-timings", action="store_true", help="Run tests one at a time per worker") - parser.add_argument("--timings-file", type=Path, default=None, help="Timing data JSON file") - parser.add_argument("--no-timing", action="store_true", help="Disable timing-based load balancing") - parser.add_argument( - "--serial-tests-file", - type=Path, - default=DEFAULT_SERIAL_TESTS_FILE, - help=f"File listing tests that must run serially (default: {DEFAULT_SERIAL_TESTS_FILE})", - ) - parser.add_argument("--no-serial", action="store_true", help="Do not run serial-only tests") - args, extra_args = parser.parse_known_args() - - if extra_args and extra_args[0] == "run": - extra_args = extra_args[1:] - elif extra_args and extra_args[0] == "list": - print("The parallel runner does not support the explicit 'list' subcommand.", file=sys.stderr) - print("Run autest.sh without -j to list tests.", file=sys.stderr) - sys.exit(2) - - if args.timings_file is None: - args.timings_file = Path(args.sandbox) / "test-timings.json" - - test_dir = resolve_test_dir(args.directory) - if not test_dir.exists(): - print(f"Error: test directory not found: {test_dir}", file=sys.stderr) - sys.exit(1) - - all_tests = discover_tests(test_dir, args.filters) - if not all_tests: - print("No tests found matching the specified filters.", file=sys.stderr) - sys.exit(1) - - serial_tests = load_serial_tests(args.serial_tests_file) - parallel_tests = [test for test in all_tests if test not in serial_tests] - serial_tests_to_run = [test for test in all_tests if test in serial_tests] - - print(f"Found {len(all_tests)} tests ({len(parallel_tests)} parallel, {len(serial_tests_to_run)} serial)") - - timings = {} - if not args.no_timing: - timings = load_timings(args.timings_file) - if timings: - known_tests = sum(1 for test in parallel_tests if test in timings) - print(f"Loaded timing data for {known_tests}/{len(parallel_tests)} parallel tests from {args.timings_file}") - - if timings and parallel_tests and not args.no_timing: - partitions, expected_loads = partition_tests_by_time(parallel_tests, args.jobs, timings) - print("Using timing-based load balancing") - for worker_id, load in enumerate(expected_loads): - preview = ", ".join(partitions[worker_id][:3]) - suffix = f", ... (+{len(partitions[worker_id]) - 3} more)" if len(partitions[worker_id]) > 3 else "" - print(f" Worker {worker_id}: {len(partitions[worker_id])} tests, ~{load:.1f}s expected [{preview}{suffix}]") - else: - partitions = partition_tests(parallel_tests, args.jobs) - print("Using round-robin partitioning") - - total_tests = len(parallel_tests) + (0 if args.no_serial else len(serial_tests_to_run)) - print(f"Running with {len(partitions)} parallel workers") - print(f"Total: {total_tests} tests") - print(f"Sandbox: {args.sandbox}") - print(f"Port offset step: {args.port_offset_step}") - - sandbox_base = Path(args.sandbox) - sandbox_base.mkdir(parents=True, exist_ok=True) - - tests_done = 0 - tests_failed = 0 - tests_skipped = 0 - workers_done = 0 - total_workers = len(partitions) - results: List[TestResult] = [] - futures = {} - start_time = time.time() - interrupted = False - serial_result = None - serial_start = None - - try: - if partitions: - print_progress("Parallel", tests_done, total_tests, workers_done, total_workers, tests_failed, tests_skipped, start_time) - print() - with ProcessPoolExecutor(max_workers=len(partitions)) as executor: - for worker_id, worker_tests in enumerate(partitions): - future = executor.submit( - run_worker, - worker_id, - worker_tests, - test_dir, - sandbox_base, - extra_args, - args.port_offset_step, - args.verbose, - args.collect_timings, - ) - futures[future] = worker_id - - for future in as_completed(futures): - worker_id = futures[future] - try: - result = future.result() - except Exception as exc: - result = TestResult( - worker_id=worker_id, - tests=partitions[worker_id], - failed=len(partitions[worker_id]), - failed_tests=list(partitions[worker_id]), - output=str(exc), - return_code=-1, - ) - results.append(result) - workers_done += 1 - tests_done += len(result.tests) - tests_failed += len(set(result.failed_tests)) - tests_skipped += result.skipped - print_progress("Parallel", tests_done, total_tests, workers_done, total_workers, tests_failed, tests_skipped, start_time) - print() - - if serial_tests_to_run and not args.no_serial: - print("=" * 70) - print("RUNNING SERIAL TESTS") - print("=" * 70) - serial_start = time.time() - serial_result = TestResult(worker_id=len(partitions), tests=serial_tests_to_run, is_serial=True) - env = os.environ.copy() - env["AUTEST_PORT_OFFSET"] = "0" - serial_sandbox = sandbox_base / "serial" - serial_sandbox.mkdir(parents=True, exist_ok=True) - - for test in serial_tests_to_run: - test_name, duration, status, output = run_single_test(test, test_dir, serial_sandbox, extra_args, env) - serial_result.test_timings[test_name] = duration - serial_result.output += output + "\n" - if status == "PASS": - serial_result.passed += 1 - elif status == "SKIP": - serial_result.skipped += 1 - tests_skipped += 1 - else: - serial_result.failed += 1 - serial_result.failed_tests.append(test_name) - tests_failed += 1 - tests_done += 1 - print_progress("Serial", tests_done, total_tests, workers_done, total_workers, tests_failed, tests_skipped, start_time) - - serial_result.duration = time.time() - serial_start - results.append(serial_result) - print() - except KeyboardInterrupt: - interrupted = True - print("\n\nInterrupted! Collecting completed results...") - seen_workers = {result.worker_id for result in results} - for future in futures: - if future.done() and not future.cancelled(): - try: - result = future.result(timeout=0) - except Exception: - continue - if result.worker_id not in seen_workers: - results.append(result) - seen_workers.add(result.worker_id) - if serial_result is not None and serial_result.worker_id not in seen_workers and serial_start is not None: - serial_result.duration = time.time() - serial_start - results.append(serial_result) - - total_duration = time.time() - start_time - results.sort(key=lambda result: result.worker_id) - - if args.collect_timings: - new_timings = dict(timings) - tests_timed = 0 - for result in results: - for test_name, duration in result.test_timings.items(): - new_timings[test_name] = duration - tests_timed += 1 - if tests_timed: - save_timings(args.timings_file, new_timings) - print(f"Saved timing data for {tests_timed} tests to {args.timings_file}") - - print_summary(results, total_duration) - - if interrupted: - print("\nRun was interrupted by user.") - sys.exit(130) - - total_failed = sum(result.failed + result.exceptions for result in results) - sys.exit(1 if total_failed > 0 else 0) - - -if __name__ == "__main__": - main() diff --git a/tests/autests/autest.sh.in b/tests/autests/autest.sh.in deleted file mode 100644 index dfd55db27..000000000 --- a/tests/autests/autest.sh.in +++ /dev/null @@ -1,55 +0,0 @@ -#!/bin/bash -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -fail() -{ - echo "$1" - exit 1 -} - -script_dir=$(cd "$(dirname "$0")" && pwd) -source_autest_dir="@CMAKE_CURRENT_SOURCE_DIR@" -default_verifier_bin="${script_dir}/bin" -parallel_runner="${script_dir}/autest-parallel.py" - -pushd "${source_autest_dir}" > /dev/null -export PYTHONPATH="$(pwd):${PYTHONPATH}" -./test-env-check.sh || fail "Setting up the test environment failed." - -args=("$@") -has_verifier_bin=false -for ((i = 0; i < ${#args[@]}; ++i)); do - case "${args[$i]}" in - --verifier-bin|--verifier-bin=*) - has_verifier_bin=true - break - ;; - esac -done - -if [[ "${has_verifier_bin}" == false && -z "${VERIFIER_BIN:-}" ]]; then - export VERIFIER_BIN="${default_verifier_bin}" -fi - -# this is for rhel or centos systems -echo "Environment config finished. Running AuTest..." -parallel_mode=false -for ((i = 0; i < ${#args[@]}; ++i)); do - case "${args[$i]}" in - -j|-j[0-9]*|--jobs|--jobs=*) - parallel_mode=true - break - ;; - esac -done - -if [[ "${parallel_mode}" == true ]]; then - python3 "${parallel_runner}" "${args[@]}" -else - uv run autest run -D gold_tests "${args[@]}" -fi -ret=$? -popd > /dev/null -exit $ret diff --git a/tests/autests/gold_tests/arguments/format_argument/format_argument.test.py b/tests/autests/gold_tests/arguments/format_argument/format_argument.test.py deleted file mode 100644 index 34ad1500e..000000000 --- a/tests/autests/gold_tests/arguments/format_argument/format_argument.test.py +++ /dev/null @@ -1,175 +0,0 @@ -''' -Verify --format argument parsing. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify --format argument parsing. -''' - -# -# Test 1: Test with the URL as a key. This is not unique across transactions, -# so only one transaction will be registered. -# -r = Test.AddTestRun('--format "{url}" against transactions unique by Host') -client = r.AddClientProcess("client1", "unique_by_host.yaml", other_args="--format '{url}'") -server = r.AddServerProcess("server1", "unique_by_host.yaml", other_args="--format '{url}'") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# The client will always see each transaction as unique and will send them as -# such. It does not record keys to detect duplicates, it just stores -# transactions in a list. -client.Streams.stdout += Testers.ContainsExpression( - "Parsed 3 transactions", "Three transactions should be parsed by the client.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different.*Actual Value: "first"', - "The server will always reply with the first response.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Key: "/same/path"', "The key should be parsed from the URL, not the uuid.") - -# The server, however, uses the key which, since we're using the URL which is -# the same across transactions, will only register the first transaction. -server.Streams.stdout += Testers.ContainsExpression( - 'Correct Value: "first", Actual Value: "second"', - "The second transaction should come up as a violation.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Correct Value: "first", Actual Value: "third"', - "The third transaction should come up as a violation.") - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 1 transaction", "Only one transaction should be parsed by the server.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Key: "/same/path"', "The key should be parsed from the URL, not the uuid.") - -client.ReturnCode = 1 -server.ReturnCode = 1 - -# -# Test 2: Verify using the host as a key, which is unique across transactions. -# -r = Test.AddTestRun('--format "{field.host}"') -client = r.AddClientProcess("client2", "unique_by_host.yaml", other_args="--format '{field.host}'") -server = r.AddServerProcess("server2", "unique_by_host.yaml", other_args="--format '{field.host}'") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Parsed 3 transactions", "Three transactions should be parsed by the client.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Key: "host.one"', "The key should be parsed from the host, not the uuid.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 3 transactions", "Three transactions should be parsed by the server.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Key: "host.one"', "The key should be parsed from the host, not the uuid.") - -# -# Test 3: Use a more complicated key made up of two specifiers. -# -r = Test.AddTestRun('--format "{field.host}/{url}"') -client = r.AddClientProcess("client3", "unique_by_host.yaml", - other_args="--format '{field.host}/{url}'") -server = r.AddServerProcess("server3", "unique_by_host.yaml", - other_args="--format '{field.host}/{url}'") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Parsed 3 transactions", "Three transactions should be parsed by the client.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Key: "host.one//same/path"', "The key should be parsed from the host/url, not the uuid.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 3 transactions", "Three transactions should be parsed by the server.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Key: "host.one//same/path"', "The key should be parsed from the host/url, not the uuid.") - -# -# Test 4: Verify that the client detects when a key is not present in a -# transaction. -# -r = Test.AddTestRun('Verify that the client detects a non-existent key') -client = r.AddClientProcess("client4", "no_uuid.yaml") - -# The client will give a non-zero return code because it found a transaction -# without a key. -client.ReturnCode = 1 -client.Streams.stdout += Testers.ContainsExpression( - 'Could not find a key of format "{field.uuid}" for transaction', - "There should be a parsing warning that a key was not found for a transaction.") - -# -# Test 5: Verify that the server detects when a key is not present in a -# transaction. -# -r = Test.AddTestRun('Verify that the server detects a non-existent key') -server = r.AddDefaultServerProcess("server5", "no_uuid.yaml") - -# The server will give a non-zero return code because it found a transaction -# without a key. -server.ReturnCode = 1 -server.Streams.stdout += Testers.ContainsExpression( - 'Could not find a key of format "{field.uuid}" for transaction', - "There should be a parsing warning that a key was not found for a transaction.") - -# -# Test 6: Verify that the server returns a 404 for an unrecognized key. -# -r = Test.AddTestRun('Verify a 404 response for an unrecognized key') -client = r.AddClientProcess("client6", "uuid1.yaml") - -# Notice that the server will be configured to recognize uuid 2, not 1. So when -# a request with uuid 1 is received, it will not recognize it and should return -# a 404 (Not Found). -server = r.AddServerProcess("server6", "uuid2.yaml") -proxy = r.AddProxyProcess("proxy6", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -server.ReturnCode = 1 - -client.Streams.stdout += Testers.ContainsExpression( - 'Received an HTTP/1 404 response', - "The client should receive a 404 response for an unrecognized key.") -server.Streams.stdout += Testers.ContainsExpression( - 'sending a 404', "The server should send a 404 response for an unrecognized key.") - -# -# Test 7: Verify that --format "{url}" can successfully differentiate -# transactions that are only unique by URL. -# -r = Test.AddTestRun('--format "{url}" against transactions unique by URL') -client = r.AddClientProcess("client7", "unique_by_url.yaml", other_args="--format '{url}'") -server = r.AddServerProcess("server7", "unique_by_url.yaml", other_args="--format '{url}'") -proxy = r.AddProxyProcess("proxy7", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Parsed 3 transactions", "Three transactions should be parsed by the client.") - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 3 transactions", "Three transactions should be parsed by the server.") diff --git a/tests/autests/gold_tests/arguments/keys_argument/keys_arg.test.py b/tests/autests/gold_tests/arguments/keys_argument/keys_arg.test.py deleted file mode 100644 index efd9371eb..000000000 --- a/tests/autests/gold_tests/arguments/keys_argument/keys_arg.test.py +++ /dev/null @@ -1,66 +0,0 @@ -''' -Verify the user can white list transactions with --keys. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify the user can white list transactions with --keys. -''' - -# -# Test 1: Verify that without the keys argument, all transactions are sent. -# -r = Test.AddTestRun("Verify all keys are sent when --keys is not used.") -client = r.AddClientProcess("client1", "five_transactions.yaml") -server = r.AddServerProcess("server1", "five_transactions.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -for uuid in range(1, 6): - client.Streams.stdout += Testers.ContainsExpression(f'uuid: {uuid}', f"Client has uuid {uuid}.") - -# -# Test 2: Verify a single transaction can be selected with --keys. -# -r = Test.AddTestRun("Verify all keys are sent when --keys is not used.") -client = r.AddClientProcess("client2", "five_transactions.yaml", other_args="--keys 2") -server = r.AddServerProcess("server2", "five_transactions.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('uuid: 2', "Client has uuid 2.") - -for uuid in [1, 3, 4, 5, 6]: - client.Streams.stdout += Testers.ExcludesExpression(f'uuid: {uuid}', f"Client has uuid {uuid}.") - -# -# Test 3: Verify a multiple transactions can be selected with --keys. -# -r = Test.AddTestRun("Verify multiple transactions can be sent with --keys.") -client = r.AddClientProcess("client3", "five_transactions.yaml", other_args="--keys 3 5") -server = r.AddServerProcess("server3", "five_transactions.yaml") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -for uuid in [3, 5]: - client.Streams.stdout += Testers.ContainsExpression(f'uuid: {uuid}', f"Client has uuid {uuid}.") - -for uuid in [1, 2, 4]: - client.Streams.stdout += Testers.ExcludesExpression(f'uuid: {uuid}', f"Client has uuid {uuid}.") - -# -# Test 4: Verify we can handle the situation if no transactions exist for the -# key. -# -r = Test.AddTestRun("Verify no transactions are sent if none match the key.") -client = r.AddClientProcess("client4", "five_transactions.yaml", other_args="--keys does_not_exist") -server = r.AddServerProcess("server4", "five_transactions.yaml") -proxy = r.AddProxyProcess("proxy4", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 0 transactions', - "Verify no transactions are found") diff --git a/tests/autests/gold_tests/arguments/port_argument/no_port_argument.test.py b/tests/autests/gold_tests/arguments/port_argument/no_port_argument.test.py deleted file mode 100644 index 3f43ed547..000000000 --- a/tests/autests/gold_tests/arguments/port_argument/no_port_argument.test.py +++ /dev/null @@ -1,34 +0,0 @@ -''' -Verify there is an error if the user provides no port arguments. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify --format argument parsing. -''' - -# -# Test 1: Verify the client complains if no ports are provided. -# -r = Test.AddTestRun('Verify the client complains if no ports are provided.') -client = r.AddClientProcess("client1", "not_used.yaml", configure_http=False, configure_https=False, - configure_http3=False) -client.Streams.stdout += Testers.ContainsExpression( - 'Must provide at least one of "--connect-http", "--connect-https", or ' - '"--connect-http3" arguments', 'The client should explain that a port argument is required') -client.ReturnCode = 1 - -# -# Test 1: Verify the server complains if no ports are provided. -# -r = Test.AddTestRun('Verify the server complains if no ports are provided.') -server = r.AddDefaultServerProcess("server1", "not_used.yaml", configure_http=False, - configure_https=False, configure_http3=False) -server.Streams.stdout += Testers.ContainsExpression( - 'Must provide at least one of "--listen-http", "--listen-https", or ' - '"--listen-http3" arguments', 'The server should explain that a port argument is required') -server.ReturnCode = 1 diff --git a/tests/autests/gold_tests/arguments/repeat_argument/repeat_argument.test.py b/tests/autests/gold_tests/arguments/repeat_argument/repeat_argument.test.py deleted file mode 100644 index 5631c4962..000000000 --- a/tests/autests/gold_tests/arguments/repeat_argument/repeat_argument.test.py +++ /dev/null @@ -1,101 +0,0 @@ -''' -Verify the user can repeat transactions with --repeat. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify the user can repeat transactions with --repeat. -''' - -# -# Test 1: Verify that without the repeat argument the transactions are -# executed once. -# -r = Test.AddTestRun("Verify transactions are executed once with no --repeat argument.") -client = r.AddClientProcess("client1", "replay_files/two_files") -server = r.AddServerProcess("server1", "replay_files/two_files") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 8 transactions', - 'Verify the 8 transactions were parsed.') -client.Streams.stdout += Testers.ContainsExpression('8 transactions in 5 sessions', - 'Verify each transaction is executed once.') - -# -# Test 2: Verify that with --repeat 1 the transactions are executed once. -# -r = Test.AddTestRun("Verify transactions are executed once with --repeat 1.") -client = r.AddClientProcess("client2", "replay_files/two_files", other_args="--repeat 1") -server = r.AddServerProcess("server2", "replay_files/two_files") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 8 transactions', - 'Verify the 8 transactions were parsed.') -client.Streams.stdout += Testers.ContainsExpression('8 transactions in 5 sessions', - 'Verify each transaction is executed once.') - -# -# Test 3: Verify that with --repeat 0 the transactions are not executed. -# -r = Test.AddTestRun("Verify no transactions are executed with --repeat 0.") -client = r.AddClientProcess("client3", "replay_files/two_files", other_args="--repeat 0") -server = r.AddServerProcess("server3", "replay_files/two_files") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 8 transactions', - 'Verify the 8 transactions were parsed.') -client.Streams.stdout += Testers.ContainsExpression('0 transactions in 0 sessions', - 'Verify no transactions are executed.') - -# -# Test 4: Verify that with --repeat 2 the transactions are executed twice. -# -r = Test.AddTestRun("Verify transactions are executed twice with --repeat 2.") -client = r.AddClientProcess("client4", "replay_files/two_files", other_args="--repeat 2") -server = r.AddServerProcess("server4", "replay_files/two_files") -proxy = r.AddProxyProcess("proxy4", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 8 transactions', - 'Verify the 8 transactions were parsed.') -client.Streams.stdout += Testers.ContainsExpression('16 transactions in 10 sessions', - 'Verify each transaction is executed twice.') - -if Condition.IsPlatform("darwin"): - # On the Mac, the test proxy closes the session prematurely for some - # transactions, causing the client to fail due to PARSE_INCOMPLETE - # warnings. This can be observed in packet captures of the running test - # (the proxy does indeed send RESET responses after the request comes in) - # and from the client output which says "Connection reset by peer". I've - # spent some time trying to get the Python server to not close the - # connections prematurely on Mac but have not been able to get it to work - # yet. The client does indeed send the desired number of transactions, as - # verified with the above stdout Testers, so this test is nevertheless - # helpful despite the annoying test proxy behavior. For now we'll just - # ignore the return code. - client.ReturnCode = Any(0, 1) - -# -# Test 5: Verify that with --repeat 10 the transactions are executed ten times -# -r = Test.AddTestRun("Verify transactions are executed ten times with --repeat 10.") -client = r.AddClientProcess("client5", "replay_files/two_files", other_args="--repeat 10") -server = r.AddServerProcess("server5", "replay_files/two_files") -proxy = r.AddProxyProcess("proxy5", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression('Parsed 8 transactions', - 'Verify the 8 transactions were parsed.') -client.Streams.stdout += Testers.ContainsExpression( - '80 transactions in 50 sessions', 'Verify each transaction is executed ten times.') - -if Condition.IsPlatform("darwin"): - # See above comment. - client.ReturnCode = Any(0, 1) diff --git a/tests/autests/gold_tests/arguments/version_argument/version_argument.test.py b/tests/autests/gold_tests/arguments/version_argument/version_argument.test.py deleted file mode 100644 index 407b54221..000000000 --- a/tests/autests/gold_tests/arguments/version_argument/version_argument.test.py +++ /dev/null @@ -1,60 +0,0 @@ -''' -Verify the version argument of Proxy Verifier. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import os -import re -from os.path import dirname - -Test.Summary = ''' -Verify the version argument of Proxy Verifier. -''' - - -def get_project_version(): - """ - Parse the top-level CMake project declaration for the specified version. - """ - repo_dir = dirname(dirname(dirname(Test.TestRoot))) - cmake_file = os.path.join(repo_dir, "CMakeLists.txt") - project_pattern = re.compile( - r"project\s*\(\s*ProxyVerifier\b.*?\bVERSION\s+([0-9.]+)", - re.IGNORECASE | re.DOTALL, - ) - - with open(cmake_file, 'r', encoding='utf-8') as f: - contents = f.read() - match = project_pattern.search(contents) - if match: - return match.group(1) - - raise ValueError(f"Could not find the ProxyVerifier version in {cmake_file}") - - -project_version = get_project_version() - -# -# Test 1: Verify that the client detects when a key is not present in a -# -r = Test.AddTestRun('Verify that the client detects a non-existent key') -client = r.AddClientProcess("client1", replay_dir=None, other_args="--version") - -client.Streams.stdout += Testers.ContainsExpression( - f'Version {project_version} of Proxy Verifier', - "The --version output should print the expected string") - -# -# Test 2: Verify that the server detects when a key is not present in a -# transaction. -# -r = Test.AddTestRun('Verify that the server detects a non-existent key') -server = r.AddDefaultServerProcess("server2", replay_dir=None, other_args="--version") - -server.Streams.stdout += Testers.ContainsExpression( - f'Version {project_version} of Proxy Verifier', - "The --version output should print the expected string") diff --git a/tests/autests/gold_tests/autest-site/client.test.ext b/tests/autests/gold_tests/autest-site/client.test.ext deleted file mode 100755 index de9a9f396..000000000 --- a/tests/autests/gold_tests/autest-site/client.test.ext +++ /dev/null @@ -1,252 +0,0 @@ -''' -Implement the Proxy Verifier client extensions. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import print_function -import os -from os.path import dirname -from ports import get_port -from socket_util import create_address_argument - - -def make_id(input_str): - """ - Given a string, create usable id for Disk.File. - - Parameters: - input_str (str): An input string. - - Returns (str): a transposed version of input_str that is usable as an id to - Disk.File. - """ - return input_str.replace(".", "_").replace('-', '_') - - -def _configure_client(obj, process, name, replay_dir, find_ports=True, use_ipv6=False, - configure_http=True, configure_https=True, configure_http3=True, - http_ports=None, https_ports=None, http3_ports=None, ssl_cert='', ca_certs='', - verbose=True, single_threaded=True, enable_qlogging=True, - enable_tls_secrets_logging=True, other_args=''): - """ - Configure the process for running the verifier-client. - - Args: - obj: An object that has a RunDirectory attribute. - - process: (Process) The test process to configure for verifier-client. - - See MakeClientProcess. - """ - - # Create the directory we will setup for the replay binaries to run under. - client_dir = os.path.join(obj.RunDirectory, name) - - # Create a link of the binary to the rundir and set up the PATH variable. - # This will be the verifier-client that gets run. - bin_dir = 'bin' - process.Env['CLIENT_BIN_PATH'] = bin_dir - bin_path = os.path.join(client_dir, bin_dir) - process.Env['PATH'] = bin_path + os.pathsep + process.ComposeEnv()['PATH'] - process.Setup.Copy(process.ComposeVariables().VERIFIER_BIN, bin_path, CopyLogic.SoftFiles) - - # Configure the verifier-client command line arguments. - command = "verifier-client run " - if replay_dir: - # Create a copy of the replay directory in the run directory. - run_replay_dir = os.path.join(client_dir, os.path.basename(replay_dir)) - process.Setup.Copy(replay_dir, run_replay_dir, CopyLogic.SoftFiles) - command += f" {run_replay_dir} " - - if configure_http and find_ports and not http_ports: - http_ports = [get_port(process, "http_port")] - - if http_ports: - command += "--connect-http " - command += create_address_argument(http_ports, use_ipv6) - command += " " - - if configure_https and find_ports and not https_ports: - https_ports = [get_port(process, "https_port")] - - if https_ports: - command += "--connect-https " - command += create_address_argument(https_ports, use_ipv6) - command += " " - - if configure_http3 and find_ports and not http3_ports: - http3_ports = [get_port(process, "http3_port")] - - if http3_ports: - command += "--connect-http3 " - command += create_address_argument(http3_ports, use_ipv6) - command += " " - - if https_ports or http3_ports: - if ssl_cert == '': - # Search for the root-level cert. - test_root = process.TestRoot - git_root = dirname(dirname(dirname(test_root))) - ssl_cert = os.path.join(git_root, "tests", "keys", "client.pem") - - if not os.path.isfile(ssl_cert): - raise ValueError("Tried to use '{}' for --client-cert, but it is not " - "a valid file.".format(ssl_cert)) - if ssl_cert: - run_ssl_cert = ssl_cert - if os.path.isfile(ssl_cert): - run_ssl_cert = os.path.join(client_dir, os.path.basename(ssl_cert)) - process.Setup.Copy(ssl_cert, run_ssl_cert, CopyLogic.SoftFiles) - command += ' --client-cert "{}" '.format(run_ssl_cert) - - if ca_certs == '': - # Search for the root-level cert. - test_root = process.TestRoot - git_root = dirname(dirname(dirname(test_root))) - ca_certs = os.path.join(git_root, "tests", "keys", "ca.pem") - - if not os.path.exists(ca_certs): - raise ValueError(f"Tried to use '{ca_certs}' for --ca-certs, but it is not " - "a valid path.") - if ca_certs: - run_ca_certs = ca_certs - if os.path.isfile(ca_certs): - run_ca_certs = os.path.join(client_dir, os.path.basename(ca_certs)) - process.Setup.Copy(ca_certs, run_ca_certs, CopyLogic.SoftFiles) - command += f' --ca-certs "{run_ca_certs}" ' - - if enable_tls_secrets_logging: - tls_secrets_log_filename = "tls_secrets.txt" - tls_secrets_log_path = os.path.join(client_dir, tls_secrets_log_filename) - command += f"--tls-secrets-log-file {tls_secrets_log_path} " - obj.Variables['tls_secrets_log_path'] = tls_secrets_log_path - obj.Disk.File(tls_secrets_log_path, id=make_id(tls_secrets_log_filename)) - - if http3_ports and enable_qlogging: - qlog_basename = "qlog_directory" - qlog_dir = os.path.join(client_dir, qlog_basename) - command += f"--qlog-dir {qlog_dir} " - obj.Variables['qlog_dir'] = qlog_dir - obj.Disk.File(qlog_dir, id=make_id(qlog_basename)) - - if verbose: - command += "--verbose diag " - - if single_threaded: - command += "--thread-limit 1 " - - if other_args: - command += f" {other_args}" - - process.Command = command - process.ReturnCode = 0 - - -def MakeClientProcess(test, name, replay_dir, find_ports=True, use_ipv6=False, configure_http=True, - configure_https=True, configure_http3=True, http_ports=None, https_ports=None, - http3_ports=None, ssl_cert='', ca_certs='', verbose=True, - single_threaded=True, enable_qlogging=True, enable_tls_secrets_logging=True, - other_args=''): - """ - Create a verifier-client process. - - Args: - test: (Test) The Test object being updated with the client process. - - name: (str) The name to apply to this particular verifier-client instance. - Individual verifier-client processes must have unique names to - distinguish between them. - - replay_dir: (path) The directory containing the replay files for the - client to run with. - - find_ports: (bool) True if the framework should find available ports - to listen on for http and https connections if none were specified via - http_ports and https_ports. - - use_ipv6: (bool) True if IPv6 localhost should be used, IPv4 otherise. - - configure_http: (bool) True if http ports should be configured, False if not. - - configure_https: (bool) True if https ports should be configured, False if not. - - configure_http3: (bool) True if http3 ports should be configured, False if not. - - http_ports: (list of ints) The set of HTTP ports to connect on. - - https_ports: (list of ints) The set of HTTPS ports to connect on. - - http3_ports: (list of ints) The set of HTTP/3 ports to connect on. - - ssl_cert: (path) The location of the cert for HTTPS encryption. If this - is not provided and https_ports is non-empty, the root-level - tests/keys/client.pem will be searched for and used if it can be found. - To explicitly test verifier-server with no --client-cert argument, pass in - None for ssl_cert. Whitespace can be passed in to test --client-cert with - an empty argument. - - ca_certs: (path) The location of the CA certificate(s) for HTTPS - encryption. This can be either a single file with multiple - certificate sections or a directory of certificate files. If this - is not provided and https_ports is non-empty, the root-level - tests/keys/ca.pem will be searched for and used if it can be found. - To explicitly test verifier-server with no --ca-certs argument, - pass in None for ca_certs. Whitespace can be passed in to test - --ca-certs with an empty argument. - - verbose: (bool) Whether to enable debug-level logging. - - single_threaded: (bool) Whether the client should run in a single thread - rather than running the sessions in parallel. - - enable_qlogging: (bool) Whether QUIC log will be configured if HTTP/3 - is configured. - - enable_tls_secrets_logging: (bool) Whether TLS secrets logging will be - configured if TLS is configured (HTTPS, HTTP/2, or HTTP/3). - - other_args: (str) Any other arbitrary options to pass to verifier-client. - - Returns: - A verifier-client process. - """ - client = test.Processes.Process(name) - _configure_client(test, client, name, replay_dir, find_ports, use_ipv6, configure_http, - configure_https, configure_http3, http_ports, https_ports, http3_ports, - ssl_cert, ca_certs, verbose, single_threaded, enable_qlogging, - enable_tls_secrets_logging, other_args) - return client - - -def AddClientProcess(run, name, replay_dir, find_ports=True, use_ipv6=False, configure_http=True, - configure_https=True, configure_http3=True, http_ports=None, https_ports=None, - http3_ports=None, ssl_cert='', ca_certs='', verbose=True, single_threaded=True, - enable_qlogging=True, enable_tls_secrets_logging=True, other_args=''): - """ - Set the Default process of the test run to a verifier-client Process. - - Args: - run: (TestRun) The test run to which the client process is added. - - For all other arguments, see MakeServerProcess. - - Returns: - The newly constructed verifier-client for the test run, which is also the - Default Process of the test run. - """ - - p = run.Processes.Default - _configure_client(run, p, name, replay_dir, find_ports, use_ipv6, configure_http, - configure_https, configure_http3, http_ports, https_ports, http3_ports, - ssl_cert, ca_certs, verbose, single_threaded, enable_qlogging, - enable_tls_secrets_logging, other_args) - return p - - -########################################################################## -ExtendTest(MakeClientProcess, name="MakeClientProcess") -ExtendTestRun(AddClientProcess, name="AddClientProcess") diff --git a/tests/autests/gold_tests/autest-site/init.cli.ext b/tests/autests/gold_tests/autest-site/init.cli.ext deleted file mode 100644 index 58d28a837..000000000 --- a/tests/autests/gold_tests/autest-site/init.cli.ext +++ /dev/null @@ -1,23 +0,0 @@ -''' -Implement Proxy Verifier command line arguments. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import sys - -if sys.version_info < (3, 10, 0): - host.WriteError("You need python 3.10 or later to run these tests\n", show_stack=False) - -autest_version = "1.7.2" -if AuTestVersion() < autest_version: - host.WriteError( - "Tests need AuTest version {ver} or better\n Please update AuTest:\n" - "pip install --upgrade autest\n".format(ver=autest_version), show_stack=False) - -Settings.path_argument( - ["--verifier-bin"], default=None, - help="The directory containing the client and server Proxy Verifier binaries") diff --git a/tests/autests/gold_tests/autest-site/ports.py b/tests/autests/gold_tests/autest-site/ports.py deleted file mode 100644 index 9b2383527..000000000 --- a/tests/autests/gold_tests/autest-site/ports.py +++ /dev/null @@ -1,271 +0,0 @@ -''' -''' -# 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. - -import socket -import subprocess -import os -import platform -import psutil - -import hosts.output as host - -try: - import queue as Queue -except ImportError: - import Queue - -g_ports = None # ports we can use - - -class PortQueueSelectionError(Exception): - """ - An exception for when there are problems selecting a port from the port - queue. - """ - pass - - -def PortOpen(port: int, address: str = None, listening_ports: set[int] = None) -> bool: - """ - Detect whether the port is open, that is a socket is currently using that port. - - Open ports are currently in use by an open socket and are therefore not available - for a server to listen on them. - - Args: - port: The port to check. - address: The address to check. Defaults to localhost. - listening_ports: A set of ports that are currently listening. If a port - is in this set, it is considered open. - - Returns: - True if there is a connection currently listening on the port, False if - there is no server listening on the port currently. - """ - ret = False - if address is None: - address = "localhost" - - if port in listening_ports: - host.WriteDebug('PortOpen', f"{port} is open because it is in the listening sockets set.") - return True - - address = (address, port) - - try: - # Try to connect on that port. If we can connect on it, then someone is - # listening on that port and therefore the port is open. - s = socket.create_connection(address, timeout=.5) - s.close() - ret = True - host.WriteDebug( - 'PortOpen', f"Connection to port {port} succeeded, the port is open, " - "and a future connection cannot use it") - except socket.error: - host.WriteDebug( - 'PortOpen', f"socket error for port {port}, port is closed, " - "and therefore a future connection can use it") - except socket.timeout: - host.WriteDebug( - 'PortOpen', f"Timeout error for port {port}, port is closed, " - "and therefore a future connection can use it") - - return ret - - -def _get_available_port(queue): - """ - Get the next available port from the port queue and return it. - - Since there can be a delay between when the queue is populated and when the - port is requested, this checks the next port in the queue to see whether it - is still not open. If it isn't, it is dropped from the queue and the next - one is inspected. This process continues until either an available port is - found, or, if the queue is exhausted, PortQueueSelectionError is raised. - - Returns: - An available (i.e., non-open) port. - - Throws: - PortQueueSelectionError if the port queue is exhausted. - """ - - if queue.qsize() == 0: - host.WriteWarning("Port queue is empty.") - raise PortQueueSelectionError("Could not get a valid port because the queue is empty") - - listening_ports = _get_listening_ports() - port = queue.get() - while PortOpen(port, listening_ports=listening_ports): - host.WriteDebug('_get_available_port', f"Port was closed but now is used: {port}") - if queue.qsize() == 0: - host.WriteWarning("Port queue is empty.") - raise PortQueueSelectionError("Could not get a valid port because the queue is empty") - port = queue.get() - return port - - -def _get_listening_ports() -> set[int]: - """Use psutil to get the set of ports that are currently listening. - - :return: The set of ports that are currently listening. - """ - ports: set[int] = set() - try: - connections = psutil.net_connections(kind='all') - for conn in connections: - if conn.status == psutil.CONN_LISTEN: - ports.add(conn.laddr.port) - except psutil.AccessDenied: - # Mac OS X doesn't allow net_connections() to be called without root. - for proc in psutil.process_iter(['pid', 'name']): - try: - connections = proc.connections(kind='all') - except (psutil.AccessDenied, psutil.NoSuchProcess): - continue - for conn in connections: - if conn.status == psutil.CONN_LISTEN: - ports.add(conn.laddr.port) - return ports - - -def _setup_port_queue(amount=1000): - """ - Build up the set of ports that the OS in theory will not use. - - The AUTEST_PORT_OFFSET environment variable can be used to offset the - starting port range. This lets multiple autest worker processes reserve - disjoint port ranges while running in parallel. - """ - global g_ports - if g_ports is None: - host.WriteDebug('_setup_port_queue', "Populating the port queue.") - g_ports = Queue.Queue() - else: - # The queue has already been populated. - host.WriteDebug('_setup_port_queue', - f"Queue was previously populated. Queue size: {g_ports.qsize()}") - return - - try: - port_offset = int(os.environ.get('AUTEST_PORT_OFFSET', 0)) - except ValueError: - host.WriteWarning("AUTEST_PORT_OFFSET is not a valid integer, defaulting to 0") - port_offset = 0 - port_offset = max(0, min(port_offset, 60000)) - if port_offset > 0: - host.WriteVerbose('_setup_port_queue', f"Using port offset: {port_offset}") - - try: - # Use sysctl to find the range of ports that the OS publishes it uses. - # some docker setups don't have sbin setup correctly - new_env = os.environ.copy() - new_env['PATH'] = "/sbin:/usr/sbin:" + new_env['PATH'] - if 'Darwin' == platform.system(): - dmin = subprocess.check_output(["sysctl", "net.inet.ip.portrange.first"], - env=new_env).decode().split(":")[1].split()[0] - dmax = subprocess.check_output(["sysctl", "net.inet.ip.portrange.last"], - env=new_env).decode().split(":")[1].split()[0] - else: - dmin, dmax = subprocess.check_output(["sysctl", "net.ipv4.ip_local_port_range"], - env=new_env).decode().split("=")[1].split() - dmin = int(dmin) - dmax = int(dmax) - except Exception: - host.WriteWarning("Unable to call sysctrl!\n Tests may fail because of bad port selection!") - return - - rmin = dmin - 2000 - rmax = 65536 - dmax - - listening_ports = _get_listening_ports() - if rmax > amount: - # Fill in ports, starting above the upper OS-usable port range. - port = dmax + 1 + port_offset - while port < 65536 and g_ports.qsize() < amount: - if PortOpen(port, listening_ports=listening_ports): - host.WriteDebug('_setup_port_queue', f"Rejecting an already open port: {port}") - else: - host.WriteDebug('_setup_port_queue', - f"Adding a possible port to connect to: {port}") - g_ports.put(port) - port += 1 - if rmin > amount and g_ports.qsize() < amount: - port = 2001 + port_offset - # Fill in more ports, starting at 2001, well above well known ports, - # and going up until the minimum port range used by the OS. - while port < dmin and g_ports.qsize() < amount: - if PortOpen(port, listening_ports=listening_ports): - host.WriteDebug('_setup_port_queue', f"Rejecting an already open port: {port}") - else: - host.WriteDebug('_setup_port_queue', - f"Adding a possible port to connect to: {port}") - g_ports.put(port) - port += 1 - - -def _get_port_by_bind(): - """ - Create a socket, bind with REUSEADDR, and return the bound port. - - Because SO_REUSEADDR is applied as an option to the socket, the future - server should be able to use this port. This method is considered - sub-optimal in comparison to the OS-unused port range method used above - because another process might bind to this port in the meantime. - - Returns: - A port value that can be used for a connection. - """ - sock = socket.socket(socket.AF_INET, socket.SOCK_STREAM) - sock.setsockopt(socket.SOL_SOCKET, socket.SO_REUSEADDR, 1) - sock.bind(('', 0)) # bind to all interfaces on an ephemeral port - port = sock.getsockname()[1] - return port - - -def get_port(obj, name): - ''' - Get a port and set it to the specified variable on the object. - - Args: - obj: The object upon which to set the port variable. - name: The name of the variable that receives the port value. - - Returns: - The port value. - ''' - _setup_port_queue() - port = 0 - if g_ports.qsize() > 0: - try: - port = _get_available_port(g_ports) - host.WriteVerbose("get_port", f"Using port from port queue: {port}") - # setup clean up step to recycle the port - obj.Setup.Lambda(func_cleanup=lambda: g_ports.put(port), description="recycling port") - except PortQueueSelectionError: - port = _get_port_by_bind() - host.WriteVerbose("get_port", - f"Queue was drained. Using port from a bound socket: {port}") - else: - # Since the queue could not be populated, use a port via bind. - port = _get_port_by_bind() - host.WriteVerbose("get_port", f"Queue is empty. Using port from a bound socket: {port}") - - # Assign to the named variable. - obj.Variables[name] = port - return port diff --git a/tests/autests/gold_tests/autest-site/proxy.test.ext b/tests/autests/gold_tests/autest-site/proxy.test.ext deleted file mode 100755 index 7cc0caa29..000000000 --- a/tests/autests/gold_tests/autest-site/proxy.test.ext +++ /dev/null @@ -1,169 +0,0 @@ -''' -Implement Proxy Verifier test proxy logic -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import print_function -import os -from os.path import dirname -import uuid - -sentinel_counter = 0 - - -def _configure_proxy(obj, process, name, listen_port=8080, server_port=8081, use_ssl=False, - https_pem=None, ca_pem=None, close_on_goaway=False, use_http2_to_1=False, - use_http2_to_2=False, use_http3_to_1=False): - """ - Configure the provided process to run the proxy command. - - Args - obj: An object that has a RunDirectory attribute. - - process: (Process) The test process to configure for replay-server. - - For all other arguments, see MakeProxyProcess - """ - - global sentinel_counter - - use_protocol_list = [use_http2_to_1, use_http2_to_2, use_http3_to_1] - if use_protocol_list.count(True) > 1: - raise ValueError("Cannot specify multiple of use_http2_to_1, " - "use_http2_to_2, and use_http3_to_1 parameters.") - - proxy_rundir = os.path.join(obj.RunDirectory, name) - process.Setup.MakeDir(proxy_rundir) - proxy_src = os.path.join(process.ComposeVariables().Autest.AutestSites[0], "test_proxy.py") - proxy_dest = os.path.join(proxy_rundir, os.path.basename(proxy_src)) - - command = f"python {proxy_dest} --listen-port {listen_port} --server-port {server_port}" - if use_ssl: - test_root = process.TestRoot - pem_file = https_pem - if not pem_file: - git_root = dirname(dirname(dirname(test_root))) - pem_file = os.path.join(git_root, "tests", "keys", "client.pem") - - if not os.path.isfile(pem_file): - raise ValueError(f"Tried to use '{pem_file}' for --https-pem, but it is not " - "a valid file.") - - run_pem = os.path.join(proxy_rundir, os.path.basename(pem_file)) - process.Setup.Copy(pem_file, run_pem, CopyLogic.SoftFiles) - command += f" --https-pem {run_pem}" - - ca_file = ca_pem - if not ca_file: - git_root = dirname(dirname(dirname(test_root))) - ca_file = os.path.join(git_root, "tests", "keys", "ca.pem") - - if not os.path.isfile(ca_file): - raise ValueError(f"Tried to use '{ca_file}' for --ca-pem, but it is not " - "a valid file.") - - run_ca_file = os.path.join(proxy_rundir, os.path.basename(ca_file)) - process.Setup.Copy(ca_file, run_ca_file, CopyLogic.SoftFiles) - - command += f" --ca-pem {run_ca_file}" - - if close_on_goaway: - command += " --close-on-goaway" - if use_http2_to_1: - command += " --http2_to_1" - - if use_http2_to_2: - command += " --http2_to_2" - - if use_http3_to_1: - listening_http3_sentinel = os.path.join( - proxy_rundir, f'quic_socket_is_ready_{sentinel_counter}_{uuid.uuid4().hex}') - sentinel_counter += 1 - command += f" --http3_to_1 --listening-http3-sentinel {listening_http3_sentinel}" - - process.Setup.Copy(proxy_src, proxy_dest, CopyLogic.SoftFiles) - process.Command = command - - if use_http3_to_1: - # UDP sockets are connectionless, so PortOpen will not work. - process.Ready = When.FileExists(listening_http3_sentinel) - else: - process.Ready = When.PortOpen(listen_port) - - process.ReturnCode = 0 - - -def MakeProxyProcess(test, name, listen_port=8080, server_port=8081, use_ssl=False, https_pem=None, - ca_pem=None, use_http2_to_1=False, use_http2_to_2=False, use_http3_to_1=False): - """ - Create a Process to run the proxy command. - - Args: - test: (Test) The Test object to add the proxy process to. - - name: (str) The identifying name for the proxy process. - - listen_port: (int) The TCP port on which to listen for connections from - the client. - - server_port: (int) The TCP port for server to which the proxy should - connect. - - use_ssl: (bool) True if the proxy should listen for an HTTPS - connection, false otherwise. - - https_pem: (str) The path to the pem file to use for the HTTPS - connections. - - ca_pem: (str) The path to the pem file containing certificate authority - information. - - use_http2_to_1: (bool) True if the connection should expect HTTP/2 - traffic for the client but send HTTP/1 to the server, false otherwise. - - use_http2_to_2: (bool) True if the connection should expect HTTP/2 - traffic for the client and also send HTTP/2 to the server, false otherwise. - - use_http3_to_1: (bool) True if the connection should expect HTTP/3 traffic - from the client and send HTTP/1 traffic to the server. - - Returns: - The newly created proxy Process. - """ - proxy = test.Processes.Process(name) - _configure_proxy(test, proxy, name, listen_port, server_port, use_ssl, https_pem, ca_pem, - use_http2_to_1, use_http2_to_2, use_http3_to_1) - return proxy - - -def AddProxyProcess(run, name, listen_port=8080, server_port=8081, use_ssl=False, https_pem=None, - ca_pem=None, close_on_goaway=False, use_http2_to_1=False, use_http2_to_2=False, - use_http3_to_1=False): - """ - Create a proxy Process and add it to the provided TestRun. - - Args: - run: (TestRun) The test run to add the new proxy Process to. - - For all other arguments, see MakeProxyProcess. - - Returns: - The newly created proxy Process. - """ - proxy = run.Processes.Process(name) - _configure_proxy(run, proxy, name, listen_port, server_port, use_ssl, https_pem, ca_pem, - close_on_goaway, use_http2_to_1, use_http2_to_2, use_http3_to_1) - - client = run.Processes.Default - client.StartBefore(proxy) - - return proxy - - -########################################################################## -ExtendTest(MakeProxyProcess, name="MakeProxyProcess") -ExtendTestRun(AddProxyProcess, name="AddProxyProcess") diff --git a/tests/autests/gold_tests/autest-site/replay_gen.test.ext b/tests/autests/gold_tests/autest-site/replay_gen.test.ext deleted file mode 100755 index 58c4f2d66..000000000 --- a/tests/autests/gold_tests/autest-site/replay_gen.test.ext +++ /dev/null @@ -1,72 +0,0 @@ -''' -Implement the replay-gen.py extension. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import print_function -import hosts.output as host -import os -from os.path import dirname - - -def ConfigureReplayGenDefaultProcess(test, name, replay_dir=None, num_transactions=1, url_file=None, - other_args=''): - """ - Create a replay-gen.py Process. - - Args: - test: (Test) The Test object being updated with the replay-gen.py process. - - name: (str) The name to apply to this particular replay-gen.py instance. - Individual verifier-client processes must have unique names to - distinguish between them. - - replay_dir: (path) The directory containing the generated replay files. This - defaults to a directory in the test's RunDirectory and is accessible - via the process's ['replay-dir']. - - num_transactions: (int) The number of transactions that should be generated. - - url_file: (path) The path to a file with a list of URLs to be passed to - replay-gen.py as --url-file. If not specified, a default one will - be created that just has http://127.0.0.1 in it. - - other_args: (str) Any other arbitrary options to pass to replay-gen.py. - - Returns: - A verifier-client process. - """ - process = test.Processes.Default - - tools_dir = os.path.join(dirname(dirname(dirname(test.TestRoot))), "tools") - replay_script = os.path.join(tools_dir, "replay-gen.py") - process.Setup.Copy(replay_script, test.RunDirectory, CopyLogic.SoftFiles) - - if url_file is None: - url_file = os.path.join(test.TestRoot, 'autest-site', "default_url_file") - process.Setup.Copy(url_file, test.RunDirectory, CopyLogic.SoftFiles) - - if replay_dir is None: - replay_dir = os.path.join(test.RunDirectory, name, "replay_dir") - process.Variables['replay_dir'] = replay_dir - - command = "python replay-gen.py " - command += f" --number {num_transactions}" - command += f" --url-file {url_file}" - command += f" --output {replay_dir}" - command += f" {other_args}" - process.Command = command - - process.ReturnCode = 0 - - host.WriteVerbose("replay_gen", f"Running '{command}' with replay_dir: '{replay_dir}'") - - return process - - -########################################################################## -ExtendTestRun(ConfigureReplayGenDefaultProcess, name="ConfigureReplayGenDefaultProcess") diff --git a/tests/autests/gold_tests/autest-site/server.test.ext b/tests/autests/gold_tests/autest-site/server.test.ext deleted file mode 100755 index b65d7f6af..000000000 --- a/tests/autests/gold_tests/autest-site/server.test.ext +++ /dev/null @@ -1,264 +0,0 @@ -''' -Implement the Proxy Verifier test server extension. -''' -# @file -# -# Copyright 2023-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -from __future__ import print_function -import os -from os.path import dirname -from ports import get_port -from socket_util import create_address_argument - - -def make_id(s): - """ - Given a string, create usable id for Disk.File. - - Parameters: - input_str (str): An input string. - - Returns (str): a transposed version of input_str that is usable as an id to - Disk.File. - """ - return s.replace(".", "_").replace('-', '_') - - -def _configure_server(obj, process, name, replay_dir, find_ports=True, use_ipv6=False, - configure_http=True, configure_https=True, configure_http3=True, - http_ports=None, https_ports=None, http3_ports=None, ssl_cert='', ca_certs='', - verbose=True, enable_tls_secrets_logging=True, other_args=''): - """ - Configure the provided process to run a verifier-server command. - - Args - obj: An object that has a RunDirectory attribute. - - process: (Process) The test process to configure for verifier-server. - - For all other arguments, see MakeServerProcess. - """ - # - # Create the directory we will setup for the replay binaries to run under. - server_dir = os.path.join(obj.RunDirectory, name) - - # Create a link of the binary to the rundir and set up the PATH variable. - # This will be the verifier-server that gets run. - bin_dir = 'bin' - process.Env['SERVER_BIN_PATH'] = bin_dir - bin_path = os.path.join(server_dir, bin_dir) - process.Env['PATH'] = bin_path + os.pathsep + process.ComposeEnv()['PATH'] - process.Setup.Copy(process.ComposeVariables().VERIFIER_BIN, bin_path, CopyLogic.SoftFiles) - - # Configure the verifier-server command line arguments. - command = "verifier-server run " - - if configure_http and find_ports and not http_ports: - http_ports = [get_port(process, "http_port")] - - if http_ports: - command += "--listen-http " - command += create_address_argument(http_ports, use_ipv6) - command += " " - - if configure_https and find_ports and not https_ports: - https_ports = [get_port(process, "https_port")] - - if https_ports: - command += '--listen-https ' - command += create_address_argument(https_ports, use_ipv6) - command += " " - - # Not implemented yet. Put this back in as we implement server-side HTTP/3. - # if configure_http3 and find_ports and not http3_ports: - # http3_ports = [get_port(process, "http3_port")] - - # if http3_ports: - # command += '--listen-http3 ' - # command += create_address_argument(http3_ports, use_ipv6) - # command += " " - - if https_ports or http3_ports: - if ssl_cert == '': - # Search for the root-level cert. - test_root = process.TestRoot - git_root = dirname(dirname(dirname(test_root))) - ssl_cert = os.path.join(git_root, "tests", "keys", "server.pem") - - if not os.path.isfile(ssl_cert): - raise ValueError(f"Tried to use '{ssl_cert}' for --server-cert, " - "but it is not a valid file.") - if ssl_cert: - run_ssl_cert = ssl_cert - if os.path.isfile(ssl_cert): - run_ssl_cert = os.path.join(server_dir, os.path.basename(ssl_cert)) - process.Setup.Copy(ssl_cert, run_ssl_cert, CopyLogic.SoftFiles) - command += f' --server-cert "{run_ssl_cert}" ' - - if ca_certs == '': - # Search for the root-level cert. - test_root = process.TestRoot - git_root = dirname(dirname(dirname(test_root))) - ca_certs = os.path.join(git_root, "tests", "keys", "ca.pem") - - if not os.path.exists(ca_certs): - raise ValueError(f"Tried to use '{ca_certs}' for --ca-certs, " - "but it is not a valid path.") - if ca_certs: - run_ca_certs = ca_certs - if os.path.isfile(ca_certs): - run_ca_certs = os.path.join(server_dir, os.path.basename(ca_certs)) - process.Setup.Copy(ca_certs, run_ca_certs, CopyLogic.SoftFiles) - command += f' --ca-certs "{run_ca_certs}" ' - - if enable_tls_secrets_logging: - tls_secrets_log_filename = "tls_secrets.txt" - tls_secrets_log_path = os.path.join(server_dir, tls_secrets_log_filename) - command += f"--tls-secrets-log-file {tls_secrets_log_path} " - obj.Variables['tls_secrets_log_path'] = tls_secrets_log_path - obj.Disk.File(tls_secrets_log_path, id=make_id(tls_secrets_log_filename)) - - if replay_dir: - # Create a copy of the replay directory in the run directory. - run_replay_dir = os.path.join(server_dir, os.path.basename(replay_dir)) - process.Setup.Copy(replay_dir, run_replay_dir, CopyLogic.SoftFiles) - command += f"{run_replay_dir} " - - if verbose: - command += ' --verbose diag ' - - if other_args: - command += f"{other_args} " - process.Command = command - - # Do not try to run PortOpen on the HTTP/3 (QUIC) socket because UDP is a - # connectionless protocol. - port_open = When.PortOpenv6 if use_ipv6 else When.PortOpenv4 - if http_ports: - process.Ready = port_open(http_ports[0]) - elif https_ports: - process.Ready = port_open(https_ports[0]) - # Tests that expect a failure due to verification issues will need - # to set this to 1. - process.ReturnCode = 0 - - -def MakeServerProcess(test, name, replay_dir, find_ports=True, use_ipv6=False, configure_http=True, - configure_https=True, configure_http3=True, http_ports=None, https_ports=None, - http3_ports=None, ssl_cert='', ca_certs='', verbose=True, - enable_tls_secrets_logging=True, other_args=''): - """ - Create a verifier-server process. - - Args: - test: (Test) The Test object being updated with the server process. - - name: (str) The name to apply to this particular verifier-server instance. - Individual verifier-server processes must have unique names to - distinguish between them. - - replay_dir: (path) The directory containing the replay files for the - server to run with. - - find_ports: (bool) True if the framework should find available ports - to listen on for http and https connections if none were specified via - http_ports and https_ports. - - use_ipv6: (bool) True if IPv6 localhost should be used, IPv4 otherise. - - configure_http: (bool) True if http ports should be configured, False if not. - - configure_https: (bool) True if https ports should be configured, False if not. - - configure_http3: (bool) True if http3 ports should be configured, False if not. - - http_ports: (list of ints) The set of HTTP ports to listen on. - - https_ports: (list of ints) The set of HTTPS ports to listen on. - - https_port3: (list of ints) The set of HTTP/3 ports to listen on. - - ssl_cert: (path) The location of the cert for HTTPS encryption. If this - is not provided and https_ports is non-empty, the root-level - tests/keys/server.pem will be searched for and used if it can be found. - To explicitly test verifier-server with no --server-cert argument, pass in - None for ssl_cert. Whitespace can be passed in to test --server-cert with - an empty argument. - - ca_certs: (path) The location of the CA certificate(s) for HTTPS - encryption. This can be either a single file with multiple - certificate sections or a directory of certificate files. If this - is not provided and https_ports is non-empty, the root-level - tests/keys/ca.pem will be searched for and used if it can be found. - To explicitly test verifier-server with no --ca-certs argument, - pass in None for ca_certs. Whitespace can be passed in to test - --ca-certs with an empty argument. - - verbose: (bool) Whether to enable debug-level logging. - - enable_tls_secrets_logging: (bool) Whether TLS secrets logging will be - configured if TLS is configured (HTTPS, HTTP/2, or HTTP/3). - - other_args: (str) Any other arbitrary options to pass to verifier-server. - - Raises: - ValueError if https_ports is non-empty and a valid ssl_cert or ca_certs - value could not be derived. - """ - server = test.Processes.Process(name) - _configure_server(test, server, name, replay_dir, find_ports, use_ipv6, configure_http, - configure_https, configure_http3, http_ports, https_ports, http3_ports, - ssl_cert, ca_certs, verbose, enable_tls_secrets_logging, other_args) - return server - - -def AddDefaultServerProcess(run, name, replay_dir, find_ports=True, use_ipv6=False, - configure_http=True, configure_https=True, configure_http3=True, - http_ports=None, https_ports=None, http3_ports=None, ssl_cert='', - ca_certs='', verbose=True, enable_tls_secrets_logging=True, - other_args=''): - - server = run.Processes.Default - _configure_server(run, server, name, replay_dir, find_ports, use_ipv6, configure_http, - configure_https, configure_http3, http_ports, https_ports, http3_ports, - ssl_cert, ca_certs, verbose, enable_tls_secrets_logging, other_args) - return server - - -def AddServerProcess(run, name, replay_dir, find_ports=True, use_ipv6=False, configure_http=True, - configure_https=True, configure_http3=True, http_ports=None, https_ports=None, - http3_ports=None, ssl_cert='', ca_certs='', verbose=True, - enable_tls_secrets_logging=True, other_args=''): - """ - Create a verifier-server process and configure it for the given TestRun. - - Args: - run: (TestRun) The test run to which the server process is added. - - For all other arguments, see MakeServerProcess. - - Returns: - The newly created server process. - """ - - server = run.Processes.Process(name) - _configure_server(run, server, name, replay_dir, find_ports, use_ipv6, configure_http, - configure_https, configure_http3, http_ports, https_ports, http3_ports, - ssl_cert, ca_certs, verbose, enable_tls_secrets_logging, other_args) - - client = run.Processes.Default - - # TODO: will have to add this back later. For some reason this crashes in - # AuTest, and only when AuTest runs. The reply file works fine. - client.StartBefore(server) - - return server - - -########################################################################## -ExtendTest(MakeServerProcess, name="MakeServerProcess") -ExtendTestRun(AddDefaultServerProcess, name="AddDefaultServerProcess") -ExtendTestRun(AddServerProcess, name="AddServerProcess") diff --git a/tests/autests/gold_tests/autest-site/setup.cli.ext b/tests/autests/gold_tests/autest-site/setup.cli.ext deleted file mode 100644 index a86ee89d5..000000000 --- a/tests/autests/gold_tests/autest-site/setup.cli.ext +++ /dev/null @@ -1,61 +0,0 @@ -''' -Implement the Test Proxy autest common setup extension. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import os - - -def check_file(dir_name, file_name): - candidate = os.path.join(dir_name, file_name) - if not os.path.isfile(candidate): - hint = '' - if os.path.isfile(os.path.join(dir_name, 'bin', file_name)): - hint = "\nDid you mean '--verifier-bin {}'?".\ - format(os.path.join(dir_name, 'bin')) - host.WriteError( - "verifier-server is not found. Aborting tests - " - "Bad build or install.{}".format(hint), show_stack=False) - return candidate - - -def resolve_verifier_bin(): - if Arguments.verifier_bin is not None: - return Arguments.verifier_bin - - env_verifier_bin = ENV.get('VERIFIER_BIN') - if env_verifier_bin is not None: - return env_verifier_bin - - return "/usr/local/bin" - - -if Arguments.verifier_bin is not None: - # Add environment variables - ENV['VERIFIER_BIN'] = Arguments.verifier_bin - -verifier_bin = resolve_verifier_bin() -if not os.path.isdir(verifier_bin): - host.WriteError( - "--verifier-bin requires a directory. " - "Prefer running the build-generated autest.sh from the build directory, " - "or use --verifier-bin /bin for build-tree binaries.", show_stack=False) - -verifier_client = check_file(verifier_bin, "verifier-client") -verifier_server = check_file(verifier_bin, "verifier-server") - -new_variables = { - 'VERIFIER_BIN': verifier_bin, - 'VERIFIER_CLIENT': verifier_client, - 'VERIFIER_SERVER': verifier_server, -} -Variables.update(new_variables) - -# Modify delay times as we always have to kill server-replay, thus there's no -# need to wait. -Variables.Autest.StopProcessLongDelaySeconds = 0 -Variables.Autest.KillDelaySecond = 30 diff --git a/tests/autests/gold_tests/autest-site/socket_util.py b/tests/autests/gold_tests/autest-site/socket_util.py deleted file mode 100755 index 49fab9540..000000000 --- a/tests/autests/gold_tests/autest-site/socket_util.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Implement socket manipulation helper functions. -''' -# @file -# -# Copyright 2020-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - - -def create_address_argument(ports, use_ipv6=False): - """ - >>> create_address_argument([8080, 8081]) - '"127.0.0.1:8080,127.0.0.1:8081"' - >>> create_address_argument([8080, 8081]) - '"[::1]:8080,[::1]:8081"' - """ - is_first = True - argument = '"' - address = '127.0.0.1' - if use_ipv6: - address = '[::1]' - for port in ports: - if is_first: - is_first = False - else: - argument += ',' - # We'll have a trailing ',', but verifier-server handles that fine. - argument += f"{address}:{port}" - argument += '"' - return argument diff --git a/tests/autests/gold_tests/body/body.test.py b/tests/autests/gold_tests/body/body.test.py deleted file mode 100644 index a1e84df6f..000000000 --- a/tests/autests/gold_tests/body/body.test.py +++ /dev/null @@ -1,28 +0,0 @@ -''' -Verify basic body reading functionality. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic HTTPS functionality. -''' - -r = Test.AddTestRun("Verify bodies can be read correctly.") -client = r.AddClientProcess("client1", "body.yaml") -server = r.AddServerProcess("server1", "body.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True) - -proxy.Streams.stdout = "gold/body_proxy.gold" -client.Streams.stdout = "gold/body_client.gold" -server.Streams.stdout = "gold/body_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/body_verification/body_verification.test.py b/tests/autests/gold_tests/body_verification/body_verification.test.py deleted file mode 100644 index 42a167e50..000000000 --- a/tests/autests/gold_tests/body_verification/body_verification.test.py +++ /dev/null @@ -1,84 +0,0 @@ -''' -Verify basic body verification functionality. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic body verification functionality. -''' - -# -# Test 1: Verify bodies can be verified correctly for HTTP/1.1. -# -r = Test.AddTestRun("Verify bodies can be verified correctly for HTTP/1.1.") -client = r.AddClientProcess("client1", "http.yaml") -server = r.AddServerProcess("server1", "http.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body"', "Verification should be happy with the body.") -client.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "2", Content Data: "body"', - "Verification should be happy with the body.") -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "3", Content Data: "body"', - "Verification should not be happy with the body.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body"', "Verification should be happy with the body.") -server.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "2", Content Data: "body"', - "Verification should be happy with the body.") -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "3", Content Data: "body"', - "Verification should not be happy with the body.") - -# -# Test 2: Verify bodies can be verified correctly for HTTP/2. -# -r = Test.AddTestRun("Verify bodies can be verified correctly for HTTP/2.") -client = r.AddClientProcess("client2", "http2.yaml") -server = r.AddServerProcess("server2", "http2.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body"', "Verification should be happy with the body.") -client.Streams.stdout += Testers.ContainsExpression( - 'No Case Prefix Success: Key: "2", Content Data: "body"', - "Verification should be happy with the body.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body"', "Verification should be happy with the body.") -server.Streams.stdout += Testers.ContainsExpression( - 'No Case Suffix Success: Key: "2", Content Data: "body"', - "Verification should be happy with the body.") - -# -# Test 3: Verify bodies can be verified correctly for HTTP/3. -# -r = Test.AddTestRun("Verify bodies can be verified correctly for HTTP/3.") -client = r.AddClientProcess("client3", "http3.yaml") -server = r.AddServerProcess("server3", "http3.yaml") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http3_port, - server_port=server.Variables.http_port, use_ssl=True, use_http3_to_1=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Not Contains Success: Not Found. Key: "1", Content Data: "body"', - "Verification should be happy with the body.") -client.Streams.stdout += Testers.ContainsExpression( - 'Not No Case Contains Violation: Key: "2", Content Data: "body"', - "Verification should not be happy with the body.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Not Equals Success: Different. Key: "1", Content Data: "body"', - "Verification should be happy with the body.") -server.Streams.stdout += Testers.ContainsExpression( - 'Not No Case Equals Violation: Key: "2", Content Data: "body"', - "Verification should not be happy with the body.") diff --git a/tests/autests/gold_tests/connect_tunnel/connect_tunnel.test.py b/tests/autests/gold_tests/connect_tunnel/connect_tunnel.test.py deleted file mode 100644 index 7dd0c35f2..000000000 --- a/tests/autests/gold_tests/connect_tunnel/connect_tunnel.test.py +++ /dev/null @@ -1,77 +0,0 @@ -''' -Verify CONNECT establishes a blind tunnel for a follow-up HTTP/1 transaction. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import re - -Test.Summary = ''' -Verify CONNECT establishes a blind tunnel for a follow-up HTTP/1 transaction. -''' - -r = Test.AddTestRun("Verify CONNECT allows a follow-up HTTP/1 transaction to be tunneled") -client = r.AddClientProcess("client", "replay_files/client", configure_https=False, - configure_http3=False) -server = r.AddServerProcess("server", "replay_files/server", configure_https=False, - configure_http3=False) -proxy = r.AddProxyProcess("proxy", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "CONNECT verifier.example:443 HTTP/1.1", - "The client should send a CONNECT request to establish the tunnel.") - -client.Streams.stdout += Testers.ContainsExpression( - "GET /through/tunnel HTTP/1.1", - "The client should send the follow-up transaction through the established tunnel.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key connect-tunnel.*" - "Received an HTTP/1 200 response for key tunneled-request", - "The client should handle the CONNECT response before the tunneled response.", - reflags=re.MULTILINE | re.DOTALL) - -client.Streams.stdout += Testers.ContainsExpression( - "tunnel-body", - "The client should receive the verifier-server response body through the tunnel.") - -client.Streams.stdout += Testers.ExcludesExpression("Violation:", - "There should be no verification errors.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Failed HTTP/1 transaction", "Both client transactions should complete successfully.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "Received CONNECT request for key connect-tunnel: target verifier.example:443", - "The test proxy should recognize the CONNECT request.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "Established CONNECT tunnel for key connect-tunnel to 127.0.0.1:", - "The test proxy should establish a tunnel to verifier-server.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "CONNECT tunnel for key connect-tunnel closed after relaying [1-9][0-9]* bytes " - "client->server and [1-9][0-9]* bytes server->client.", - "The test proxy should blindly relay data in both directions through the tunnel.", - reflags=re.MULTILINE) - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 1 transaction.", "Only the tunneled transaction should reach verifier-server.") - -server.Streams.stdout += Testers.ContainsExpression( - "GET /through/tunnel HTTP/1.1", "Verifier-server should receive the tunneled request.") - -server.Streams.stdout += Testers.ContainsExpression( - "Request with key tunneled-request passed validation.", - "Verifier-server should validate the tunneled request.") - -server.Streams.stdout += Testers.ContainsExpression( - "Sent the following HTTP/1 response headers for key tunneled-request", - "Verifier-server should send the tunneled response back to the client.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors on verifier-server.") diff --git a/tests/autests/gold_tests/delay/delay.test.py b/tests/autests/gold_tests/delay/delay.test.py deleted file mode 100644 index 70247914c..000000000 --- a/tests/autests/gold_tests/delay/delay.test.py +++ /dev/null @@ -1,97 +0,0 @@ -''' -Verify correct handling of session and transaction delay. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct handling of session and transaction delay. -''' - -# -# Test 1: Run a few sessions and transactions with client-side delay. -# -r = Test.AddTestRun("Verify the handling of the client-side delay specification.") -client = r.AddClientProcess("client_client_delay", "client-side-delay.yaml") -server = r.AddServerProcess("server_client_delay", "client-side-delay.yaml") - -# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 -# traffic. Thankfully this is easily addressed by running a separate process -# for each. -proxy = r.AddProxyProcess("proxy_http_client_delay", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) -proxy = r.AddProxyProcess("proxy_https_client_delay", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 3 transactions.", "The server should have parsed 2 transactions.") - -client.Streams.stdout += Testers.ContainsExpression( - "3 transactions in 2 sessions .* in .* milliseconds", - "The client should have reported running the transactions with timing data.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 2: Verify that the timing data indicates that the delays took place. -# -r = Test.AddTestRun("Verify the client-side delay replay took an expected amount of time to run.") -verifier_script = 'verify_duration.py' -client_output = client.Streams.stdout.AbsTestPath -expected_min_delay_ms = "3000" -r.Processes.Default.Setup.Copy(verifier_script) - -r.Processes.Default.Command = \ - f'python3 {verifier_script} {client_output} {expected_min_delay_ms}' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression('Good', 'The verifier script should report success.') - -# -# Test 3: Run a few sessions and transactions with server-side delay. -# -r = Test.AddTestRun("Verify the handling of the server-side delay specification.") -client = r.AddClientProcess("client_server_delay", "server-side-delay.yaml") -server = r.AddServerProcess("server_server_delay", "server-side-delay.yaml") - -# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 -# traffic. Thankfully this is easily addressed by running a separate process -# for each. -proxy = r.AddProxyProcess("proxy_http_server_delay", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) -proxy = r.AddProxyProcess("proxy_https_server_delay", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 2 transactions.", "The server should have parsed 2 transactions.") - -client.Streams.stdout += Testers.ContainsExpression( - "2 transactions in 2 sessions .* in .* milliseconds", - "The client should have reported running the transactions with timing data.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 4: Verify that the timing data indicates that the delays took place. -# -r = Test.AddTestRun("Verify the server-side delay replay took an expected amount of time to run.") -client_output = client.Streams.stdout.AbsTestPath -expected_min_delay_ms = "1000" -r.Processes.Default.Setup.Copy(verifier_script) - -r.Processes.Default.Command = \ - f'python3 {verifier_script} {client_output} {expected_min_delay_ms}' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression('Good', 'The verifier script should report success.') diff --git a/tests/autests/gold_tests/doctest/doctest.test.py b/tests/autests/gold_tests/doctest/doctest.test.py deleted file mode 100644 index 4e11dd99e..000000000 --- a/tests/autests/gold_tests/doctest/doctest.test.py +++ /dev/null @@ -1,57 +0,0 @@ -''' -Verify the example replay file from the README. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import os -from os.path import dirname - -Test.Summary = ''' -Verify the example replay file from the README. -''' - -# -# Test 1: Verify that we're testing with the example replay file -# that is in the README. -# -r = Test.AddTestRun("Verify the tested replay file is in README.md.") -verifier_script = 'verify_example_replay_contents.py' -repo_dir = dirname(dirname(dirname(Test.TestRoot))) -readme_path = os.path.join(repo_dir, 'README.md') -example_yaml = 'example_replay.yaml' -r.Processes.Default.Setup.Copy(verifier_script) -r.Processes.Default.Setup.Copy(example_yaml) - -r.Processes.Default.Command = f'python3 {verifier_script} {example_yaml} {readme_path}' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression( - 'Good', f'The contents of {example_yaml} should be in {readme_path}') - -# -# Test 2: Verify correct behavior of a single client-side HTTP/2 transaction. -# -r = Test.AddTestRun("Verify the example replay file from the README.") -client = r.AddClientProcess("client", example_yaml) -server = r.AddServerProcess("server", example_yaml) - -# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 -# traffic. Thankfully this is easily addressed by running a separate process -# for each. -proxy = r.AddProxyProcess("proxy_http", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) -proxy = r.AddProxyProcess("proxy_https", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout = "gold/doctest_client.gold" -server.Streams.stdout = "gold/doctest_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/field_parsing/duplicate_fields.test.py b/tests/autests/gold_tests/field_parsing/duplicate_fields.test.py deleted file mode 100644 index 81985ef2f..000000000 --- a/tests/autests/gold_tests/field_parsing/duplicate_fields.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify correct handling of duplicate fields in a message. -''' -# @file -# -# Copyright 2020-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct handling of duplicate fields in a message. -''' - -# -# Test 1: Verify correct behavior when there are duplicate HTTP fields. -# -r = Test.AddTestRun("Verify correct handling of duplicate fields in a message.") -client = r.AddClientProcess("client1", "replay_files/duplicate_fields.yaml") -server = r.AddServerProcess("server1", "replay_files/duplicate_fields.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/duplicate_fields_proxy.gold" -client.Streams.stdout = "gold/duplicate_fields_client.gold" -server.Streams.stdout = "gold/duplicate_fields_server.gold" diff --git a/tests/autests/gold_tests/field_parsing/empty_field.test.py b/tests/autests/gold_tests/field_parsing/empty_field.test.py deleted file mode 100644 index 58a04b530..000000000 --- a/tests/autests/gold_tests/field_parsing/empty_field.test.py +++ /dev/null @@ -1,35 +0,0 @@ -''' -Verify correct handling of malformed replay files. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct handling of malformed replay files. -''' - -# -# Test 1: Verify correct handling of empty fields. -# -r = Test.AddTestRun("Verify correct handling of empty header fields") -client = r.AddClientProcess("client1", "replay_files/empty_field.yaml") -server = r.AddServerProcess("server1", "replay_files/empty_field.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.ReturnCode = 1 -server.ReturnCode = 1 - -# Due to the parsing failure, the server will not listen on the port. -# Thus the standard ready criteria will not work. -server.Ready = None - -client.Streams.stdout = Testers.ContainsExpression( - "Field or rule at line .* is not a sequence as required", - "Verify that we inform the user of the malformed field.") -server.Streams.stdout = Testers.ContainsExpression( - "Field or rule at line .* is not a sequence as required", - "Verify that we inform the user of the malformed field.") diff --git a/tests/autests/gold_tests/field_parsing/empty_proxy.test.py b/tests/autests/gold_tests/field_parsing/empty_proxy.test.py deleted file mode 100644 index 6a8bdb030..000000000 --- a/tests/autests/gold_tests/field_parsing/empty_proxy.test.py +++ /dev/null @@ -1,25 +0,0 @@ -''' -Verify correct handling of empty proxy nodes. -''' -# @file -# -# Copyright 2020-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct handling of empty proxy nodes. -''' - -# -# Test 1: Verify correct handling of empty proxy-request and response nodes. -# -r = Test.AddTestRun("Verify correct handling of empty proxy nodes") -client = r.AddClientProcess("client1", "replay_files/empty_proxy.yaml") -server = r.AddServerProcess("server1", "replay_files/empty_proxy.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/empty_proxy_proxy.gold" -client.Streams.stdout = "gold/empty_proxy_client.gold" -server.Streams.stdout = "gold/empty_proxy_server.gold" diff --git a/tests/autests/gold_tests/field_parsing/parse_yaml.test.py b/tests/autests/gold_tests/field_parsing/parse_yaml.test.py deleted file mode 100644 index 729d3e5b0..000000000 --- a/tests/autests/gold_tests/field_parsing/parse_yaml.test.py +++ /dev/null @@ -1,42 +0,0 @@ -''' -Verify correct parsing of YAML replay files. -''' -# @file -# -# Copyright 2020-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct parsing of a YAML replay files. -''' - -# -# Test 1: Verify correct behavior with a YAML-specified replay file. -# -r = Test.AddTestRun("Verify parsing of a YAML-specified replay file") -client = r.AddClientProcess("client1", "replay_files/yaml_specified.yaml") -server = r.AddServerProcess("server1", "replay_files/yaml_specified.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/yaml_specified_proxy.gold" -client.Streams.stdout = "gold/yaml_specified_client.gold" -server.Streams.stdout = "gold/yaml_specified_server.gold" - -# These expect verification errors. -client.ReturnCode = 1 -server.ReturnCode = 1 - -# -# Test 2: Verify correct parsing of transaction-level fields. -# -r = Test.AddTestRun("Verify parsing of transaction-level fields") -client = r.AddClientProcess("client2", "replay_files/transaction_fields.yaml") -server = r.AddServerProcess("server2", "replay_files/transaction_fields.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/transaction_fields_proxy.gold" -client.Streams.stdout = "gold/transaction_fields_client.gold" -server.Streams.stdout = "gold/transaction_fields_server.gold" diff --git a/tests/autests/gold_tests/field_verification/field_verification.test.py b/tests/autests/gold_tests/field_verification/field_verification.test.py deleted file mode 100644 index ce8593962..000000000 --- a/tests/autests/gold_tests/field_verification/field_verification.test.py +++ /dev/null @@ -1,252 +0,0 @@ -''' -Verify correct field verification behavior. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct field verification behavior. -''' - -# -# Test 1: Verify field verification in a JSON replay file. -# -r = Test.AddTestRun("Verify field verification works for a simple HTTP transaction") -client = r.AddClientProcess("client1", "replay_files/various_verification.json") -server = r.AddServerProcess("server1", "replay_files/various_verification.json") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# Verify a success and failure of each validation in the request. -server.Streams.stdout = Testers.ContainsExpression( - 'Absence Success: Key: "1", Field Name: "x-candy"', - 'Validation should be happy that the proxy removed X-CANDY.') -server.Streams.stdout += Testers.ContainsExpression( - 'Absence Violation: Present. Key: "1", Field Name: "content-type", Value: "application/octet-stream"', - 'Validation should complain that "content-type" is present') -server.Streams.stdout += Testers.ContainsExpression( - 'Presence Success: Key: "1", Field Name: "content-length", Value: "399"', - 'Validation should be happy that "content-length" is present.') -server.Streams.stdout += Testers.ContainsExpression( - 'Presence Success: Key: "1", Field Name: "exampleremoteip", Value: "10.10.10.4"', - 'Validation should be happy that "ExampleRemoteIP" is present even though its value differs.') -server.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. Key: "1", Field Name: "client-ip"', - 'Validation should complain that "client-ip" is misssing') -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "x-someid", Value: "21djfk39jfkds"', - 'Validation should be happy that "S-SomeId" has the expected value.') -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "1", Field Name: "host", Correct Value: "example.com", Actual Value: "test.example.com"', - 'Validation should complain that the "Host" value differs from the expected value.') -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "1", Field Name: "x-test-case", Correct Value: "CASEmatters", Actual Value: "caseMATTERS"', - 'Equals validation must be case-sensitive.') - -# Verify a success and failure of each validation in the response. -client.Streams.stdout = Testers.ContainsExpression( - 'Absence Success: Key: "1", Field Name: "x-newtestheader"', - 'Validation should be happy that the proxy removed X-NewTestHeader.') -client.Streams.stdout += Testers.ContainsExpression( - 'Absence Violation: Present. Key: "1", Field Name: "x-shouldexist", Value: "trustme; it=will"', - 'Validation should complain that "X-ShouldExist" is present') -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Success: Key: "1", Field Name: "content-length", Value: "0"', - 'Validation should be happy that "content-length" is present.') -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Success: Key: "1", Field Name: "age", Value: "4"', - 'Validation should be happy that "Age" is present even though its value differs.') -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. Key: "1", Field Name: "x-request-id"', - 'Validation should complain that "x-request-id" is misssing') -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "date", Value: "Sat, 16 Mar 2019 03:11:36 GMT"', - 'Validation should be happy that "date" has the expected value.') -client.Streams.stdout += Testers.ContainsExpression( - ('Equals Violation: Different. Key: "1", Field Name: "x-testheader", ' - 'Correct Value: "from_proxy_response", Actual Value: "from_server_response"'), - 'Validation should complain that the "x-testheader" value differs from the expected value.') - -client.ReturnCode = 1 -server.ReturnCode = 1 - -# -# Test 2: Verify field verification in a YAML replay file. -# -r = Test.AddTestRun("Verify field verification works for a simple HTTP transaction") -client = r.AddClientProcess("client2", "replay_files/cookie_equal.yaml") -server = r.AddServerProcess("server2", "replay_files/cookie_equal.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - 'Absence Success: Key: "5", Field Name: "x-not-a-header"', - 'Validation should be happy that "X-Not-A-Header" is missing.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "5", Field Name: "set-cookie", Value: "ABCD"', - 'Validation should be happy that "Set-Cookie" had the expected header.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "5", Field Name: "set-cookie", Value: "ABCD"', - 'Validation should be happy that "Set-Cookie" had the expected header.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. Key: "5", Field Name: "x-does-not-exist"', - 'Validation should complain that "X-Does-Not-Exist" is not present.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "5", Field Name: "x-test-request", Correct Value: "rEQUESTdATA", Actual Value: "RequestData"', - 'Validation should complain that "X-Test-Request" is different.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Absence Violation: Present. Key: "5", Field Name: "x-test-present", Value: "It\'s there"', - 'Validation should complain that "X-Test-Pressent" is present.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "5", Field Name: "cookie", Value: "', - 'Validation should be happy with the cookie value.') - -client.ReturnCode = 1 -server.ReturnCode = 1 - -# -# Test 3: Verify repeated RFC-combinable header fields are normalized for verification. -# -r = Test.AddTestRun("Verify repeated RFC-combinable header fields are normalized for verification") -client = r.AddClientProcess("client3", "replay_files/duplicate_fields.yaml") -server = r.AddServerProcess("server3", "replay_files/duplicate_fields.yaml") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "cache-control", Value: "no-store, max-age=0"', - 'Validation should combine repeated Cache-Control field lines before comparing them.') - -client.Streams.stdout += Testers.ContainsExpression( - ('Equals Violation: Different. Key: "1", Field Name: "cache-control", ' - 'Correct Value: "max-age=0, no-store", Actual Value: "no-store, max-age=0"'), - 'Validation should preserve the order of repeated Cache-Control values.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "vary", Value: "accept-encoding, accept-language"', - 'Validation should treat a single comma-separated Vary field line like repeated field lines.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "x-join", Value: "alpha, beta"', - 'Validation should join repeated request field lines with comma SP before equality checks.') - -server.Streams.stdout += Testers.ContainsExpression( - ('Equals Violation: Different. Key: "1", Field Name: "x-join", ' - 'Correct Value: "beta, alpha", Actual Value: "alpha, beta"'), - 'Validation should fail when the repeated request field order changes.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "1", Field Name: "x-join", Required Value: "beta", Value: "alpha, beta"', - 'Validation should run substring checks against the combined request field value.') - -client.ReturnCode = 1 -server.ReturnCode = 1 - -# Test 4: Verify Set-Cookie verification uses the dedicated set-cookie-verifications node. -r = Test.AddTestRun( - "Verify Set-Cookie verification works with the dedicated set-cookie-verifications node") -client = r.AddClientProcess("client4", "replay_files/multi_value_includes.yaml") -server = r.AddServerProcess("server4", "replay_files/multi_value_includes.yaml") -proxy = r.AddProxyProcess("proxy4", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "B1=333"', - "Verification should match a Set-Cookie rule even when the replay lists it out of order.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "A1=111"', - "Verification should match the remaining Set-Cookie rule after the first match is consumed.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -# Test 5: Verify Set-Cookie verification allows extra cookies after the expected list. -r = Test.AddTestRun("Verify Set-Cookie verification allows extra cookies after the expected list") -client = r.AddClientProcess("client5", "replay_files/multi_value_equal.yaml") -server = r.AddServerProcess("server5", "replay_files/multi_value_equal.yaml") -proxy = r.AddProxyProcess("proxy5", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1=", Value: "A1=111"', - "Verification should match the first expected Set-Cookie rule.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "B1=333"', - "Verification should match the second expected Set-Cookie rule and ignore later cookies.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -# Test 6: Verify Set-Cookie negative checks do not consume cookies and value arrays expand. -r = Test.AddTestRun("Verify negative Set-Cookie checks assert no matching cookie line") -client = r.AddClientProcess("client6", "replay_files/set_cookie_negative.yaml") -server = r.AddServerProcess("server6", "replay_files/set_cookie_negative.yaml") -proxy = r.AddProxyProcess("proxy6", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1=", Value: "A1=111"', - "Verification should expand a Set-Cookie contains array into individual checks.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1S=", Value: "A1S=555"', - "Verification should match each expanded Set-Cookie contains rule independently.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Not Contains Success: Absent. Key: "1", Field Name: "set-cookie", Required Missing Value: "D1="', - "Verification should treat Set-Cookie absent-with-value as a negative non-consuming match.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Not Contains Success: Absent. Key: "1", Field Name: "set-cookie", Required Missing Value: "_ebd"', - "Verification should expand a Set-Cookie absent array into individual negative checks.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -# Test 7: Verify legacy Set-Cookie absence and negative pattern checks still work. -r = Test.AddTestRun("Verify Set-Cookie absence works in both legacy and dedicated syntax") -client = r.AddClientProcess("client7", "replay_files/set_cookie_absent.yaml") -server = r.AddServerProcess("server7", "replay_files/set_cookie_absent.yaml") -proxy = r.AddProxyProcess("proxy7", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Absence Success: Key: "1", Field Name: "set-cookie"', - "Legacy Set-Cookie absence verification under fields should still work.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -# Test 8: Verify a negative Set-Cookie pattern check fails on a matching cookie line. -r = Test.AddTestRun("Verify a negative Set-Cookie pattern fails when a cookie matches") -client = r.AddClientProcess("client8", "replay_files/set_cookie_negative_failure.yaml") -server = r.AddServerProcess("server8", "replay_files/set_cookie_negative_failure.yaml") -proxy = r.AddProxyProcess("proxy8", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "A1=111"', - "Verification should still match the positive Set-Cookie rule.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Not Contains Violation: Key: "1", Field Name: "set-cookie", Required Missing Value: "B1=333", Value: "B1=333"', - "Verification should fail when an absent Set-Cookie pattern matches a received cookie.") - -client.ReturnCode = 1 -server.ReturnCode = 0 diff --git a/tests/autests/gold_tests/http/http.test.py b/tests/autests/gold_tests/http/http.test.py deleted file mode 100644 index aabd0998d..000000000 --- a/tests/autests/gold_tests/http/http.test.py +++ /dev/null @@ -1,69 +0,0 @@ -''' -Verify basic HTTP/1.x functionality. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic HTTP/1.x functionality. -''' - -# -# Test 1: Verify correct behavior of a single HTTP transaction. -# -r = Test.AddTestRun("Verify HTTP/1 processing of a single HTTP transaction") - -# Add configure_https=False to verify ATS client and server work when the https -# optional arguments are not provided. -client = r.AddClientProcess("client1", "replay_files/single_transaction.yaml", - configure_https=False) -server = r.AddServerProcess("server1", "replay_files/single_transaction.yaml", - configure_https=False) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/single_transaction_proxy.gold" -client.Streams.stdout = "gold/single_transaction_client.gold" -server.Streams.stdout = "gold/single_transaction_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 2: Verify correct behavior of multiple HTTP sessions. -# -r = Test.AddTestRun("Verify HTTP/1 processing of multiple HTTP transactions") -client = r.AddClientProcess("client2", "replay_files/multiple_transactions") -server = r.AddServerProcess("server2", "replay_files/multiple_transactions") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout = Testers.ContainsExpression("7 transactions in 5 sessions", - "Verify that 7 transactions were parsed.") - -client.Streams.stdout += Testers.ContainsExpression("Loading 3 replay files.", - "Verify that 3 replay files were parsesd.") - -client.Streams.stdout += Testers.ContainsExpression("204 No Content", - "Verify the No Content reason string.") - -client.Streams.stdout += Testers.ContainsExpression("POST /some/request HTTP/1.0", - "Verify that we sent the HTTP/1.0 version.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout = Testers.ContainsExpression("Ready with 7 transactions.", - "Verify that 7 transactions were parsed.") - -server.Streams.stdout += Testers.ContainsExpression("Loading 3 replay files", - "Verify that 3 replay files were parsed.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/http2/http2.test.py b/tests/autests/gold_tests/http2/http2.test.py deleted file mode 100644 index 2950f503e..000000000 --- a/tests/autests/gold_tests/http2/http2.test.py +++ /dev/null @@ -1,180 +0,0 @@ -''' -Verify basic HTTP/2 functionality. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic HTTP/2 functionality. -''' - -# -# Test 1: Verify correct behavior of a single client-side HTTP/2 transaction. -# -r = Test.AddTestRun("Verify HTTP/2 behavior on client-side only") -client = r.AddClientProcess("client1", "replay_files/http2_to_http1.yaml") -server = r.AddServerProcess("server1", "replay_files/http2_to_http1.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_1=True) - -proxy.Streams.stdout = "gold/http2_to_http1_proxy.gold" -client.Streams.stdout = "gold/http2_to_http1_client.gold" -server.Streams.stdout = "gold/http2_to_http1_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 2: Verify field verification: all success. -# -r = Test.AddTestRun("Verify HTTP/2 behavior on both the client and server sides") -client = r.AddClientProcess("client2", "replay_files/http2_to_http2.yaml") -server = r.AddServerProcess("server2", "replay_files/http2_to_http2.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -proxy.Streams.stdout = "gold/http2_to_http2_proxy.gold" - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 1 with stream id 1:", - "The client should receive the first HTTP/2 response.") -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 4 with stream id 7:", - "The client should receive the empty-body HTTP/2 response with Content-Length: 0.") -client.Streams.stdout += Testers.ContainsExpression( - "Received HTTP/2 response trailers for key 5 with stream id 9:", - "The client should receive the HTTP/2 response trailers.") -client.Streams.stdout += Testers.ContainsExpression( - "HTTP/2 replay metrics: requests-submitted=5", - "The HTTP/2 replay should complete and emit replay metrics.") -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ContainsExpression( - "Request with key 2 passed validation.", - "The server should validate the POST transaction on the HTTP/2 session.") -server.Streams.stdout += Testers.ContainsExpression( - "Submitted the following HTTP/2 response headers for key 4 on stream 7:", - "The server should send the empty-body HTTP/2 response.") -server.Streams.stdout += Testers.ContainsExpression( - "Request with key 5 passed validation.", - "The server should validate the trailer-bearing HTTP/2 transaction.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 3: Verify field verification: failures. -# -r = Test.AddTestRun("Verify HTTP/2 field verification") -client = r.AddClientProcess("client3", "replay_files/http2_to_http2_verification_failures.yaml") -server = r.AddServerProcess("server3", "replay_files/http2_to_http2_verification_failures.yaml") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.ReturnCode = 1 -server.ReturnCode = 1 - -client.Streams.stdout += Testers.ContainsExpression( - 'Contains Violation: Not Found. Key: "1", Field Name: "x-added-header", Required Value: "lmno", Actual Value: "abcdefg"', - "There should be a verification about a field that doesn't contain the expected content.") -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. Key: "1", Field Name: "x-deleted-header"', - "There should be a verification about a missing field.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "1", Field Name: "x-added-header", Correct Value: "4", Actual Value: "3', - "There should be a verification error about an incorrect value.") -server.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. Key: "1", Field Name: "x-deleted-header', - "There should be a verification error about a missing field.") - -# -# Test 4: Verify the ability to control server protocol negotiation via ALPN. -# -r = Test.AddTestRun("Verify HTTP/2 behavior on both the client and server sides") -client = r.AddClientProcess("client4", "replay_files/set_alpn.yaml") -server = r.AddServerProcess("server4", "replay_files/set_alpn.yaml") -proxy = r.AddProxyProcess("proxy4", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -# The two sessions race with each other, so don't use a gold file here. -proxy.Streams.stdout += Testers.ContainsExpression( - "Got SNI from client: b'test_sni_no_h2'", - "Verify that the SNI associated with no HTTP/2 support was sent.") -proxy.Streams.stdout += Testers.ContainsExpression( - "Got SNI from client: b'test_sni_with_h2'", - "Verify that the SNI associated with HTTP/2 support was sent.") - -# The client sent and received HTTP/2 for both transactions because -# only the server side should down-negotiate HTTP/2, not the proxy. -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 1 with stream id 1:", - "The client should receive an HTTP/2 response for both transactions.") -client.Streams.stdout += Testers.ExcludesExpression("HTTP/1", - "Neither of the transactions should be HTTP/1") -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Using ALPN protocol string "h2,http/1.1,http1.1" for SNI "test_sni_with_h2"', - "Verify that the correctly parsed ALPN string for the SNI supporting HTTP/2.") -server.Streams.stdout += Testers.ContainsExpression( - 'Using ALPN protocol string "http/1.1,http1.1" for SNI "test_sni_no_h2"', - "Verify that the correctly parsed ALPN string for the SNI not supporting HTTP/2.") -server.Streams.stdout += Testers.ContainsExpression( - "Negotiated ALPN: h2", "Verify that HTTP/2 was negotiated for one session.") -server.Streams.stdout += Testers.ContainsExpression( - "HTTP/2 is not negotiated. Assuming HTTP/1", - "Verify that HTTP/1 was negotiated for one session.") -server.Streams.stdout += Testers.ContainsExpression( - "Sent the following HTTP/2 response headers for key 1 with stream id 1", - "Verify that an HTTP/2 response was sent to the client.") -server.Streams.stdout += Testers.ContainsExpression( - "Sent the following HTTP/1 response headers for key 2", - "Verify that an HTTP/1 response was sent to the client.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 5: Verify connection-specific headers are sanitized for HTTP/2 replays. -# -r = Test.AddTestRun("Verify HTTP/2 strips connection-specific headers") -client = r.AddClientProcess("client5", "replay_files/http2_hop_by_hop.yaml") -server = r.AddServerProcess("server5", "replay_files/http2_hop_by_hop.yaml") -proxy = r.AddProxyProcess("proxy5", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key h2-hop-by-hop-1 with stream id 1:", - "The first concurrent HTTP/2 response should complete successfully.") -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key h2-hop-by-hop-2 with stream id 3:", - "The second concurrent HTTP/2 response should complete successfully.") -client.Streams.stdout += Testers.ExcludesExpression( - "HTTP/2 final response drain made no forward progress", - "The client should not hit the HTTP/2 stuck-session timeout summary.") -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The client should not report verification failures after sanitizing headers.") - -server.Streams.stdout += Testers.ContainsExpression( - "Skipping HTTP/2 connection-specific field connection: keep-alive", - "The server should strip invalid connection-specific response headers.") -server.Streams.stdout += Testers.ContainsExpression( - "Skipping HTTP/2 connection-specific field upgrade: h2c", - "The server should strip invalid upgrade headers from HTTP/2 responses.") -server.Streams.stdout += Testers.ExcludesExpression( - "Received GOAWAY frame with last stream id 0, error code 1", - "The client should no longer send a protocol-error GOAWAY for these streams.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The server should not report verification failures after sanitizing headers.") diff --git a/tests/autests/gold_tests/http2_abort/http2_abort.test.py b/tests/autests/gold_tests/http2_abort/http2_abort.test.py deleted file mode 100644 index 09de6fc83..000000000 --- a/tests/autests/gold_tests/http2_abort/http2_abort.test.py +++ /dev/null @@ -1,184 +0,0 @@ -''' -Abort HTTP/2 connection. -''' -# @file -# -# Copyright 2023-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Abort HTTP/2 connection. -''' - -# -# Test 1: Client sends RST_STREAM after DATA frame -# -r = Test.AddTestRun('Client sends RST_STREAM after DATA frame') -client = r.AddClientProcess('client1', 'replay_files/client_rst_stream_after_data.yaml') -server = r.AddServerProcess('server1', 'replay_files/client_rst_stream_after_data.yaml') -proxy = r.AddProxyProcess('proxy1', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', - 'Detect client abort flag.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Submitted RST_STREAM frame.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Received an HTTP/2 request for key 1 with stream id 1', 'Server is functional.') - -server.Streams.stdout += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Received RST_STREAM frame with error code INTERNAL_ERROR', 'Received RST_STREAM frame.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Frame sequence from client: HEADERS, DATA, RST_STREAM', 'Frame sequence.') - -# -# Test 2: Client sends RST_STREAM after HEADERS frame -# -r = Test.AddTestRun('Client sends RST_STREAM after HEADERS frame') -client = r.AddClientProcess('client2', 'replay_files/client_rst_stream_after_headers.yaml') -server = r.AddServerProcess('server2', 'replay_files/client_rst_stream_after_headers.yaml') -proxy = r.AddProxyProcess('proxy2', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', - 'Detect client abort flag.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Submitted RST_STREAM frame.') - -client.Streams.stdout += Testers.ExcludesExpression('Timed out waiting for frame: HEADERS', - 'Await HEADERS') - -server.Streams.stdout += Testers.ContainsExpression( - 'Received an HTTP/2 request for key 1 with stream id 1', 'Server is functional.') - -server.Streams.stdout += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Received RST_STREAM frame with error code STREAM_CLOSED', 'Received RST_STREAM frame.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Frame sequence from client: HEADERS, RST_STREAM', 'Frame sequence.') - -# -# Test 3: Server sends RST_STREAM after HEADERS frame -# -r = Test.AddTestRun('Server sends RST_STREAM after HEADERS frame') -client = r.AddClientProcess('client3', 'replay_files/server_rst_stream_after_headers.yaml') -server = r.AddServerProcess('server3', 'replay_files/server_rst_stream_after_headers.yaml') -proxy = r.AddProxyProcess('proxy3', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression('Received RST_STREAM frame with stream id 1', - 'RST_STREAM pass through.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code ENHANCE_YOUR_CALM.', - 'Detect client abort flag.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Submitted RST_STREAM frame.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'StreamReset stream_id:1, error_code:(11|ErrorCodes.ENHANCE_YOUR_CALM), remote_reset:True', - 'Received RST_STREAM frame.') - -# -# Test 4: Client sends GOAWAY after HEADERS frame -# -r = Test.AddTestRun('Client sends GOAWAY after HEADERS frame') -client = r.AddClientProcess('client4', 'replay_files/client_goaway_after_headers.yaml') -server = r.AddServerProcess('server4', 'replay_files/client_goaway_after_headers.yaml') -proxy = r.AddProxyProcess('proxy4', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitting GOAWAY frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', - 'Detect client abort flag.') - -client.Streams.stdout += Testers.ContainsExpression('Submitted GOAWAY frame for key 1.', - 'Submitted GOAWAY frame.') - -client.Streams.stdout += Testers.ExcludesExpression('should_not_send', - 'Client connection should terminate.') - -server.Streams.stdout += Testers.ExcludesExpression('GOAWAY', 'Server is not affected.') - -server.Streams.stdout += Testers.ExcludesExpression('should_not_send', - 'Server connection should terminate.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Received GOAWAY frame with error code STREAM_CLOSED', 'Received GOAWAY frame.') - -proxy.Streams.stdout += Testers.ContainsExpression('Frame sequence from client: HEADERS, GOAWAY', - 'Frame sequence.') - -# -# Test 5: Server sends GOAWAY after HEADERS frame -# -r = Test.AddTestRun('Server sends GOAWAY after HEADERS frame') -client = r.AddClientProcess('client5', 'replay_files/server_goaway_after_headers.yaml') -server = r.AddServerProcess('server5', 'replay_files/server_goaway_after_headers.yaml') -proxy = r.AddProxyProcess('proxy5', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression('Received GOAWAY frame with last stream id 0', - 'GOAWAY pass through.') - -client.Streams.stdout += Testers.ExcludesExpression('should_not_send', - 'Client connection should terminate.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Submitting GOAWAY frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', - 'Detect server abort flag.') - -server.Streams.stdout += Testers.ContainsExpression('Submitted GOAWAY frame for key 1.', - 'Submitted GOAWAY frame.') - -server.Streams.stdout += Testers.ExcludesExpression('should_not_send', - 'Server connection should terminate.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'ConnectionTerminated error_code:(5|ErrorCodes.STREAM_CLOSED), last_stream_id:0, additional_data:None', - 'Received GOAWAY frame.') - -# -# Test 6: Client sends RST_STREAM mixed within multiple DATA frames -# -r = Test.AddTestRun('Client sends RST_STREAM mixed within multiple DATA frames') -client = r.AddClientProcess('client6', 'replay_files/client_rst_stream_mixed_data.yaml') -server = r.AddServerProcess('server6', 'replay_files/client_rst_stream_mixed_data.yaml') -proxy = r.AddProxyProcess('proxy6', listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', - 'Detect client abort flag.') - -client.Streams.stdout += Testers.ContainsExpression( - 'Submitted RST_STREAM frame for key 1 on stream 1.', 'Submitted RST_STREAM frame.') - -server.Streams.stdout += Testers.ContainsExpression( - 'Received an HTTP/2 request for key 1 with stream id 1', 'Server is functional.') - -server.Streams.stdout += Testers.ExcludesExpression('RST_STREAM', 'Server is not affected.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Received RST_STREAM frame with error code INTERNAL_ERROR', 'Received RST_STREAM frame.') - -proxy.Streams.stdout += Testers.ContainsExpression( - 'Frame sequence from client: HEADERS, DATA, RST_STREAM', 'Frame sequence.') diff --git a/tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.test.py b/tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.test.py deleted file mode 100644 index 91d4ea2b7..000000000 --- a/tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.test.py +++ /dev/null @@ -1,32 +0,0 @@ -''' -GOAWAY frame tests. -''' -# @file -# -# Copyright 2024-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# -Test.Summary = ''' -GOAWAY frame tests. -''' - -tr = Test.AddTestRun("GOAWAY frame tests.") -client = tr.AddClientProcess("client", "http2_close_on_goaway.yaml") -server = tr.AddServerProcess("server", "http2_close_on_goaway.yaml") -proxy = tr.AddProxyProcess("proxy", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - close_on_goaway=True, use_http2_to_2=True) - -tr.Streams.stdout += Testers.ContainsExpression("uuid: 1", "uuid: 1") -tr.Streams.stdout += Testers.ContainsExpression("uuid: 3", "uuid: 3") -tr.Streams.stdout += Testers.ContainsExpression("uuid: 4", "uuid: 4") -tr.Streams.stdout += Testers.ContainsExpression( - "Failed to submit DATA frame for key 4 on stream 3: -510", "uuid: 4 should fail") - -tr.Streams.stdout += Testers.ExcludesExpression("uuid: 2", "uuid: 2") - -server.Streams.stdout += Testers.ContainsExpression("uuid: 1", "uuid: 1") -server.Streams.stdout += Testers.ContainsExpression("uuid: 3", "uuid: 3") - -server.Streams.stdout += Testers.ExcludesExpression("uuid: 2", "uuid: 2") -server.Streams.stdout += Testers.ExcludesExpression("uuid: 4", "uuid: 4") diff --git a/tests/autests/gold_tests/http2_frames/http2_frames.test.py b/tests/autests/gold_tests/http2_frames/http2_frames.test.py deleted file mode 100644 index 0701bf79b..000000000 --- a/tests/autests/gold_tests/http2_frames/http2_frames.test.py +++ /dev/null @@ -1,40 +0,0 @@ -''' -Specify h2 frame sequence. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Specify h2 frame sequence. -''' - -# -# Test 1: Abort after DATA frame -# -r = Test.AddTestRun("Specify h2 frame sequence") -client = r.AddClientProcess("client1", "http2_frames.yaml") -server = r.AddServerProcess("server1", "http2_frames.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -client.Streams.stdout = "gold/client.gold" -server.Streams.stdout = "gold/server.gold" -proxy.Streams.stdout = "gold/proxy.gold" - -# -# Test 2: Verify that the timing data indicates that the delays took place. -# -r = Test.AddTestRun("Verify the client-side delay replay took an expected amount of time to run.") -verifier_script = 'verify_duration.py' -client_output = client.Streams.stdout.AbsTestPath -expected_min_delay_ms = "1500" -r.Processes.Default.Setup.Copy(verifier_script) - -r.Processes.Default.Command = \ - f'python3 {verifier_script} {client_output} {expected_min_delay_ms}' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression('Good', 'The verifier script should report success.') diff --git a/tests/autests/gold_tests/http2_frames/http2_multi_data_frames.test.py b/tests/autests/gold_tests/http2_frames/http2_multi_data_frames.test.py deleted file mode 100644 index 2d3c9f5df..000000000 --- a/tests/autests/gold_tests/http2_frames/http2_multi_data_frames.test.py +++ /dev/null @@ -1,63 +0,0 @@ -''' -Send multiple data frames. -''' -# @file -# -# Copyright 2023-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Send multiple data frames. -''' - -# -# Test 1: Send multiple DATA frames -# -r = Test.AddTestRun("Send multiple data frames") -client = r.AddClientProcess("client1", "http2_multi_data_frames.yaml") -server = r.AddServerProcess("server1", "http2_multi_data_frames.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) - -proxy.Streams.stdout = "gold/multi_data_frame_proxy.gold" - -client.Streams.stdout += Testers.ContainsExpression( - "Submitted DATA frame for key 1 on stream 1.", - "The client should submit the replayed HTTP/2 DATA frames.") -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "server_data_1server_data_2server_data_3"', - "The client should verify the combined HTTP/2 response body.") -client.Streams.stdout += Testers.ContainsExpression( - r"HTTP/2 replay metrics: requests-submitted=1, max-in-flight-streams=1, " - r"send-phase-bytes-drained=0, final-drain-duration=[0-9]+ms\.", - "The client should complete the HTTP/2 replay and emit replay metrics.") -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The client should not report verification failures for the multi-frame replay.") - -server.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 body of 39 bytes for key 1 with stream id 1:", - "The server should receive the combined HTTP/2 request body.") -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Content Data: "body", Value: "client_data_1client_data_2client_data_3"', - "The server should verify the combined replayed request body.") -server.Streams.stdout += Testers.ContainsExpression( - "Sent an HTTP/2 body of 13 bytes for key 1 of stream id 1:", - "The server should send the replayed HTTP/2 DATA frames.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The server should not report verification failures for the multi-frame replay.") - -# -# Test 2: Verify that the timing data indicates that the delays took place on the client side. -# -r = Test.AddTestRun("Verify the client-side delay replay took an expected amount of time to run.") -verifier_script = 'verify_duration.py' -client_output = client.Streams.stdout.AbsTestPath -expected_min_delay_ms = "5000" -r.Processes.Default.Setup.Copy(verifier_script) - -r.Processes.Default.Command = \ - f'python3 {verifier_script} {client_output} {expected_min_delay_ms}' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression('Good', 'The verifier script should report success.') diff --git a/tests/autests/gold_tests/http3/http3.test.py b/tests/autests/gold_tests/http3/http3.test.py deleted file mode 100644 index 39b7e110b..000000000 --- a/tests/autests/gold_tests/http3/http3.test.py +++ /dev/null @@ -1,87 +0,0 @@ -''' -Verify basic HTTP/3 functionality. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic HTTP/3 functionality. -''' - -r_http3_args = "--poll-timeout 10000" - -# -# Test 1: Verify correct behavior of a various HTTP/3 transactions. -# -r = Test.AddTestRun("Verify HTTP/3") -client = r.AddClientProcess("client1", "replay_files/http3_to_http1.yaml", other_args=r_http3_args, - verbose=False) -server = r.AddServerProcess("server1", "replay_files/http3_to_http1.yaml", other_args=r_http3_args, - verbose=False) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http3_port, - server_port=server.Variables.http_port, use_ssl=True, use_http3_to_1=True) - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Filter stable verification lines before comparing against gold. Raw HTTP/3 -# output includes asynchronous debug ordering and large generated bodies that -# are both expensive and noisy to compare directly. -# -r = Test.AddTestRun("Verify filtered HTTP/3 output") -client_output = client.Streams.stdout.AbsTestPath -server_output = server.Streams.stdout.AbsTestPath -r.Processes.Default.Setup.Copy("filter_http3_output.py") -r.Processes.Default.Command = f"python3 filter_http3_output.py {client_output} {server_output}" -r.Streams.stdout = "gold/http3_to_http1_client_server.gold" - -# -# Keep the proxy coverage focused on request and response headers. The proxy -# output includes generated body bytes, so filtering avoids comparing the large -# 300 KB payloads while still catching missing or incorrect headers. -# -r = Test.AddTestRun("Verify filtered HTTP/3 proxy output") -proxy_output = proxy.Streams.stdout.AbsTestPath -r.Processes.Default.Setup.Copy("filter_http3_output.py") -r.Processes.Default.Command = f"python3 filter_http3_output.py --proxy-output {proxy_output}" -r.Streams.stdout = "gold/http3_to_http1_proxy.gold" - -# -# Test 2: Verify correct verification failure behaviors. -# -r = Test.AddTestRun("Verify HTTP/3 with verification failures") -client = r.AddClientProcess("client2", "replay_files/http3_to_http1_failures.yaml", - other_args=r_http3_args) -server = r.AddServerProcess("server2", "replay_files/http3_to_http1_failures.yaml", - other_args=r_http3_args) -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http3_port, - server_port=server.Variables.http_port, use_ssl=True, use_http3_to_1=True) - -client.ReturnCode = 1 - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. .* Field Name: "x-equal-header", Correct Value: "other_content", Actual Value: "some_content"', - "There should be an equal violation for x-equal-header.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: .* Field Name: "x-response-header", Value: "response"', - "Verification should be happy with the X-Response-Header.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Absence Violation: Present. .* Field Name: "x-added-header", Value: "1"', - "There should be an absence violation for X-Added-Header because it will be present.") - -client.Streams.stdout += Testers.ContainsExpression( - 'Presence Violation: Absent. .* Field Name: "x-deleted-header"', - "There should be an presence violation for X-Deleted-Header because it will be absent.") - -client.Streams.stdout += Testers.ContainsExpression( - 'HTTP/3 Status Violation: expected 502 got 200', - "There should a status violation for an unexpected 502 response.") diff --git a/tests/autests/gold_tests/https/https.test.py b/tests/autests/gold_tests/https/https.test.py deleted file mode 100644 index 944549b98..000000000 --- a/tests/autests/gold_tests/https/https.test.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Verify basic HTTPS functionality. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic HTTPS functionality. -''' - -r = Test.AddTestRun("Verify processing of a simple HTTPS transaction") - -# Add configure_http=False to verify ATS client and server work when the http -# optional arguments are not provided. -client = r.AddClientProcess("client1", "replay_files/single_transaction.yaml", configure_http=False) -server = r.AddServerProcess("server1", "replay_files/single_transaction.yaml", configure_http=False) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True) - -proxy.Streams.stdout = "gold/single_transaction_proxy.gold" -client.Streams.stdout = "gold/single_transaction_client.gold" -server.Streams.stdout = "gold/single_transaction_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/https/mtls.test.py b/tests/autests/gold_tests/https/mtls.test.py deleted file mode 100644 index b6bf6e66e..000000000 --- a/tests/autests/gold_tests/https/mtls.test.py +++ /dev/null @@ -1,33 +0,0 @@ -''' -Verify correct TLS client and server verification behavior. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct TLS client and server verification behavior. -''' - -# -# Test 1: Verify that both the client and server can verify the proxy -# if specified to do so in the "tls" node. -# -r = Test.AddTestRun("Verify parsing of a YAML-specified replay file") -client = r.AddClientProcess("client1", "replay_files/mtls.yaml") -server = r.AddServerProcess("server1", "replay_files/mtls.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True) - -client.Streams.stdout += Testers.ContainsExpression( - r"Proxy TLS verification result: 0 \(X509_V_OK\)", - "Verify that the client verified the proxy's cert.") - -server.Streams.stdout += Testers.ContainsExpression( - "Sending a certificate request to client with SNI: bob", - "Verify that the server requested a cert from the proxy.") -server.Streams.stdout += Testers.ContainsExpression( - "Client TLS verification result for client with SNI bob: passed", - "Verify that the proxy's cert was verified.") diff --git a/tests/autests/gold_tests/ipv6/ipv6.test.py b/tests/autests/gold_tests/ipv6/ipv6.test.py deleted file mode 100644 index e89e0cac9..000000000 --- a/tests/autests/gold_tests/ipv6/ipv6.test.py +++ /dev/null @@ -1,31 +0,0 @@ -''' -Verify basic IPv6 support. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic IPv6 support. -''' - -# -# Test 1: Verify transactions can be exchanged over IPv6. -# -r = Test.AddTestRun("Verify the correct handling of an HTTP/1, IPv6 transaction") -server = r.AddServerProcess("server1", "replay_files/single_transaction.yaml", use_ipv6=True) -client = r.AddClientProcess("client1", "replay_files/single_transaction.yaml", use_ipv6=True, - http_ports=[server.Variables.http_port], other_args="--no-proxy") - -# Note that this test involves no proxy and, instead, the client talks directly -# to the server. - -client.Streams.stdout = "gold/single_transaction_client.gold" -server.Streams.stdout = "gold/single_transaction_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/method_verification/method_verification.test.py b/tests/autests/gold_tests/method_verification/method_verification.test.py deleted file mode 100644 index fddc8e26d..000000000 --- a/tests/autests/gold_tests/method_verification/method_verification.test.py +++ /dev/null @@ -1,87 +0,0 @@ -''' -Verify request method verification behavior across protocol stacks. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify request method verification behavior across protocol stacks. -''' - -HTTP3_ARGS = "--poll-timeout 10000" - -# -# Test 1: Verify HTTP/1 method verification uses the top-level proxy-request -# method node. -# -r = Test.AddTestRun("Verify HTTP/1 request method verification") -client = r.AddClientProcess("client1", "replay_files/http1_method_verification.yaml", - configure_https=False) -server = r.AddServerProcess("server1", "replay_files/http1_method_verification.yaml", - configure_https=False) -r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -server.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1 Method Success: Key: "1", Method: "GET"', - "The HTTP/1 request method should verify successfully for the matching transaction.") -server.Streams.stdout += Testers.ExcludesExpression( - 'HTTP/1 Method Violation: .*Key: "1"', - "The matching HTTP/1 transaction should not report a method verification violation.") -server.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1 Method Violation: Different. Key: "2", Expected Method: "POST", Received Method: "GET"', - "The HTTP/1 request method should fail when the proxy sends the wrong method.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -# -# Test 2: Verify HTTP/2 request method verification continues to work via the -# :method pseudo header field. -# -r = Test.AddTestRun("Verify HTTP/2 request method verification") -client = r.AddClientProcess("client2", "replay_files/http2_method_verification.yaml") -server = r.AddServerProcess("server2", "replay_files/http2_method_verification.yaml") -r.AddProxyProcess("proxy2", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, use_http2_to_2=True) - -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Success: Key: "1", Field Name: ":method", Value: "GET"', - "The HTTP/2 :method pseudo header should verify successfully for the matching transaction.") -server.Streams.stdout += Testers.ExcludesExpression( - 'Equals Violation: .*Key: "1".*Field Name: ":method"', - "The matching HTTP/2 transaction should not report a :method verification violation.") -server.Streams.stdout += Testers.ContainsExpression( - 'Equals Violation: Different. Key: "2", Field Name: ":method", Correct Value: "POST", Actual Value: "GET"', - "The HTTP/2 :method pseudo header should fail when the proxy sends the wrong method.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -# -# Test 3: Verify an HTTP/3 client path can exercise the HTTP/1 request method -# verification on the downstream side. -# -r = Test.AddTestRun("Verify HTTP/3 request method verification") -client = r.AddClientProcess("client3", "replay_files/http3_method_verification.yaml", - other_args=HTTP3_ARGS) -server = r.AddServerProcess("server3", "replay_files/http3_method_verification.yaml", - other_args=HTTP3_ARGS) -r.AddProxyProcess("proxy3", listen_port=client.Variables.http3_port, - server_port=server.Variables.http_port, use_ssl=True, use_http3_to_1=True) - -server.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1 Method Success: Key: "1", Method: "GET"', - "The HTTP/3-originated request should pass downstream HTTP/1 method verification.") -server.Streams.stdout += Testers.ExcludesExpression( - 'HTTP/1 Method Violation: .*Key: "1"', - "The matching HTTP/3 transaction should not report a downstream method violation.") -server.Streams.stdout += Testers.ContainsExpression( - 'HTTP/1 Method Violation: Different. Key: "2", Expected Method: "POST", Received Method: "GET"', - "The HTTP/3-originated request should fail downstream method verification on mismatch.") - -client.ReturnCode = 0 -server.ReturnCode = 1 diff --git a/tests/autests/gold_tests/no_proxy/no_proxy.test.py b/tests/autests/gold_tests/no_proxy/no_proxy.test.py deleted file mode 100644 index 27d21e82f..000000000 --- a/tests/autests/gold_tests/no_proxy/no_proxy.test.py +++ /dev/null @@ -1,75 +0,0 @@ -''' -Verify basic --no-proxy functionality. -''' -# @file -# -# Copyright 2022-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify basic --no-proxy functionality. -''' - -r = Test.AddTestRun("Verify no-proxy mode works for a simple HTTP transaction") -server = r.AddServerProcess("server", "replay/single_transaction.json") -client = r.AddClientProcess("client", "replay/single_transaction.json", - http_ports=[server.Variables.http_port], - https_ports=[server.Variables.https_port], other_args="--no-proxy") - -client.Streams.stdout = Testers.ContainsExpression( - 'Received an HTTP/1 200 response for .*', - "Verify that the response came back from replay-server") - -client.Streams.stdout += Testers.ContainsExpression( - 'x-testheader: from_server_response', - "Verify that the server response headers were used by the replay-server.") - -client.Streams.stdout += Testers.ExcludesExpression( - 'from_proxy_response', - "Verify that the proxy response headers were not used by the replay-server.") - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout = Testers.ContainsExpression( - "POST /proxy.do HTTP/1.1", "Verify that the proxy request path was used by the replay-client.") - -server.Streams.stdout += Testers.ContainsExpression( - 'client-ip: 10.10.10.1', - "Verify that the proxy request headers were used by the replay-client.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -r = Test.AddTestRun("Verify no-proxy mode works for a simple HTTP/2 transaction") -server = r.AddServerProcess("server-h2", "replay/h2.yaml") -client = r.AddClientProcess("client-h2", "replay/h2.yaml", http_ports=[server.Variables.http_port], - https_ports=[server.Variables.https_port], other_args="--no-proxy") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 1 with stream id 1:", - "The client should receive the zero-length HTTP/2 response from the replay server.") -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 3 with stream id 1:", - "The client should receive the POST replay response on the second HTTP/2 session.") -client.Streams.stdout += Testers.ContainsExpression( - r"HTTP/2 replay metrics: requests-submitted=2, max-in-flight-streams=2, " - r"send-phase-bytes-drained=[0-9]+, final-drain-duration=[0-9]+ms\.", - "The client should complete the HTTP/2 no-proxy replay and emit replay metrics.") -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The client should not report verification failures in HTTP/2 no-proxy mode.") -client.Streams.stdout += Testers.ExcludesExpression( - "HTTP/2 final response drain made no forward progress", - "The client should not hit the HTTP/2 stuck-session summary in HTTP/2 no-proxy mode.") - -server.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 request for key 1 with stream id 1:", - "The server should receive the first no-proxy HTTP/2 request.") -server.Streams.stdout += Testers.ContainsExpression( - ":path: /some/path4", "The server should receive the final no-proxy HTTP/2 request path.") -server.Streams.stdout += Testers.ContainsExpression( - "Request with key 4 passed validation.", - "The server should validate the final HTTP/2 no-proxy request.") -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "The server should not report verification failures in HTTP/2 no-proxy mode.") diff --git a/tests/autests/gold_tests/on_connect/on_connect.test.py b/tests/autests/gold_tests/on_connect/on_connect.test.py deleted file mode 100644 index e5c1940d3..000000000 --- a/tests/autests/gold_tests/on_connect/on_connect.test.py +++ /dev/null @@ -1,124 +0,0 @@ -''' -Verify server-response on_connect behavior. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify server-response on_connect behavior. -''' - -r = Test.AddTestRun("Verify on_connect accept, refuse, and reset behavior") -client = r.AddClientProcess("client", "replay_files/on_connect.yaml", configure_https=False) -server = r.AddServerProcess("server", "replay_files/on_connect.yaml", configure_https=False) -proxy = r.AddProxyProcess("proxy", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "3 transactions in 3 sessions", "The client should have parsed all three transactions.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key 1", "The accept case should return a normal response.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 502 response for key 2", "The refuse case should surface as a proxy 502.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 502 response for key 3", "The reset case should surface as a proxy 502.") - -client.Streams.stdout += Testers.ExcludesExpression("Violation:", - "There should be no verification errors.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "Upstream connection for key 2 closed with TCP FIN before any response bytes were received.", - "The proxy should observe an orderly upstream close for refuse.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "Upstream connection for key 3 closed with TCP RST before any response bytes were received.", - "The proxy should observe an abortive upstream close for reset.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Applying "refuse" on_connect action for key 2.', "The server should apply the refuse action.") - -server.Streams.stdout += Testers.ContainsExpression('Applying "reset" on_connect action for key 3.', - "The server should apply the reset action.") - -server.Streams.stdout += Testers.ContainsExpression( - "Sent the following HTTP/1 response headers for key 1", - "The accept case should still write a normal response.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Sent the following HTTP/1 response headers for key 2", - "The refuse case should not write response headers.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Sent the following HTTP/1 response headers for key 3", - "The reset case should not write response headers.") - -server.Streams.stdout += Testers.ExcludesExpression("Violation:", - "There should be no verification errors.") - -r = Test.AddTestRun("Verify on_connect accept, refuse, and reset behavior over HTTP/2") -client = r.AddClientProcess("client_h2", "replay_files/on_connect_http2.yaml") -server = r.AddServerProcess("server_h2", "replay_files/on_connect_http2.yaml") -proxy = r.AddProxyProcess("proxy_h2", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_1=True) - -client.Streams.stdout += Testers.ContainsExpression( - "3 transactions in 3 sessions", "The HTTP/2 client should parse all three transactions.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 1 with stream id 1:", - "The accept case should return a normal HTTP/2 response.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 2 with stream id 1:", - "The refuse case should surface as an HTTP/2 502 response.") - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key 3 with stream id 1:", - "The reset case should surface as an HTTP/2 502 response.") - -client.Streams.stdout += Testers.ContainsExpression( - ":status: 502", "The HTTP/2 client should observe 502 responses for failure cases.") - -client.Streams.stdout += Testers.ExcludesExpression("Violation:", - "There should be no verification errors.") - -proxy.Streams.stdout += Testers.ContainsExpression( - "Upstream connection for key 2 closed with TCP FIN before any response bytes were received.", - "The HTTP/2 proxy should observe an orderly upstream close for refuse.") - -# On Rocky 8, the Python HTTP/2 test proxy uses the system Python/OpenSSL 1.1.1 -# TLS stack. On that HTTPS path, our Python HTTP/2 test client may surface a -# server-side RST as a generic disconnect and report it as a FIN, so this -# assertion accepts either FIN or RST even though the verifier server still -# performs an abortive close which should result in a RST. -proxy.Streams.stdout += Testers.ContainsExpression( - "Upstream connection for key 3 closed with TCP (FIN|RST) before any response bytes were received.", - "The HTTP/2 proxy should observe the upstream close for reset.") - -server.Streams.stdout += Testers.ContainsExpression( - 'Applying "refuse" on_connect action for key 2.', "The server should apply the refuse action.") - -server.Streams.stdout += Testers.ContainsExpression('Applying "reset" on_connect action for key 3.', - "The server should apply the reset action.") - -server.Streams.stdout += Testers.ContainsExpression( - "Sent the following HTTP/1 response headers for key 1", - "The accept case should still write a normal upstream HTTP/1 response.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Sent the following HTTP/1 response headers for key 2", - "The refuse case should not write response headers.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Sent the following HTTP/1 response headers for key 3", - "The reset case should not write response headers.") - -server.Streams.stdout += Testers.ExcludesExpression("Violation:", - "There should be no verification errors.") diff --git a/tests/autests/gold_tests/protocol_syntax/protocol_syntax.test.py b/tests/autests/gold_tests/protocol_syntax/protocol_syntax.test.py deleted file mode 100644 index e06eb2f98..000000000 --- a/tests/autests/gold_tests/protocol_syntax/protocol_syntax.test.py +++ /dev/null @@ -1,29 +0,0 @@ -''' -Verify protocol syntax compatibility. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify protocol syntax compatibility. -''' - -r = Test.AddTestRun("Verify the verbose protocol sequence remains supported") - -client = r.AddClientProcess("client1", "replay_files/single_transaction.yaml", configure_http=False) -server = r.AddServerProcess("server1", "replay_files/single_transaction.yaml", configure_http=False) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True) - -proxy.Streams.stdout = "gold/single_transaction_proxy.gold" -client.Streams.stdout = "gold/single_transaction_client.gold" -server.Streams.stdout = "gold/single_transaction_server.gold" - -client.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout += Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") diff --git a/tests/autests/gold_tests/proxy-protocol/proxy_protocol.test.py b/tests/autests/gold_tests/proxy-protocol/proxy_protocol.test.py deleted file mode 100644 index ae5261a32..000000000 --- a/tests/autests/gold_tests/proxy-protocol/proxy_protocol.test.py +++ /dev/null @@ -1,219 +0,0 @@ -''' -Verify basic PROXY protocol functionality. -''' -# @file -# -# Copyright 2023-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import re - -Test.Summary = ''' -Verify basic PROXY protocol functionality. -''' - - -class PPTest: - # static id for client, server and proxy processes - test_id = 1 - - def __init__(self, testBaseName, ppVersion, isHTTPS, testRunDesc=""): - self.testRun = Test.AddTestRun(testRunDesc) - self.testBaseName = testBaseName - self.ppVersion = ppVersion - self.replayFile = f'replay_files/{testBaseName}_pp_v{ppVersion}.replay.yaml' - self.isHTTPS = isHTTPS - - def setupClient(self, isHTTPS): - self.client = self.testRun.AddClientProcess(f"client-{PPTest.test_id}", self.replayFile, - configure_http=not isHTTPS, - configure_https=isHTTPS) - - def setupServer(self, isHTTPS): - self.server = self.testRun.AddServerProcess(f"server-{PPTest.test_id}", self.replayFile, - configure_http=not isHTTPS, - configure_https=isHTTPS) - - def setupProxy(self, isHTTPS): - self.proxyListenPort = self.client.Variables.https_port if isHTTPS else self.client.Variables.http_port - self.serverListenPort = self.server.Variables.https_port if isHTTPS else self.server.Variables.http_port - self.proxy = self.testRun.AddProxyProcess(f"proxy-{PPTest.test_id}", - listen_port=self.proxyListenPort, - server_port=self.serverListenPort, - use_ssl=isHTTPS) - - def setupTransactionLogsVerification(self): - # Verify that the http trasactions are successful(not hindered by the - # PROXY protocol processing). - self.proxy.Streams.stdout = f"gold/{self.testBaseName}_proxy.gold" - self.client.Streams.stdout = f"gold/{self.testBaseName}_client.gold" - self.server.Streams.stdout = f"gold/{self.testBaseName}_server.gold" - - def setupPPLogsVerification(self): - # Verify the PROXY protocol related logs - self.client.Streams.stdout += Testers.ContainsExpression( - rf"Sending PROXY header", - "Verify that the PROXY header is sent from the client to the proxy.") - - self.proxy.Streams.stdout += Testers.ContainsExpression( - f"Received .* bytes of Proxy Protocol V{self.ppVersion}", - "Verify that the PROXY header is received by the proxy.") - self.proxy.Streams.stdout += Testers.ContainsExpression( - rf"PROXY TCP4 127\.0\.0\.1 127\.0\.0\.1 [0-9]+ {self.proxyListenPort}", - "Verify the client sends a valid PROXY header.") - - self.server.Streams.stdout += Testers.ContainsExpression( - rf"Received PROXY header v{self.ppVersion}:.*\nPROXY TCP4 127\.0\.0\.1 127\.0\.0\.1 [0-9]+ {self.proxyListenPort}", - "Verify that the server receives the PROXY header and parsed sucessfully.", - reflags=re.MULTILINE) - - def run(self): - self.setupClient(self.isHTTPS) - self.setupServer(self.isHTTPS) - self.setupProxy(self.isHTTPS) - self.setupTransactionLogsVerification() - self.setupPPLogsVerification() - PPTest.test_id += 1 - - -# Test 1: Verify the PROXY header v1 is sent and received in a HTTP transaction. -PPTest("http_single_transaction", ppVersion=1, isHTTPS=False, - testRunDesc="Verify PROXY protocol v1 is sent and received in a HTTP connection").run() - -# Test 2: Verify the PROXY header v2 is sent and received in a HTTP transaction. -PPTest("http_single_transaction", ppVersion=2, isHTTPS=False, - testRunDesc="Verify PROXY protocol v2 is sent and received in a HTTP connection").run() - -# Test 3: Verify the PROXY header v1 is sent and received in a HTTPS -# transaction. -PPTest("https_single_transaction", ppVersion=1, isHTTPS=True, - testRunDesc="Verify PROXY protocol v1 is sent and received in a HTTPS connection").run() - -# Test 4: Verify the PROXY header v2 is sent and received in a HTTPS -# transaction. -PPTest("https_single_transaction", ppVersion=2, isHTTPS=True, - testRunDesc="Verify PROXY protocol v2 is sent and received in a HTTPS connection").run() - -# Test 5: Verify the PROXY protocol is not sent when not specified in the replay -# file -r = Test.AddTestRun("Verify the PROXY protocol is not sent when not specified in the replay file") - -# Add configure_https=False to verify ATS client and server work when the https -# optional arguments are not provided. -client = r.AddClientProcess("no-pp-client1", - "replay_files/http_single_transaction_no_pp.replay.yaml", - configure_https=False) -server = r.AddServerProcess("no-pp-server1", - "replay_files/http_single_transaction_no_pp.replay.yaml", - configure_https=False) -proxy = r.AddProxyProcess("no-pp-proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# Verify the http transaction finishes successfully. -proxy.Streams.stdout = "gold/http_single_transaction_proxy.gold" -client.Streams.stdout = "gold/http_single_transaction_client.gold" -server.Streams.stdout = "gold/http_single_transaction_server.gold" - -# Verify the PROXY protocol related logs. Since the replay file does not have -# the PROXY protocol related configuration, the client, proxy and server should -# not have these logs -client.Streams.stdout += Testers.ExcludesExpression( - "Sending PROXY header", "Client should not send PROXY header if not asked to.") -proxy.Streams.stdout += Testers.ExcludesExpression("Received .* bytes of Proxy Protocol", - "Proxy should not receive the PROXY header.") -server.Streams.stdout += Testers.ExcludesExpression( - "Received PROXY header", "The server should not receive the PROXY header.") - -# -# Test 6: Verify the PROXY v1 message can be sent and received on IPv6 -# connection. -# -r = Test.AddTestRun("Verify the PROXY message can be sent and received on IPv6 connection") -server = r.AddServerProcess("ipv6-server1", - "replay_files/single_transaction_ipv6_pp_v1.replay.yaml", use_ipv6=True) -client = r.AddClientProcess("ipv6-client1", - "replay_files/single_transaction_ipv6_pp_v1.replay.yaml", use_ipv6=True, - http_ports=[server.Variables.http_port], other_args="--no-proxy") - -# Verify successful transaction despite PROXY protocol processing -client.Streams.stdout = "gold/ipv6_client.gold" -server.Streams.stdout = "gold/ipv6_server.gold" - -# Verify the PROXY protocol related logs -client.Streams.stdout += Testers.ContainsExpression( - rf"Sending PROXY header", "Verify that the PROXY header is sent from the client to the server.") - -server.Streams.stdout += Testers.ContainsExpression( - f"Received PROXY header v1:.*\nPROXY TCP6 ::1 ::1 [0-9]+ {server.Variables.http_port}", - "Verify that the server receives the PROXY header and parsed sucessfully.", - reflags=re.MULTILINE) - -# -# Test 7: Verify the PROXY v2 message can be sent and received on IPv6 -# connection. -# -r = Test.AddTestRun("Verify the PROXY message can be sent and received on IPv6 connection") -server = r.AddServerProcess("ipv6-server2", - "replay_files/single_transaction_ipv6_pp_v2.replay.yaml", use_ipv6=True) -client = r.AddClientProcess("ipv6-client2", - "replay_files/single_transaction_ipv6_pp_v2.replay.yaml", use_ipv6=True, - http_ports=[server.Variables.http_port], other_args="--no-proxy") - -# Verify successful transaction despite PROXY protocol processing -client.Streams.stdout = "gold/ipv6_client.gold" -server.Streams.stdout = "gold/ipv6_server.gold" - -# Verify the PROXY protocol related logs -client.Streams.stdout += Testers.ContainsExpression( - rf"Sending PROXY header", "Verify that the PROXY header is sent from the client to the server.") - -server.Streams.stdout += Testers.ContainsExpression( - f"Received PROXY header v2:.*\nPROXY TCP6 ::1 ::1 [0-9]+ {server.Variables.http_port}", - "Verify that the server receives the PROXY header and parsed sucessfully.", - reflags=re.MULTILINE) - -# Test 8: Verify the PROXY protocol is sent with source and -# destination addresses specified in the replay file -r = Test.AddTestRun( - "Verify the PROXY protocol is sent with the source and destination specified addresses in the replay file" -) - -EXPECTED_SRC_ADDRESS = "111.111.111.111" -EXPECTED_SRC_PORT = "11111" -EXPECTED_DST_ADDRESS = "222.222.222.222" -EXPECTED_DST_PORT = "22222" -EXPECTED_PROXY_HEADER = f"PROXY TCP4 {EXPECTED_SRC_ADDRESS} {EXPECTED_DST_ADDRESS} {EXPECTED_SRC_PORT} {EXPECTED_DST_PORT}" - -# Add configure_https=False to verify ATS client and server work when the https -# optional arguments are not provided. -client = r.AddClientProcess("specified-addr-pp-client1", - "replay_files/http_pp_v1_with_address.replay.yaml", - configure_https=False) -server = r.AddServerProcess("specified-addr-pp-server1", - "replay_files/http_pp_v1_with_address.replay.yaml", - configure_https=False) -proxy = r.AddProxyProcess("specified-addr-pp-proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# Verify the http transaction finishes successfully. -proxy.Streams.stdout = "gold/http_single_transaction_proxy.gold" -client.Streams.stdout = "gold/http_single_transaction_client.gold" -server.Streams.stdout = "gold/http_single_transaction_server.gold" - -# Verify the PROXY protocol related logs. Make sure the source and destination -# addresses of the PROXY header match the ones specified in the replay file -client.Streams.stdout += Testers.ContainsExpression( - rf"Sending PROXY header", "Verify that the PROXY header is sent from the client to the proxy.") - -proxy.Streams.stdout += Testers.ContainsExpression( - f"Received .* bytes of Proxy Protocol", - "Verify that the PROXY header is received by the proxy.") -proxy.Streams.stdout += Testers.ContainsExpression( - rf"{EXPECTED_PROXY_HEADER}", - "Verify the client sends a PROXY header with the specified source and destination addresses.") - -server.Streams.stdout += Testers.ContainsExpression( - rf"Received PROXY header v1:.*\n{EXPECTED_PROXY_HEADER}", - "Verify that the server receives the PROXY header and parsed sucessfully.", - reflags=re.MULTILINE) diff --git a/tests/autests/gold_tests/replay_gen/replay_gen.test.py b/tests/autests/gold_tests/replay_gen/replay_gen.test.py deleted file mode 100644 index b1e64f1c6..000000000 --- a/tests/autests/gold_tests/replay_gen/replay_gen.test.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Verify replay-gen.py can generate parsable replay files. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify replay-gen.py can generate parsable replay files. -''' - -# -# Test 1: Generate replay files via replay_gen and verify they can be replayed. -# -r = Test.AddTestRun("Generate replay files via replay-gen.py") -replay_gen = r.ConfigureReplayGenDefaultProcess("replay_gen1", num_transactions=20) - -r = Test.AddTestRun("Make sure we can use the generated replay files") -client = r.AddClientProcess("client1", replay_gen.Variables.replay_dir) -server = r.AddServerProcess("server1", replay_gen.Variables.replay_dir) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# The Python test proxy may close the connection part way through the -# transactions. I've verified that if the client talks directly to the server, -# there are no problems. -client.ReturnCode = Any(0, 1) - -client.Streams.stdout += Testers.ContainsExpression( - "Parsed 20 transactions", - "Verify that the verifier client was able to parse the expected 20 transactions.") - -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 20 transactions", - "Verify that the verifier server was able to parse the expected 20 transactions.") diff --git a/tests/autests/gold_tests/request_absence/request_absence.test.py b/tests/autests/gold_tests/request_absence/request_absence.test.py deleted file mode 100644 index 3d7623459..000000000 --- a/tests/autests/gold_tests/request_absence/request_absence.test.py +++ /dev/null @@ -1,183 +0,0 @@ -''' -Verify server-side request presence expectation behavior. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify server-side request presence expectation behavior. -''' - -missing_present_message = ('never reached the verifier-server before shutdown even though "expect" ' - 'was explicitly set to "present"') - -r = Test.AddTestRun("Verify expect: absent succeeds when the proxy replies directly") -client = r.AddClientProcess("client1", "replay_files/http1_cache_hit.yaml", configure_https=False) -server = r.AddServerProcess("server1", "replay_files/http1_cache_hit.yaml", configure_https=False) -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key cache-hit-1", - "The client should receive the direct cached response.") -server.Streams.stdout += Testers.ExcludesExpression( - 'unexpectedly reached the verifier-server', - "The server should not report a forbidden request arrival.") -server.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before shutdown', - "An absent expectation should not count as an unprocessed verification.") -proxy.Streams.stdout += Testers.ContainsExpression( - 'Serving local response for key cache-hit-1 with status 200.', - "The shared proxy should synthesize the cached response locally.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -r = Test.AddTestRun("Verify expect: present succeeds when the proxy forwards the request") -client = r.AddClientProcess("client2", "replay_files/http1_present_success.yaml", - configure_https=False) -server = r.AddServerProcess("server2", "replay_files/http1_present_success.yaml", - configure_https=False) -r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key present-http1-1", - "The client should receive the upstream response when the request is forwarded.") -server.Streams.stdout += Testers.ExcludesExpression( - missing_present_message, - "The server should not report a missing request when the request arrives.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -r = Test.AddTestRun("Verify expect: present fails when the proxy replies directly") -client = r.AddClientProcess("client3", "replay_files/http1_present_cache_hit.yaml", - configure_https=False) -server = r.AddServerProcess("server3", "replay_files/http1_present_cache_hit.yaml", - configure_https=False) -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key present-cache-hit-1", - "The client should still receive the direct cached response.") -server.Streams.stdout += Testers.ContainsExpression( - missing_present_message, "The server should fail when an expected request never arrives.") -server.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before shutdown', - "The explicit present failure should not fall back to the generic unprocessed check.") -proxy.Streams.stdout += Testers.ContainsExpression( - 'Serving local response for key present-cache-hit-1 with status 200.', - "The shared proxy should synthesize the cached response locally.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -r = Test.AddTestRun("Verify expect: present still fails with --allow-unprocessed-verifications") -client = r.AddClientProcess("client4", "replay_files/http1_present_cache_hit.yaml", - configure_https=False) -server = r.AddServerProcess("server4", "replay_files/http1_present_cache_hit.yaml", - configure_https=False, other_args="--allow-unprocessed-verifications") -proxy = r.AddProxyProcess("proxy4", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -server.Streams.stdout += Testers.ContainsExpression( - missing_present_message, "The allow flag should not suppress explicit present failures.") -server.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before shutdown', - "The allow flag is not involved because this is not a generic " - "unprocessed verification failure.") -proxy.Streams.stdout += Testers.ContainsExpression( - 'Serving local response for key present-cache-hit-1 with status 200.', - "The shared proxy should synthesize the cached response locally.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -r = Test.AddTestRun("Verify omitted expect allows the proxy to reply directly") -client = r.AddClientProcess("client5", "replay_files/http1_unspecified_cache_hit.yaml", - configure_https=False) -server = r.AddServerProcess("server5", "replay_files/http1_unspecified_cache_hit.yaml", - configure_https=False) -proxy = r.AddProxyProcess("proxy5", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 200 response for key unspecified-cache-hit-1", - "The client should receive the direct cached response.") -server.Streams.stdout += Testers.ExcludesExpression( - missing_present_message, - "No explicit presence expectation should mean no missing-request failure.") -server.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before shutdown', - "Without request verification rules, omitting expect should not trigger " - "an unprocessed verification failure.") -proxy.Streams.stdout += Testers.ContainsExpression( - 'Serving local response for key unspecified-cache-hit-1 with status 200.', - "The shared proxy should synthesize the cached response locally.") - -client.ReturnCode = 0 -server.ReturnCode = 0 - -r = Test.AddTestRun("Verify expect: absent fails when HTTP/1 traffic reaches the server") -client = r.AddClientProcess("client6", "replay_files/http1_violation.yaml", configure_https=False) -server = r.AddServerProcess("server6", "replay_files/http1_violation.yaml", configure_https=False) -r.AddProxyProcess("proxy6", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 404 response for key absent-http1-1", - "The client should receive the fallback 404 when the forbidden request arrives upstream.") -server.Streams.stdout += Testers.ContainsExpression( - 'Proxy request with key "absent-http1-1" unexpectedly reached the verifier-server', - "The server should fail as soon as the forbidden request arrives.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -r = Test.AddTestRun("Verify expect: absent still honors an explicit server-response") -client = r.AddClientProcess("client7", "replay_files/http1_explicit_response.yaml", - configure_https=False) -server = r.AddServerProcess("server7", "replay_files/http1_explicit_response.yaml", - configure_https=False) -r.AddProxyProcess("proxy7", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/1 418 response for key absent-http1-explicit", - "The client should receive the configured response when the forbidden request arrives.") -server.Streams.stdout += Testers.ContainsExpression( - 'Proxy request with key "absent-http1-explicit" unexpectedly reached the verifier-server', - "The server should still fail when the forbidden request arrives.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -r = Test.AddTestRun("Verify expect: absent fails for HTTP/2 traffic reaching the server") -client = r.AddClientProcess("client8", "replay_files/http2_violation.yaml") -server = r.AddServerProcess("server8", "replay_files/http2_violation.yaml") -r.AddProxyProcess("proxy8", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, use_http2_to_2=True) - -client.Streams.stdout += Testers.ContainsExpression( - "Received an HTTP/2 response for key absent-http2-1 with stream id 1:", - "The HTTP/2 client should receive the response generated for the forbidden upstream arrival.") -server.Streams.stdout += Testers.ContainsExpression( - 'Proxy request with key "absent-http2-1" unexpectedly reached the verifier-server', - "The server should fail when the forbidden HTTP/2 request arrives.") - -client.ReturnCode = 0 -server.ReturnCode = 1 - -r = Test.AddTestRun("Verify present expectations still require server-response") -server = r.AddDefaultServerProcess("server9", "replay_files/missing_server_response.yaml", - configure_https=False) - -server.Streams.stdout += Testers.ContainsExpression( - 'does not have a server response', - "Transactions without expect: absent should still require a server-response node.") -server.ReturnCode = 1 diff --git a/tests/autests/gold_tests/sigint_shutdown/sigint_shutdown.test.py b/tests/autests/gold_tests/sigint_shutdown/sigint_shutdown.test.py deleted file mode 100644 index f8cfbd10a..000000000 --- a/tests/autests/gold_tests/sigint_shutdown/sigint_shutdown.test.py +++ /dev/null @@ -1,37 +0,0 @@ -''' -Verify verifier-client and verifier-server honor SIGINT promptly during replay. -''' -# @file -# -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -import os - -Test.Summary = ''' -Verify verifier-client and verifier-server honor SIGINT promptly during replay. -''' - -verifier_script = 'verify_sigint_shutdown.py' -verifier_bin = os.environ['VERIFIER_BIN'] -verifier_server = os.path.join(verifier_bin, 'verifier-server') -verifier_client = os.path.join(verifier_bin, 'verifier-client') - -r = Test.AddTestRun("Verify verifier-server exits promptly on SIGINT during replay.") -r.Processes.Default.Setup.Copy(verifier_script) -r.Processes.Default.Command = \ - f'python3 {verifier_script} --mode server --verifier-server "{verifier_server}"' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression( - 'OK: server exited promptly', - 'The helper script should confirm verifier-server shuts down quickly on SIGINT.') - -r = Test.AddTestRun("Verify verifier-client exits promptly on SIGINT during replay.") -r.Processes.Default.Setup.Copy(verifier_script) -r.Processes.Default.Command = \ - f'python3 {verifier_script} --mode client --verifier-client "{verifier_client}"' -r.ReturnCode = 0 -r.Streams.stdout += Testers.ContainsExpression( - 'OK: client exited promptly', - 'The helper script should confirm verifier-client shuts down quickly on SIGINT.') diff --git a/tests/autests/gold_tests/strict_mode/strict_mode.test.py b/tests/autests/gold_tests/strict_mode/strict_mode.test.py deleted file mode 100644 index bd76e5546..000000000 --- a/tests/autests/gold_tests/strict_mode/strict_mode.test.py +++ /dev/null @@ -1,51 +0,0 @@ -''' -Verify strict mode functionality. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify strict mode functionality. -''' - -# -# Test 1: Verify there are no warnings when the fields match. -# -r = Test.AddTestRun("Verify strict mode is silent when the fields match.") -client = r.AddClientProcess("client1", "replay_files/fields_match.json", other_args="--strict") -server = r.AddServerProcess("server1", "replay_files/fields_match.json", other_args="--strict") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/fields_match_proxy.gold" - -client.Streams.stdout = Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -server.Streams.stdout = Testers.ExcludesExpression( - "Violation:", "There should be no verification errors because there are none added.") - -# -# Test 2: Verify there are warnings when the fields don't match. -# -r = Test.AddTestRun("Verify strict mode warns when the fields don't match") -client = r.AddClientProcess("client2", "replay_files/fields_differ.json", other_args="--strict") -server = r.AddServerProcess("server2", "replay_files/fields_differ.json", other_args="--strict") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -proxy.Streams.stdout = "gold/fields_differ_proxy.gold" - -client.Streams.stdout = Testers.ContainsExpression( - 'Violation: Absent. Key: "cb9b4e94-5d42-43d4-8545-320033298ba2-226381119", Field Name: "x-thisresponseheaderwontexist", Correct Value: "ThereforeTheClientShouldWarn', - "There should be a warning about the missing response header") - -server.Streams.stdout = Testers.ContainsExpression( - 'Violation: Absent. Key: "cb9b4e94-5d42-43d4-8545-320033298ba2-226381119", Field Name: "x-thisrequestheaderwontexist", Correct Value: "ThereforeTheServerShouldWarn', - "There should be a warning about the missing proxy request header.") - -client.ReturnCode = 1 -server.ReturnCode = 1 diff --git a/tests/autests/gold_tests/url_verification/url_verification.test.py b/tests/autests/gold_tests/url_verification/url_verification.test.py deleted file mode 100644 index 80a01e682..000000000 --- a/tests/autests/gold_tests/url_verification/url_verification.test.py +++ /dev/null @@ -1,67 +0,0 @@ -''' -Verify correct URL verification behavior. -''' -# @file -# -# Copyright 2021-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - -Test.Summary = ''' -Verify correct URL verification behavior. -''' - -# -# Test 1: Verify field verification in a YAML replay file. -# -r = Test.AddTestRun("Verify URL verification works for a simple HTTP transaction") -client = r.AddClientProcess("client1", "url_verification.yaml") -server = r.AddServerProcess("server1", "url_verification.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) - -# Verify a success and failure of each validation in the request. -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "host", Value: "example.one"', - 'Validation should be happy that the host matches the client host.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "path", Value: "config/settings.yaml"', - 'Validation should be happy that the path matches the client path.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "scheme", Value: "http"', - 'Validation should be happy that the scheme matches the client scheme.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "net-loc", Value: "example.one:8080"', - 'Validation should be happy that the authority matches the client authority.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "net-loc", Value: "example.one:8080"', - 'Validation should be happy that the authority matches the client authority.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", "port", Value: "8080"', - 'Validation should be happy that the port matches the client paportth.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Success: Key: "5", URI Part: "query", Value: "q=3"', - 'Validation should be happy that the query matches the client query.') -server.Streams.stdout = Testers.ContainsExpression( - 'Absence Success: Key: "5", URI Part: "fragment"', - 'Validation should be happy that the fragment was missing.') -server.Streams.stdout = Testers.ContainsExpression( - 'Presence Success: Key: "5", URI Part: "host"', - 'Validation should be happy that the host was not missing.') -server.Streams.stdout = Testers.ContainsExpression( - 'Contains Success: Key: "5", URI Part: "path", Value: "yaml"', - 'Validation should be happy that the client path contains the value.') -server.Streams.stdout = Testers.ContainsExpression( - 'Suffix Success: Key: "5", URI Part: "scheme", Value: "p"', - 'Validation should be happy that the client scheme ends with the value.') -server.Streams.stdout = Testers.ContainsExpression( - 'Prefix Success: Key: "5", URI Part: "port", Value: "8"', - 'Validation should be happy that the client port begins with the value.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Violation: Different. Key: "5", URI Part: "net-loc", Correct Value: "8080", Actual Value: "example.one:8080"', - 'Validation should be unhappy that the authority was too long.') -server.Streams.stdout = Testers.ContainsExpression( - 'Equals Violation: Absent. Key: "5", URI Part: "fragment", Correct Value: "F"', - 'Validation should be unhappy that the fragment was missing.') - -server.ReturnCode = 1 diff --git a/tests/autests/serial_tests.txt b/tests/autests/serial_tests.txt deleted file mode 100644 index ff763a61f..000000000 --- a/tests/autests/serial_tests.txt +++ /dev/null @@ -1,5 +0,0 @@ -# Copyright 2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# -# Add one top-level AuTest name per line when a test must not run in parallel. -# Paths ending in .test.py are also accepted. diff --git a/tests/autests/test-env-check.sh b/tests/autests/test-env-check.sh deleted file mode 100755 index 976417c92..000000000 --- a/tests/autests/test-env-check.sh +++ /dev/null @@ -1,89 +0,0 @@ -#!/bin/bash -# Copyright 2020-2026, Verizon Media -# SPDX-License-Identifier: Apache-2.0 -# - - -# check for python3 -python3 - << _END_ -import sys - -if sys.version_info.major < 3 or sys.version_info.minor < 10: - exit(1) -_END_ - -if [ $? = 1 ] -then - echo "Python 3.10 or newer is not installed/enabled." - exit 1 -else - echo "Python 3.10 or newer detected!" -fi - -# check for python development header -- for autest -python3-config &> /dev/null -if [ $? = 1 ] -then - echo "python3-dev/devel detected!" -else - echo "python3-dev/devel is not installed. " - exit 1 -fi - -# check for uv -uv --version &> /dev/null -if [ $? -eq 0 ]; then - echo "uv detected!" - - recreate_venv=false - sync_venv=false - if [ ! -d .venv ]; then - recreate_venv=true - elif ! .venv/bin/python3 --version &> /dev/null; then - echo "The existing virtual environment is stale. Recreating it." - recreate_venv=true - elif ! .venv/bin/autest --help &> /dev/null; then - echo "The existing AuTest entry point is stale. Recreating the virtual environment." - recreate_venv=true - elif ! uv sync --check --locked &> /dev/null; then - echo "The existing virtual environment does not match the locked dependencies. Syncing it." - sync_venv=true - fi - - if [ "${recreate_venv}" = true ]; then - rm -rf .venv - echo "Installing a new virtual environment via uv" - sync_venv=true - fi - - if [ "${sync_venv}" = true ]; then - - os_name=$(uname) - if [ "${os_name}" == "Darwin" ] - then - # MacOS has its own SSL version. The PyOpenSSL Python package - # installed via the following uv command will build the - # crytpography package which will require the brew-installed openssl - # version. We set the following variables to point the cryptography - # build to the brew openssl. - brew_openssl_lib="/usr/local/opt/openssl/lib" - if [ ! -d "${brew_openssl_lib}" ] - then - echo "WARNING:" - echo "Could not find ${brew_openssl_lib}. Have you run \"brew install openssl\"?" - echo "If the cryptography package fails to install, the lack of brew's openssl may be why." - else - export LDFLAGS="-L/usr/local/opt/openssl/lib" - export CPPFLAGS="-I/usr/local/opt/openssl/include" - export PKG_CONFIG_PATH="/usr/local/opt/openssl/lib/pkgconfig" - fi - fi - - uv sync --locked - else - echo "Using the pre-existing virtual environment." - fi -else - echo "uv is not installed/enabled. " - exit 1 -fi diff --git a/tests/autests/pyproject.toml b/tests/pyproject.toml similarity index 62% rename from tests/autests/pyproject.toml rename to tests/pyproject.toml index 79eab5153..6cd2013b9 100644 --- a/tests/autests/pyproject.toml +++ b/tests/pyproject.toml @@ -5,22 +5,26 @@ # [project] -name = "proxy-verifier-autests" +name = "proxy-verifier-urtests" version = "0.1.0" -description = "AuTest dependencies for Proxy Verifier" +description = "Pytest dependencies for Proxy Verifier Uranium tests" requires-python = ">=3.10" dependencies = [ "aioquic==1.3.0", - "autest", "eventlet", "h2", "psutil", + "pytest>=8.0", + "pytest-xdist>=3.6", "pyOpenSSL", "ruamel_yaml", ] [project.optional-dependencies] +fast-diff = [ + "cdifflib>=1.2.9", +] dev = [ "pyflakes", ] diff --git a/tests/tools/uranium/__init__.py b/tests/tools/uranium/__init__.py new file mode 100644 index 000000000..b94dc7bd1 --- /dev/null +++ b/tests/tools/uranium/__init__.py @@ -0,0 +1,16 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Public helpers for Proxy Verifier Uranium tests.""" + +from .model import CaseSuite, OutputRef, PortRef, is_platform +from .runtime import Uranium + +__all__ = [ + "CaseSuite", + "OutputRef", + "PortRef", + "Uranium", + "is_platform", +] diff --git a/tests/tools/uranium/assertions.py b/tests/tools/uranium/assertions.py new file mode 100644 index 000000000..f65271415 --- /dev/null +++ b/tests/tools/uranium/assertions.py @@ -0,0 +1,49 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Assertions shared by Uranium tests.""" + +from __future__ import annotations + +from pathlib import Path +import difflib +import re + +try: + from cdifflib import CSequenceMatcher +except ImportError: + USING_ACCELERATED_DIFF = False +else: + difflib.SequenceMatcher = CSequenceMatcher + USING_ACCELERATED_DIFF = True + + +def assert_matches_gold(actual: str | Path, expected: Path) -> None: + """Assert that output matches a wildcard-aware gold file.""" + + if isinstance(actual, Path): + if not actual.is_file(): + raise AssertionError(f"Actual output file does not exist: {actual}") + actual_text = actual.read_text(errors="replace") + actual_name = str(actual) + else: + actual_text = actual + actual_name = "actual" + + actual_text = actual_text.replace("\r\n", "\n") + if not expected.is_file(): + raise AssertionError(f"Gold file does not exist: {expected}") + expected_text = expected.read_text(errors="replace").replace("\r\n", "\n") + expected_text = expected_text.replace("\n``\n", "``") + pattern = "\\A" + ".*?".join( + re.escape(part) for part in re.split(r"(?:\{\}|``)", expected_text)) + "\\Z" + if re.match(pattern, actual_text, re.DOTALL) is None: + difference = "".join( + difflib.unified_diff( + expected_text.splitlines(True), + actual_text.splitlines(True), + str(expected), + actual_name, + )) + raise AssertionError(f"Output did not match gold file:\n{difference}") diff --git a/tests/autests/gold_tests/autest-site/default_url_file b/tests/tools/uranium/default_url_file similarity index 100% rename from tests/autests/gold_tests/autest-site/default_url_file rename to tests/tools/uranium/default_url_file diff --git a/tests/tools/uranium/model.py b/tests/tools/uranium/model.py new file mode 100644 index 000000000..4d6e718bd --- /dev/null +++ b/tests/tools/uranium/model.py @@ -0,0 +1,332 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Declarative model used by ordinary pytest-based Uranium tests.""" + +from __future__ import annotations + +from dataclasses import dataclass, field +from pathlib import Path +from typing import Any, Callable, Iterable +import platform +import re + +from .assertions import assert_matches_gold + + +@dataclass(frozen=True) +class Expectation: + """One assertion applied to a process stream after a case completes.""" + + kind: str + value: str + explanation: str = "" + flags: int = 0 + + +def is_platform(name: str) -> bool: + """Return whether the current platform matches @a name.""" + + return platform.system().lower() == name.lower() + + +@dataclass(frozen=True) +class PortRef: + """A listener port allocated when a Uranium case starts.""" + + process: "ProcessSpec" + protocol: str + index: int = 0 + + def __str__(self) -> str: + return f"{{uranium-port:{self.process.identity}:{self.protocol}:{self.index}}}" + + +@dataclass(frozen=True) +class OutputRef: + """A process output path resolved after its case has run.""" + + process: "ProcessSpec" + stream: str + + def __str__(self) -> str: + return f"{{uranium-output:{self.process.identity}:{self.stream}}}" + + +@dataclass(frozen=True) +class ArtifactRef: + """A generated process artifact resolved in its case directory.""" + + process: "ProcessSpec" + name: str + + def __str__(self) -> str: + return f"{{uranium-artifact:{self.process.identity}:{self.name}}}" + + +class StreamExpectations: + """Assertions and the eventual path for one captured process stream.""" + + def __init__(self, process: "ProcessSpec", name: str) -> None: + self.process = process + self.name = name + self._expectations: list[Expectation] = [] + + @property + def expectations(self) -> tuple[Expectation, ...]: + """Return the expectations registered for this stream.""" + + return tuple(self._expectations) + + @property + def path(self) -> OutputRef: + """Return a symbolic reference to the captured stream path.""" + + return OutputRef(self.process, self.name) + + def contains(self, expression: str, explanation: str, *, reflags: int = 0) -> None: + """Require this stream to contain a regular expression.""" + + self._require_explanation(explanation, "contains") + self._expectations.append(Expectation("contains", expression, explanation, reflags)) + + def excludes(self, expression: str, explanation: str, *, reflags: int = 0) -> None: + """Require this stream to exclude a regular expression.""" + + self._require_explanation(explanation, "excludes") + self._expectations.append(Expectation("excludes", expression, explanation, reflags)) + + def matches_gold(self, path: str, explanation: str = "") -> None: + """Require this stream to match a wildcard-aware gold file.""" + + self._expectations.append(Expectation("gold", path, explanation)) + + def reset(self) -> None: + """Remove every expectation previously registered for this stream.""" + + self._expectations.clear() + + @staticmethod + def _require_explanation(explanation: str, method: str) -> None: + if not explanation.strip(): + raise ValueError(f"{method}() requires a non-empty explanation") + + +@dataclass(eq=False) +class ProcessSpec: + """Declarative configuration for one process in a Uranium case.""" + + case: "Case" + kind: str + name: str + options: dict[str, Any] + identity: int + ready: bool = True + _expected_return_codes: tuple[int, ...] = field(default=(0, ), init=False, repr=False) + _stdout: StreamExpectations = field(init=False, repr=False) + _stderr: StreamExpectations = field(init=False, repr=False) + + def __post_init__(self) -> None: + self._stdout = StreamExpectations(self, "stdout") + self._stderr = StreamExpectations(self, "stderr") + + @property + def stdout(self) -> StreamExpectations: + """Return the read-only standard-output expectation API.""" + + return self._stdout + + @stdout.setter + def stdout(self, value: Any) -> None: + raise AttributeError( + "stdout is read-only; use stdout.contains(), stdout.excludes(), or stdout.matches_gold()" + ) + + @property + def stderr(self) -> StreamExpectations: + """Return the read-only standard-error expectation API.""" + + return self._stderr + + @stderr.setter + def stderr(self, value: Any) -> None: + raise AttributeError( + "stderr is read-only; use stderr.contains(), stderr.excludes(), or stderr.matches_gold()" + ) + + @property + def return_codes(self) -> tuple[int, ...]: + """Return the acceptable exit statuses for this process.""" + + return self._expected_return_codes + + @return_codes.setter + def return_codes(self, values: Any) -> None: + raise AttributeError("return_codes is read-only; use expect_return_codes()") + + def expect_return_codes(self, *values: int) -> None: + """Set the acceptable exit statuses for this process.""" + + if not values: + raise ValueError("expect_return_codes() requires at least one status") + if not all(isinstance(value, int) for value in values): + raise TypeError("expect_return_codes() statuses must be integers") + self._expected_return_codes = values + + def _port(self, protocol: str) -> PortRef: + ports = self.options.setdefault(f"{protocol}_ports", []) + if not ports: + ports.append(PortRef(self, protocol)) + return ports[0] + + @property + def http_port(self) -> PortRef: + """Return this process's first HTTP port.""" + + return self._port("http") + + @property + def https_port(self) -> PortRef: + """Return this process's first HTTPS port.""" + + return self._port("https") + + @property + def http3_port(self) -> PortRef: + """Return this process's first HTTP/3 port.""" + + return self._port("http3") + + def artifact(self, name: str) -> ArtifactRef: + """Return a symbolic reference to a generated artifact.""" + + return ArtifactRef(self, name) + + +class Case: + """A group of processes that execute together.""" + + def __init__(self, suite: "CaseSuite", name: str, index: int) -> None: + self.suite = suite + self.name = name + self.index = index + self.processes: list[ProcessSpec] = [] + + def _add(self, kind: str, name: str, **options: Any) -> ProcessSpec: + process = ProcessSpec(self, kind, name, options, self.suite.next_process_identity()) + self.processes.append(process) + return process + + def add_client( + self, + name: str, + replay_path: str | Path | ArtifactRef | None = None, + **options: Any, + ) -> ProcessSpec: + """Add a verifier-client process to this case.""" + + replay_path = options.pop("replay_dir", replay_path) + return self._add("client", name, replay_path=replay_path, **options) + + def add_server( + self, + name: str, + replay_path: str | Path | ArtifactRef | None = None, + **options: Any, + ) -> ProcessSpec: + """Add a verifier-server process to this case.""" + + replay_path = options.pop("replay_dir", replay_path) + return self._add("server", name, replay_path=replay_path, **options) + + def add_proxy(self, name: str, **options: Any) -> ProcessSpec: + """Add the Python test proxy to this case.""" + + return self._add("proxy", name, **options) + + def add_process( + self, + name: str, + command: str | list[str | Path | OutputRef | ArtifactRef], + *, + copies: Iterable[str | Path] = (), + environment: dict[str, str] | None = None, + ) -> ProcessSpec: + """Add an arbitrary foreground command to this case.""" + + return self._add("process", name, command=command, copies=tuple(copies), + environment=environment) + + def add_replay_generator( + self, + name: str, + *, + num_transactions: int = 1, + url_file: str | Path | None = None, + other_args: str = "", + ) -> ProcessSpec: + """Add a replay-gen.py invocation to this case.""" + + return self._add( + "replay_generator", + name, + num_transactions=num_transactions, + url_file=url_file, + other_args=other_args, + ) + + +class CaseSuite: + """Sequential Uranium cases contained in one normal pytest module.""" + + def __init__(self, module_path: str | Path) -> None: + self.module_path = Path(module_path).resolve() + self.cases: list[Case] = [] + self._process_identity = 0 + + @property + def test_directory(self) -> Path: + """Return the directory holding this suite's test module.""" + + return self.module_path.parent + + def case(self, name: str) -> Case: + """Append and return a test case.""" + + case = Case(self, name, len(self.cases)) + self.cases.append(case) + return case + + def next_process_identity(self) -> int: + """Return a suite-unique process identifier.""" + + identity = self._process_identity + self._process_identity += 1 + return identity + + +def assert_stream( + expectations: StreamExpectations, + content: str, + test_directory: Path, + resolve: Callable[[str], str] = str, +) -> None: + """Apply the configured expectations to captured stream content.""" + + normalized = content.replace("\r\n", "\n") + for expectation in expectations.expectations: + detail = expectation.explanation or expectation.value + if expectation.kind == "contains": + assert re.search(resolve(expectation.value), normalized, expectation.flags), ( + f"Expected {expectations.process.name} {expectations.name} to contain {detail!r}.\n{normalized}" + ) + elif expectation.kind == "excludes": + assert not re.search(resolve(expectation.value), normalized, expectation.flags), ( + f"Expected {expectations.process.name} {expectations.name} to exclude {detail!r}.\n{normalized}" + ) + elif expectation.kind == "gold": + path = test_directory / expectation.value + assert_matches_gold(normalized, path) + else: + raise ValueError(f"Unknown stream expectation: {expectation.kind}") diff --git a/tests/tools/uranium/plugin.py b/tests/tools/uranium/plugin.py new file mode 100644 index 000000000..5ba8cd37b --- /dev/null +++ b/tests/tools/uranium/plugin.py @@ -0,0 +1,107 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Pytest fixtures and lifecycle hooks for Proxy Verifier Uranium tests.""" + +from __future__ import annotations + +from collections.abc import Generator, Iterator +from pathlib import Path +import os +import shutil + +import pytest + +from .runtime import Uranium + + +def pytest_addoption(parser: pytest.Parser) -> None: + """Register Uranium runtime paths.""" + + group = parser.getgroup("Proxy Verifier Uranium tests") + group.addoption("--verifier-bin", + help="Directory containing verifier-client and verifier-server") + group.addoption("--sandbox", help="Directory for isolated Uranium test process trees") + group.addoption("--urtest-shard-index", type=int, help="Zero-based CI shard to collect") + group.addoption("--urtest-shard-count", type=int, help="Total number of CI shards") + + +def pytest_configure(config: pytest.Config) -> None: + """Register the Uranium marker and xdist scheduling mode.""" + + config.addinivalue_line("markers", "uranium: Proxy Verifier end-to-end test") + if getattr(config.option, "numprocesses", None) and getattr(config.option, "dist", + None) == "load": + config.option.dist = "loadscope" + + +def pytest_collection_modifyitems(config: pytest.Config, items: list[pytest.Item]) -> None: + """Mark Uranium tests and apply deterministic CI sharding.""" + + uranium_items = [] + for item in items: + if "uranium_tests" in Path(item.path).parts: + item.add_marker("uranium") + uranium_items.append(item) + shard_index = config.getoption("urtest_shard_index") + shard_count = config.getoption("urtest_shard_count") + if (shard_index is None) != (shard_count is None): + raise pytest.UsageError( + "--urtest-shard-index and --urtest-shard-count must be provided together") + if shard_count is None: + return + if shard_count <= 0 or shard_index < 0 or shard_index >= shard_count: + raise pytest.UsageError("Uranium shard values must satisfy 0 <= index < count") + selected_ids = { + item.nodeid + for position, item in enumerate( + sorted(uranium_items, key=lambda candidate: candidate.nodeid)) + if position % shard_count == shard_index + } + selected = [item for item in items if item not in uranium_items or item.nodeid in selected_ids] + deselected = [item for item in uranium_items if item.nodeid not in selected_ids] + if deselected: + config.hook.pytest_deselected(items=deselected) + items[:] = selected + + +@pytest.hookimpl(wrapper=True) +def pytest_runtest_makereport( + item: pytest.Item, + call: pytest.CallInfo[None], +) -> Generator[None, pytest.TestReport, pytest.TestReport]: + """Retain Uranium sandboxes only for failed tests.""" + + report = yield + if item.get_closest_marker("uranium") is None: + return report + if report.failed: + setattr(item, "_uranium_failed", True) + if report.when == "teardown" and not getattr(item, "_uranium_failed", False): + runtime = getattr(item, "_uranium_runtime", None) + if runtime is not None: + shutil.rmtree(runtime.run_directory, ignore_errors=True) + return report + + +@pytest.fixture +def uranium(pytestconfig: pytest.Config, request: pytest.FixtureRequest) -> Iterator[Uranium]: + """Provide a test-owned Uranium runtime.""" + + repository_root = Path(__file__).resolve().parents[3] + verifier_bin = pytestconfig.getoption("verifier_bin") or os.environ.get("VERIFIER_BIN") + sandbox = pytestconfig.getoption("sandbox") or os.environ.get("URTEST_SANDBOX") + missing = [] + if not verifier_bin: + missing.append("--verifier-bin") + if not sandbox: + missing.append("--sandbox") + if missing: + raise pytest.UsageError("Uranium tests require " + " and ".join(missing)) + worker = os.environ.get("PYTEST_XDIST_WORKER", "main") + runtime = Uranium(repository_root, Path(verifier_bin), + Path(sandbox) / worker, request.node.nodeid) + setattr(request.node, "_uranium_runtime", runtime) + with runtime.execution_lock(): + yield runtime diff --git a/tests/tools/uranium/process.py b/tests/tools/uranium/process.py new file mode 100644 index 000000000..9d381ca93 --- /dev/null +++ b/tests/tools/uranium/process.py @@ -0,0 +1,152 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Subprocess lifecycle support for Proxy Verifier Uranium tests.""" + +from __future__ import annotations + +from collections.abc import Callable, Iterable, Sequence +from pathlib import Path +import os +import signal +import subprocess +import time + + +class ProcessError(RuntimeError): + """Report an unexpected process exit or readiness failure.""" + + +class ManagedProcess: + """Own a subprocess and its captured output files.""" + + def __init__( + self, + name: str, + command: Sequence[str | Path], + run_directory: Path, + environment: dict[str, str] | None = None, + expected_return_codes: Iterable[int] = (0, ), + ) -> None: + self.name = name + self.command = [str(argument) for argument in command] + self.run_directory = run_directory + self.environment = environment + self.expected_return_codes = set(expected_return_codes) + self.stdout_path = run_directory / f"{name}.stdout" + self.stderr_path = run_directory / f"{name}.stderr" + self._stdout = None + self._stderr = None + self._process: subprocess.Popen[bytes] | None = None + + @property + def return_code(self) -> int | None: + """Return the process exit status, or None while it is running.""" + + return None if self._process is None else self._process.poll() + + @property + def stdout(self) -> str: + """Return captured standard output.""" + + self._flush_streams() + return self.stdout_path.read_text(errors="replace") if self.stdout_path.exists() else "" + + @property + def stderr(self) -> str: + """Return captured standard error.""" + + self._flush_streams() + return self.stderr_path.read_text(errors="replace") if self.stderr_path.exists() else "" + + @property + def output(self) -> str: + """Return captured standard output and error.""" + + return self.stdout + self.stderr + + def start(self) -> None: + """Start the process in a new process group.""" + + self.run_directory.mkdir(parents=True, exist_ok=True) + self._stdout = self.stdout_path.open("wb") + self._stderr = self.stderr_path.open("wb") + self._process = subprocess.Popen( + self.command, + cwd=self.run_directory, + env=self.environment, + stdout=self._stdout, + stderr=self._stderr, + start_new_session=True, + ) + + def wait_until(self, ready: Callable[[], bool], timeout: float, description: str) -> None: + """Wait for readiness while ensuring the process remains alive.""" + + deadline = time.monotonic() + timeout + while time.monotonic() < deadline: + if self.return_code is not None: + self._close_streams() + raise ProcessError( + f"{self.name} exited with status {self.return_code} while waiting for {description}.\n{self.output}" + ) + if ready(): + return + time.sleep(0.05) + raise ProcessError( + f"Timed out after {timeout:g}s waiting for {self.name}: {description}.\n{self.output}") + + def wait(self, timeout: float = 60, *, validate: bool = True) -> int: + """Wait for completion and optionally validate the return code.""" + + if self._process is None: + raise ProcessError(f"{self.name} has not been started") + try: + return_code = self._process.wait(timeout=timeout) + except subprocess.TimeoutExpired as error: + self.stop() + raise ProcessError( + f"{self.name} timed out after {timeout:g}s.\n{self.output}") from error + finally: + self._close_streams() + if validate and return_code not in self.expected_return_codes: + raise ProcessError(f"{self.name} exited with status {return_code}; expected " + f"{sorted(self.expected_return_codes)}.\n{self.output}") + return return_code + + def interrupt(self, timeout: float = 30) -> int: + """Send SIGINT and wait for a graceful, validated shutdown.""" + + if self._process is None: + raise ProcessError(f"{self.name} has not been started") + if self._process.poll() is None: + self._process.send_signal(signal.SIGINT) + return self.wait(timeout) + + def stop(self, timeout: float = 5) -> None: + """Stop the process group, escalating to SIGKILL if necessary.""" + + if self._process is None: + self._close_streams() + return + if self._process.poll() is None: + try: + os.killpg(self._process.pid, signal.SIGTERM) + self._process.wait(timeout=timeout) + except ProcessLookupError: + pass + except subprocess.TimeoutExpired: + os.killpg(self._process.pid, signal.SIGKILL) + self._process.wait(timeout=timeout) + self._close_streams() + + def _flush_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.flush() + + def _close_streams(self) -> None: + for stream in (self._stdout, self._stderr): + if stream is not None and not stream.closed: + stream.close() diff --git a/tests/autests/gold_tests/autest-site/directive_engine.py b/tests/tools/uranium/proxy/directive_engine.py similarity index 100% rename from tests/autests/gold_tests/autest-site/directive_engine.py rename to tests/tools/uranium/proxy/directive_engine.py diff --git a/tests/autests/gold_tests/autest-site/proxy_http1.py b/tests/tools/uranium/proxy/proxy_http1.py similarity index 100% rename from tests/autests/gold_tests/autest-site/proxy_http1.py rename to tests/tools/uranium/proxy/proxy_http1.py diff --git a/tests/autests/gold_tests/autest-site/proxy_http2.py b/tests/tools/uranium/proxy/proxy_http2.py similarity index 99% rename from tests/autests/gold_tests/autest-site/proxy_http2.py rename to tests/tools/uranium/proxy/proxy_http2.py index eb5511034..f280a9931 100644 --- a/tests/autests/gold_tests/autest-site/proxy_http2.py +++ b/tests/tools/uranium/proxy/proxy_http2.py @@ -7,7 +7,6 @@ # SPDX-License-Identifier: Apache-2.0 # -from email.message import EmailMessage as HttpHeaders import sys import ssl from OpenSSL.SSL import Error as SSLError diff --git a/tests/autests/gold_tests/autest-site/proxy_http3.py b/tests/tools/uranium/proxy/proxy_http3.py similarity index 99% rename from tests/autests/gold_tests/autest-site/proxy_http3.py rename to tests/tools/uranium/proxy/proxy_http3.py index a0e93d8a1..5a768f567 100644 --- a/tests/autests/gold_tests/autest-site/proxy_http3.py +++ b/tests/tools/uranium/proxy/proxy_http3.py @@ -13,7 +13,6 @@ from typing import Callable, Dict, Optional, Union # For HTTP/1 to origin. -from email.message import EmailMessage as HttpHeaders import http.client import sys import threading @@ -286,7 +285,6 @@ def http_event_received(self, event: H3Event) -> None: http_version = "0.9" if isinstance(self._http, H0Connection) else "3" raw_path = b"" method = "" - protocol = None for header, value in event.headers: if header == b":authority": authority = value @@ -296,7 +294,7 @@ def http_event_received(self, event: H3Event) -> None: elif header == b":path": raw_path = value elif header == b":protocol": - protocol = value.decode() + continue elif header and not header.startswith(b":"): headers.append((header, value)) diff --git a/tests/autests/gold_tests/autest-site/proxy_protocol_context.py b/tests/tools/uranium/proxy/proxy_protocol_context.py similarity index 100% rename from tests/autests/gold_tests/autest-site/proxy_protocol_context.py rename to tests/tools/uranium/proxy/proxy_protocol_context.py diff --git a/tests/autests/gold_tests/autest-site/test_proxy.py b/tests/tools/uranium/proxy/test_proxy.py similarity index 100% rename from tests/autests/gold_tests/autest-site/test_proxy.py rename to tests/tools/uranium/proxy/test_proxy.py diff --git a/tests/tools/uranium/runtime.py b/tests/tools/uranium/runtime.py new file mode 100644 index 000000000..aeae73b46 --- /dev/null +++ b/tests/tools/uranium/runtime.py @@ -0,0 +1,427 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Execution runtime for Proxy Verifier Uranium test suites.""" + +from __future__ import annotations + +from collections.abc import Iterator, Sequence +from contextlib import contextmanager +from pathlib import Path +from typing import Any +import fcntl +import hashlib +import os +import re +import shlex +import shutil +import socket +import sys + +from .model import ArtifactRef, Case, CaseSuite, OutputRef, PortRef, ProcessSpec, assert_stream +from .process import ManagedProcess + + +class RuntimeConfigError(ValueError): + """Report invalid Uranium runtime configuration.""" + + +def _tcp_open(port: int, address: str = "127.0.0.1") -> bool: + family = socket.AF_INET6 if ":" in address else socket.AF_INET + try: + with socket.socket(family, socket.SOCK_STREAM) as connection: + connection.settimeout(0.1) + return connection.connect_ex((address, port)) == 0 + except OSError: + return False + + +class Uranium: + """Own paths, ports, processes, and sandboxes for one pytest item.""" + + def __init__(self, repository_root: Path, verifier_bin: Path, sandbox_root: Path, + nodeid: str) -> None: + self.repository_root = repository_root.resolve() + self.verifier_bin = verifier_bin.resolve() + self.sandbox_root = sandbox_root.resolve() + self.nodeid = nodeid + required = [self.verifier_bin / "verifier-client", self.verifier_bin / "verifier-server"] + if missing := [str(path) for path in required if not path.is_file()]: + raise RuntimeConfigError("Missing required test programs: " + ", ".join(missing)) + self.run_directory = self.sandbox_for(nodeid) + self._prepare_sandbox(self.run_directory) + self._ports: dict[PortRef, int] = {} + self._outputs: dict[OutputRef, Path] = {} + self._artifacts: dict[ArtifactRef, Path] = {} + + def sandbox_for(self, nodeid: str) -> Path: + """Return a short deterministic sandbox path for a pytest item.""" + + digest = hashlib.sha256(nodeid.encode()).hexdigest()[:12] + return self.sandbox_root / f"test-{digest}" + + def allocate_port(self, socket_type: int = socket.SOCK_STREAM) -> int: + """Allocate a unique available port across xdist workers.""" + + common_sandbox = self.sandbox_root.parent + common_sandbox.mkdir(parents=True, exist_ok=True) + state_path = common_sandbox / ".port-counter" + with state_path.open("a+") as state: + fcntl.flock(state, fcntl.LOCK_EX) + state.seek(0) + candidate = int(state.read().strip() or "10000") + for _ in range(30000): + candidate = 10000 if candidate >= 40000 else candidate + 1 + with socket.socket(socket.AF_INET, socket_type) as probe: + try: + probe.bind(("127.0.0.1", candidate)) + except OSError: + continue + state.seek(0) + state.truncate() + state.write(str(candidate)) + state.flush() + return candidate + raise RuntimeConfigError("Could not allocate a test port") + + def run(self, suite: CaseSuite) -> None: + """Run every case in @a suite sequentially.""" + + for case in suite.cases: + try: + self._run_case(case) + except Exception as error: + if add_note := getattr(error, "add_note", None): + add_note(f"Uranium case: {case.name}") + raise + + def _run_case(self, case: Case) -> None: + case_directory = self.run_directory / f"case-{case.index:03d}" + case_directory.mkdir(parents=True) + processes = {spec: self._make_process(spec, case_directory) for spec in case.processes} + servers = [spec for spec in case.processes if spec.kind == "server"] + proxies = [spec for spec in case.processes if spec.kind == "proxy"] + foreground = [ + spec for spec in case.processes + if spec.kind in ("client", "process", "replay_generator") + ] + has_traffic_driver = bool(proxies or any(spec.kind == "client" for spec in foreground)) + started: list[ProcessSpec] = [] + try: + if has_traffic_driver: + for spec in servers: + process = processes[spec] + process.start() + started.append(spec) + if spec.ready: + address, port = self._server_ready_address(spec) + process.wait_until( + lambda address=address, port=port: _tcp_open(port, address), + 10, + f"listener on {address}:{port}", + ) + for spec in proxies: + process = processes[spec] + process.start() + started.append(spec) + self._wait_for_proxy(spec, process) + + for spec in foreground: + process = processes[spec] + process.start() + process.wait(timeout=float(spec.options.get("timeout", 120))) + + for spec in reversed(proxies): + processes[spec].interrupt() + started.remove(spec) + for spec in reversed(servers): + if spec in started: + processes[spec].interrupt() + started.remove(spec) + + if not has_traffic_driver: + for spec in servers: + process = processes[spec] + process.start() + process.wait(timeout=float(spec.options.get("timeout", 30))) + finally: + for spec in reversed(started): + processes[spec].stop() + + for spec, process in processes.items(): + assert_stream(spec.stdout, process.stdout, case.suite.test_directory, + self._resolve_text) + assert_stream(spec.stderr, process.stderr, case.suite.test_directory, + self._resolve_text) + + def _make_process(self, spec: ProcessSpec, case_directory: Path) -> ManagedProcess: + process_directory = case_directory / spec.name + process_directory.mkdir(parents=True) + self._outputs[spec.stdout.path] = process_directory / f"{spec.name}.stdout" + self._outputs[spec.stderr.path] = process_directory / f"{spec.name}.stderr" + if spec.kind == "client": + command = self._client_command(spec, process_directory) + elif spec.kind == "server": + command = self._server_command(spec, process_directory) + elif spec.kind == "proxy": + command = self._proxy_command(spec, process_directory) + elif spec.kind == "replay_generator": + command = self._replay_generator_command(spec, process_directory) + elif spec.kind == "process": + command = self._generic_command(spec, process_directory) + else: + raise RuntimeConfigError(f"Unknown process kind: {spec.kind}") + environment = os.environ.copy() + if additions := spec.options.get("environment"): + environment.update({key: self._resolve_text(value) for key, value in additions.items()}) + return ManagedProcess(spec.name, command, process_directory, environment, spec.return_codes) + + def _client_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: + options = spec.options + command: list[str | Path] = [self.verifier_bin / "verifier-client", "run"] + if replay_path := options.get("replay_path"): + command.append(self._resolve_path(spec.case.suite, replay_path)) + use_ipv6 = bool(options.get("use_ipv6", False)) + for protocol, argument in ( + ("http", "--connect-http"), + ("https", "--connect-https"), + ("http3", "--connect-http3"), + ): + ports = self._configured_ports(spec, protocol) + if ports: + command.extend([argument, self._address_argument(ports, use_ipv6)]) + if self._configured_ports(spec, "https") or self._configured_ports(spec, "http3"): + self._add_tls_arguments(command, spec.case.suite, options, directory, client=True) + if options.get("enable_tls_secrets_logging", True): + command.extend(["--tls-secrets-log-file", directory / "tls_secrets.txt"]) + if self._configured_ports(spec, "http3") and options.get("enable_qlogging", True): + command.extend(["--qlog-dir", directory / "qlog"]) + if options.get("verbose", True): + command.extend(["--verbose", "diag"]) + if options.get("single_threaded", True): + command.extend(["--thread-limit", "1"]) + command.extend(shlex.split(str(options.get("other_args", "")))) + return command + + def _server_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: + options = spec.options + command: list[str | Path] = [self.verifier_bin / "verifier-server", "run"] + use_ipv6 = bool(options.get("use_ipv6", False)) + for protocol, argument in (("http", "--listen-http"), ("https", "--listen-https")): + ports = self._configured_ports(spec, protocol) + if ports: + command.extend([argument, self._address_argument(ports, use_ipv6)]) + if self._configured_ports(spec, "https") or self._configured_ports(spec, "http3"): + self._add_tls_arguments(command, spec.case.suite, options, directory, client=False) + if options.get("enable_tls_secrets_logging", True): + command.extend(["--tls-secrets-log-file", directory / "tls_secrets.txt"]) + if replay_path := options.get("replay_path"): + command.append(self._resolve_path(spec.case.suite, replay_path)) + if options.get("verbose", True): + command.extend(["--verbose", "diag"]) + command.extend(shlex.split(str(options.get("other_args", "")))) + return command + + def _proxy_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: + options = spec.options + proxy_script = self.repository_root / "tests" / "tools" / "uranium" / "proxy" / "test_proxy.py" + listen_port = int(self._resolve(options.get("listen_port", 8080))) + server_port = int(self._resolve(options.get("server_port", 8081))) + command: list[str | Path] = [ + sys.executable, + "-u", + proxy_script, + "--listen-port", + str(listen_port), + "--server-port", + str(server_port), + ] + if options.get("use_ssl", False): + pem = options.get("https_pem") or self.repository_root / "tests" / "keys" / "client.pem" + ca = options.get("ca_pem") or self.repository_root / "tests" / "keys" / "ca.pem" + command.extend([ + "--https-pem", + self._resolve_path(spec.case.suite, pem), "--ca-pem", + self._resolve_path(spec.case.suite, ca) + ]) + for option, flag in ( + ("close_on_goaway", "--close-on-goaway"), + ("use_http2_to_1", "--http2_to_1"), + ("use_http2_to_2", "--http2_to_2"), + ("use_http3_to_1", "--http3_to_1"), + ): + if options.get(option, False): + command.append(flag) + if options.get("use_http3_to_1", False): + sentinel = directory / "http3-listener-ready" + options["ready_sentinel"] = sentinel + command.extend(["--listening-http3-sentinel", sentinel]) + return command + + def _replay_generator_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: + replay_directory = directory / "replay_dir" + self._artifacts[spec.artifact("replay_dir")] = replay_directory + url_file = spec.options.get("url_file") + if url_file is None: + url_file = self.repository_root / "tests" / "tools" / "uranium" / "default_url_file" + command: list[str | Path] = [ + sys.executable, + self.repository_root / "tools" / "replay-gen.py", + "--number", + str(spec.options.get("num_transactions", 1)), + "--url-file", + self._resolve_path(spec.case.suite, url_file), + "--output", + replay_directory, + ] + command.extend(shlex.split(str(spec.options.get("other_args", "")))) + return command + + def _generic_command(self, spec: ProcessSpec, directory: Path) -> list[str | Path]: + for path in spec.options.get("copies", ()): + source = self._resolve_path(spec.case.suite, path) + destination = directory / source.name + if source.is_dir(): + shutil.copytree(source, destination) + else: + shutil.copy2(source, destination) + command = spec.options["command"] + if isinstance(command, str): + return shlex.split(self._resolve_text(command)) + return [self._resolve_text(value) for value in command] + + def _configured_ports(self, spec: ProcessSpec, protocol: str) -> list[int]: + key = f"{protocol}_ports" + options = spec.options + if key not in options: + enabled = options.get("find_ports", True) and options.get(f"configure_{protocol}", True) + options[key] = [PortRef(spec, protocol)] if enabled else [] + return [int(self._resolve(value)) for value in options[key]] + + def _add_tls_arguments( + self, + command: list[str | Path], + suite: CaseSuite, + options: dict[str, Any], + directory: Path, + *, + client: bool, + ) -> None: + certificate_key = "ssl_cert" + certificate = options.get(certificate_key, "") + if certificate == "": + certificate = self.repository_root / "tests" / "keys" / ("client.pem" + if client else "server.pem") + if certificate is not None: + command.extend([ + "--client-cert" if client else "--server-cert", + self._resolve_path(suite, certificate) + ]) + ca_certs = options.get("ca_certs", "") + if ca_certs == "": + ca_certs = self.repository_root / "tests" / "keys" / "ca.pem" + if ca_certs is not None: + command.extend(["--ca-certs", self._resolve_path(suite, ca_certs)]) + + def _server_ready_address(self, spec: ProcessSpec) -> tuple[str, int]: + address = "::1" if spec.options.get("use_ipv6", False) else "127.0.0.1" + for protocol in ("http", "https"): + if ports := self._configured_ports(spec, protocol): + return address, ports[0] + raise RuntimeConfigError(f"No TCP readiness port configured for {spec.name}") + + def _wait_for_proxy(self, spec: ProcessSpec, process: ManagedProcess) -> None: + sentinel = spec.options.get("ready_sentinel") + if sentinel: + process.wait_until(lambda: Path(sentinel).exists(), 10, f"sentinel {sentinel}") + return + port = int(self._resolve(spec.options.get("listen_port", 8080))) + process.wait_until(lambda: _tcp_open(port), 10, f"listener on 127.0.0.1:{port}") + + def _resolve(self, value: Any) -> Any: + if isinstance(value, PortRef): + if value not in self._ports: + self._ports[value] = self.allocate_port(socket.SOCK_DGRAM if value.protocol == + "http3" else socket.SOCK_STREAM) + return self._ports[value] + if isinstance(value, OutputRef): + return self._outputs[value] + if isinstance(value, ArtifactRef): + return self._artifacts[value] + return value + + def _resolve_text(self, value: Any) -> str: + if isinstance(value, (PortRef, OutputRef, ArtifactRef)): + return str(self._resolve(value)) + text = str(value) + text = text.replace("{verifier-client}", str(self.verifier_bin / "verifier-client")) + text = text.replace("{verifier-server}", str(self.verifier_bin / "verifier-server")) + patterns = [ + (r"\{uranium-port:(\d+):(http|https|http3):(\d+)\}", self._replace_port), + (r"\{uranium-output:(\d+):(stdout|stderr)\}", self._replace_output), + (r"\{uranium-artifact:(\d+):([^}]+)\}", self._replace_artifact), + ] + for pattern, replacement in patterns: + text = re.sub(pattern, replacement, text) + return text + + def _replace_port(self, match: re.Match[str]) -> str: + identity, protocol, index = match.groups() + ref = next(ref for ref in self._ports.keys() | self._all_port_refs() + if ref.process.identity == int(identity) and ref.protocol == protocol + and ref.index == int(index)) + return str(self._resolve(ref)) + + def _replace_output(self, match: re.Match[str]) -> str: + identity, stream = match.groups() + ref = next(ref for ref in self._outputs + if ref.process.identity == int(identity) and ref.stream == stream) + return str(self._outputs[ref]) + + def _replace_artifact(self, match: re.Match[str]) -> str: + identity, name = match.groups() + ref = next(ref for ref in self._artifacts + if ref.process.identity == int(identity) and ref.name == name) + return str(self._artifacts[ref]) + + def _all_port_refs(self) -> set[PortRef]: + refs: set[PortRef] = set() + for output in self._outputs: + spec = output.process + for protocol in ("http", "https", "http3"): + for index, value in enumerate(spec.options.get(f"{protocol}_ports", [])): + if isinstance(value, PortRef): + refs.add(PortRef(value.process, value.protocol, index)) + return refs + + def _resolve_path(self, suite: CaseSuite, value: Any) -> Path: + if isinstance(value, ArtifactRef): + return Path(self._resolve(value)) + path = Path(self._resolve_text(value)) + return path if path.is_absolute() else suite.test_directory / path + + @staticmethod + def _address_argument(ports: Sequence[int], use_ipv6: bool) -> str: + address = "[::1]" if use_ipv6 else "127.0.0.1" + return ",".join(f"{address}:{port}" for port in ports) + + def _prepare_sandbox(self, path: Path) -> None: + if path.parent != self.sandbox_root or path == self.sandbox_root: + raise RuntimeConfigError(f"Refusing to clean unsafe sandbox path: {path}") + if path.exists(): + shutil.rmtree(path) + path.mkdir(parents=True) + + @contextmanager + def execution_lock(self) -> Iterator[None]: + """Hold a shared execution lock while this test runs.""" + + common_sandbox = self.sandbox_root.parent + common_sandbox.mkdir(parents=True, exist_ok=True) + with (common_sandbox / ".execution-lock").open("a+") as lock: + fcntl.flock(lock, fcntl.LOCK_SH) + try: + yield + finally: + fcntl.flock(lock, fcntl.LOCK_UN) diff --git a/tests/tools/uranium/tests/test_assertions.py b/tests/tools/uranium/tests/test_assertions.py new file mode 100644 index 000000000..870bd03a7 --- /dev/null +++ b/tests/tools/uranium/tests/test_assertions.py @@ -0,0 +1,65 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for shared Uranium assertions.""" + +from pathlib import Path +import difflib + +import pytest + +from tools.uranium.assertions import USING_ACCELERATED_DIFF, assert_matches_gold + + +def test_gold_file_wildcards_match_variable_text(tmp_path: Path) -> None: + """Preserve both established wildcard tokens in gold files.""" + + expected = tmp_path / "expected.gold" + expected.write_text("port=`` id={} done\n") + + assert_matches_gold("port=43127 id=abc-123 done\n", expected) + + +def test_standalone_gold_wildcard_absorbs_its_line_breaks(tmp_path: Path) -> None: + """Let a standalone wildcard span arbitrary text without fixed newlines.""" + + expected = tmp_path / "expected.gold" + expected.write_text("before\n``\nafter\n") + + assert_matches_gold("before arbitrary text after\n", expected) + + +def test_gold_file_difference_reports_unified_diff(tmp_path: Path) -> None: + """Show the expected and actual lines when gold comparison fails.""" + + expected = tmp_path / "expected.gold" + expected.write_text("expected\n") + + with pytest.raises(AssertionError, match=r"-expected\n\+actual"): + assert_matches_gold("actual\n", expected) + + +def test_missing_gold_file_has_an_assertion_message(tmp_path: Path) -> None: + """Report a missing gold file as an assertion failure.""" + + with pytest.raises(AssertionError, match="Gold file does not exist"): + assert_matches_gold("actual\n", tmp_path / "missing.gold") + + +def test_missing_actual_file_has_an_assertion_message(tmp_path: Path) -> None: + """Report a missing captured-output file as an assertion failure.""" + + expected = tmp_path / "expected.gold" + expected.write_text("expected\n") + + with pytest.raises(AssertionError, match="Actual output file does not exist"): + assert_matches_gold(tmp_path / "missing.out", expected) + + +def test_cdifflib_is_used_when_available() -> None: + """Select the C sequence matcher whenever the optional module imports.""" + + if not USING_ACCELERATED_DIFF: + pytest.skip("cdifflib is unavailable") + assert difflib.SequenceMatcher.__name__ == "CSequenceMatcher" diff --git a/tests/tools/uranium/tests/test_model.py b/tests/tools/uranium/tests/test_model.py new file mode 100644 index 000000000..6ad0ba0ee --- /dev/null +++ b/tests/tools/uranium/tests/test_model.py @@ -0,0 +1,133 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for the Uranium declarative model.""" + +from pathlib import Path +import re + +import pytest + +from tools.uranium.model import CaseSuite, assert_stream + + +def test_process_references_are_stable(tmp_path: Path) -> None: + """Port and output references retain their process identity.""" + + suite = CaseSuite(tmp_path / "test_example.py") + case = suite.case("example") + client = case.add_client("client", "replay.yaml") + + assert str(client.http_port) == f"{{uranium-port:{client.identity}:http:0}}" + assert str(client.stdout.path) == f"{{uranium-output:{client.identity}:stdout}}" + + +def test_stream_regular_expressions(tmp_path: Path) -> None: + """Contains and excludes expectations use regular expressions.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + result = process.stdout.contains( + r"^value=[0-9]+$", + "The value should be numeric.", + reflags=re.MULTILINE, + ) + process.stdout.excludes("failure", "The command should not report failure.") + + assert result is None + assert_stream(process.stdout, "value=42\n", tmp_path) + + +def test_stream_regular_expression_failure(tmp_path: Path) -> None: + """A missing required expression reports captured output.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + process.stdout.contains("expected", "The expected value should be present.") + + with pytest.raises(AssertionError, match="The expected value should be present") as error: + assert_stream(process.stdout, "actual\n", tmp_path) + assert "actual" in str(error.value) + + +def test_stream_exclusion_failure_reports_explanation(tmp_path: Path) -> None: + """A forbidden expression reports the reason it must be absent.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + process.stdout.excludes("forbidden", "Forbidden output should be absent.") + + with pytest.raises(AssertionError, match="Forbidden output should be absent"): + assert_stream(process.stdout, "forbidden\n", tmp_path) + + +def test_gold_wildcards_ignore_variable_content(tmp_path: Path) -> None: + """Legacy gold wildcard tokens continue to ignore variable spans.""" + + (tmp_path / "output.gold").write_text("prefix``suffix\n") + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + process.stdout.matches_gold("output.gold") + + assert_stream(process.stdout, "prefix variable content suffix\n", tmp_path) + + +def test_stream_reset_removes_registered_expectations(tmp_path: Path) -> None: + """Reset explicitly discards every earlier stream expectation.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + output_path = process.stdout.path + process.stdout.contains("discarded", "This expectation should be reset.") + + process.stdout.reset() + process.stdout.contains("retained", "This expectation should remain.") + + assert len(process.stdout.expectations) == 1 + assert process.stdout.path == output_path + assert_stream(process.stdout, "retained\n", tmp_path) + + +def test_stream_expectations_require_explanations(tmp_path: Path) -> None: + """Regex expectations reject missing explanations at declaration time.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + + with pytest.raises(ValueError, match="contains.*non-empty explanation"): + process.stdout.contains("value", "") + with pytest.raises(ValueError, match="excludes.*non-empty explanation"): + process.stdout.excludes("value", " ") + + +def test_process_streams_are_read_only(tmp_path: Path) -> None: + """Assignment cannot silently replace a process stream's expectations.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + + with pytest.raises(AttributeError, match=r"stdout\.contains"): + process.stdout = object() + with pytest.raises(AttributeError, match=r"stderr\.contains"): + process.stderr = object() + with pytest.raises(TypeError): + process.stdout += object() + + +def test_expected_return_codes_use_explicit_api(tmp_path: Path) -> None: + """Return-code expectations cannot be overwritten by assignment.""" + + suite = CaseSuite(tmp_path / "test_example.py") + process = suite.case("example").add_process("command", ["true"]) + + assert process.return_codes == (0, ) + process.expect_return_codes(0, 1) + assert process.return_codes == (0, 1) + + with pytest.raises(AttributeError, match="expect_return_codes"): + process.return_codes = 1 + with pytest.raises(ValueError, match="at least one"): + process.expect_return_codes() + with pytest.raises(TypeError, match="integers"): + process.expect_return_codes("1") diff --git a/tests/tools/uranium/tests/test_process.py b/tests/tools/uranium/tests/test_process.py new file mode 100644 index 000000000..8d6bd251c --- /dev/null +++ b/tests/tools/uranium/tests/test_process.py @@ -0,0 +1,41 @@ +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 +"""Unit tests for Uranium subprocess management.""" + +from pathlib import Path +import sys + +import pytest + +from tools.uranium.process import ManagedProcess, ProcessError + + +def test_managed_process_captures_output(tmp_path: Path) -> None: + """A successful process exposes its captured streams.""" + + process = ManagedProcess( + "example", + [sys.executable, "-c", "import sys; print('out'); print('err', file=sys.stderr)"], + tmp_path, + ) + process.start() + process.wait() + + assert process.stdout == "out\n" + assert process.stderr == "err\n" + + +def test_managed_process_validates_return_code(tmp_path: Path) -> None: + """An unexpected exit status includes captured diagnostics.""" + + process = ManagedProcess( + "example", + [sys.executable, "-c", "print('diagnostic'); raise SystemExit(7)"], + tmp_path, + ) + process.start() + + with pytest.raises(ProcessError, match="diagnostic"): + process.wait() diff --git a/tests/autests/gold_tests/arguments/format_argument/no_uuid.yaml b/tests/uranium_tests/arguments/format_argument/no_uuid.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/format_argument/no_uuid.yaml rename to tests/uranium_tests/arguments/format_argument/no_uuid.yaml diff --git a/tests/uranium_tests/arguments/format_argument/test_format_argument.py b/tests/uranium_tests/arguments/format_argument/test_format_argument.py new file mode 100644 index 000000000..e6ac18ef7 --- /dev/null +++ b/tests/uranium_tests/arguments/format_argument/test_format_argument.py @@ -0,0 +1,166 @@ +''' +Verify --format argument parsing. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Test with the URL as a key. This is not unique across transactions, +# so only one transaction will be registered. +# +case = suite.case('--format "{url}" against transactions unique by Host') +client = case.add_client("client1", "unique_by_host.yaml", other_args="--format '{url}'") +server = case.add_server("server1", "unique_by_host.yaml", other_args="--format '{url}'") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +# The client will always see each transaction as unique and will send them as +# such. It does not record keys to detect duplicates, it just stores +# transactions in a list. +client.stdout.contains("Parsed 3 transactions", + "Three transactions should be parsed by the client.") + +client.stdout.contains('Equals Violation: Different.*Actual Value: "first"', + "The server will always reply with the first response.") + +client.stdout.contains('Key: "/same/path"', "The key should be parsed from the URL, not the uuid.") + +# The server, however, uses the key which, since we're using the URL which is +# the same across transactions, will only register the first transaction. +server.stdout.contains('Correct Value: "first", Actual Value: "second"', + "The second transaction should come up as a violation.") + +server.stdout.contains('Correct Value: "first", Actual Value: "third"', + "The third transaction should come up as a violation.") + +server.stdout.contains("Ready with 1 transaction", + "Only one transaction should be parsed by the server.") + +server.stdout.contains('Key: "/same/path"', "The key should be parsed from the URL, not the uuid.") + +client.expect_return_codes(1) +server.expect_return_codes(1) + +# +# Test 2: Verify using the host as a key, which is unique across transactions. +# +case = suite.case('--format "{field.host}"') +client = case.add_client("client2", "unique_by_host.yaml", other_args="--format '{field.host}'") +server = case.add_server("server2", "unique_by_host.yaml", other_args="--format '{field.host}'") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Parsed 3 transactions", + "Three transactions should be parsed by the client.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +client.stdout.contains('Key: "host.one"', "The key should be parsed from the host, not the uuid.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains("Ready with 3 transactions", + "Three transactions should be parsed by the server.") + +server.stdout.contains('Key: "host.one"', "The key should be parsed from the host, not the uuid.") + +# +# Test 3: Use a more complicated key made up of two specifiers. +# +case = suite.case('--format "{field.host}/{url}"') +client = case.add_client("client3", "unique_by_host.yaml", + other_args="--format '{field.host}/{url}'") +server = case.add_server("server3", "unique_by_host.yaml", + other_args="--format '{field.host}/{url}'") +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Parsed 3 transactions", + "Three transactions should be parsed by the client.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +client.stdout.contains('Key: "host.one//same/path"', + "The key should be parsed from the host/url, not the uuid.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains("Ready with 3 transactions", + "Three transactions should be parsed by the server.") + +server.stdout.contains('Key: "host.one//same/path"', + "The key should be parsed from the host/url, not the uuid.") + +# +# Test 4: Verify that the client detects when a key is not present in a +# transaction. +# +case = suite.case('Verify that the client detects a non-existent key') +client = case.add_client("client4", "no_uuid.yaml") + +# The client will give a non-zero return code because it found a transaction +# without a key. +client.expect_return_codes(1) +client.stdout.contains( + 'Could not find a key of format "{field.uuid}" for transaction', + "There should be a parsing warning that a key was not found for a transaction.") + +# +# Test 5: Verify that the server detects when a key is not present in a +# transaction. +# +case = suite.case('Verify that the server detects a non-existent key') +server = case.add_server("server5", "no_uuid.yaml") + +# The server will give a non-zero return code because it found a transaction +# without a key. +server.expect_return_codes(1) +server.stdout.contains( + 'Could not find a key of format "{field.uuid}" for transaction', + "There should be a parsing warning that a key was not found for a transaction.") + +# +# Test 6: Verify that the server returns a 404 for an unrecognized key. +# +case = suite.case('Verify a 404 response for an unrecognized key') +client = case.add_client("client6", "uuid1.yaml") + +# Notice that the server will be configured to recognize uuid 2, not 1. So when +# a request with uuid 1 is received, it will not recognize it and should return +# a 404 (Not Found). +server = case.add_server("server6", "uuid2.yaml") +proxy = case.add_proxy("proxy6", listen_port=client.http_port, server_port=server.http_port) + +server.expect_return_codes(1) + +client.stdout.contains('Received an HTTP/1 404 response', + "The client should receive a 404 response for an unrecognized key.") +server.stdout.contains('sending a 404', + "The server should send a 404 response for an unrecognized key.") + +# +# Test 7: Verify that --format "{url}" can successfully differentiate +# transactions that are only unique by URL. +# +case = suite.case('--format "{url}" against transactions unique by URL') +client = case.add_client("client7", "unique_by_url.yaml", other_args="--format '{url}'") +server = case.add_server("server7", "unique_by_url.yaml", other_args="--format '{url}'") +proxy = case.add_proxy("proxy7", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Parsed 3 transactions", + "Three transactions should be parsed by the client.") + +server.stdout.contains("Ready with 3 transactions", + "Three transactions should be parsed by the server.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/arguments/format_argument/unique_by_host.yaml b/tests/uranium_tests/arguments/format_argument/unique_by_host.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/format_argument/unique_by_host.yaml rename to tests/uranium_tests/arguments/format_argument/unique_by_host.yaml diff --git a/tests/autests/gold_tests/arguments/format_argument/unique_by_url.yaml b/tests/uranium_tests/arguments/format_argument/unique_by_url.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/format_argument/unique_by_url.yaml rename to tests/uranium_tests/arguments/format_argument/unique_by_url.yaml diff --git a/tests/autests/gold_tests/arguments/format_argument/uuid1.yaml b/tests/uranium_tests/arguments/format_argument/uuid1.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/format_argument/uuid1.yaml rename to tests/uranium_tests/arguments/format_argument/uuid1.yaml diff --git a/tests/autests/gold_tests/arguments/format_argument/uuid2.yaml b/tests/uranium_tests/arguments/format_argument/uuid2.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/format_argument/uuid2.yaml rename to tests/uranium_tests/arguments/format_argument/uuid2.yaml diff --git a/tests/autests/gold_tests/arguments/keys_argument/five_transactions.yaml b/tests/uranium_tests/arguments/keys_argument/five_transactions.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/keys_argument/five_transactions.yaml rename to tests/uranium_tests/arguments/keys_argument/five_transactions.yaml diff --git a/tests/uranium_tests/arguments/keys_argument/test_keys_arg.py b/tests/uranium_tests/arguments/keys_argument/test_keys_arg.py new file mode 100644 index 000000000..757571570 --- /dev/null +++ b/tests/uranium_tests/arguments/keys_argument/test_keys_arg.py @@ -0,0 +1,65 @@ +''' +Verify the user can white list transactions with --keys. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify that without the keys argument, all transactions are sent. +# +case = suite.case("Verify all keys are sent when --keys is not used.") +client = case.add_client("client1", "five_transactions.yaml") +server = case.add_server("server1", "five_transactions.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +for uuid in range(1, 6): + client.stdout.contains(f'uuid: {uuid}', f"Client has uuid {uuid}.") + +# +# Test 2: Verify a single transaction can be selected with --keys. +# +case = suite.case("Verify all keys are sent when --keys is not used.") +client = case.add_client("client2", "five_transactions.yaml", other_args="--keys 2") +server = case.add_server("server2", "five_transactions.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('uuid: 2', "Client has uuid 2.") + +for uuid in [1, 3, 4, 5, 6]: + client.stdout.excludes(f'uuid: {uuid}', f"Client has uuid {uuid}.") + +# +# Test 3: Verify a multiple transactions can be selected with --keys. +# +case = suite.case("Verify multiple transactions can be sent with --keys.") +client = case.add_client("client3", "five_transactions.yaml", other_args="--keys 3 5") +server = case.add_server("server3", "five_transactions.yaml") +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) + +for uuid in [3, 5]: + client.stdout.contains(f'uuid: {uuid}', f"Client has uuid {uuid}.") + +for uuid in [1, 2, 4]: + client.stdout.excludes(f'uuid: {uuid}', f"Client has uuid {uuid}.") + +# +# Test 4: Verify we can handle the situation if no transactions exist for the +# key. +# +case = suite.case("Verify no transactions are sent if none match the key.") +client = case.add_client("client4", "five_transactions.yaml", other_args="--keys does_not_exist") +server = case.add_server("server4", "five_transactions.yaml") +proxy = case.add_proxy("proxy4", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 0 transactions', "Verify no transactions are found") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/arguments/port_argument/not_used.yaml b/tests/uranium_tests/arguments/port_argument/not_used.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/port_argument/not_used.yaml rename to tests/uranium_tests/arguments/port_argument/not_used.yaml diff --git a/tests/uranium_tests/arguments/port_argument/test_no_port_argument.py b/tests/uranium_tests/arguments/port_argument/test_no_port_argument.py new file mode 100644 index 000000000..c277da5e4 --- /dev/null +++ b/tests/uranium_tests/arguments/port_argument/test_no_port_argument.py @@ -0,0 +1,38 @@ +''' +Verify there is an error if the user provides no port arguments. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify the client complains if no ports are provided. +# +case = suite.case('Verify the client complains if no ports are provided.') +client = case.add_client("client1", "not_used.yaml", configure_http=False, configure_https=False, + configure_http3=False) +client.stdout.contains( + 'Must provide at least one of "--connect-http", "--connect-https", or ' + '"--connect-http3" arguments', 'The client should explain that a port argument is required') +client.expect_return_codes(1) + +# +# Test 2: Verify the server complains if no ports are provided. +# +case = suite.case('Verify the server complains if no ports are provided.') +server = case.add_server("server1", "not_used.yaml", configure_http=False, configure_https=False, + configure_http3=False) +server.stdout.contains( + 'Must provide at least one of "--listen-http", "--listen-https", or ' + '"--listen-http3" arguments', 'The server should explain that a port argument is required') +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/arguments/repeat_argument/replay_files/two_files/five_transactions.yaml b/tests/uranium_tests/arguments/repeat_argument/replay_files/two_files/five_transactions.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/repeat_argument/replay_files/two_files/five_transactions.yaml rename to tests/uranium_tests/arguments/repeat_argument/replay_files/two_files/five_transactions.yaml diff --git a/tests/autests/gold_tests/arguments/repeat_argument/replay_files/two_files/three_transactions.yaml b/tests/uranium_tests/arguments/repeat_argument/replay_files/two_files/three_transactions.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/repeat_argument/replay_files/two_files/three_transactions.yaml rename to tests/uranium_tests/arguments/repeat_argument/replay_files/two_files/three_transactions.yaml diff --git a/tests/uranium_tests/arguments/repeat_argument/test_repeat_argument.py b/tests/uranium_tests/arguments/repeat_argument/test_repeat_argument.py new file mode 100644 index 000000000..fbb828ace --- /dev/null +++ b/tests/uranium_tests/arguments/repeat_argument/test_repeat_argument.py @@ -0,0 +1,92 @@ +''' +Verify the user can repeat transactions with --repeat. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite, is_platform + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify that without the repeat argument the transactions are +# executed once. +# +case = suite.case("Verify transactions are executed once with no --repeat argument.") +client = case.add_client("client1", "replay_files/two_files") +server = case.add_server("server1", "replay_files/two_files") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 8 transactions', 'Verify the 8 transactions were parsed.') +client.stdout.contains('8 transactions in 5 sessions', 'Verify each transaction is executed once.') + +# +# Test 2: Verify that with --repeat 1 the transactions are executed once. +# +case = suite.case("Verify transactions are executed once with --repeat 1.") +client = case.add_client("client2", "replay_files/two_files", other_args="--repeat 1") +server = case.add_server("server2", "replay_files/two_files") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 8 transactions', 'Verify the 8 transactions were parsed.') +client.stdout.contains('8 transactions in 5 sessions', 'Verify each transaction is executed once.') + +# +# Test 3: Verify that with --repeat 0 the transactions are not executed. +# +case = suite.case("Verify no transactions are executed with --repeat 0.") +client = case.add_client("client3", "replay_files/two_files", other_args="--repeat 0") +server = case.add_server("server3", "replay_files/two_files") +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 8 transactions', 'Verify the 8 transactions were parsed.') +client.stdout.contains('0 transactions in 0 sessions', 'Verify no transactions are executed.') + +# +# Test 4: Verify that with --repeat 2 the transactions are executed twice. +# +case = suite.case("Verify transactions are executed twice with --repeat 2.") +client = case.add_client("client4", "replay_files/two_files", other_args="--repeat 2") +server = case.add_server("server4", "replay_files/two_files") +proxy = case.add_proxy("proxy4", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 8 transactions', 'Verify the 8 transactions were parsed.') +client.stdout.contains('16 transactions in 10 sessions', + 'Verify each transaction is executed twice.') + +if is_platform("darwin"): + # On the Mac, the test proxy closes the session prematurely for some + # transactions, causing the client to fail due to PARSE_INCOMPLETE + # warnings. This can be observed in packet captures of the running test + # (the proxy does indeed send RESET responses after the request comes in) + # and from the client output which says "Connection reset by peer". I've + # spent some time trying to get the Python server to not close the + # connections prematurely on Mac but have not been able to get it to work + # yet. The client does indeed send the desired number of transactions, as + # verified with the above stdout Testers, so this test is nevertheless + # helpful despite the annoying test proxy behavior. For now we'll just + # ignore the return code. + client.expect_return_codes(0, 1) + +# +# Test 5: Verify that with --repeat 10 the transactions are executed ten times +# +case = suite.case("Verify transactions are executed ten times with --repeat 10.") +client = case.add_client("client5", "replay_files/two_files", other_args="--repeat 10") +server = case.add_server("server5", "replay_files/two_files") +proxy = case.add_proxy("proxy5", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Parsed 8 transactions', 'Verify the 8 transactions were parsed.') +client.stdout.contains('80 transactions in 50 sessions', + 'Verify each transaction is executed ten times.') + +if is_platform("darwin"): + # See above comment. + client.expect_return_codes(0, 1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/client_subset_http1.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/client_subset_http1.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/client_subset_http1.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/client_subset_http1.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/client_subset_http2.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/client_subset_http2.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/client_subset_http2.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/client_subset_http2.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/server_superset_http1.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/server_superset_http1.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/server_superset_http1.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/server_superset_http1.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/server_superset_http2.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/server_superset_http2.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/server_superset_http2.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/server_superset_http2.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http1.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http1.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http1.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http1.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http2.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http2.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http2.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http2.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http3.yaml b/tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http3.yaml similarity index 100% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http3.yaml rename to tests/uranium_tests/arguments/unprocessed_verifications/replay_files/verify_unprocessed_response_http3.yaml diff --git a/tests/autests/gold_tests/arguments/unprocessed_verifications/unprocessed_verifications.test.py b/tests/uranium_tests/arguments/unprocessed_verifications/test_unprocessed_verifications.py similarity index 56% rename from tests/autests/gold_tests/arguments/unprocessed_verifications/unprocessed_verifications.test.py rename to tests/uranium_tests/arguments/unprocessed_verifications/test_unprocessed_verifications.py index fc1aac21e..36eb52d57 100644 --- a/tests/autests/gold_tests/arguments/unprocessed_verifications/unprocessed_verifications.test.py +++ b/tests/uranium_tests/arguments/unprocessed_verifications/test_unprocessed_verifications.py @@ -5,11 +5,11 @@ # # Copyright 2026, Verizon Media # SPDX-License-Identifier: Apache-2.0 -# -Test.Summary = ''' -Verify shutdown handling for unprocessed verification rules. -''' +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# HTTP3_ARGS = "--poll-timeout 10000" @@ -20,38 +20,45 @@ def add_server_failure_run(description, client_name, client_replay, server_name, server_replay, proxy_name, client_port_attr, server_port_attr, expected_key, **proxy_kwargs): - r = Test.AddTestRun(description) - client = r.AddClientProcess(client_name, client_replay, **proxy_kwargs.pop('client_kwargs', {})) - server = r.AddServerProcess(server_name, server_replay, **proxy_kwargs.pop('server_kwargs', {})) - r.AddProxyProcess(proxy_name, listen_port=getattr(client.Variables, client_port_attr), - server_port=getattr(server.Variables, server_port_attr), **proxy_kwargs) - - server.Streams.stdout += Testers.ContainsExpression( + case = suite.case(description) + client = case.add_client(client_name, client_replay, **proxy_kwargs.pop('client_kwargs', {})) + server = case.add_server(server_name, server_replay, **proxy_kwargs.pop('server_kwargs', {})) + case.add_proxy( + proxy_name, + listen_port=getattr(client, client_port_attr), + server_port=getattr(server, server_port_attr), + **proxy_kwargs, + ) + + server.stdout.contains( f'Verification rules for transaction key "{expected_key}" were never processed before shutdown.', "The server should report the transaction whose verification rules were skipped.") - server.Streams.stdout += Testers.ContainsExpression( - SERVER_SUMMARY, "The server should summarize the shutdown failure.") - client.ReturnCode = 0 - server.ReturnCode = 1 + server.stdout.contains(SERVER_SUMMARY, "The server should summarize the shutdown failure.") + client.expect_return_codes(0) + server.expect_return_codes(1) def add_client_failure_run(description, client_name, replay_file, proxy_name, listen_port_attr, expected_key, **proxy_kwargs): - r = Test.AddTestRun(description) + case = suite.case(description) client_kwargs = proxy_kwargs.pop('client_kwargs', {}) - client = r.AddClientProcess(client_name, replay_file, **client_kwargs) - proxy = r.AddProxyProcess(proxy_name, listen_port=getattr(client.Variables, listen_port_attr), - server_port=1, **proxy_kwargs) - - client.Streams.stdout += Testers.ContainsExpression( + client = case.add_client(client_name, replay_file, **client_kwargs) + proxy = case.add_proxy( + proxy_name, + listen_port=getattr(client, listen_port_attr), + server_port=1, + **proxy_kwargs, + ) + + client.stdout.contains( f'Verification rules for transaction key "{expected_key}" were never processed before replay completed.', "The client should report the response verification rules that were skipped.") - client.Streams.stdout += Testers.ContainsExpression( - CLIENT_SUMMARY, "The client should summarize the replay completion failure.") - proxy.Streams.stdout += Testers.ContainsExpression( + client.stdout.contains(CLIENT_SUMMARY, + "The client should summarize the replay completion failure.") + proxy.stdout.contains( f'Closing downstream connection for key {expected_key} without sending a response.', "The proxy should close the connection before sending any response bytes.") - client.ReturnCode = 1 + client.expect_return_codes(1) add_server_failure_run( @@ -67,19 +74,16 @@ def add_client_failure_run(description, client_name, replay_file, proxy_name, li client_kwargs={"configure_https": False}, ) -r = Test.AddTestRun("Verify shutdown can allow unprocessed verification rules when requested.") -client = r.AddClientProcess("client2", "replay_files/client_subset_http1.yaml", - configure_https=False) -server = r.AddServerProcess("server2", "replay_files/server_superset_http1.yaml", - other_args="--allow-unprocessed-verifications") -r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify shutdown can allow unprocessed verification rules when requested.") +client = case.add_client("client2", "replay_files/client_subset_http1.yaml", configure_https=False) +server = case.add_server("server2", "replay_files/server_superset_http1.yaml", + other_args="--allow-unprocessed-verifications") +case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) -server.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before shutdown', - "The opt-out should suppress the unprocessed verification error.") -client.ReturnCode = 0 -server.ReturnCode = 0 +server.stdout.excludes('were never processed before shutdown', + "The opt-out should suppress the unprocessed verification error.") +client.expect_return_codes(0) +server.expect_return_codes(0) add_server_failure_run( "Verify HTTP/2 shutdown fails when request verification rules were never processed.", @@ -105,18 +109,17 @@ def add_client_failure_run(description, client_name, replay_file, proxy_name, li client_kwargs={"configure_https": False}, ) -r = Test.AddTestRun( +case = suite.case( "Verify the client can allow unprocessed response verification rules when requested.") -client = r.AddClientProcess("client5", "replay_files/verify_unprocessed_response_http1.yaml", - configure_https=False, other_args="--allow-unprocessed-verifications") -proxy = r.AddProxyProcess("proxy5", listen_port=client.Variables.http_port, server_port=1) -client.Streams.stdout += Testers.ExcludesExpression( - 'were never processed before replay completed', - "The client opt-out should suppress the unprocessed verification error.") -proxy.Streams.stdout += Testers.ContainsExpression( +client = case.add_client("client5", "replay_files/verify_unprocessed_response_http1.yaml", + configure_https=False, other_args="--allow-unprocessed-verifications") +proxy = case.add_proxy("proxy5", listen_port=client.http_port, server_port=1) +client.stdout.excludes('were never processed before replay completed', + "The client opt-out should suppress the unprocessed verification error.") +proxy.stdout.contains( 'Closing downstream connection for key unprocessed-response-1 without sending a response.', "The proxy should close the connection before sending any response bytes.") -client.ReturnCode = 1 +client.expect_return_codes(1) add_client_failure_run( "Verify the HTTP/2 client fails when response verification rules were never processed.", @@ -140,3 +143,7 @@ def add_client_failure_run(description, client_name, replay_file, proxy_name, li use_ssl=True, use_http3_to_1=True, ) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/arguments/version_argument/test_version_argument.py b/tests/uranium_tests/arguments/version_argument/test_version_argument.py new file mode 100644 index 000000000..0ab504059 --- /dev/null +++ b/tests/uranium_tests/arguments/version_argument/test_version_argument.py @@ -0,0 +1,58 @@ +''' +Verify the version argument of Proxy Verifier. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +import re + + +def get_project_version(): + """ + Parse the top-level CMake project declaration for the specified version. + """ + cmake_file = suite.module_path.parents[4] / "CMakeLists.txt" + project_pattern = re.compile( + r"project\s*\(\s*ProxyVerifier\b.*?\bVERSION\s+([0-9.]+)", + re.IGNORECASE | re.DOTALL, + ) + + contents = cmake_file.read_text(encoding='utf-8') + match = project_pattern.search(contents) + if match: + return match.group(1) + + raise ValueError(f"Could not find the ProxyVerifier version in {cmake_file}") + + +project_version = get_project_version() + +# +# Test 1: Verify that the client detects when a key is not present in a +# +case = suite.case('Verify that the client detects a non-existent key') +client = case.add_client("client1", replay_dir=None, other_args="--version") + +client.stdout.contains(f'Version {project_version} of Proxy Verifier', + "The --version output should print the expected string") + +# +# Test 2: Verify that the server detects when a key is not present in a +# transaction. +# +case = suite.case('Verify that the server detects a non-existent key') +server = case.add_server("server2", replay_dir=None, other_args="--version") + +server.stdout.contains(f'Version {project_version} of Proxy Verifier', + "The --version output should print the expected string") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/await/await.replay.yaml b/tests/uranium_tests/await/await.replay.yaml similarity index 100% rename from tests/autests/gold_tests/await/await.replay.yaml rename to tests/uranium_tests/await/await.replay.yaml diff --git a/tests/autests/gold_tests/await/await.test.py b/tests/uranium_tests/await/test_await.py similarity index 60% rename from tests/autests/gold_tests/await/await.test.py rename to tests/uranium_tests/await/test_await.py index 6bbe506c7..a4c08f0d7 100644 --- a/tests/autests/gold_tests/await/await.test.py +++ b/tests/uranium_tests/await/test_await.py @@ -5,34 +5,39 @@ # # Copyright 2022-2026, Verizon Media # SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) # import re -Test.Summary = __doc__ - # # Test 1: Run a few transactions with the await directive. # -r = Test.AddTestRun("Verify correct handling of the await directive") -client = r.AddClientProcess("client_await", "await.replay.yaml") -server = r.AddServerProcess("server_await", "await.replay.yaml") -proxy = r.AddProxyProcess("proxy_await", listen_port=client.Variables.https_port, - server_port=server.Variables.https_port, use_ssl=True, - use_http2_to_2=True) +case = suite.case("Verify correct handling of the await directive") +client = case.add_client("client_await", "await.replay.yaml") +server = case.add_server("server_await", "await.replay.yaml") +proxy = case.add_proxy("proxy_await", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) -server.Streams.stdout += Testers.ContainsExpression( - "Ready with 3 transactions.", "The server should have parsed 3 transactions.") +server.stdout.contains("Ready with 3 transactions.", + "The server should have parsed 3 transactions.") # Make sure that the entire first-request finishes before second-request # starts. And, further, that the entire second-request finishes before # third-request starts. -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( "Submitted the following HTTP/2 request headers for key first-request.*" "Received an HTTP/2 body of 3432 bytes for key first-request.*" "Submitted the following HTTP/2 request headers for key second-request.*" - "Received an HTTP/2 body of 3432 bytes for key second-request.*", + "Received an HTTP/2 body of 3432 bytes for key second-request.*" "Submitted the following HTTP/2 request headers for key third-request.*" "Received an HTTP/2 body of 3432 bytes for key third-request.*", "second-request should start only after first-request finishes.", reflags=re.MULTILINE | re.DOTALL) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/body/body.yaml b/tests/uranium_tests/body/body.yaml similarity index 100% rename from tests/autests/gold_tests/body/body.yaml rename to tests/uranium_tests/body/body.yaml diff --git a/tests/autests/gold_tests/body/gold/body_client.gold b/tests/uranium_tests/body/gold/body_client.gold similarity index 100% rename from tests/autests/gold_tests/body/gold/body_client.gold rename to tests/uranium_tests/body/gold/body_client.gold diff --git a/tests/autests/gold_tests/body/gold/body_proxy.gold b/tests/uranium_tests/body/gold/body_proxy.gold similarity index 100% rename from tests/autests/gold_tests/body/gold/body_proxy.gold rename to tests/uranium_tests/body/gold/body_proxy.gold diff --git a/tests/autests/gold_tests/body/gold/body_server.gold b/tests/uranium_tests/body/gold/body_server.gold similarity index 100% rename from tests/autests/gold_tests/body/gold/body_server.gold rename to tests/uranium_tests/body/gold/body_server.gold diff --git a/tests/uranium_tests/body/test_body.py b/tests/uranium_tests/body/test_body.py new file mode 100644 index 000000000..9db638666 --- /dev/null +++ b/tests/uranium_tests/body/test_body.py @@ -0,0 +1,32 @@ +''' +Verify basic body reading functionality. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("Verify bodies can be read correctly.") +client = case.add_client("client1", "body.yaml") +server = case.add_server("server1", "body.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True) + +proxy.stdout.matches_gold("gold/body_proxy.gold") +client.stdout.matches_gold("gold/body_client.gold") +server.stdout.matches_gold("gold/body_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/body_verification/http.yaml b/tests/uranium_tests/body_verification/http.yaml similarity index 100% rename from tests/autests/gold_tests/body_verification/http.yaml rename to tests/uranium_tests/body_verification/http.yaml diff --git a/tests/autests/gold_tests/body_verification/http2.yaml b/tests/uranium_tests/body_verification/http2.yaml similarity index 100% rename from tests/autests/gold_tests/body_verification/http2.yaml rename to tests/uranium_tests/body_verification/http2.yaml diff --git a/tests/autests/gold_tests/body_verification/http3.yaml b/tests/uranium_tests/body_verification/http3.yaml similarity index 100% rename from tests/autests/gold_tests/body_verification/http3.yaml rename to tests/uranium_tests/body_verification/http3.yaml diff --git a/tests/uranium_tests/body_verification/test_body_verification.py b/tests/uranium_tests/body_verification/test_body_verification.py new file mode 100644 index 000000000..1a2a28a25 --- /dev/null +++ b/tests/uranium_tests/body_verification/test_body_verification.py @@ -0,0 +1,77 @@ +''' +Verify basic body verification functionality. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify bodies can be verified correctly for HTTP/1.1. +# +case = suite.case("Verify bodies can be verified correctly for HTTP/1.1.") +client = case.add_client("client1", "http.yaml") +server = case.add_server("server1", "http.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True) + +client.stdout.contains('Equals Success: Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +client.stdout.contains('Contains Success: Key: "2", Content Data: "body"', + "Verification should be happy with the body.") +client.stdout.contains('Equals Violation: Different. Key: "3", Content Data: "body"', + "Verification should not be happy with the body.") + +server.stdout.contains('Equals Success: Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +server.stdout.contains('Contains Success: Key: "2", Content Data: "body"', + "Verification should be happy with the body.") +server.stdout.contains('Equals Violation: Different. Key: "3", Content Data: "body"', + "Verification should not be happy with the body.") + +# +# Test 2: Verify bodies can be verified correctly for HTTP/2. +# +case = suite.case("Verify bodies can be verified correctly for HTTP/2.") +client = case.add_client("client2", "http2.yaml") +server = case.add_server("server2", "http2.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains('Equals Success: Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +client.stdout.contains('No Case Prefix Success: Key: "2", Content Data: "body"', + "Verification should be happy with the body.") + +server.stdout.contains('Equals Success: Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +server.stdout.contains('No Case Suffix Success: Key: "2", Content Data: "body"', + "Verification should be happy with the body.") + +# +# Test 3: Verify bodies can be verified correctly for HTTP/3. +# +case = suite.case("Verify bodies can be verified correctly for HTTP/3.") +client = case.add_client("client3", "http3.yaml") +server = case.add_server("server3", "http3.yaml") +proxy = case.add_proxy("proxy3", listen_port=client.http3_port, server_port=server.http_port, + use_ssl=True, use_http3_to_1=True) + +client.stdout.contains('Not Contains Success: Not Found. Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +client.stdout.contains('Not No Case Contains Violation: Key: "2", Content Data: "body"', + "Verification should not be happy with the body.") + +server.stdout.contains('Not Equals Success: Different. Key: "1", Content Data: "body"', + "Verification should be happy with the body.") +server.stdout.contains('Not No Case Equals Violation: Key: "2", Content Data: "body"', + "Verification should not be happy with the body.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/connect_tunnel/replay_files/client/connect_tunnel.yaml b/tests/uranium_tests/connect_tunnel/replay_files/client/connect_tunnel.yaml similarity index 100% rename from tests/autests/gold_tests/connect_tunnel/replay_files/client/connect_tunnel.yaml rename to tests/uranium_tests/connect_tunnel/replay_files/client/connect_tunnel.yaml diff --git a/tests/autests/gold_tests/connect_tunnel/replay_files/server/connect_tunnel.yaml b/tests/uranium_tests/connect_tunnel/replay_files/server/connect_tunnel.yaml similarity index 100% rename from tests/autests/gold_tests/connect_tunnel/replay_files/server/connect_tunnel.yaml rename to tests/uranium_tests/connect_tunnel/replay_files/server/connect_tunnel.yaml diff --git a/tests/uranium_tests/connect_tunnel/test_connect_tunnel.py b/tests/uranium_tests/connect_tunnel/test_connect_tunnel.py new file mode 100644 index 000000000..91d5569ef --- /dev/null +++ b/tests/uranium_tests/connect_tunnel/test_connect_tunnel.py @@ -0,0 +1,74 @@ +''' +Verify CONNECT establishes a blind tunnel for a follow-up HTTP/1 transaction. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +import re + +case = suite.case("Verify CONNECT allows a follow-up HTTP/1 transaction to be tunneled") +client = case.add_client("client", "replay_files/client", configure_https=False, + configure_http3=False) +server = case.add_server("server", "replay_files/server", configure_https=False, + configure_http3=False) +proxy = case.add_proxy("proxy", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("CONNECT verifier.example:443 HTTP/1.1", + "The client should send a CONNECT request to establish the tunnel.") + +client.stdout.contains( + "GET /through/tunnel HTTP/1.1", + "The client should send the follow-up transaction through the established tunnel.") + +client.stdout.contains( + "Received an HTTP/1 200 response for key connect-tunnel.*" + "Received an HTTP/1 200 response for key tunneled-request", + "The client should handle the CONNECT response before the tunneled response.", + reflags=re.MULTILINE | re.DOTALL) + +client.stdout.contains( + "tunnel-body", + "The client should receive the verifier-server response body through the tunnel.") + +client.stdout.excludes("Violation:", "There should be no verification errors.") + +client.stdout.excludes("Failed HTTP/1 transaction", + "Both client transactions should complete successfully.") + +proxy.stdout.contains( + "Received CONNECT request for key connect-tunnel: target verifier.example:443", + "The test proxy should recognize the CONNECT request.") + +proxy.stdout.contains("Established CONNECT tunnel for key connect-tunnel to 127.0.0.1:", + "The test proxy should establish a tunnel to verifier-server.") + +proxy.stdout.contains( + "CONNECT tunnel for key connect-tunnel closed after relaying [1-9][0-9]* bytes " + "client->server and [1-9][0-9]* bytes server->client.", + "The test proxy should blindly relay data in both directions through the tunnel.", + reflags=re.MULTILINE) + +server.stdout.contains("Ready with 1 transaction.", + "Only the tunneled transaction should reach verifier-server.") + +server.stdout.contains("GET /through/tunnel HTTP/1.1", + "Verifier-server should receive the tunneled request.") + +server.stdout.contains("Request with key tunneled-request passed validation.", + "Verifier-server should validate the tunneled request.") + +server.stdout.contains("Sent the following HTTP/1 response headers for key tunneled-request", + "Verifier-server should send the tunneled response back to the client.") + +server.stdout.excludes("Violation:", "There should be no verification errors on verifier-server.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/delay/client-side-delay.yaml b/tests/uranium_tests/delay/client-side-delay.yaml similarity index 100% rename from tests/autests/gold_tests/delay/client-side-delay.yaml rename to tests/uranium_tests/delay/client-side-delay.yaml diff --git a/tests/autests/gold_tests/delay/server-side-delay.yaml b/tests/uranium_tests/delay/server-side-delay.yaml similarity index 100% rename from tests/autests/gold_tests/delay/server-side-delay.yaml rename to tests/uranium_tests/delay/server-side-delay.yaml diff --git a/tests/uranium_tests/delay/test_delay.py b/tests/uranium_tests/delay/test_delay.py new file mode 100644 index 000000000..d9bd2aba3 --- /dev/null +++ b/tests/uranium_tests/delay/test_delay.py @@ -0,0 +1,99 @@ +''' +Verify correct handling of session and transaction delay. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Run a few sessions and transactions with client-side delay. +# +case = suite.case("Verify the handling of the client-side delay specification.") +client = case.add_client("client_client_delay", "client-side-delay.yaml") +server = case.add_server("server_client_delay", "client-side-delay.yaml") + +# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 +# traffic. Thankfully this is easily addressed by running a separate process +# for each. +proxy = case.add_proxy("proxy_http_client_delay", listen_port=client.http_port, + server_port=server.http_port) +proxy = case.add_proxy("proxy_https_client_delay", listen_port=client.https_port, + server_port=server.https_port, use_ssl=True, use_http2_to_2=True) + +server.stdout.contains("Ready with 3 transactions.", + "The server should have parsed 2 transactions.") + +client.stdout.contains( + "3 transactions in 2 sessions .* in .* milliseconds", + "The client should have reported running the transactions with timing data.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 2: Verify that the timing data indicates that the delays took place. +# +case = suite.case("Verify the client-side delay replay took an expected amount of time to run.") +verifier_script = 'verify_duration.py' +client_output = client.stdout.path +expected_min_delay_ms = "3000" +process = case.add_process( + "verify-client-delay", + ["python3", verifier_script, client_output, expected_min_delay_ms], + copies=[verifier_script], +) +process.stdout.contains('Good', 'The verifier script should report success.') + +# +# Test 3: Run a few sessions and transactions with server-side delay. +# +case = suite.case("Verify the handling of the server-side delay specification.") +client = case.add_client("client_server_delay", "server-side-delay.yaml") +server = case.add_server("server_server_delay", "server-side-delay.yaml") + +# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 +# traffic. Thankfully this is easily addressed by running a separate process +# for each. +proxy = case.add_proxy("proxy_http_server_delay", listen_port=client.http_port, + server_port=server.http_port) +proxy = case.add_proxy("proxy_https_server_delay", listen_port=client.https_port, + server_port=server.https_port, use_ssl=True, use_http2_to_2=True) + +server.stdout.contains("Ready with 2 transactions.", + "The server should have parsed 2 transactions.") + +client.stdout.contains( + "2 transactions in 2 sessions .* in .* milliseconds", + "The client should have reported running the transactions with timing data.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 4: Verify that the timing data indicates that the delays took place. +# +case = suite.case("Verify the server-side delay replay took an expected amount of time to run.") +client_output = client.stdout.path +expected_min_delay_ms = "1000" +process = case.add_process( + "verify-server-delay", + ["python3", verifier_script, client_output, expected_min_delay_ms], + copies=[verifier_script], +) +process.stdout.contains('Good', 'The verifier script should report success.') + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/delay/verify_duration.py b/tests/uranium_tests/delay/verify_duration.py similarity index 100% rename from tests/autests/gold_tests/delay/verify_duration.py rename to tests/uranium_tests/delay/verify_duration.py diff --git a/tests/autests/gold_tests/doctest/example_replay.yaml b/tests/uranium_tests/doctest/example_replay.yaml similarity index 100% rename from tests/autests/gold_tests/doctest/example_replay.yaml rename to tests/uranium_tests/doctest/example_replay.yaml diff --git a/tests/autests/gold_tests/doctest/gold/doctest_client.gold b/tests/uranium_tests/doctest/gold/doctest_client.gold similarity index 100% rename from tests/autests/gold_tests/doctest/gold/doctest_client.gold rename to tests/uranium_tests/doctest/gold/doctest_client.gold diff --git a/tests/autests/gold_tests/doctest/gold/doctest_server.gold b/tests/uranium_tests/doctest/gold/doctest_server.gold similarity index 100% rename from tests/autests/gold_tests/doctest/gold/doctest_server.gold rename to tests/uranium_tests/doctest/gold/doctest_server.gold diff --git a/tests/uranium_tests/doctest/test_doctest.py b/tests/uranium_tests/doctest/test_doctest.py new file mode 100644 index 000000000..963d48830 --- /dev/null +++ b/tests/uranium_tests/doctest/test_doctest.py @@ -0,0 +1,54 @@ +''' +Verify the example replay file from the README. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify that we're testing with the example replay file +# that is in the README. +# +case = suite.case("Verify the tested replay file is in README.md.") +verifier_script = 'verify_example_replay_contents.py' +readme_path = suite.module_path.parents[3] / 'README.md' +example_yaml = 'example_replay.yaml' +process = case.add_process( + "verify-readme-example", + ["python3", verifier_script, example_yaml, readme_path], + copies=[verifier_script, example_yaml], +) +process.stdout.contains('Good', f'The contents of {example_yaml} should be in {readme_path}') + +# +# Test 2: Verify correct behavior of a single client-side HTTP/2 transaction. +# +case = suite.case("Verify the example replay file from the README.") +client = case.add_client("client", example_yaml) +server = case.add_server("server", example_yaml) + +# The test proxy is not featureful enough to handle both HTTP/1 and HTTP/2 +# traffic. Thankfully this is easily addressed by running a separate process +# for each. +proxy = case.add_proxy("proxy_http", listen_port=client.http_port, server_port=server.http_port) +proxy = case.add_proxy("proxy_https", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.matches_gold("gold/doctest_client.gold") +server.stdout.matches_gold("gold/doctest_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/doctest/verify_example_replay_contents.py b/tests/uranium_tests/doctest/verify_example_replay_contents.py similarity index 100% rename from tests/autests/gold_tests/doctest/verify_example_replay_contents.py rename to tests/uranium_tests/doctest/verify_example_replay_contents.py diff --git a/tests/autests/gold_tests/field_parsing/gold/duplicate_fields_client.gold b/tests/uranium_tests/field_parsing/gold/duplicate_fields_client.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/duplicate_fields_client.gold rename to tests/uranium_tests/field_parsing/gold/duplicate_fields_client.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/duplicate_fields_proxy.gold b/tests/uranium_tests/field_parsing/gold/duplicate_fields_proxy.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/duplicate_fields_proxy.gold rename to tests/uranium_tests/field_parsing/gold/duplicate_fields_proxy.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/duplicate_fields_server.gold b/tests/uranium_tests/field_parsing/gold/duplicate_fields_server.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/duplicate_fields_server.gold rename to tests/uranium_tests/field_parsing/gold/duplicate_fields_server.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/empty_proxy_client.gold b/tests/uranium_tests/field_parsing/gold/empty_proxy_client.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/empty_proxy_client.gold rename to tests/uranium_tests/field_parsing/gold/empty_proxy_client.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/empty_proxy_proxy.gold b/tests/uranium_tests/field_parsing/gold/empty_proxy_proxy.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/empty_proxy_proxy.gold rename to tests/uranium_tests/field_parsing/gold/empty_proxy_proxy.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/empty_proxy_server.gold b/tests/uranium_tests/field_parsing/gold/empty_proxy_server.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/empty_proxy_server.gold rename to tests/uranium_tests/field_parsing/gold/empty_proxy_server.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/transaction_fields_client.gold b/tests/uranium_tests/field_parsing/gold/transaction_fields_client.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/transaction_fields_client.gold rename to tests/uranium_tests/field_parsing/gold/transaction_fields_client.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/transaction_fields_proxy.gold b/tests/uranium_tests/field_parsing/gold/transaction_fields_proxy.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/transaction_fields_proxy.gold rename to tests/uranium_tests/field_parsing/gold/transaction_fields_proxy.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/transaction_fields_server.gold b/tests/uranium_tests/field_parsing/gold/transaction_fields_server.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/transaction_fields_server.gold rename to tests/uranium_tests/field_parsing/gold/transaction_fields_server.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/yaml_specified_client.gold b/tests/uranium_tests/field_parsing/gold/yaml_specified_client.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/yaml_specified_client.gold rename to tests/uranium_tests/field_parsing/gold/yaml_specified_client.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/yaml_specified_proxy.gold b/tests/uranium_tests/field_parsing/gold/yaml_specified_proxy.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/yaml_specified_proxy.gold rename to tests/uranium_tests/field_parsing/gold/yaml_specified_proxy.gold diff --git a/tests/autests/gold_tests/field_parsing/gold/yaml_specified_server.gold b/tests/uranium_tests/field_parsing/gold/yaml_specified_server.gold similarity index 100% rename from tests/autests/gold_tests/field_parsing/gold/yaml_specified_server.gold rename to tests/uranium_tests/field_parsing/gold/yaml_specified_server.gold diff --git a/tests/autests/gold_tests/field_parsing/replay_files/duplicate_fields.yaml b/tests/uranium_tests/field_parsing/replay_files/duplicate_fields.yaml similarity index 100% rename from tests/autests/gold_tests/field_parsing/replay_files/duplicate_fields.yaml rename to tests/uranium_tests/field_parsing/replay_files/duplicate_fields.yaml diff --git a/tests/autests/gold_tests/field_parsing/replay_files/empty_field.yaml b/tests/uranium_tests/field_parsing/replay_files/empty_field.yaml similarity index 100% rename from tests/autests/gold_tests/field_parsing/replay_files/empty_field.yaml rename to tests/uranium_tests/field_parsing/replay_files/empty_field.yaml diff --git a/tests/autests/gold_tests/field_parsing/replay_files/empty_proxy.yaml b/tests/uranium_tests/field_parsing/replay_files/empty_proxy.yaml similarity index 100% rename from tests/autests/gold_tests/field_parsing/replay_files/empty_proxy.yaml rename to tests/uranium_tests/field_parsing/replay_files/empty_proxy.yaml diff --git a/tests/autests/gold_tests/field_parsing/replay_files/transaction_fields.yaml b/tests/uranium_tests/field_parsing/replay_files/transaction_fields.yaml similarity index 100% rename from tests/autests/gold_tests/field_parsing/replay_files/transaction_fields.yaml rename to tests/uranium_tests/field_parsing/replay_files/transaction_fields.yaml diff --git a/tests/autests/gold_tests/field_parsing/replay_files/yaml_specified.yaml b/tests/uranium_tests/field_parsing/replay_files/yaml_specified.yaml similarity index 100% rename from tests/autests/gold_tests/field_parsing/replay_files/yaml_specified.yaml rename to tests/uranium_tests/field_parsing/replay_files/yaml_specified.yaml diff --git a/tests/uranium_tests/field_parsing/test_duplicate_fields.py b/tests/uranium_tests/field_parsing/test_duplicate_fields.py new file mode 100644 index 000000000..d58d75801 --- /dev/null +++ b/tests/uranium_tests/field_parsing/test_duplicate_fields.py @@ -0,0 +1,28 @@ +''' +Verify correct handling of duplicate fields in a message. +''' +# @file +# +# Copyright 2020-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct behavior when there are duplicate HTTP fields. +# +case = suite.case("Verify correct handling of duplicate fields in a message.") +client = case.add_client("client1", "replay_files/duplicate_fields.yaml") +server = case.add_server("server1", "replay_files/duplicate_fields.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/duplicate_fields_proxy.gold") +client.stdout.matches_gold("gold/duplicate_fields_client.gold") +server.stdout.matches_gold("gold/duplicate_fields_server.gold") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/field_parsing/test_empty_field.py b/tests/uranium_tests/field_parsing/test_empty_field.py new file mode 100644 index 000000000..992e0000e --- /dev/null +++ b/tests/uranium_tests/field_parsing/test_empty_field.py @@ -0,0 +1,36 @@ +''' +Verify correct handling of malformed replay files. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct handling of empty fields. +# +case = suite.case("Verify correct handling of empty header fields") +client = case.add_client("client1", "replay_files/empty_field.yaml") +server = case.add_server("server1", "replay_files/empty_field.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +client.expect_return_codes(1) +server.expect_return_codes(1) + +# Due to the parsing failure, the server will not listen on the port. +# Thus the standard ready criteria will not work. +server.ready = False + +client.stdout.contains("Field or rule at line .* is not a sequence as required", + "Verify that we inform the user of the malformed field.") +server.stdout.contains("Field or rule at line .* is not a sequence as required", + "Verify that we inform the user of the malformed field.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/field_parsing/test_empty_proxy.py b/tests/uranium_tests/field_parsing/test_empty_proxy.py new file mode 100644 index 000000000..69aa1a8b8 --- /dev/null +++ b/tests/uranium_tests/field_parsing/test_empty_proxy.py @@ -0,0 +1,28 @@ +''' +Verify correct handling of empty proxy nodes. +''' +# @file +# +# Copyright 2020-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct handling of empty proxy-request and response nodes. +# +case = suite.case("Verify correct handling of empty proxy nodes") +client = case.add_client("client1", "replay_files/empty_proxy.yaml") +server = case.add_server("server1", "replay_files/empty_proxy.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/empty_proxy_proxy.gold") +client.stdout.matches_gold("gold/empty_proxy_client.gold") +server.stdout.matches_gold("gold/empty_proxy_server.gold") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/field_parsing/test_parse_yaml.py b/tests/uranium_tests/field_parsing/test_parse_yaml.py new file mode 100644 index 000000000..b212cfe17 --- /dev/null +++ b/tests/uranium_tests/field_parsing/test_parse_yaml.py @@ -0,0 +1,44 @@ +''' +Verify correct parsing of YAML replay files. +''' +# @file +# +# Copyright 2020-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct behavior with a YAML-specified replay file. +# +case = suite.case("Verify parsing of a YAML-specified replay file") +client = case.add_client("client1", "replay_files/yaml_specified.yaml") +server = case.add_server("server1", "replay_files/yaml_specified.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/yaml_specified_proxy.gold") +client.stdout.matches_gold("gold/yaml_specified_client.gold") +server.stdout.matches_gold("gold/yaml_specified_server.gold") + +# These expect verification errors. +client.expect_return_codes(1) +server.expect_return_codes(1) + +# +# Test 2: Verify correct parsing of transaction-level fields. +# +case = suite.case("Verify parsing of transaction-level fields") +client = case.add_client("client2", "replay_files/transaction_fields.yaml") +server = case.add_server("server2", "replay_files/transaction_fields.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/transaction_fields_proxy.gold") +client.stdout.matches_gold("gold/transaction_fields_client.gold") +server.stdout.matches_gold("gold/transaction_fields_server.gold") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/field_verification/replay_files/cookie_equal.yaml b/tests/uranium_tests/field_verification/replay_files/cookie_equal.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/cookie_equal.yaml rename to tests/uranium_tests/field_verification/replay_files/cookie_equal.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/duplicate_fields.yaml b/tests/uranium_tests/field_verification/replay_files/duplicate_fields.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/duplicate_fields.yaml rename to tests/uranium_tests/field_verification/replay_files/duplicate_fields.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/map_specification.yaml b/tests/uranium_tests/field_verification/replay_files/map_specification.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/map_specification.yaml rename to tests/uranium_tests/field_verification/replay_files/map_specification.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/multi_value_equal.yaml b/tests/uranium_tests/field_verification/replay_files/multi_value_equal.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/multi_value_equal.yaml rename to tests/uranium_tests/field_verification/replay_files/multi_value_equal.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/multi_value_includes.yaml b/tests/uranium_tests/field_verification/replay_files/multi_value_includes.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/multi_value_includes.yaml rename to tests/uranium_tests/field_verification/replay_files/multi_value_includes.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/not_nocase.yaml b/tests/uranium_tests/field_verification/replay_files/not_nocase.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/not_nocase.yaml rename to tests/uranium_tests/field_verification/replay_files/not_nocase.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/set_cookie_absent.yaml b/tests/uranium_tests/field_verification/replay_files/set_cookie_absent.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/set_cookie_absent.yaml rename to tests/uranium_tests/field_verification/replay_files/set_cookie_absent.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/set_cookie_negative.yaml b/tests/uranium_tests/field_verification/replay_files/set_cookie_negative.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/set_cookie_negative.yaml rename to tests/uranium_tests/field_verification/replay_files/set_cookie_negative.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/set_cookie_negative_failure.yaml b/tests/uranium_tests/field_verification/replay_files/set_cookie_negative_failure.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/set_cookie_negative_failure.yaml rename to tests/uranium_tests/field_verification/replay_files/set_cookie_negative_failure.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/substr_rules.yaml b/tests/uranium_tests/field_verification/replay_files/substr_rules.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/substr_rules.yaml rename to tests/uranium_tests/field_verification/replay_files/substr_rules.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/substr_rules_duplicate.yaml b/tests/uranium_tests/field_verification/replay_files/substr_rules_duplicate.yaml similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/substr_rules_duplicate.yaml rename to tests/uranium_tests/field_verification/replay_files/substr_rules_duplicate.yaml diff --git a/tests/autests/gold_tests/field_verification/replay_files/various_verification.json b/tests/uranium_tests/field_verification/replay_files/various_verification.json similarity index 100% rename from tests/autests/gold_tests/field_verification/replay_files/various_verification.json rename to tests/uranium_tests/field_verification/replay_files/various_verification.json diff --git a/tests/uranium_tests/field_verification/test_field_verification.py b/tests/uranium_tests/field_verification/test_field_verification.py new file mode 100644 index 000000000..99d9c66d6 --- /dev/null +++ b/tests/uranium_tests/field_verification/test_field_verification.py @@ -0,0 +1,233 @@ +''' +Verify correct field verification behavior. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify field verification in a JSON replay file. +# +case = suite.case("Verify field verification works for a simple HTTP transaction") +client = case.add_client("client1", "replay_files/various_verification.json") +server = case.add_server("server1", "replay_files/various_verification.json") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +# Verify a success and failure of each validation in the request. +server.stdout.contains('Absence Success: Key: "1", Field Name: "x-candy"', + 'Validation should be happy that the proxy removed X-CANDY.') +server.stdout.contains( + 'Absence Violation: Present. Key: "1", Field Name: "content-type", Value: "application/octet-stream"', + 'Validation should complain that "content-type" is present') +server.stdout.contains('Presence Success: Key: "1", Field Name: "content-length", Value: "399"', + 'Validation should be happy that "content-length" is present.') +server.stdout.contains( + 'Presence Success: Key: "1", Field Name: "exampleremoteip", Value: "10.10.10.4"', + 'Validation should be happy that "ExampleRemoteIP" is present even though its value differs.') +server.stdout.contains('Presence Violation: Absent. Key: "1", Field Name: "client-ip"', + 'Validation should complain that "client-ip" is misssing') +server.stdout.contains('Equals Success: Key: "1", Field Name: "x-someid", Value: "21djfk39jfkds"', + 'Validation should be happy that "S-SomeId" has the expected value.') +server.stdout.contains( + 'Equals Violation: Different. Key: "1", Field Name: "host", Correct Value: "example.com", Actual Value: "test.example.com"', + 'Validation should complain that the "Host" value differs from the expected value.') +server.stdout.contains( + 'Equals Violation: Different. Key: "1", Field Name: "x-test-case", Correct Value: "CASEmatters", Actual Value: "caseMATTERS"', + 'Equals validation must be case-sensitive.') + +# Verify a success and failure of each validation in the response. +client.stdout.contains('Absence Success: Key: "1", Field Name: "x-newtestheader"', + 'Validation should be happy that the proxy removed X-NewTestHeader.') +client.stdout.contains( + 'Absence Violation: Present. Key: "1", Field Name: "x-shouldexist", Value: "trustme; it=will"', + 'Validation should complain that "X-ShouldExist" is present') +client.stdout.contains('Presence Success: Key: "1", Field Name: "content-length", Value: "0"', + 'Validation should be happy that "content-length" is present.') +client.stdout.contains( + 'Presence Success: Key: "1", Field Name: "age", Value: "4"', + 'Validation should be happy that "Age" is present even though its value differs.') +client.stdout.contains('Presence Violation: Absent. Key: "1", Field Name: "x-request-id"', + 'Validation should complain that "x-request-id" is misssing') +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "date", Value: "Sat, 16 Mar 2019 03:11:36 GMT"', + 'Validation should be happy that "date" has the expected value.') +client.stdout.contains( + ('Equals Violation: Different. Key: "1", Field Name: "x-testheader", ' + 'Correct Value: "from_proxy_response", Actual Value: "from_server_response"'), + 'Validation should complain that the "x-testheader" value differs from the expected value.') + +client.expect_return_codes(1) +server.expect_return_codes(1) + +# +# Test 2: Verify field verification in a YAML replay file. +# +case = suite.case("Verify field verification works for a simple HTTP transaction") +client = case.add_client("client2", "replay_files/cookie_equal.yaml") +server = case.add_server("server2", "replay_files/cookie_equal.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains('Absence Success: Key: "5", Field Name: "x-not-a-header"', + 'Validation should be happy that "X-Not-A-Header" is missing.') + +client.stdout.contains('Equals Success: Key: "5", Field Name: "set-cookie", Value: "ABCD"', + 'Validation should be happy that "Set-Cookie" had the expected header.') + +client.stdout.contains('Equals Success: Key: "5", Field Name: "set-cookie", Value: "ABCD"', + 'Validation should be happy that "Set-Cookie" had the expected header.') + +client.stdout.contains('Presence Violation: Absent. Key: "5", Field Name: "x-does-not-exist"', + 'Validation should complain that "X-Does-Not-Exist" is not present.') + +server.stdout.contains( + 'Equals Violation: Different. Key: "5", Field Name: "x-test-request", Correct Value: "rEQUESTdATA", Actual Value: "RequestData"', + 'Validation should complain that "X-Test-Request" is different.') + +server.stdout.contains( + 'Absence Violation: Present. Key: "5", Field Name: "x-test-present", Value: "It\'s there"', + 'Validation should complain that "X-Test-Pressent" is present.') + +server.stdout.contains('Equals Success: Key: "5", Field Name: "cookie", Value: "', + 'Validation should be happy with the cookie value.') + +client.expect_return_codes(1) +server.expect_return_codes(1) + +# +# Test 3: Verify repeated RFC-combinable header fields are normalized for verification. +# +case = suite.case("Verify repeated RFC-combinable header fields are normalized for verification") +client = case.add_client("client3", "replay_files/duplicate_fields.yaml") +server = case.add_server("server3", "replay_files/duplicate_fields.yaml") +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "cache-control", Value: "no-store, max-age=0"', + 'Validation should combine repeated Cache-Control field lines before comparing them.') + +client.stdout.contains( + ('Equals Violation: Different. Key: "1", Field Name: "cache-control", ' + 'Correct Value: "max-age=0, no-store", Actual Value: "no-store, max-age=0"'), + 'Validation should preserve the order of repeated Cache-Control values.') + +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "vary", Value: "accept-encoding, accept-language"', + 'Validation should treat a single comma-separated Vary field line like repeated field lines.') + +server.stdout.contains( + 'Equals Success: Key: "1", Field Name: "x-join", Value: "alpha, beta"', + 'Validation should join repeated request field lines with comma SP before equality checks.') + +server.stdout.contains(('Equals Violation: Different. Key: "1", Field Name: "x-join", ' + 'Correct Value: "beta, alpha", Actual Value: "alpha, beta"'), + 'Validation should fail when the repeated request field order changes.') + +server.stdout.contains( + 'Contains Success: Key: "1", Field Name: "x-join", Required Value: "beta", Value: "alpha, beta"', + 'Validation should run substring checks against the combined request field value.') + +client.expect_return_codes(1) +server.expect_return_codes(1) + +# Test 4: Verify Set-Cookie verification uses the dedicated set-cookie-verifications node. +case = suite.case( + "Verify Set-Cookie verification works with the dedicated set-cookie-verifications node") +client = case.add_client("client4", "replay_files/multi_value_includes.yaml") +server = case.add_server("server4", "replay_files/multi_value_includes.yaml") +proxy = case.add_proxy("proxy4", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "B1=333"', + "Verification should match a Set-Cookie rule even when the replay lists it out of order.") + +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "A1=111"', + "Verification should match the remaining Set-Cookie rule after the first match is consumed.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +# Test 5: Verify Set-Cookie verification allows extra cookies after the expected list. +case = suite.case("Verify Set-Cookie verification allows extra cookies after the expected list") +client = case.add_client("client5", "replay_files/multi_value_equal.yaml") +server = case.add_server("server5", "replay_files/multi_value_equal.yaml") +proxy = case.add_proxy("proxy5", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1=", Value: "A1=111"', + "Verification should match the first expected Set-Cookie rule.") + +client.stdout.contains( + 'Equals Success: Key: "1", Field Name: "set-cookie", Value: "B1=333"', + "Verification should match the second expected Set-Cookie rule and ignore later cookies.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +# Test 6: Verify Set-Cookie negative checks do not consume cookies and value arrays expand. +case = suite.case("Verify negative Set-Cookie checks assert no matching cookie line") +client = case.add_client("client6", "replay_files/set_cookie_negative.yaml") +server = case.add_server("server6", "replay_files/set_cookie_negative.yaml") +proxy = case.add_proxy("proxy6", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1=", Value: "A1=111"', + "Verification should expand a Set-Cookie contains array into individual checks.") + +client.stdout.contains( + 'Contains Success: Key: "1", Field Name: "set-cookie", Required Value: "A1S=", Value: "A1S=555"', + "Verification should match each expanded Set-Cookie contains rule independently.") + +client.stdout.contains( + 'Not Contains Success: Absent. Key: "1", Field Name: "set-cookie", Required Missing Value: "D1="', + "Verification should treat Set-Cookie absent-with-value as a negative non-consuming match.") + +client.stdout.contains( + 'Not Contains Success: Absent. Key: "1", Field Name: "set-cookie", Required Missing Value: "_ebd"', + "Verification should expand a Set-Cookie absent array into individual negative checks.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +# Test 7: Verify legacy Set-Cookie absence and negative pattern checks still work. +case = suite.case("Verify Set-Cookie absence works in both legacy and dedicated syntax") +client = case.add_client("client7", "replay_files/set_cookie_absent.yaml") +server = case.add_server("server7", "replay_files/set_cookie_absent.yaml") +proxy = case.add_proxy("proxy7", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains('Absence Success: Key: "1", Field Name: "set-cookie"', + "Legacy Set-Cookie absence verification under fields should still work.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +# Test 8: Verify a negative Set-Cookie pattern check fails on a matching cookie line. +case = suite.case("Verify a negative Set-Cookie pattern fails when a cookie matches") +client = case.add_client("client8", "replay_files/set_cookie_negative_failure.yaml") +server = case.add_server("server8", "replay_files/set_cookie_negative_failure.yaml") +proxy = case.add_proxy("proxy8", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains('Equals Success: Key: "1", Field Name: "set-cookie", Value: "A1=111"', + "Verification should still match the positive Set-Cookie rule.") + +client.stdout.contains( + 'Not Contains Violation: Key: "1", Field Name: "set-cookie", Required Missing Value: "B1=333", Value: "B1=333"', + "Verification should fail when an absent Set-Cookie pattern matches a received cookie.") + +client.expect_return_codes(1) +server.expect_return_codes(0) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/field_verification/not_nocase.test.py b/tests/uranium_tests/field_verification/test_not_nocase.py similarity index 68% rename from tests/autests/gold_tests/field_verification/not_nocase.test.py rename to tests/uranium_tests/field_verification/test_not_nocase.py index d09e6212a..62f4ca8ea 100644 --- a/tests/autests/gold_tests/field_verification/not_nocase.test.py +++ b/tests/uranium_tests/field_verification/test_not_nocase.py @@ -6,178 +6,174 @@ # # Copyright 2026, Verizon Media # SPDX-License-Identifier: Apache-2.0 -# -Test.Summary = ''' -Verify correct field and URL verification behavior for -equals, absent, present, contains, prefix, and suffix -with not, nocase, and both not and nocase modifiers -''' +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# # # Test 1: Verify field verification in a YAML replay file. # Each combinaton of test type, not/as, and case/nocase, and positive/negative result # are tested for client, and a mixture for server # -r = Test.AddTestRun("Verify 'not' and 'nocase' directives work for a single HTTP transaction") -client = r.AddClientProcess("client1", "replay_files/not_nocase.yaml") -server = r.AddServerProcess("server1", "replay_files/not_nocase.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify 'not' and 'nocase' directives work for a single HTTP transaction") +client = case.add_client("client1", "replay_files/not_nocase.yaml") +server = case.add_server("server1", "replay_files/not_nocase.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Equals Success: Different. Key: "5", Field Name: "host", Correct Value: "le.on", Actual Value: "example.one"', 'Validation should be happy that "le.on" is not equal to "example.one".') -server.Streams.stdout += Testers.ContainsExpression( - 'Not Presence Success: Absent. Key: "5", Field Name: "x-test-absent"', - 'Validation should be happy that "X-Test-Absent" has no value.') +server.stdout.contains('Not Presence Success: Absent. Key: "5", Field Name: "x-test-absent"', + 'Validation should be happy that "X-Test-Absent" has no value.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Absence Success: Present. Key: "5", Field Name: "x-test-present", Value: "It\'s there"', 'Validation should be happy that "X-Test-Present" has a value.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Contains Success: Not Found. Key: "5", Field Name: "host", Required Missing Value: "leo", Actual Value: "example.one"', 'Validation should be happy that "leo" is not contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Contains Success: Not Found. Key: "5", Field Name: "cookie", Required Missing Value: "12345678", Actual Value: "foo=bar"', 'Validation should be happy that a longer required substring is treated as not found.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Prefix Success: Not Found. Key: "5", Field Name: "x-test-request", Required Missing Value: "equ", Actual Value: "RequestData"', 'Validation should be happy that "equ" does not prefix "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Suffix Success: Not Found. Key: "5", Field Name: "x-test-present", Required Missing Value: "It\'s", Actual Value: "It\'s there"', 'Validation should be happy that "It\'s" does not suffix "It\'s there".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Equals Success: Key: "5", Field Name: "host", Required Value: "EXAMpLE.ONE", Value: "example.one"', 'Validation should be happy that "EXAMpLE.ONE" nocase equals "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Contains Success: Key: "5", Field Name: "host", Required Value: "Le.ON", Value: "example.one"', 'Validation should be happy that "Le.ON" is nocase contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Prefix Success: Key: "5", Field Name: "x-test-request", Required Value: "rEQ", Value: "RequestData"', 'Validation should be happy that "rEQ" nocase prefixes "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Suffix Success: Key: "5", Field Name: "x-test-present", Required Value: "heRe", Value: "It\'s there"', 'Validation should be happy that "heRe" nocase suffixes "It\'s there".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Equals Success: Different. Key: "5", Field Name: "host", Correct Value: "example.ON", Actual Value: "example.one"', 'Validation should be happy that "le.on" does not nocase equal "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Contains Success: Not Found. Key: "5", Field Name: "host", Required Missing Value: "U", Actual Value: "example.one"', 'Validation should be happy that "leo" is not nocase contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Prefix Success: Not Found. Key: "5", Field Name: "x-test-request", Required Missing Value: "EQU", Actual Value: "RequestData"', 'Validation should be happy that "equ" does not nocase prefix "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Suffix Success: Not Found. Key: "5", Field Name: "x-test-present", Required Missing Value: "hre", Actual Value: "It\'s there"', 'Validation should be happy that "hre" does not nocase suffix "It\'s there".') -server.Streams.stdout += Testers.ContainsExpression( - 'Not Equals Violation: Key: "5", Field Name: "host", Value: "example.one"', - 'Validation should complain that "example.on" equals "example.one".') +server.stdout.contains('Not Equals Violation: Key: "5", Field Name: "host", Value: "example.one"', + 'Validation should complain that "example.on" equals "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Presence Violation: Key: "5", Field Name: "x-test-present", Value: "It\'s there"', 'Validation should complain that "X-Test-Present" has a value.') -server.Streams.stdout += Testers.ContainsExpression( - 'Not Absence Violation: Key: "5", Field Name: "x-test-absent"', - 'Validation should complain that "X-Test-Absent" has no value.') +server.stdout.contains('Not Absence Violation: Key: "5", Field Name: "x-test-absent"', + 'Validation should complain that "X-Test-Absent" has no value.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Contains Violation: Key: "5", Field Name: "host", Required Missing Value: "le.on", Value: "example.one"', 'Validation should complain that "le.on" is contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Prefix Violation: Key: "5", Field Name: "x-test-request", Required Missing Value: "Req", Value: "RequestData"', 'Validation should complain that "Req" prefixes "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not Suffix Violation: Key: "5", Field Name: "x-test-present", Required Missing Value: "there", Value: "It\'s there"', 'Validation should complain that "there" suffixes "It\'s there".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Equals Violation: Different. Key: "5", Field Name: "host", Correct Value: "EXAMPLE.ON", Actual Value: "example.one"', 'Validation should complain that "EXAMPL.ON" does not nocase equal "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Contains Violation: Not Found. Key: "5", Field Name: "host", Required Value: "LE..On", Actual Value: "example.one"', 'Validation should complain that "LE..On" is not nocase contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Prefix Violation: Not Found. Key: "5", Field Name: "x-test-request", Required Value: "-TE", Actual Value: "RequestData"', 'Validation should complain that "-TE" does not nocase prefix "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'No Case Suffix Violation: Not Found. Key: "5", Field Name: "x-test-present", Required Value: "THER", Actual Value: "It\'s there"', 'Validation should complain that "THER" does not nocase suffix "It\'s there".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Equals Violation: Key: "5", Field Name: "host", Required Value: "Example.one", Value: "example.one"', 'Validation should complain that "Example.one" nocase equals "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Contains Violation: Key: "5", Field Name: "host", Required Missing Value: "le.oN", Value: "example.one"', 'Validation should complain that "le.oN" is nocase contained in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Prefix Violation: Key: "5", Field Name: "x-test-request", Required Missing Value: "req", Value: "RequestData"', 'Validation should complain that "req" nocase prefixes "RequestData".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Suffix Violation: Key: "5", Field Name: "x-test-present", Required Missing Value: "eRE", Value: "It\'s there"', 'Validation should complain that "eRE" nocase suffixes "It\'s there".') -server.Streams.stdout = Testers.ContainsExpression( +server.stdout.contains( 'Not No Case Contains Violation: Key: "5", URI Part: "path", Required Missing Value: "iG/S", Value: "/config/settings.yaml"', 'Validation should complain that "iG/S" is nocase contained in the path.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Not Equals Success: Different. Key: "5", Field Name: "content-type", Correct Value: "text", Actual Value: "text/html"', 'Validation should be happy that "text" does not equal "text/html".') -client.Streams.stdout += Testers.ContainsExpression( - 'Not Presence Violation: Key: "5", Field Name: "set-cookie", Value: "ABCD"', - 'Validation should complain that "set-cookie" is present.') +client.stdout.contains('Not Presence Violation: Key: "5", Field Name: "set-cookie", Value: "ABCD"', + 'Validation should complain that "set-cookie" is present.') -client.Streams.stdout += Testers.ContainsExpression( - 'Not Absence Violation: Key: "5", Field Name: "fake-cookie"', - 'Validation should complain that "fake-cookie" is absent.') +client.stdout.contains('Not Absence Violation: Key: "5", Field Name: "fake-cookie"', + 'Validation should complain that "fake-cookie" is absent.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Not No Case Contains Violation: Key: "5", Field Name: "content-type", Required Missing Value: "Tex", Value: "text/html"', 'Validation should complain that "Tex" is nocase contained in "text/html".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Not No Case Prefix Success: Absent. Key: "5", Field Name: "fake-cookie", Required Missing Value: "B"', 'Validation should be happy that "B" does not nocase prefix a nonexistent header.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'No Case Suffix Success: Key: "5", Field Name: "content-type", Required Value: "L", Value: "text/html"', 'Validation should be happy that "L" nocase suffixes "text/html".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Not Prefix Success: Not Found. Key: "5", Field Name: "multiple", Required Missing Value: "Abc, DEF", Actual Value: "abc, DEF"', 'Validation should be happy that "Abc" does not prefix "abc", even though "DEF" prefixes "DEF".' ) -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Not No Case Equals Violation: Key: "5", Field Name: "multiple", Required Value: "Abc, DEF", Value: "abc, DEF"', 'Validation should complain that each required value nocase equals the corresponding received value.' ) -client.ReturnCode = 1 -server.ReturnCode = 1 +client.expect_return_codes(1) +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/field_verification/substr_rules.test.py b/tests/uranium_tests/field_verification/test_substr_rules.py similarity index 65% rename from tests/autests/gold_tests/field_verification/substr_rules.test.py rename to tests/uranium_tests/field_verification/test_substr_rules.py index 2fa9b5ba6..524eb9661 100644 --- a/tests/autests/gold_tests/field_verification/substr_rules.test.py +++ b/tests/uranium_tests/field_verification/test_substr_rules.py @@ -5,164 +5,164 @@ # # Copyright 2026, Verizon Media # SPDX-License-Identifier: Apache-2.0 -# -Test.Summary = ''' -Verify correct field verification behavior for contains, prefix, and suffix. -''' +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# # # Test 1: Verify field verification in a YAML replay file. # -r = Test.AddTestRun("Verify field verification works for a simple HTTP transaction") -client = r.AddClientProcess("client1", "replay_files/substr_rules.yaml") -server = r.AddServerProcess("server1", "replay_files/substr_rules.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify field verification works for a simple HTTP transaction") +client = case.add_client("client1", "replay_files/substr_rules.yaml") +server = case.add_server("server1", "replay_files/substr_rules.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Success: Key: "5", Field Name: "host", Required Value: "le.on", Value: "example.one"', 'Validation should be happy that "le.on" is in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Success: Key: "5", Field Name: "x-test-request", Required Value: "Req", Value: "RequestData"', 'Validation should be happy that "RequestData" began with "Req".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Success: Key: "5", Field Name: "x-test-present", Required Value: "there", Value: "It\'s there"', 'Validation should be happy that "It\'s there" ended with "there.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Violation: Not Found. Key: "5", Field Name: "host", Required Value: "two", Actual Value: "example.one"', 'Validation should complain that "two" is not in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Violation: Not Found. Key: "5", Field Name: "x-test-request", Required Value: "equest", Actual Value: "RequestData"', 'Validation should complain that "RequestData" did not begin with "equest".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Violation: Not Found. Key: "5", Field Name: "x-test-present", Required Value: "er", Actual Value: "It\'s there"', 'Validation should complain that "It\'s there" did not end with "er".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Success: Key: "5", Field Name: "content-type", Required Value: "html", Value: "text/html"', 'Validation should be happy that "html" is in "text/html".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Violation: Not Found. Key: "5", Field Name: "set-cookie", Required Value: "ABCDE", Actual Value: "ABCD"', 'Validation should complain that "ABCDE" is not in "ABCD".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Prefix Violation: Absent. Key: "5", Field Name: "x-not-a-header", Required Value: "Whatever"', 'Validation should complain that "X-Not-A-Header" is missing.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Violation: Absent. Key: "5", Field Name: "x-does-not-exist", Required Value: "NotHere"', 'Validation should complain that "X-Does-Not-Exist" is missing.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Suffix Violation: Absent. Key: "5", Field Name: "x-does-not-exist", Required Value: "NotHere"', 'Validation should complain that "X-Does-Not-Exist" is missing.') -client.ReturnCode = 1 -server.ReturnCode = 1 +client.expect_return_codes(1) +server.expect_return_codes(1) # # Test 2: Verify substring rules run against the RFC-combined value for repeated field lines. # -r = Test.AddTestRun("Verify substring field verification works for repeated RFC-combinable fields") -client = r.AddClientProcess("client2", "replay_files/substr_rules_duplicate.yaml") -server = r.AddServerProcess("server2", "replay_files/substr_rules_duplicate.yaml") -proxy = r.AddProxyProcess("proxy2", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify substring field verification works for repeated RFC-combinable fields") +client = case.add_client("client2", "replay_files/substr_rules_duplicate.yaml") +server = case.add_server("server2", "replay_files/substr_rules_duplicate.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Success: Key: "1", Field Name: "x-test-contains", Required Value: "alpha, beta", Value: "alpha, beta"', 'Validation should be happy that repeated field lines are combined before contains checks.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Violation: Not Found. Key: "1", Field Name: "x-test-contains", Required Value: "gamma", Actual Value: "alpha, beta"', 'Validation should complain that the combined field value does not contain "gamma".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Success: Key: "1", Field Name: "x-test-prefix", Required Value: "start, e", Value: "start, end"', 'Validation should be happy that prefix checks use the combined field value.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Violation: Not Found. Key: "1", Field Name: "x-test-prefix", Required Value: "end", Actual Value: "start, end"', 'Validation should complain that the combined field value does not begin with "end".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Success: Key: "1", Field Name: "x-test-suffix", Required Value: "right", Value: "left, right"', 'Validation should be happy that suffix checks use the combined field value.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Violation: Not Found. Key: "1", Field Name: "x-test-suffix", Required Value: "left", Actual Value: "left, right"', 'Validation should complain that the combined field value does not end with "left".') -client.ReturnCode = 0 -server.ReturnCode = 1 +client.expect_return_codes(0) +server.expect_return_codes(1) # # Test 3: Verify field verification using the map specification syntax. # -r = Test.AddTestRun("Verify field verification works with the map specification syntax") -client = r.AddClientProcess("client3", "replay_files/map_specification.yaml") -server = r.AddServerProcess("server3", "replay_files/map_specification.yaml") -proxy = r.AddProxyProcess("proxy3", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify field verification works with the map specification syntax") +client = case.add_client("client3", "replay_files/map_specification.yaml") +server = case.add_server("server3", "replay_files/map_specification.yaml") +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Success: Key: "13", Field Name: "host", Required Value: "le.on", Value: "example.one"', 'Validation should be happy that "le.on" is in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Success: Key: "13", Field Name: "x-test-request", Required Value: "Req", Value: "RequestData"', 'Validation should be happy that "RequestData" began with "Req".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Success: Key: "13", Field Name: "x-test-present", Required Value: "there", Value: "It\'s there"', 'Validation should be happy that "It\'s there" ended with "there.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Contains Violation: Not Found. Key: "13", Field Name: "host", Required Value: "two", Actual Value: "example.one"', 'Validation should complain that "two" is not in "example.one".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Prefix Violation: Not Found. Key: "13", Field Name: "x-test-request", Required Value: "equest", Actual Value: "RequestData"', 'Validation should complain that "RequestData" did not begin with "equest".') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Suffix Violation: Not Found. Key: "13", Field Name: "x-test-present", Required Value: "er", Actual Value: "It\'s there"', 'Validation should complain that "It\'s there" did not end with "er".') -server.Streams.stdout += Testers.ContainsExpression( - 'Absence Success: Key: "13", Field Name: "x-test-absent"', - 'Validation should be happy that "X-Test-Absent" is not there.') +server.stdout.contains('Absence Success: Key: "13", Field Name: "x-test-absent"', + 'Validation should be happy that "X-Test-Absent" is not there.') -server.Streams.stdout += Testers.ContainsExpression( +server.stdout.contains( 'Presence Success: Key: "13", Field Name: "x-test-present", Value: "It\'s there"', 'Validation should be happy that "X-Test-Present" is there.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Success: Key: "13", Field Name: "content-type", Required Value: "html", Value: "text/html"', 'Validation should be happy that "html" is in "text/html".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Violation: Not Found. Key: "13", Field Name: "set-cookie", Required Value: "ABCDE", Actual Value: "ABCD"', 'Validation should complain that "ABCDE" is not in "ABCD".') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Prefix Violation: Absent. Key: "13", Field Name: "x-not-a-header", Required Value: "Whatever"', 'Validation should complain that "X-Not-A-Header" is missing.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Contains Violation: Absent. Key: "13", Field Name: "x-does-not-exist", Required Value: "NotHere"', 'Validation should complain that "X-Does-Not-Exist" is missing.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Suffix Violation: Absent. Key: "13", Field Name: "x-does-not-exist", Required Value: "NotHere"', 'Validation should complain that "X-Does-Not-Exist" is missing.') -client.ReturnCode = 1 -server.ReturnCode = 1 +client.expect_return_codes(1) +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http/gold/single_transaction_client.gold b/tests/uranium_tests/http/gold/single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/http/gold/single_transaction_client.gold rename to tests/uranium_tests/http/gold/single_transaction_client.gold diff --git a/tests/autests/gold_tests/http/gold/single_transaction_proxy.gold b/tests/uranium_tests/http/gold/single_transaction_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http/gold/single_transaction_proxy.gold rename to tests/uranium_tests/http/gold/single_transaction_proxy.gold diff --git a/tests/autests/gold_tests/http/gold/single_transaction_server.gold b/tests/uranium_tests/http/gold/single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/http/gold/single_transaction_server.gold rename to tests/uranium_tests/http/gold/single_transaction_server.gold diff --git a/tests/autests/gold_tests/http/replay_files/multiple_transactions/file1.json b/tests/uranium_tests/http/replay_files/multiple_transactions/file1.json similarity index 100% rename from tests/autests/gold_tests/http/replay_files/multiple_transactions/file1.json rename to tests/uranium_tests/http/replay_files/multiple_transactions/file1.json diff --git a/tests/autests/gold_tests/http/replay_files/multiple_transactions/file2.json b/tests/uranium_tests/http/replay_files/multiple_transactions/file2.json similarity index 100% rename from tests/autests/gold_tests/http/replay_files/multiple_transactions/file2.json rename to tests/uranium_tests/http/replay_files/multiple_transactions/file2.json diff --git a/tests/autests/gold_tests/http/replay_files/multiple_transactions/file3.yaml b/tests/uranium_tests/http/replay_files/multiple_transactions/file3.yaml similarity index 100% rename from tests/autests/gold_tests/http/replay_files/multiple_transactions/file3.yaml rename to tests/uranium_tests/http/replay_files/multiple_transactions/file3.yaml diff --git a/tests/autests/gold_tests/http/replay_files/single_transaction.yaml b/tests/uranium_tests/http/replay_files/single_transaction.yaml similarity index 100% rename from tests/autests/gold_tests/http/replay_files/single_transaction.yaml rename to tests/uranium_tests/http/replay_files/single_transaction.yaml diff --git a/tests/uranium_tests/http/test_http.py b/tests/uranium_tests/http/test_http.py new file mode 100644 index 000000000..8e263934f --- /dev/null +++ b/tests/uranium_tests/http/test_http.py @@ -0,0 +1,63 @@ +''' +Verify basic HTTP/1.x functionality. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct behavior of a single HTTP transaction. +# +case = suite.case("Verify HTTP/1 processing of a single HTTP transaction") + +# Add configure_https=False to verify ATS client and server work when the https +# optional arguments are not provided. +client = case.add_client("client1", "replay_files/single_transaction.yaml", configure_https=False) +server = case.add_server("server1", "replay_files/single_transaction.yaml", configure_https=False) +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/single_transaction_proxy.gold") +client.stdout.matches_gold("gold/single_transaction_client.gold") +server.stdout.matches_gold("gold/single_transaction_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 2: Verify correct behavior of multiple HTTP sessions. +# +case = suite.case("Verify HTTP/1 processing of multiple HTTP transactions") +client = case.add_client("client2", "replay_files/multiple_transactions") +server = case.add_server("server2", "replay_files/multiple_transactions") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("7 transactions in 5 sessions", "Verify that 7 transactions were parsed.") + +client.stdout.contains("Loading 3 replay files.", "Verify that 3 replay files were parsesd.") + +client.stdout.contains("204 No Content", "Verify the No Content reason string.") + +client.stdout.contains("POST /some/request HTTP/1.0", "Verify that we sent the HTTP/1.0 version.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains("Ready with 7 transactions.", "Verify that 7 transactions were parsed.") + +server.stdout.contains("Loading 3 replay files", "Verify that 3 replay files were parsed.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http1_client.gold b/tests/uranium_tests/http2/gold/http2_to_http1_client.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/http2_to_http1_client.gold rename to tests/uranium_tests/http2/gold/http2_to_http1_client.gold diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http1_proxy.gold b/tests/uranium_tests/http2/gold/http2_to_http1_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/http2_to_http1_proxy.gold rename to tests/uranium_tests/http2/gold/http2_to_http1_proxy.gold diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http1_server.gold b/tests/uranium_tests/http2/gold/http2_to_http1_server.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/http2_to_http1_server.gold rename to tests/uranium_tests/http2/gold/http2_to_http1_server.gold diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http2_client.gold b/tests/uranium_tests/http2/gold/http2_to_http2_client.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/http2_to_http2_client.gold rename to tests/uranium_tests/http2/gold/http2_to_http2_client.gold diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http2_proxy.gold b/tests/uranium_tests/http2/gold/http2_to_http2_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/http2_to_http2_proxy.gold rename to tests/uranium_tests/http2/gold/http2_to_http2_proxy.gold diff --git a/tests/autests/gold_tests/http2/gold/http2_to_http2_server.gold b/tests/uranium_tests/http2/gold/http2_to_http2_server.gold similarity index 98% rename from tests/autests/gold_tests/http2/gold/http2_to_http2_server.gold rename to tests/uranium_tests/http2/gold/http2_to_http2_server.gold index 26fe9b6f9..e27107724 100644 --- a/tests/autests/gold_tests/http2/gold/http2_to_http2_server.gold +++ b/tests/uranium_tests/http2/gold/http2_to_http2_server.gold @@ -1,5 +1,5 @@ [DEBUG]: Configured logging at level DEBUG - [DEBUG]: Writing TLS secrets to: /home/bneradt/shared/proxy-verifier/tests/autests/_sandbox/http2/server2/tls_secrets.txt + [DEBUG]: Writing TLS secrets to: ``/server2/tls_secrets.txt [DEBUG]: Finished TLSSession::init [DEBUG]: Finished H2Session::init [INFO]: Contrary to spec, a transaction is specified with both :authority and host header fields: line 44 diff --git a/tests/autests/gold_tests/http2/gold/set_alpn_proxy.gold b/tests/uranium_tests/http2/gold/set_alpn_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http2/gold/set_alpn_proxy.gold rename to tests/uranium_tests/http2/gold/set_alpn_proxy.gold diff --git a/tests/autests/gold_tests/http2/replay_files/http2_hop_by_hop.yaml b/tests/uranium_tests/http2/replay_files/http2_hop_by_hop.yaml similarity index 100% rename from tests/autests/gold_tests/http2/replay_files/http2_hop_by_hop.yaml rename to tests/uranium_tests/http2/replay_files/http2_hop_by_hop.yaml diff --git a/tests/autests/gold_tests/http2/replay_files/http2_to_http1.yaml b/tests/uranium_tests/http2/replay_files/http2_to_http1.yaml similarity index 100% rename from tests/autests/gold_tests/http2/replay_files/http2_to_http1.yaml rename to tests/uranium_tests/http2/replay_files/http2_to_http1.yaml diff --git a/tests/autests/gold_tests/http2/replay_files/http2_to_http2.yaml b/tests/uranium_tests/http2/replay_files/http2_to_http2.yaml similarity index 100% rename from tests/autests/gold_tests/http2/replay_files/http2_to_http2.yaml rename to tests/uranium_tests/http2/replay_files/http2_to_http2.yaml diff --git a/tests/autests/gold_tests/http2/replay_files/http2_to_http2_verification_failures.yaml b/tests/uranium_tests/http2/replay_files/http2_to_http2_verification_failures.yaml similarity index 100% rename from tests/autests/gold_tests/http2/replay_files/http2_to_http2_verification_failures.yaml rename to tests/uranium_tests/http2/replay_files/http2_to_http2_verification_failures.yaml diff --git a/tests/autests/gold_tests/http2/replay_files/set_alpn.yaml b/tests/uranium_tests/http2/replay_files/set_alpn.yaml similarity index 100% rename from tests/autests/gold_tests/http2/replay_files/set_alpn.yaml rename to tests/uranium_tests/http2/replay_files/set_alpn.yaml diff --git a/tests/uranium_tests/http2/test_http2.py b/tests/uranium_tests/http2/test_http2.py new file mode 100644 index 000000000..d8df70a8c --- /dev/null +++ b/tests/uranium_tests/http2/test_http2.py @@ -0,0 +1,158 @@ +''' +Verify basic HTTP/2 functionality. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify correct behavior of a single client-side HTTP/2 transaction. +# +case = suite.case("Verify HTTP/2 behavior on client-side only") +client = case.add_client("client1", "replay_files/http2_to_http1.yaml") +server = case.add_server("server1", "replay_files/http2_to_http1.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_1=True) + +proxy.stdout.matches_gold("gold/http2_to_http1_proxy.gold") +client.stdout.matches_gold("gold/http2_to_http1_client.gold") +server.stdout.matches_gold("gold/http2_to_http1_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 2: Verify field verification: all success. +# +case = suite.case("Verify HTTP/2 behavior on both the client and server sides") +client = case.add_client("client2", "replay_files/http2_to_http2.yaml") +server = case.add_server("server2", "replay_files/http2_to_http2.yaml") +proxy = case.add_proxy("proxy2", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +proxy.stdout.matches_gold("gold/http2_to_http2_proxy.gold") + +client.stdout.contains("Received an HTTP/2 response for key 1 with stream id 1:", + "The client should receive the first HTTP/2 response.") +client.stdout.contains( + "Received an HTTP/2 response for key 4 with stream id 7:", + "The client should receive the empty-body HTTP/2 response with Content-Length: 0.") +client.stdout.contains("Received HTTP/2 response trailers for key 5 with stream id 9:", + "The client should receive the HTTP/2 response trailers.") +client.stdout.contains("HTTP/2 replay metrics: requests-submitted=5", + "The HTTP/2 replay should complete and emit replay metrics.") +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains("Request with key 2 passed validation.", + "The server should validate the POST transaction on the HTTP/2 session.") +server.stdout.contains("Submitted the following HTTP/2 response headers for key 4 on stream 7:", + "The server should send the empty-body HTTP/2 response.") +server.stdout.contains("Request with key 5 passed validation.", + "The server should validate the trailer-bearing HTTP/2 transaction.") +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 3: Verify field verification: failures. +# +case = suite.case("Verify HTTP/2 field verification") +client = case.add_client("client3", "replay_files/http2_to_http2_verification_failures.yaml") +server = case.add_server("server3", "replay_files/http2_to_http2_verification_failures.yaml") +proxy = case.add_proxy("proxy3", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.expect_return_codes(1) +server.expect_return_codes(1) + +client.stdout.contains( + 'Contains Violation: Not Found. Key: "1", Field Name: "x-added-header", Required Value: "lmno", Actual Value: "abcdefg"', + "There should be a verification about a field that doesn't contain the expected content.") +client.stdout.contains('Presence Violation: Absent. Key: "1", Field Name: "x-deleted-header"', + "There should be a verification about a missing field.") + +server.stdout.contains( + 'Equals Violation: Different. Key: "1", Field Name: "x-added-header", Correct Value: "4", Actual Value: "3', + "There should be a verification error about an incorrect value.") +server.stdout.contains('Presence Violation: Absent. Key: "1", Field Name: "x-deleted-header', + "There should be a verification error about a missing field.") + +# +# Test 4: Verify the ability to control server protocol negotiation via ALPN. +# +case = suite.case("Verify HTTP/2 behavior on both the client and server sides") +client = case.add_client("client4", "replay_files/set_alpn.yaml") +server = case.add_server("server4", "replay_files/set_alpn.yaml") +proxy = case.add_proxy("proxy4", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +# The two sessions race with each other, so don't use a gold file here. +proxy.stdout.contains("Got SNI from client: b'test_sni_no_h2'", + "Verify that the SNI associated with no HTTP/2 support was sent.") +proxy.stdout.contains("Got SNI from client: b'test_sni_with_h2'", + "Verify that the SNI associated with HTTP/2 support was sent.") + +# The client sent and received HTTP/2 for both transactions because +# only the server side should down-negotiate HTTP/2, not the proxy. +client.stdout.contains("Received an HTTP/2 response for key 1 with stream id 1:", + "The client should receive an HTTP/2 response for both transactions.") +client.stdout.excludes("HTTP/1", "Neither of the transactions should be HTTP/1") +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains( + 'Using ALPN protocol string "h2,http/1.1,http1.1" for SNI "test_sni_with_h2"', + "Verify that the correctly parsed ALPN string for the SNI supporting HTTP/2.") +server.stdout.contains( + 'Using ALPN protocol string "http/1.1,http1.1" for SNI "test_sni_no_h2"', + "Verify that the correctly parsed ALPN string for the SNI not supporting HTTP/2.") +server.stdout.contains("Negotiated ALPN: h2", "Verify that HTTP/2 was negotiated for one session.") +server.stdout.contains("HTTP/2 is not negotiated. Assuming HTTP/1", + "Verify that HTTP/1 was negotiated for one session.") +server.stdout.contains("Sent the following HTTP/2 response headers for key 1 with stream id 1", + "Verify that an HTTP/2 response was sent to the client.") +server.stdout.contains("Sent the following HTTP/1 response headers for key 2", + "Verify that an HTTP/1 response was sent to the client.") +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 5: Verify connection-specific headers are sanitized for HTTP/2 replays. +# +case = suite.case("Verify HTTP/2 strips connection-specific headers") +client = case.add_client("client5", "replay_files/http2_hop_by_hop.yaml") +server = case.add_server("server5", "replay_files/http2_hop_by_hop.yaml") +proxy = case.add_proxy("proxy5", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains("Received an HTTP/2 response for key h2-hop-by-hop-1 with stream id 1:", + "The first concurrent HTTP/2 response should complete successfully.") +client.stdout.contains("Received an HTTP/2 response for key h2-hop-by-hop-2 with stream id 3:", + "The second concurrent HTTP/2 response should complete successfully.") +client.stdout.excludes("HTTP/2 final response drain made no forward progress", + "The client should not hit the HTTP/2 stuck-session timeout summary.") +client.stdout.excludes( + "Violation:", "The client should not report verification failures after sanitizing headers.") + +server.stdout.contains("Skipping HTTP/2 connection-specific field connection: keep-alive", + "The server should strip invalid connection-specific response headers.") +server.stdout.contains("Skipping HTTP/2 connection-specific field upgrade: h2c", + "The server should strip invalid upgrade headers from HTTP/2 responses.") +server.stdout.excludes( + "Received GOAWAY frame with last stream id 0, error code 1", + "The client should no longer send a protocol-error GOAWAY for these streams.") +server.stdout.excludes( + "Violation:", "The server should not report verification failures after sanitizing headers.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http2_abort/replay_files/client_goaway_after_headers.yaml b/tests/uranium_tests/http2_abort/replay_files/client_goaway_after_headers.yaml similarity index 94% rename from tests/autests/gold_tests/http2_abort/replay_files/client_goaway_after_headers.yaml rename to tests/uranium_tests/http2_abort/replay_files/client_goaway_after_headers.yaml index 8589f27a8..994eddb79 100644 --- a/tests/autests/gold_tests/http2_abort/replay_files/client_goaway_after_headers.yaml +++ b/tests/uranium_tests/http2_abort/replay_files/client_goaway_after_headers.yaml @@ -11,7 +11,7 @@ sessions: stack: http2 tls: sni: test_sni - # Let the proxy process GOAWAY before AuTest tears down background processes. + # Let the proxy process GOAWAY before Uranium stops background processes. keep-connection-open: 2s transactions: - client-request: diff --git a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml similarity index 92% rename from tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml rename to tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml index e96b2507e..cdfac4a9a 100644 --- a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml +++ b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_data.yaml @@ -11,7 +11,7 @@ sessions: stack: http2 tls: sni: test_sni - # Let the proxy process the reset before AuTest tears down background processes. + # Let the proxy process the reset before Uranium stops background processes. keep-connection-open: 2s transactions: - client-request: diff --git a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml similarity index 91% rename from tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml rename to tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml index 65cd9f257..843582190 100644 --- a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml +++ b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_after_headers.yaml @@ -11,7 +11,7 @@ sessions: stack: http2 tls: sni: test_sni - # Let the proxy process the reset before AuTest tears down background processes. + # Let the proxy process the reset before Uranium stops background processes. keep-connection-open: 2s transactions: - client-request: diff --git a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml similarity index 92% rename from tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml rename to tests/uranium_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml index 5c63fedfb..86efa571f 100644 --- a/tests/autests/gold_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml +++ b/tests/uranium_tests/http2_abort/replay_files/client_rst_stream_mixed_data.yaml @@ -11,7 +11,7 @@ sessions: stack: http2 tls: sni: test_sni - # Let the proxy process the reset before AuTest tears down background processes. + # Let the proxy process the reset before Uranium stops background processes. keep-connection-open: 2s transactions: - client-request: diff --git a/tests/autests/gold_tests/http2_abort/replay_files/server_goaway_after_headers.yaml b/tests/uranium_tests/http2_abort/replay_files/server_goaway_after_headers.yaml similarity index 100% rename from tests/autests/gold_tests/http2_abort/replay_files/server_goaway_after_headers.yaml rename to tests/uranium_tests/http2_abort/replay_files/server_goaway_after_headers.yaml diff --git a/tests/autests/gold_tests/http2_abort/replay_files/server_rst_stream_after_headers.yaml b/tests/uranium_tests/http2_abort/replay_files/server_rst_stream_after_headers.yaml similarity index 100% rename from tests/autests/gold_tests/http2_abort/replay_files/server_rst_stream_after_headers.yaml rename to tests/uranium_tests/http2_abort/replay_files/server_rst_stream_after_headers.yaml diff --git a/tests/uranium_tests/http2_abort/test_http2_abort.py b/tests/uranium_tests/http2_abort/test_http2_abort.py new file mode 100644 index 000000000..f94fcf84c --- /dev/null +++ b/tests/uranium_tests/http2_abort/test_http2_abort.py @@ -0,0 +1,169 @@ +''' +Abort HTTP/2 connection. +''' +# @file +# +# Copyright 2023-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Client sends RST_STREAM after DATA frame +# +case = suite.case('Client sends RST_STREAM after DATA frame') +client = case.add_client('client1', 'replay_files/client_rst_stream_after_data.yaml') +server = case.add_server('server1', 'replay_files/client_rst_stream_after_data.yaml') +proxy = case.add_proxy('proxy1', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', + 'Detect client abort flag.') + +client.stdout.contains('Submitted RST_STREAM frame for key 1 on stream 1.', + 'Submitted RST_STREAM frame.') + +server.stdout.contains('Received an HTTP/2 request for key 1 with stream id 1', + 'Server is functional.') + +server.stdout.excludes('RST_STREAM', 'Server is not affected.') + +proxy.stdout.contains('Received RST_STREAM frame with error code INTERNAL_ERROR', + 'Received RST_STREAM frame.') + +proxy.stdout.contains('Frame sequence from client: HEADERS, DATA, RST_STREAM', 'Frame sequence.') + +# +# Test 2: Client sends RST_STREAM after HEADERS frame +# +case = suite.case('Client sends RST_STREAM after HEADERS frame') +client = case.add_client('client2', 'replay_files/client_rst_stream_after_headers.yaml') +server = case.add_server('server2', 'replay_files/client_rst_stream_after_headers.yaml') +proxy = case.add_proxy('proxy2', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', + 'Detect client abort flag.') + +client.stdout.contains('Submitted RST_STREAM frame for key 1 on stream 1.', + 'Submitted RST_STREAM frame.') + +client.stdout.excludes('Timed out waiting for frame: HEADERS', 'Await HEADERS') + +server.stdout.contains('Received an HTTP/2 request for key 1 with stream id 1', + 'Server is functional.') + +server.stdout.excludes('RST_STREAM', 'Server is not affected.') + +proxy.stdout.contains('Received RST_STREAM frame with error code STREAM_CLOSED', + 'Received RST_STREAM frame.') + +proxy.stdout.contains('Frame sequence from client: HEADERS, RST_STREAM', 'Frame sequence.') + +# +# Test 3: Server sends RST_STREAM after HEADERS frame +# +case = suite.case('Server sends RST_STREAM after HEADERS frame') +client = case.add_client('client3', 'replay_files/server_rst_stream_after_headers.yaml') +server = case.add_server('server3', 'replay_files/server_rst_stream_after_headers.yaml') +proxy = case.add_proxy('proxy3', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains('Received RST_STREAM frame with stream id 1', 'RST_STREAM pass through.') + +server.stdout.contains( + 'Submitting RST_STREAM frame for key 1 after HEADERS frame with error code ENHANCE_YOUR_CALM.', + 'Detect client abort flag.') + +server.stdout.contains('Submitted RST_STREAM frame for key 1 on stream 1.', + 'Submitted RST_STREAM frame.') + +proxy.stdout.contains( + 'StreamReset stream_id:1, error_code:(11|ErrorCodes.ENHANCE_YOUR_CALM), remote_reset:True', + 'Received RST_STREAM frame.') + +# +# Test 4: Client sends GOAWAY after HEADERS frame +# +case = suite.case('Client sends GOAWAY after HEADERS frame') +client = case.add_client('client4', 'replay_files/client_goaway_after_headers.yaml') +server = case.add_server('server4', 'replay_files/client_goaway_after_headers.yaml') +proxy = case.add_proxy('proxy4', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Submitting GOAWAY frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', + 'Detect client abort flag.') + +client.stdout.contains('Submitted GOAWAY frame for key 1.', 'Submitted GOAWAY frame.') + +client.stdout.excludes('should_not_send', 'Client connection should terminate.') + +server.stdout.excludes('GOAWAY', 'Server is not affected.') + +server.stdout.excludes('should_not_send', 'Server connection should terminate.') + +proxy.stdout.contains('Received GOAWAY frame with error code STREAM_CLOSED', + 'Received GOAWAY frame.') + +proxy.stdout.contains('Frame sequence from client: HEADERS, GOAWAY', 'Frame sequence.') + +# +# Test 5: Server sends GOAWAY after HEADERS frame +# +case = suite.case('Server sends GOAWAY after HEADERS frame') +client = case.add_client('client5', 'replay_files/server_goaway_after_headers.yaml') +server = case.add_server('server5', 'replay_files/server_goaway_after_headers.yaml') +proxy = case.add_proxy('proxy5', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains('Received GOAWAY frame with last stream id 0', 'GOAWAY pass through.') + +client.stdout.excludes('should_not_send', 'Client connection should terminate.') + +server.stdout.contains( + 'Submitting GOAWAY frame for key 1 after HEADERS frame with error code STREAM_CLOSED.', + 'Detect server abort flag.') + +server.stdout.contains('Submitted GOAWAY frame for key 1.', 'Submitted GOAWAY frame.') + +server.stdout.excludes('should_not_send', 'Server connection should terminate.') + +proxy.stdout.contains( + 'ConnectionTerminated error_code:(5|ErrorCodes.STREAM_CLOSED), last_stream_id:0, additional_data:None', + 'Received GOAWAY frame.') + +# +# Test 6: Client sends RST_STREAM mixed within multiple DATA frames +# +case = suite.case('Client sends RST_STREAM mixed within multiple DATA frames') +client = case.add_client('client6', 'replay_files/client_rst_stream_mixed_data.yaml') +server = case.add_server('server6', 'replay_files/client_rst_stream_mixed_data.yaml') +proxy = case.add_proxy('proxy6', listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.contains( + 'Submitting RST_STREAM frame for key 1 after DATA frame with error code INTERNAL_ERROR.', + 'Detect client abort flag.') + +client.stdout.contains('Submitted RST_STREAM frame for key 1 on stream 1.', + 'Submitted RST_STREAM frame.') + +server.stdout.contains('Received an HTTP/2 request for key 1 with stream id 1', + 'Server is functional.') + +server.stdout.excludes('RST_STREAM', 'Server is not affected.') + +proxy.stdout.contains('Received RST_STREAM frame with error code INTERNAL_ERROR', + 'Received RST_STREAM frame.') + +proxy.stdout.contains('Frame sequence from client: HEADERS, DATA, RST_STREAM', 'Frame sequence.') + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.yaml b/tests/uranium_tests/http2_close_on_goaway/http2_close_on_goaway.yaml similarity index 100% rename from tests/autests/gold_tests/http2_close_on_goaway/http2_close_on_goaway.yaml rename to tests/uranium_tests/http2_close_on_goaway/http2_close_on_goaway.yaml diff --git a/tests/uranium_tests/http2_close_on_goaway/test_http2_close_on_goaway.py b/tests/uranium_tests/http2_close_on_goaway/test_http2_close_on_goaway.py new file mode 100644 index 000000000..b012eff5d --- /dev/null +++ b/tests/uranium_tests/http2_close_on_goaway/test_http2_close_on_goaway.py @@ -0,0 +1,36 @@ +''' +GOAWAY frame tests. +''' +# @file +# +# Copyright 2024-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("GOAWAY frame tests.") +client = case.add_client("client", "http2_close_on_goaway.yaml") +server = case.add_server("server", "http2_close_on_goaway.yaml") +proxy = case.add_proxy("proxy", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, close_on_goaway=True, use_http2_to_2=True) + +client.stdout.contains("uuid: 1", "uuid: 1") +client.stdout.contains("uuid: 3", "uuid: 3") +client.stdout.contains("uuid: 4", "uuid: 4") +client.stdout.contains("Failed to submit DATA frame for key 4 on stream 3: -510", + "uuid: 4 should fail") + +client.stdout.excludes("uuid: 2", "uuid: 2") + +server.stdout.contains("uuid: 1", "uuid: 1") +server.stdout.contains("uuid: 3", "uuid: 3") + +server.stdout.excludes("uuid: 2", "uuid: 2") +server.stdout.excludes("uuid: 4", "uuid: 4") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http2_frames/gold/client.gold b/tests/uranium_tests/http2_frames/gold/client.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/client.gold rename to tests/uranium_tests/http2_frames/gold/client.gold diff --git a/tests/autests/gold_tests/http2_frames/gold/multi_data_frame_client.gold b/tests/uranium_tests/http2_frames/gold/multi_data_frame_client.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/multi_data_frame_client.gold rename to tests/uranium_tests/http2_frames/gold/multi_data_frame_client.gold diff --git a/tests/autests/gold_tests/http2_frames/gold/multi_data_frame_proxy.gold b/tests/uranium_tests/http2_frames/gold/multi_data_frame_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/multi_data_frame_proxy.gold rename to tests/uranium_tests/http2_frames/gold/multi_data_frame_proxy.gold diff --git a/tests/autests/gold_tests/http2_frames/gold/multi_data_frame_server.gold b/tests/uranium_tests/http2_frames/gold/multi_data_frame_server.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/multi_data_frame_server.gold rename to tests/uranium_tests/http2_frames/gold/multi_data_frame_server.gold diff --git a/tests/autests/gold_tests/http2_frames/gold/proxy.gold b/tests/uranium_tests/http2_frames/gold/proxy.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/proxy.gold rename to tests/uranium_tests/http2_frames/gold/proxy.gold diff --git a/tests/autests/gold_tests/http2_frames/gold/server.gold b/tests/uranium_tests/http2_frames/gold/server.gold similarity index 100% rename from tests/autests/gold_tests/http2_frames/gold/server.gold rename to tests/uranium_tests/http2_frames/gold/server.gold diff --git a/tests/autests/gold_tests/http2_frames/http2_frames.yaml b/tests/uranium_tests/http2_frames/http2_frames.yaml similarity index 100% rename from tests/autests/gold_tests/http2_frames/http2_frames.yaml rename to tests/uranium_tests/http2_frames/http2_frames.yaml diff --git a/tests/autests/gold_tests/http2_frames/http2_multi_data_frames.yaml b/tests/uranium_tests/http2_frames/http2_multi_data_frames.yaml similarity index 100% rename from tests/autests/gold_tests/http2_frames/http2_multi_data_frames.yaml rename to tests/uranium_tests/http2_frames/http2_multi_data_frames.yaml diff --git a/tests/uranium_tests/http2_frames/test_http2_frames.py b/tests/uranium_tests/http2_frames/test_http2_frames.py new file mode 100644 index 000000000..2a5fb868e --- /dev/null +++ b/tests/uranium_tests/http2_frames/test_http2_frames.py @@ -0,0 +1,43 @@ +''' +Specify h2 frame sequence. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Abort after DATA frame +# +case = suite.case("Specify h2 frame sequence") +client = case.add_client("client1", "http2_frames.yaml") +server = case.add_server("server1", "http2_frames.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +client.stdout.matches_gold("gold/client.gold") +server.stdout.matches_gold("gold/server.gold") +proxy.stdout.matches_gold("gold/proxy.gold") + +# +# Test 2: Verify that the timing data indicates that the delays took place. +# +case = suite.case("Verify the client-side delay replay took an expected amount of time to run.") +verifier_script = 'verify_duration.py' +client_output = client.stdout.path +expected_min_delay_ms = "1500" +process = case.add_process( + "verify-duration", + ["python3", verifier_script, client_output, expected_min_delay_ms], + copies=[verifier_script], +) +process.stdout.contains('Good', 'The verifier script should report success.') + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/http2_frames/test_http2_multi_data_frames.py b/tests/uranium_tests/http2_frames/test_http2_multi_data_frames.py new file mode 100644 index 000000000..2cc499327 --- /dev/null +++ b/tests/uranium_tests/http2_frames/test_http2_multi_data_frames.py @@ -0,0 +1,63 @@ +''' +Send multiple data frames. +''' +# @file +# +# Copyright 2023-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Send multiple DATA frames +# +case = suite.case("Send multiple data frames") +client = case.add_client("client1", "http2_multi_data_frames.yaml") +server = case.add_server("server1", "http2_multi_data_frames.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_2=True) + +proxy.stdout.matches_gold("gold/multi_data_frame_proxy.gold") + +client.stdout.contains("Submitted DATA frame for key 1 on stream 1.", + "The client should submit the replayed HTTP/2 DATA frames.") +client.stdout.contains( + 'Equals Success: Key: "1", Content Data: "body", Value: "server_data_1server_data_2server_data_3"', + "The client should verify the combined HTTP/2 response body.") +client.stdout.contains( + r"HTTP/2 replay metrics: requests-submitted=1, max-in-flight-streams=1, " + r"send-phase-bytes-drained=0, final-drain-duration=[0-9]+ms\.", + "The client should complete the HTTP/2 replay and emit replay metrics.") +client.stdout.excludes( + "Violation:", "The client should not report verification failures for the multi-frame replay.") + +server.stdout.contains("Received an HTTP/2 body of 39 bytes for key 1 with stream id 1:", + "The server should receive the combined HTTP/2 request body.") +server.stdout.contains( + 'Equals Success: Key: "1", Content Data: "body", Value: "client_data_1client_data_2client_data_3"', + "The server should verify the combined replayed request body.") +server.stdout.contains("Sent an HTTP/2 body of 13 bytes for key 1 of stream id 1:", + "The server should send the replayed HTTP/2 DATA frames.") +server.stdout.excludes( + "Violation:", "The server should not report verification failures for the multi-frame replay.") + +# +# Test 2: Verify that the timing data indicates that the delays took place on the client side. +# +case = suite.case("Verify the client-side delay replay took an expected amount of time to run.") +verifier_script = 'verify_duration.py' +client_output = client.stdout.path +expected_min_delay_ms = "5000" +process = case.add_process( + "verify-duration", + ["python3", verifier_script, client_output, expected_min_delay_ms], + copies=[verifier_script], +) +process.stdout.contains('Good', 'The verifier script should report success.') + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/http2_frames/verify_duration.py b/tests/uranium_tests/http2_frames/verify_duration.py similarity index 100% rename from tests/autests/gold_tests/http2_frames/verify_duration.py rename to tests/uranium_tests/http2_frames/verify_duration.py diff --git a/tests/autests/gold_tests/http3/filter_http3_output.py b/tests/uranium_tests/http3/filter_http3_output.py similarity index 100% rename from tests/autests/gold_tests/http3/filter_http3_output.py rename to tests/uranium_tests/http3/filter_http3_output.py diff --git a/tests/autests/gold_tests/http3/gold/http3_to_http1_client_server.gold b/tests/uranium_tests/http3/gold/http3_to_http1_client_server.gold similarity index 100% rename from tests/autests/gold_tests/http3/gold/http3_to_http1_client_server.gold rename to tests/uranium_tests/http3/gold/http3_to_http1_client_server.gold diff --git a/tests/autests/gold_tests/http3/gold/http3_to_http1_proxy.gold b/tests/uranium_tests/http3/gold/http3_to_http1_proxy.gold similarity index 100% rename from tests/autests/gold_tests/http3/gold/http3_to_http1_proxy.gold rename to tests/uranium_tests/http3/gold/http3_to_http1_proxy.gold diff --git a/tests/autests/gold_tests/http3/replay_files/http3_to_http1.yaml b/tests/uranium_tests/http3/replay_files/http3_to_http1.yaml similarity index 100% rename from tests/autests/gold_tests/http3/replay_files/http3_to_http1.yaml rename to tests/uranium_tests/http3/replay_files/http3_to_http1.yaml diff --git a/tests/autests/gold_tests/http3/replay_files/http3_to_http1_failures.yaml b/tests/uranium_tests/http3/replay_files/http3_to_http1_failures.yaml similarity index 100% rename from tests/autests/gold_tests/http3/replay_files/http3_to_http1_failures.yaml rename to tests/uranium_tests/http3/replay_files/http3_to_http1_failures.yaml diff --git a/tests/uranium_tests/http3/test_http3.py b/tests/uranium_tests/http3/test_http3.py new file mode 100644 index 000000000..63c94bd41 --- /dev/null +++ b/tests/uranium_tests/http3/test_http3.py @@ -0,0 +1,95 @@ +''' +Verify basic HTTP/3 functionality. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +r_http3_args = "--poll-timeout 10000" + +# +# Test 1: Verify correct behavior of a various HTTP/3 transactions. +# +case = suite.case("Verify HTTP/3") +client = case.add_client("client1", "replay_files/http3_to_http1.yaml", other_args=r_http3_args, + verbose=False) +server = case.add_server("server1", "replay_files/http3_to_http1.yaml", other_args=r_http3_args, + verbose=False) +proxy = case.add_proxy("proxy1", listen_port=client.http3_port, server_port=server.http_port, + use_ssl=True, use_http3_to_1=True) + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Filter stable verification lines before comparing against gold. Raw HTTP/3 +# output includes asynchronous debug ordering and large generated bodies that +# are both expensive and noisy to compare directly. +# +case = suite.case("Verify filtered HTTP/3 output") +client_output = client.stdout.path +server_output = server.stdout.path +process = case.add_process( + "filter-client-server", + ["python3", "filter_http3_output.py", client_output, server_output], + copies=["filter_http3_output.py"], +) +process.stdout.matches_gold("gold/http3_to_http1_client_server.gold") + +# +# Keep the proxy coverage focused on request and response headers. The proxy +# output includes generated body bytes, so filtering avoids comparing the large +# 300 KB payloads while still catching missing or incorrect headers. +# +case = suite.case("Verify filtered HTTP/3 proxy output") +proxy_output = proxy.stdout.path +process = case.add_process( + "filter-proxy", + ["python3", "filter_http3_output.py", "--proxy-output", proxy_output], + copies=["filter_http3_output.py"], +) +process.stdout.matches_gold("gold/http3_to_http1_proxy.gold") + +# +# Test 2: Verify correct verification failure behaviors. +# +case = suite.case("Verify HTTP/3 with verification failures") +client = case.add_client("client2", "replay_files/http3_to_http1_failures.yaml", + other_args=r_http3_args) +server = case.add_server("server2", "replay_files/http3_to_http1_failures.yaml", + other_args=r_http3_args) +proxy = case.add_proxy("proxy2", listen_port=client.http3_port, server_port=server.http_port, + use_ssl=True, use_http3_to_1=True) + +client.expect_return_codes(1) + +client.stdout.contains( + 'Equals Violation: Different. .* Field Name: "x-equal-header", Correct Value: "other_content", Actual Value: "some_content"', + "There should be an equal violation for x-equal-header.") + +client.stdout.contains('Equals Success: .* Field Name: "x-response-header", Value: "response"', + "Verification should be happy with the X-Response-Header.") + +client.stdout.contains( + 'Absence Violation: Present. .* Field Name: "x-added-header", Value: "1"', + "There should be an absence violation for X-Added-Header because it will be present.") + +client.stdout.contains( + 'Presence Violation: Absent. .* Field Name: "x-deleted-header"', + "There should be an presence violation for X-Deleted-Header because it will be absent.") + +client.stdout.contains('HTTP/3 Status Violation: expected 502 got 200', + "There should a status violation for an unexpected 502 response.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/https/gold/single_transaction_client.gold b/tests/uranium_tests/https/gold/single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/https/gold/single_transaction_client.gold rename to tests/uranium_tests/https/gold/single_transaction_client.gold diff --git a/tests/autests/gold_tests/https/gold/single_transaction_proxy.gold b/tests/uranium_tests/https/gold/single_transaction_proxy.gold similarity index 100% rename from tests/autests/gold_tests/https/gold/single_transaction_proxy.gold rename to tests/uranium_tests/https/gold/single_transaction_proxy.gold diff --git a/tests/autests/gold_tests/https/gold/single_transaction_server.gold b/tests/uranium_tests/https/gold/single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/https/gold/single_transaction_server.gold rename to tests/uranium_tests/https/gold/single_transaction_server.gold diff --git a/tests/autests/gold_tests/https/replay_files/mtls.yaml b/tests/uranium_tests/https/replay_files/mtls.yaml similarity index 100% rename from tests/autests/gold_tests/https/replay_files/mtls.yaml rename to tests/uranium_tests/https/replay_files/mtls.yaml diff --git a/tests/autests/gold_tests/https/replay_files/single_transaction.yaml b/tests/uranium_tests/https/replay_files/single_transaction.yaml similarity index 100% rename from tests/autests/gold_tests/https/replay_files/single_transaction.yaml rename to tests/uranium_tests/https/replay_files/single_transaction.yaml diff --git a/tests/uranium_tests/https/test_https.py b/tests/uranium_tests/https/test_https.py new file mode 100644 index 000000000..4abef3c40 --- /dev/null +++ b/tests/uranium_tests/https/test_https.py @@ -0,0 +1,35 @@ +''' +Verify basic HTTPS functionality. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("Verify processing of a simple HTTPS transaction") + +# Add configure_http=False to verify ATS client and server work when the http +# optional arguments are not provided. +client = case.add_client("client1", "replay_files/single_transaction.yaml", configure_http=False) +server = case.add_server("server1", "replay_files/single_transaction.yaml", configure_http=False) +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True) + +proxy.stdout.matches_gold("gold/single_transaction_proxy.gold") +client.stdout.matches_gold("gold/single_transaction_client.gold") +server.stdout.matches_gold("gold/single_transaction_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/https/test_mtls.py b/tests/uranium_tests/https/test_mtls.py new file mode 100644 index 000000000..21e21f0c6 --- /dev/null +++ b/tests/uranium_tests/https/test_mtls.py @@ -0,0 +1,34 @@ +''' +Verify correct TLS client and server verification behavior. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify that both the client and server can verify the proxy +# if specified to do so in the "tls" node. +# +case = suite.case("Verify parsing of a YAML-specified replay file") +client = case.add_client("client1", "replay_files/mtls.yaml") +server = case.add_server("server1", "replay_files/mtls.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True) + +client.stdout.contains(r"Proxy TLS verification result: 0 \(X509_V_OK\)", + "Verify that the client verified the proxy's cert.") + +server.stdout.contains("Sending a certificate request to client with SNI: bob", + "Verify that the server requested a cert from the proxy.") +server.stdout.contains("Client TLS verification result for client with SNI bob: passed", + "Verify that the proxy's cert was verified.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/ipv6/gold/single_transaction_client.gold b/tests/uranium_tests/ipv6/gold/single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/ipv6/gold/single_transaction_client.gold rename to tests/uranium_tests/ipv6/gold/single_transaction_client.gold diff --git a/tests/autests/gold_tests/ipv6/gold/single_transaction_server.gold b/tests/uranium_tests/ipv6/gold/single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/ipv6/gold/single_transaction_server.gold rename to tests/uranium_tests/ipv6/gold/single_transaction_server.gold diff --git a/tests/autests/gold_tests/ipv6/replay_files/single_transaction.yaml b/tests/uranium_tests/ipv6/replay_files/single_transaction.yaml similarity index 100% rename from tests/autests/gold_tests/ipv6/replay_files/single_transaction.yaml rename to tests/uranium_tests/ipv6/replay_files/single_transaction.yaml diff --git a/tests/uranium_tests/ipv6/test_ipv6.py b/tests/uranium_tests/ipv6/test_ipv6.py new file mode 100644 index 000000000..c38fd03bd --- /dev/null +++ b/tests/uranium_tests/ipv6/test_ipv6.py @@ -0,0 +1,35 @@ +''' +Verify basic IPv6 support. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify transactions can be exchanged over IPv6. +# +case = suite.case("Verify the correct handling of an HTTP/1, IPv6 transaction") +server = case.add_server("server1", "replay_files/single_transaction.yaml", use_ipv6=True) +client = case.add_client("client1", "replay_files/single_transaction.yaml", use_ipv6=True, + http_ports=[server.http_port], other_args="--no-proxy") + +# Note that this test involves no proxy and, instead, the client talks directly +# to the server. + +client.stdout.matches_gold("gold/single_transaction_client.gold") +server.stdout.matches_gold("gold/single_transaction_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/method_verification/replay_files/http1_method_verification.yaml b/tests/uranium_tests/method_verification/replay_files/http1_method_verification.yaml similarity index 100% rename from tests/autests/gold_tests/method_verification/replay_files/http1_method_verification.yaml rename to tests/uranium_tests/method_verification/replay_files/http1_method_verification.yaml diff --git a/tests/autests/gold_tests/method_verification/replay_files/http2_method_verification.yaml b/tests/uranium_tests/method_verification/replay_files/http2_method_verification.yaml similarity index 100% rename from tests/autests/gold_tests/method_verification/replay_files/http2_method_verification.yaml rename to tests/uranium_tests/method_verification/replay_files/http2_method_verification.yaml diff --git a/tests/autests/gold_tests/method_verification/replay_files/http3_method_verification.yaml b/tests/uranium_tests/method_verification/replay_files/http3_method_verification.yaml similarity index 100% rename from tests/autests/gold_tests/method_verification/replay_files/http3_method_verification.yaml rename to tests/uranium_tests/method_verification/replay_files/http3_method_verification.yaml diff --git a/tests/uranium_tests/method_verification/test_method_verification.py b/tests/uranium_tests/method_verification/test_method_verification.py new file mode 100644 index 000000000..b1bba12ef --- /dev/null +++ b/tests/uranium_tests/method_verification/test_method_verification.py @@ -0,0 +1,90 @@ +''' +Verify request method verification behavior across protocol stacks. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +HTTP3_ARGS = "--poll-timeout 10000" + +# +# Test 1: Verify HTTP/1 method verification uses the top-level proxy-request +# method node. +# +case = suite.case("Verify HTTP/1 request method verification") +client = case.add_client("client1", "replay_files/http1_method_verification.yaml", + configure_https=False) +server = case.add_server("server1", "replay_files/http1_method_verification.yaml", + configure_https=False) +case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +server.stdout.contains( + 'HTTP/1 Method Success: Key: "1", Method: "GET"', + "The HTTP/1 request method should verify successfully for the matching transaction.") +server.stdout.excludes( + 'HTTP/1 Method Violation: .*Key: "1"', + "The matching HTTP/1 transaction should not report a method verification violation.") +server.stdout.contains( + 'HTTP/1 Method Violation: Different. Key: "2", Expected Method: "POST", Received Method: "GET"', + "The HTTP/1 request method should fail when the proxy sends the wrong method.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +# +# Test 2: Verify HTTP/2 request method verification continues to work via the +# :method pseudo header field. +# +case = suite.case("Verify HTTP/2 request method verification") +client = case.add_client("client2", "replay_files/http2_method_verification.yaml") +server = case.add_server("server2", "replay_files/http2_method_verification.yaml") +case.add_proxy("proxy2", listen_port=client.https_port, server_port=server.https_port, use_ssl=True, + use_http2_to_2=True) + +server.stdout.contains( + 'Equals Success: Key: "1", Field Name: ":method", Value: "GET"', + "The HTTP/2 :method pseudo header should verify successfully for the matching transaction.") +server.stdout.excludes( + 'Equals Violation: .*Key: "1".*Field Name: ":method"', + "The matching HTTP/2 transaction should not report a :method verification violation.") +server.stdout.contains( + 'Equals Violation: Different. Key: "2", Field Name: ":method", Correct Value: "POST", Actual Value: "GET"', + "The HTTP/2 :method pseudo header should fail when the proxy sends the wrong method.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +# +# Test 3: Verify an HTTP/3 client path can exercise the HTTP/1 request method +# verification on the downstream side. +# +case = suite.case("Verify HTTP/3 request method verification") +client = case.add_client("client3", "replay_files/http3_method_verification.yaml", + other_args=HTTP3_ARGS) +server = case.add_server("server3", "replay_files/http3_method_verification.yaml", + other_args=HTTP3_ARGS) +case.add_proxy("proxy3", listen_port=client.http3_port, server_port=server.http_port, use_ssl=True, + use_http3_to_1=True) + +server.stdout.contains( + 'HTTP/1 Method Success: Key: "1", Method: "GET"', + "The HTTP/3-originated request should pass downstream HTTP/1 method verification.") +server.stdout.excludes( + 'HTTP/1 Method Violation: .*Key: "1"', + "The matching HTTP/3 transaction should not report a downstream method violation.") +server.stdout.contains( + 'HTTP/1 Method Violation: Different. Key: "2", Expected Method: "POST", Received Method: "GET"', + "The HTTP/3-originated request should fail downstream method verification on mismatch.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/no_proxy/gold/h2_client.gold b/tests/uranium_tests/no_proxy/gold/h2_client.gold similarity index 100% rename from tests/autests/gold_tests/no_proxy/gold/h2_client.gold rename to tests/uranium_tests/no_proxy/gold/h2_client.gold diff --git a/tests/autests/gold_tests/no_proxy/gold/h2_server.gold b/tests/uranium_tests/no_proxy/gold/h2_server.gold similarity index 100% rename from tests/autests/gold_tests/no_proxy/gold/h2_server.gold rename to tests/uranium_tests/no_proxy/gold/h2_server.gold diff --git a/tests/autests/gold_tests/no_proxy/replay/h2.yaml b/tests/uranium_tests/no_proxy/replay/h2.yaml similarity index 100% rename from tests/autests/gold_tests/no_proxy/replay/h2.yaml rename to tests/uranium_tests/no_proxy/replay/h2.yaml diff --git a/tests/autests/gold_tests/no_proxy/replay/single_transaction.json b/tests/uranium_tests/no_proxy/replay/single_transaction.json similarity index 100% rename from tests/autests/gold_tests/no_proxy/replay/single_transaction.json rename to tests/uranium_tests/no_proxy/replay/single_transaction.json diff --git a/tests/uranium_tests/no_proxy/test_no_proxy.py b/tests/uranium_tests/no_proxy/test_no_proxy.py new file mode 100644 index 000000000..434838ec2 --- /dev/null +++ b/tests/uranium_tests/no_proxy/test_no_proxy.py @@ -0,0 +1,73 @@ +''' +Verify basic --no-proxy functionality. +''' +# @file +# +# Copyright 2022-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("Verify no-proxy mode works for a simple HTTP transaction") +server = case.add_server("server", "replay/single_transaction.json") +client = case.add_client("client", "replay/single_transaction.json", http_ports=[server.http_port], + https_ports=[server.https_port], other_args="--no-proxy") + +client.stdout.contains('Received an HTTP/1 200 response for .*', + "Verify that the response came back from replay-server") + +client.stdout.contains('x-testheader: from_server_response', + "Verify that the server response headers were used by the replay-server.") + +client.stdout.excludes( + 'from_proxy_response', + "Verify that the proxy response headers were not used by the replay-server.") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.contains("POST /proxy.do HTTP/1.1", + "Verify that the proxy request path was used by the replay-client.") + +server.stdout.contains('client-ip: 10.10.10.1', + "Verify that the proxy request headers were used by the replay-client.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +case = suite.case("Verify no-proxy mode works for a simple HTTP/2 transaction") +server = case.add_server("server-h2", "replay/h2.yaml") +client = case.add_client("client-h2", "replay/h2.yaml", http_ports=[server.http_port], + https_ports=[server.https_port], other_args="--no-proxy") + +client.stdout.contains( + "Received an HTTP/2 response for key 1 with stream id 1:", + "The client should receive the zero-length HTTP/2 response from the replay server.") +client.stdout.contains( + "Received an HTTP/2 response for key 3 with stream id 1:", + "The client should receive the POST replay response on the second HTTP/2 session.") +client.stdout.contains( + r"HTTP/2 replay metrics: requests-submitted=2, max-in-flight-streams=2, " + r"send-phase-bytes-drained=[0-9]+, final-drain-duration=[0-9]+ms\.", + "The client should complete the HTTP/2 no-proxy replay and emit replay metrics.") +client.stdout.excludes( + "Violation:", "The client should not report verification failures in HTTP/2 no-proxy mode.") +client.stdout.excludes( + "HTTP/2 final response drain made no forward progress", + "The client should not hit the HTTP/2 stuck-session summary in HTTP/2 no-proxy mode.") + +server.stdout.contains("Received an HTTP/2 request for key 1 with stream id 1:", + "The server should receive the first no-proxy HTTP/2 request.") +server.stdout.contains(":path: /some/path4", + "The server should receive the final no-proxy HTTP/2 request path.") +server.stdout.contains("Request with key 4 passed validation.", + "The server should validate the final HTTP/2 no-proxy request.") +server.stdout.excludes( + "Violation:", "The server should not report verification failures in HTTP/2 no-proxy mode.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/on_connect/replay_files/on_connect.yaml b/tests/uranium_tests/on_connect/replay_files/on_connect.yaml similarity index 100% rename from tests/autests/gold_tests/on_connect/replay_files/on_connect.yaml rename to tests/uranium_tests/on_connect/replay_files/on_connect.yaml diff --git a/tests/autests/gold_tests/on_connect/replay_files/on_connect_http2.yaml b/tests/uranium_tests/on_connect/replay_files/on_connect_http2.yaml similarity index 100% rename from tests/autests/gold_tests/on_connect/replay_files/on_connect_http2.yaml rename to tests/uranium_tests/on_connect/replay_files/on_connect_http2.yaml diff --git a/tests/uranium_tests/on_connect/test_on_connect.py b/tests/uranium_tests/on_connect/test_on_connect.py new file mode 100644 index 000000000..3f37b574a --- /dev/null +++ b/tests/uranium_tests/on_connect/test_on_connect.py @@ -0,0 +1,113 @@ +''' +Verify server-response on_connect behavior. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("Verify on_connect accept, refuse, and reset behavior") +client = case.add_client("client", "replay_files/on_connect.yaml", configure_https=False) +server = case.add_server("server", "replay_files/on_connect.yaml", configure_https=False) +proxy = case.add_proxy("proxy", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("3 transactions in 3 sessions", + "The client should have parsed all three transactions.") + +client.stdout.contains("Received an HTTP/1 200 response for key 1", + "The accept case should return a normal response.") + +client.stdout.contains("Received an HTTP/1 502 response for key 2", + "The refuse case should surface as a proxy 502.") + +client.stdout.contains("Received an HTTP/1 502 response for key 3", + "The reset case should surface as a proxy 502.") + +client.stdout.excludes("Violation:", "There should be no verification errors.") + +proxy.stdout.contains( + "Upstream connection for key 2 closed with TCP FIN before any response bytes were received.", + "The proxy should observe an orderly upstream close for refuse.") + +proxy.stdout.contains( + "Upstream connection for key 3 closed with TCP RST before any response bytes were received.", + "The proxy should observe an abortive upstream close for reset.") + +server.stdout.contains('Applying "refuse" on_connect action for key 2.', + "The server should apply the refuse action.") + +server.stdout.contains('Applying "reset" on_connect action for key 3.', + "The server should apply the reset action.") + +server.stdout.contains("Sent the following HTTP/1 response headers for key 1", + "The accept case should still write a normal response.") + +server.stdout.excludes("Sent the following HTTP/1 response headers for key 2", + "The refuse case should not write response headers.") + +server.stdout.excludes("Sent the following HTTP/1 response headers for key 3", + "The reset case should not write response headers.") + +server.stdout.excludes("Violation:", "There should be no verification errors.") + +case = suite.case("Verify on_connect accept, refuse, and reset behavior over HTTP/2") +client = case.add_client("client_h2", "replay_files/on_connect_http2.yaml") +server = case.add_server("server_h2", "replay_files/on_connect_http2.yaml") +proxy = case.add_proxy("proxy_h2", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True, use_http2_to_1=True) + +client.stdout.contains("3 transactions in 3 sessions", + "The HTTP/2 client should parse all three transactions.") + +client.stdout.contains("Received an HTTP/2 response for key 1 with stream id 1:", + "The accept case should return a normal HTTP/2 response.") + +client.stdout.contains("Received an HTTP/2 response for key 2 with stream id 1:", + "The refuse case should surface as an HTTP/2 502 response.") + +client.stdout.contains("Received an HTTP/2 response for key 3 with stream id 1:", + "The reset case should surface as an HTTP/2 502 response.") + +client.stdout.contains(":status: 502", + "The HTTP/2 client should observe 502 responses for failure cases.") + +client.stdout.excludes("Violation:", "There should be no verification errors.") + +proxy.stdout.contains( + "Upstream connection for key 2 closed with TCP FIN before any response bytes were received.", + "The HTTP/2 proxy should observe an orderly upstream close for refuse.") + +# On Rocky 8, the Python HTTP/2 test proxy uses the system Python/OpenSSL 1.1.1 +# TLS stack. On that HTTPS path, our Python HTTP/2 test client may surface a +# server-side RST as a generic disconnect and report it as a FIN, so this +# assertion accepts either FIN or RST even though the verifier server still +# performs an abortive close which should result in a RST. +proxy.stdout.contains( + "Upstream connection for key 3 closed with TCP (FIN|RST) before any response bytes were received.", + "The HTTP/2 proxy should observe the upstream close for reset.") + +server.stdout.contains('Applying "refuse" on_connect action for key 2.', + "The server should apply the refuse action.") + +server.stdout.contains('Applying "reset" on_connect action for key 3.', + "The server should apply the reset action.") + +server.stdout.contains("Sent the following HTTP/1 response headers for key 1", + "The accept case should still write a normal upstream HTTP/1 response.") + +server.stdout.excludes("Sent the following HTTP/1 response headers for key 2", + "The refuse case should not write response headers.") + +server.stdout.excludes("Sent the following HTTP/1 response headers for key 3", + "The reset case should not write response headers.") + +server.stdout.excludes("Violation:", "There should be no verification errors.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/protocol_syntax/gold/single_transaction_client.gold b/tests/uranium_tests/protocol_syntax/gold/single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/protocol_syntax/gold/single_transaction_client.gold rename to tests/uranium_tests/protocol_syntax/gold/single_transaction_client.gold diff --git a/tests/autests/gold_tests/protocol_syntax/gold/single_transaction_proxy.gold b/tests/uranium_tests/protocol_syntax/gold/single_transaction_proxy.gold similarity index 100% rename from tests/autests/gold_tests/protocol_syntax/gold/single_transaction_proxy.gold rename to tests/uranium_tests/protocol_syntax/gold/single_transaction_proxy.gold diff --git a/tests/autests/gold_tests/protocol_syntax/gold/single_transaction_server.gold b/tests/uranium_tests/protocol_syntax/gold/single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/protocol_syntax/gold/single_transaction_server.gold rename to tests/uranium_tests/protocol_syntax/gold/single_transaction_server.gold diff --git a/tests/autests/gold_tests/protocol_syntax/replay_files/single_transaction.yaml b/tests/uranium_tests/protocol_syntax/replay_files/single_transaction.yaml similarity index 100% rename from tests/autests/gold_tests/protocol_syntax/replay_files/single_transaction.yaml rename to tests/uranium_tests/protocol_syntax/replay_files/single_transaction.yaml diff --git a/tests/uranium_tests/protocol_syntax/test_protocol_syntax.py b/tests/uranium_tests/protocol_syntax/test_protocol_syntax.py new file mode 100644 index 000000000..d26d3dc64 --- /dev/null +++ b/tests/uranium_tests/protocol_syntax/test_protocol_syntax.py @@ -0,0 +1,33 @@ +''' +Verify protocol syntax compatibility. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +case = suite.case("Verify the verbose protocol sequence remains supported") + +client = case.add_client("client1", "replay_files/single_transaction.yaml", configure_http=False) +server = case.add_server("server1", "replay_files/single_transaction.yaml", configure_http=False) +proxy = case.add_proxy("proxy1", listen_port=client.https_port, server_port=server.https_port, + use_ssl=True) + +proxy.stdout.matches_gold("gold/single_transaction_proxy.gold") +client.stdout.matches_gold("gold/single_transaction_client.gold") +server.stdout.matches_gold("gold/single_transaction_server.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_client.gold b/tests/uranium_tests/proxy-protocol/gold/http_single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_client.gold rename to tests/uranium_tests/proxy-protocol/gold/http_single_transaction_client.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_proxy.gold b/tests/uranium_tests/proxy-protocol/gold/http_single_transaction_proxy.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_proxy.gold rename to tests/uranium_tests/proxy-protocol/gold/http_single_transaction_proxy.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_server.gold b/tests/uranium_tests/proxy-protocol/gold/http_single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/http_single_transaction_server.gold rename to tests/uranium_tests/proxy-protocol/gold/http_single_transaction_server.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_client.gold b/tests/uranium_tests/proxy-protocol/gold/https_single_transaction_client.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_client.gold rename to tests/uranium_tests/proxy-protocol/gold/https_single_transaction_client.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_proxy.gold b/tests/uranium_tests/proxy-protocol/gold/https_single_transaction_proxy.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_proxy.gold rename to tests/uranium_tests/proxy-protocol/gold/https_single_transaction_proxy.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_server.gold b/tests/uranium_tests/proxy-protocol/gold/https_single_transaction_server.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/https_single_transaction_server.gold rename to tests/uranium_tests/proxy-protocol/gold/https_single_transaction_server.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/ipv6_client.gold b/tests/uranium_tests/proxy-protocol/gold/ipv6_client.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/ipv6_client.gold rename to tests/uranium_tests/proxy-protocol/gold/ipv6_client.gold diff --git a/tests/autests/gold_tests/proxy-protocol/gold/ipv6_server.gold b/tests/uranium_tests/proxy-protocol/gold/ipv6_server.gold similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/gold/ipv6_server.gold rename to tests/uranium_tests/proxy-protocol/gold/ipv6_server.gold diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/http_pp_v1_with_address.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/http_pp_v1_with_address.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/http_pp_v1_with_address.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/http_pp_v1_with_address.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_no_pp.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_no_pp.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_no_pp.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_no_pp.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_pp_v1.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_pp_v1.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_pp_v1.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_pp_v1.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_pp_v2.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_pp_v2.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/http_single_transaction_pp_v2.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/http_single_transaction_pp_v2.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/https_single_transaction_pp_v1.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/https_single_transaction_pp_v1.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/https_single_transaction_pp_v1.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/https_single_transaction_pp_v1.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/https_single_transaction_pp_v2.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/https_single_transaction_pp_v2.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/https_single_transaction_pp_v2.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/https_single_transaction_pp_v2.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v1.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v1.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v1.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v1.replay.yaml diff --git a/tests/autests/gold_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v2.replay.yaml b/tests/uranium_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v2.replay.yaml similarity index 100% rename from tests/autests/gold_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v2.replay.yaml rename to tests/uranium_tests/proxy-protocol/replay_files/single_transaction_ipv6_pp_v2.replay.yaml diff --git a/tests/uranium_tests/proxy-protocol/test_proxy_protocol.py b/tests/uranium_tests/proxy-protocol/test_proxy_protocol.py new file mode 100644 index 000000000..bf48dafd4 --- /dev/null +++ b/tests/uranium_tests/proxy-protocol/test_proxy_protocol.py @@ -0,0 +1,218 @@ +''' +Verify basic PROXY protocol functionality. +''' +# @file +# +# Copyright 2023-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +import re + + +class PPTest: + # static id for client, server and proxy processes + test_id = 1 + + def __init__(self, testBaseName, ppVersion, isHTTPS, testRunDesc=""): + self.testRun = suite.case(testRunDesc) + self.testBaseName = testBaseName + self.ppVersion = ppVersion + self.replayFile = f'replay_files/{testBaseName}_pp_v{ppVersion}.replay.yaml' + self.isHTTPS = isHTTPS + + def setupClient(self, isHTTPS): + self.client = self.testRun.add_client( + f"client-{PPTest.test_id}", + self.replayFile, + configure_http=not isHTTPS, + configure_https=isHTTPS, + ) + + def setupServer(self, isHTTPS): + self.server = self.testRun.add_server( + f"server-{PPTest.test_id}", + self.replayFile, + configure_http=not isHTTPS, + configure_https=isHTTPS, + ) + + def setupProxy(self, isHTTPS): + self.proxyListenPort = self.client.https_port if isHTTPS else self.client.http_port + self.serverListenPort = self.server.https_port if isHTTPS else self.server.http_port + self.proxy = self.testRun.add_proxy( + f"proxy-{PPTest.test_id}", + listen_port=self.proxyListenPort, + server_port=self.serverListenPort, + use_ssl=isHTTPS, + ) + + def setupTransactionLogsVerification(self): + # Verify that the http trasactions are successful(not hindered by the + # PROXY protocol processing). + self.proxy.stdout.matches_gold(f"gold/{self.testBaseName}_proxy.gold") + self.client.stdout.matches_gold(f"gold/{self.testBaseName}_client.gold") + self.server.stdout.matches_gold(f"gold/{self.testBaseName}_server.gold") + + def setupPPLogsVerification(self): + # Verify the PROXY protocol related logs + self.client.stdout.contains( + r"Sending PROXY header", + "Verify that the PROXY header is sent from the client to the proxy.") + + self.proxy.stdout.contains(f"Received .* bytes of Proxy Protocol V{self.ppVersion}", + "Verify that the PROXY header is received by the proxy.") + self.proxy.stdout.contains( + rf"PROXY TCP4 127\.0\.0\.1 127\.0\.0\.1 [0-9]+ {self.proxyListenPort}", + "Verify the client sends a valid PROXY header.") + + self.server.stdout.contains( + rf"Received PROXY header v{self.ppVersion}:.*\nPROXY TCP4 127\.0\.0\.1 127\.0\.0\.1 [0-9]+ {self.proxyListenPort}", + "Verify that the server receives the PROXY header and parsed sucessfully.", + reflags=re.MULTILINE) + + def run(self): + self.setupClient(self.isHTTPS) + self.setupServer(self.isHTTPS) + self.setupProxy(self.isHTTPS) + self.setupTransactionLogsVerification() + self.setupPPLogsVerification() + PPTest.test_id += 1 + + +# Test 1: Verify the PROXY header v1 is sent and received in a HTTP transaction. +PPTest("http_single_transaction", ppVersion=1, isHTTPS=False, + testRunDesc="Verify PROXY protocol v1 is sent and received in a HTTP connection").run() + +# Test 2: Verify the PROXY header v2 is sent and received in a HTTP transaction. +PPTest("http_single_transaction", ppVersion=2, isHTTPS=False, + testRunDesc="Verify PROXY protocol v2 is sent and received in a HTTP connection").run() + +# Test 3: Verify the PROXY header v1 is sent and received in a HTTPS +# transaction. +PPTest("https_single_transaction", ppVersion=1, isHTTPS=True, + testRunDesc="Verify PROXY protocol v1 is sent and received in a HTTPS connection").run() + +# Test 4: Verify the PROXY header v2 is sent and received in a HTTPS +# transaction. +PPTest("https_single_transaction", ppVersion=2, isHTTPS=True, + testRunDesc="Verify PROXY protocol v2 is sent and received in a HTTPS connection").run() + +# Test 5: Verify the PROXY protocol is not sent when not specified in the replay +# file +case = suite.case("Verify the PROXY protocol is not sent when not specified in the replay file") + +# Add configure_https=False to verify ATS client and server work when the https +# optional arguments are not provided. +client = case.add_client("no-pp-client1", "replay_files/http_single_transaction_no_pp.replay.yaml", + configure_https=False) +server = case.add_server("no-pp-server1", "replay_files/http_single_transaction_no_pp.replay.yaml", + configure_https=False) +proxy = case.add_proxy("no-pp-proxy1", listen_port=client.http_port, server_port=server.http_port) + +# Verify the http transaction finishes successfully. +proxy.stdout.matches_gold("gold/http_single_transaction_proxy.gold") +client.stdout.matches_gold("gold/http_single_transaction_client.gold") +server.stdout.matches_gold("gold/http_single_transaction_server.gold") + +# Verify the PROXY protocol related logs. Since the replay file does not have +# the PROXY protocol related configuration, the client, proxy and server should +# not have these logs +client.stdout.excludes("Sending PROXY header", + "Client should not send PROXY header if not asked to.") +proxy.stdout.excludes("Received .* bytes of Proxy Protocol", + "Proxy should not receive the PROXY header.") +server.stdout.excludes("Received PROXY header", "The server should not receive the PROXY header.") + +# +# Test 6: Verify the PROXY v1 message can be sent and received on IPv6 +# connection. +# +case = suite.case("Verify the PROXY message can be sent and received on IPv6 connection") +server = case.add_server("ipv6-server1", "replay_files/single_transaction_ipv6_pp_v1.replay.yaml", + use_ipv6=True) +client = case.add_client("ipv6-client1", "replay_files/single_transaction_ipv6_pp_v1.replay.yaml", + use_ipv6=True, http_ports=[server.http_port], other_args="--no-proxy") + +# Verify successful transaction despite PROXY protocol processing +client.stdout.matches_gold("gold/ipv6_client.gold") +server.stdout.matches_gold("gold/ipv6_server.gold") + +# Verify the PROXY protocol related logs +client.stdout.contains(r"Sending PROXY header", + "Verify that the PROXY header is sent from the client to the server.") + +server.stdout.contains(f"Received PROXY header v1:.*\nPROXY TCP6 ::1 ::1 [0-9]+ {server.http_port}", + "Verify that the server receives the PROXY header and parsed sucessfully.", + reflags=re.MULTILINE) + +# +# Test 7: Verify the PROXY v2 message can be sent and received on IPv6 +# connection. +# +case = suite.case("Verify the PROXY message can be sent and received on IPv6 connection") +server = case.add_server("ipv6-server2", "replay_files/single_transaction_ipv6_pp_v2.replay.yaml", + use_ipv6=True) +client = case.add_client("ipv6-client2", "replay_files/single_transaction_ipv6_pp_v2.replay.yaml", + use_ipv6=True, http_ports=[server.http_port], other_args="--no-proxy") + +# Verify successful transaction despite PROXY protocol processing +client.stdout.matches_gold("gold/ipv6_client.gold") +server.stdout.matches_gold("gold/ipv6_server.gold") + +# Verify the PROXY protocol related logs +client.stdout.contains(r"Sending PROXY header", + "Verify that the PROXY header is sent from the client to the server.") + +server.stdout.contains(f"Received PROXY header v2:.*\nPROXY TCP6 ::1 ::1 [0-9]+ {server.http_port}", + "Verify that the server receives the PROXY header and parsed sucessfully.", + reflags=re.MULTILINE) + +# Test 8: Verify the PROXY protocol is sent with source and +# destination addresses specified in the replay file +case = suite.case( + "Verify the PROXY protocol is sent with the source and destination specified addresses in the replay file" +) + +EXPECTED_SRC_ADDRESS = "111.111.111.111" +EXPECTED_SRC_PORT = "11111" +EXPECTED_DST_ADDRESS = "222.222.222.222" +EXPECTED_DST_PORT = "22222" +EXPECTED_PROXY_HEADER = f"PROXY TCP4 {EXPECTED_SRC_ADDRESS} {EXPECTED_DST_ADDRESS} {EXPECTED_SRC_PORT} {EXPECTED_DST_PORT}" + +# Add configure_https=False to verify ATS client and server work when the https +# optional arguments are not provided. +client = case.add_client("specified-addr-pp-client1", + "replay_files/http_pp_v1_with_address.replay.yaml", configure_https=False) +server = case.add_server("specified-addr-pp-server1", + "replay_files/http_pp_v1_with_address.replay.yaml", configure_https=False) +proxy = case.add_proxy("specified-addr-pp-proxy1", listen_port=client.http_port, + server_port=server.http_port) + +# Verify the http transaction finishes successfully. +proxy.stdout.matches_gold("gold/http_single_transaction_proxy.gold") +client.stdout.matches_gold("gold/http_single_transaction_client.gold") +server.stdout.matches_gold("gold/http_single_transaction_server.gold") + +# Verify the PROXY protocol related logs. Make sure the source and destination +# addresses of the PROXY header match the ones specified in the replay file +client.stdout.contains(r"Sending PROXY header", + "Verify that the PROXY header is sent from the client to the proxy.") + +proxy.stdout.contains("Received .* bytes of Proxy Protocol", + "Verify that the PROXY header is received by the proxy.") +proxy.stdout.contains( + rf"{EXPECTED_PROXY_HEADER}", + "Verify the client sends a PROXY header with the specified source and destination addresses.") + +server.stdout.contains(rf"Received PROXY header v1:.*\n{EXPECTED_PROXY_HEADER}", + "Verify that the server receives the PROXY header and parsed sucessfully.", + reflags=re.MULTILINE) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/replay_gen/test_replay_gen.py b/tests/uranium_tests/replay_gen/test_replay_gen.py new file mode 100644 index 000000000..fa676c5fa --- /dev/null +++ b/tests/uranium_tests/replay_gen/test_replay_gen.py @@ -0,0 +1,40 @@ +''' +Verify replay-gen.py can generate parsable replay files. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Generate replay files via replay_gen and verify they can be replayed. +# +case = suite.case("Generate replay files via replay-gen.py") +replay_gen = case.add_replay_generator("replay_gen1", num_transactions=20) + +case = suite.case("Make sure we can use the generated replay files") +client = case.add_client("client1", replay_gen.artifact("replay_dir")) +server = case.add_server("server1", replay_gen.artifact("replay_dir")) +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +# The Python test proxy may close the connection part way through the +# transactions. I've verified that if the client talks directly to the server, +# there are no problems. +client.expect_return_codes(0, 1) + +client.stdout.contains( + "Parsed 20 transactions", + "Verify that the verifier client was able to parse the expected 20 transactions.") + +server.stdout.contains( + "Ready with 20 transactions", + "Verify that the verifier server was able to parse the expected 20 transactions.") + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_cache_hit.yaml b/tests/uranium_tests/request_absence/replay_files/http1_cache_hit.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_cache_hit.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_cache_hit.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_explicit_response.yaml b/tests/uranium_tests/request_absence/replay_files/http1_explicit_response.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_explicit_response.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_explicit_response.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_present_cache_hit.yaml b/tests/uranium_tests/request_absence/replay_files/http1_present_cache_hit.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_present_cache_hit.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_present_cache_hit.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_present_success.yaml b/tests/uranium_tests/request_absence/replay_files/http1_present_success.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_present_success.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_present_success.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_unspecified_cache_hit.yaml b/tests/uranium_tests/request_absence/replay_files/http1_unspecified_cache_hit.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_unspecified_cache_hit.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_unspecified_cache_hit.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http1_violation.yaml b/tests/uranium_tests/request_absence/replay_files/http1_violation.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http1_violation.yaml rename to tests/uranium_tests/request_absence/replay_files/http1_violation.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/http2_violation.yaml b/tests/uranium_tests/request_absence/replay_files/http2_violation.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/http2_violation.yaml rename to tests/uranium_tests/request_absence/replay_files/http2_violation.yaml diff --git a/tests/autests/gold_tests/request_absence/replay_files/missing_server_response.yaml b/tests/uranium_tests/request_absence/replay_files/missing_server_response.yaml similarity index 100% rename from tests/autests/gold_tests/request_absence/replay_files/missing_server_response.yaml rename to tests/uranium_tests/request_absence/replay_files/missing_server_response.yaml diff --git a/tests/uranium_tests/request_absence/test_request_absence.py b/tests/uranium_tests/request_absence/test_request_absence.py new file mode 100644 index 000000000..ce861f223 --- /dev/null +++ b/tests/uranium_tests/request_absence/test_request_absence.py @@ -0,0 +1,169 @@ +''' +Verify server-side request presence expectation behavior. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +missing_present_message = ('never reached the verifier-server before shutdown even though "expect" ' + 'was explicitly set to "present"') + +case = suite.case("Verify expect: absent succeeds when the proxy replies directly") +client = case.add_client("client1", "replay_files/http1_cache_hit.yaml", configure_https=False) +server = case.add_server("server1", "replay_files/http1_cache_hit.yaml", configure_https=False) +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Received an HTTP/1 200 response for key cache-hit-1", + "The client should receive the direct cached response.") +server.stdout.excludes('unexpectedly reached the verifier-server', + "The server should not report a forbidden request arrival.") +server.stdout.excludes('were never processed before shutdown', + "An absent expectation should not count as an unprocessed verification.") +proxy.stdout.contains('Serving local response for key cache-hit-1 with status 200.', + "The shared proxy should synthesize the cached response locally.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +case = suite.case("Verify expect: present succeeds when the proxy forwards the request") +client = case.add_client("client2", "replay_files/http1_present_success.yaml", + configure_https=False) +server = case.add_server("server2", "replay_files/http1_present_success.yaml", + configure_https=False) +case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains( + "Received an HTTP/1 200 response for key present-http1-1", + "The client should receive the upstream response when the request is forwarded.") +server.stdout.excludes(missing_present_message, + "The server should not report a missing request when the request arrives.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +case = suite.case("Verify expect: present fails when the proxy replies directly") +client = case.add_client("client3", "replay_files/http1_present_cache_hit.yaml", + configure_https=False) +server = case.add_server("server3", "replay_files/http1_present_cache_hit.yaml", + configure_https=False) +proxy = case.add_proxy("proxy3", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Received an HTTP/1 200 response for key present-cache-hit-1", + "The client should still receive the direct cached response.") +server.stdout.contains(missing_present_message, + "The server should fail when an expected request never arrives.") +server.stdout.excludes( + 'were never processed before shutdown', + "The explicit present failure should not fall back to the generic unprocessed check.") +proxy.stdout.contains('Serving local response for key present-cache-hit-1 with status 200.', + "The shared proxy should synthesize the cached response locally.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +case = suite.case("Verify expect: present still fails with --allow-unprocessed-verifications") +client = case.add_client("client4", "replay_files/http1_present_cache_hit.yaml", + configure_https=False) +server = case.add_server("server4", "replay_files/http1_present_cache_hit.yaml", + configure_https=False, other_args="--allow-unprocessed-verifications") +proxy = case.add_proxy("proxy4", listen_port=client.http_port, server_port=server.http_port) + +server.stdout.contains(missing_present_message, + "The allow flag should not suppress explicit present failures.") +server.stdout.excludes( + 'were never processed before shutdown', + "The allow flag is not involved because this is not a generic " + "unprocessed verification failure.") +proxy.stdout.contains('Serving local response for key present-cache-hit-1 with status 200.', + "The shared proxy should synthesize the cached response locally.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +case = suite.case("Verify omitted expect allows the proxy to reply directly") +client = case.add_client("client5", "replay_files/http1_unspecified_cache_hit.yaml", + configure_https=False) +server = case.add_server("server5", "replay_files/http1_unspecified_cache_hit.yaml", + configure_https=False) +proxy = case.add_proxy("proxy5", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains("Received an HTTP/1 200 response for key unspecified-cache-hit-1", + "The client should receive the direct cached response.") +server.stdout.excludes(missing_present_message, + "No explicit presence expectation should mean no missing-request failure.") +server.stdout.excludes( + 'were never processed before shutdown', + "Without request verification rules, omitting expect should not trigger " + "an unprocessed verification failure.") +proxy.stdout.contains('Serving local response for key unspecified-cache-hit-1 with status 200.', + "The shared proxy should synthesize the cached response locally.") + +client.expect_return_codes(0) +server.expect_return_codes(0) + +case = suite.case("Verify expect: absent fails when HTTP/1 traffic reaches the server") +client = case.add_client("client6", "replay_files/http1_violation.yaml", configure_https=False) +server = case.add_server("server6", "replay_files/http1_violation.yaml", configure_https=False) +case.add_proxy("proxy6", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains( + "Received an HTTP/1 404 response for key absent-http1-1", + "The client should receive the fallback 404 when the forbidden request arrives upstream.") +server.stdout.contains( + 'Proxy request with key "absent-http1-1" unexpectedly reached the verifier-server', + "The server should fail as soon as the forbidden request arrives.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +case = suite.case("Verify expect: absent still honors an explicit server-response") +client = case.add_client("client7", "replay_files/http1_explicit_response.yaml", + configure_https=False) +server = case.add_server("server7", "replay_files/http1_explicit_response.yaml", + configure_https=False) +case.add_proxy("proxy7", listen_port=client.http_port, server_port=server.http_port) + +client.stdout.contains( + "Received an HTTP/1 418 response for key absent-http1-explicit", + "The client should receive the configured response when the forbidden request arrives.") +server.stdout.contains( + 'Proxy request with key "absent-http1-explicit" unexpectedly reached the verifier-server', + "The server should still fail when the forbidden request arrives.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +case = suite.case("Verify expect: absent fails for HTTP/2 traffic reaching the server") +client = case.add_client("client8", "replay_files/http2_violation.yaml") +server = case.add_server("server8", "replay_files/http2_violation.yaml") +case.add_proxy("proxy8", listen_port=client.https_port, server_port=server.https_port, use_ssl=True, + use_http2_to_2=True) + +client.stdout.contains( + "Received an HTTP/2 response for key absent-http2-1 with stream id 1:", + "The HTTP/2 client should receive the response generated for the forbidden upstream arrival.") +server.stdout.contains( + 'Proxy request with key "absent-http2-1" unexpectedly reached the verifier-server', + "The server should fail when the forbidden HTTP/2 request arrives.") + +client.expect_return_codes(0) +server.expect_return_codes(1) + +case = suite.case("Verify present expectations still require server-response") +server = case.add_server("server9", "replay_files/missing_server_response.yaml", + configure_https=False) + +server.stdout.contains( + 'does not have a server response', + "Transactions without expect: absent should still require a server-response node.") +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py b/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py new file mode 100644 index 000000000..850f254db --- /dev/null +++ b/tests/uranium_tests/sigint_shutdown/test_sigint_shutdown.py @@ -0,0 +1,38 @@ +''' +Verify verifier-client and verifier-server honor SIGINT promptly during replay. +''' +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +verifier_script = 'verify_sigint_shutdown.py' + +case = suite.case("Verify verifier-server exits promptly on SIGINT during replay.") +process = case.add_process( + "server-sigint", + ["python3", verifier_script, "--mode", "server", "--verifier-server", "{verifier-server}"], + copies=[verifier_script], +) +process.stdout.contains( + 'OK: server exited promptly', + 'The helper script should confirm verifier-server shuts down quickly on SIGINT.') + +case = suite.case("Verify verifier-client exits promptly on SIGINT during replay.") +process = case.add_process( + "client-sigint", + ["python3", verifier_script, "--mode", "client", "--verifier-client", "{verifier-client}"], + copies=[verifier_script], +) +process.stdout.contains( + 'OK: client exited promptly', + 'The helper script should confirm verifier-client shuts down quickly on SIGINT.') + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/sigint_shutdown/verify_sigint_shutdown.py b/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py similarity index 99% rename from tests/autests/gold_tests/sigint_shutdown/verify_sigint_shutdown.py rename to tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py index 28ae8e1af..6ee4fc229 100644 --- a/tests/autests/gold_tests/sigint_shutdown/verify_sigint_shutdown.py +++ b/tests/uranium_tests/sigint_shutdown/verify_sigint_shutdown.py @@ -6,7 +6,6 @@ # import argparse -import os import shutil import signal import socket diff --git a/tests/autests/gold_tests/status_verification/replay_files/status_verification.yaml b/tests/uranium_tests/status_verification/replay_files/status_verification.yaml similarity index 100% rename from tests/autests/gold_tests/status_verification/replay_files/status_verification.yaml rename to tests/uranium_tests/status_verification/replay_files/status_verification.yaml diff --git a/tests/autests/gold_tests/status_verification/status_verification.test.py b/tests/uranium_tests/status_verification/test_status_verification.py similarity index 53% rename from tests/autests/gold_tests/status_verification/status_verification.test.py rename to tests/uranium_tests/status_verification/test_status_verification.py index 6daf8be4c..84e607557 100644 --- a/tests/autests/gold_tests/status_verification/status_verification.test.py +++ b/tests/uranium_tests/status_verification/test_status_verification.py @@ -5,35 +5,38 @@ # # Copyright 2023-2026, Verizon Media # SPDX-License-Identifier: Apache-2.0 -# -Test.Summary = ''' -Verify correct response status(status code and reason) verification behavior. -''' +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# # # Test 1: Verify status verification in a YAML replay file. # -r = Test.AddTestRun("Verify status verification works for simple HTTP transactions") -client = r.AddClientProcess("client1", "replay_files/status_verification.yaml") -server = r.AddServerProcess("server1", "replay_files/status_verification.yaml") -proxy = r.AddProxyProcess("proxy1", listen_port=client.Variables.http_port, - server_port=server.Variables.http_port) +case = suite.case("Verify status verification works for simple HTTP transactions") +client = case.add_client("client1", "replay_files/status_verification.yaml") +server = case.add_server("server1", "replay_files/status_verification.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) # Verify status validation logs. -client.Streams.stdout = Testers.ExcludesExpression( +client.stdout.excludes( 'Violation.*key: 1', 'Transaction 1 should not have any violation since the status code matches the expected.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Status Violation:.*key: 2', 'Transaction 2 should have status violation as the status code does not match the expected.') -client.Streams.stdout += Testers.ExcludesExpression( +client.stdout.excludes( 'Violation.*key: 3', 'Transaction 3 should not have violation since the reason string matches the expected.') -client.Streams.stdout += Testers.ContainsExpression( +client.stdout.contains( 'Reason String Violation:.*key: 4', 'Transaction 4 should have violation as the reason string does not match the expected.') -client.Streams.stdout += Testers.ExcludesExpression( +client.stdout.excludes( 'Violation.*key: 5', 'Transaction 5 should not have a violation for the tolerated 200/304 cache crossover.') -client.ReturnCode = 1 +client.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/strict_mode/gold/fields_differ_proxy.gold b/tests/uranium_tests/strict_mode/gold/fields_differ_proxy.gold similarity index 100% rename from tests/autests/gold_tests/strict_mode/gold/fields_differ_proxy.gold rename to tests/uranium_tests/strict_mode/gold/fields_differ_proxy.gold diff --git a/tests/autests/gold_tests/strict_mode/gold/fields_match_proxy.gold b/tests/uranium_tests/strict_mode/gold/fields_match_proxy.gold similarity index 100% rename from tests/autests/gold_tests/strict_mode/gold/fields_match_proxy.gold rename to tests/uranium_tests/strict_mode/gold/fields_match_proxy.gold diff --git a/tests/autests/gold_tests/strict_mode/replay_files/fields_differ.json b/tests/uranium_tests/strict_mode/replay_files/fields_differ.json similarity index 100% rename from tests/autests/gold_tests/strict_mode/replay_files/fields_differ.json rename to tests/uranium_tests/strict_mode/replay_files/fields_differ.json diff --git a/tests/autests/gold_tests/strict_mode/replay_files/fields_match.json b/tests/uranium_tests/strict_mode/replay_files/fields_match.json similarity index 100% rename from tests/autests/gold_tests/strict_mode/replay_files/fields_match.json rename to tests/uranium_tests/strict_mode/replay_files/fields_match.json diff --git a/tests/uranium_tests/strict_mode/test_strict_mode.py b/tests/uranium_tests/strict_mode/test_strict_mode.py new file mode 100644 index 000000000..eff954d23 --- /dev/null +++ b/tests/uranium_tests/strict_mode/test_strict_mode.py @@ -0,0 +1,53 @@ +''' +Verify strict mode functionality. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify there are no warnings when the fields match. +# +case = suite.case("Verify strict mode is silent when the fields match.") +client = case.add_client("client1", "replay_files/fields_match.json", other_args="--strict") +server = case.add_server("server1", "replay_files/fields_match.json", other_args="--strict") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/fields_match_proxy.gold") + +client.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +server.stdout.excludes("Violation:", + "There should be no verification errors because there are none added.") + +# +# Test 2: Verify there are warnings when the fields don't match. +# +case = suite.case("Verify strict mode warns when the fields don't match") +client = case.add_client("client2", "replay_files/fields_differ.json", other_args="--strict") +server = case.add_server("server2", "replay_files/fields_differ.json", other_args="--strict") +proxy = case.add_proxy("proxy2", listen_port=client.http_port, server_port=server.http_port) + +proxy.stdout.matches_gold("gold/fields_differ_proxy.gold") + +client.stdout.contains( + 'Violation: Absent. Key: "cb9b4e94-5d42-43d4-8545-320033298ba2-226381119", Field Name: "x-thisresponseheaderwontexist", Correct Value: "ThereforeTheClientShouldWarn', + "There should be a warning about the missing response header") + +server.stdout.contains( + 'Violation: Absent. Key: "cb9b4e94-5d42-43d4-8545-320033298ba2-226381119", Field Name: "x-thisrequestheaderwontexist", Correct Value: "ThereforeTheServerShouldWarn', + "There should be a warning about the missing proxy request header.") + +client.expect_return_codes(1) +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/uranium_tests/url_verification/test_url_verification.py b/tests/uranium_tests/url_verification/test_url_verification.py new file mode 100644 index 000000000..034b55457 --- /dev/null +++ b/tests/uranium_tests/url_verification/test_url_verification.py @@ -0,0 +1,31 @@ +''' +Verify correct URL verification behavior. +''' +# @file +# +# Copyright 2021-2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +from tools.uranium import CaseSuite + +suite = CaseSuite(__file__) +# + +# +# Test 1: Verify field verification in a YAML replay file. +# +case = suite.case("Verify URL verification works for a simple HTTP transaction") +client = case.add_client("client1", "url_verification.yaml") +server = case.add_server("server1", "url_verification.yaml") +proxy = case.add_proxy("proxy1", listen_port=client.http_port, server_port=server.http_port) + +# Preserve the prior test's effective final stream assertion. +server.stdout.contains( + 'Equals Violation: Absent. Key: "5", URI Part: "fragment", Correct Value: "F"', + 'Validation should be unhappy that the fragment was missing.') + +server.expect_return_codes(1) + + +def test_uranium_suite(uranium): + uranium.run(suite) diff --git a/tests/autests/gold_tests/url_verification/url_verification.yaml b/tests/uranium_tests/url_verification/url_verification.yaml similarity index 100% rename from tests/autests/gold_tests/url_verification/url_verification.yaml rename to tests/uranium_tests/url_verification/url_verification.yaml diff --git a/tests/urtest.sh.in b/tests/urtest.sh.in new file mode 100644 index 000000000..5ea50d9a3 --- /dev/null +++ b/tests/urtest.sh.in @@ -0,0 +1,35 @@ +#!/bin/bash +# @file +# +# Copyright 2026, Verizon Media +# SPDX-License-Identifier: Apache-2.0 + +set -u + +source_root="@CMAKE_SOURCE_DIR@" +project_directory="@PV_URTEST_PROJECT_DIRECTORY@" +verifier_bin="@CMAKE_BINARY_DIR@/bin" +sandbox="@URTEST_SANDBOX@" + +export PYTHONPATH="${source_root}/tests${PYTHONPATH:+:${PYTHONPATH}}" +export PYTHONDONTWRITEBYTECODE=1 + +pytest_arguments=("$@") +if [[ -n "${SHARDCNT:-}" ]]; then + if [[ -z "${SHARD:-}" ]]; then + echo "urtest.sh: SHARD is required when SHARDCNT is set" >&2 + exit 2 + fi + pytest_arguments+=(--urtest-shard-index "${SHARD}" --urtest-shard-count "${SHARDCNT}") +fi + +exec uv --project "${project_directory}" run --extra fast-diff pytest \ + --import-mode=importlib \ + -p tools.uranium.plugin \ + "${source_root}/tests/tools/uranium/tests" \ + "${source_root}/tests/uranium_tests" \ + --verifier-bin "${verifier_bin}" \ + --sandbox "${sandbox}" \ + -o "cache_dir=${project_directory}/.pytest_cache" \ + @URTEST_OPTIONS@ \ + "${pytest_arguments[@]}" diff --git a/tests/autests/uv.lock b/tests/uv.lock similarity index 79% rename from tests/autests/uv.lock rename to tests/uv.lock index 65d205833..a00288c5e 100644 --- a/tests/autests/uv.lock +++ b/tests/uv.lock @@ -42,36 +42,15 @@ wheels = [ ] [[package]] -name = "autest" -version = "1.10.5" +name = "cdifflib" +version = "1.2.9" source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version < '3.11'", -] -dependencies = [ - { name = "colorama" }, - { name = "future" }, - { name = "psutil" }, -] -wheels = [ - { url = "https://files.pythonhosted.org/packages/4a/01/f4be5eeecc515496dbd1046fb4a789236bb2897cf012953a5debd23cece0/autest-1.10.5-py3-none-any.whl", hash = "sha256:84630b2dd0aff3f5b4f96e33ec2e1f98b159af6c872c9fe34ce4e2f8cb304686", size = 138745, upload-time = "2023-08-28T16:54:17.161Z" }, -] - -[[package]] -name = "autest" -version = "1.10.6" -source = { registry = "https://pypi.org/simple" } -resolution-markers = [ - "python_full_version >= '3.11'", -] -dependencies = [ - { name = "colorama" }, - { name = "future" }, - { name = "psutil" }, -] -sdist = { url = "https://files.pythonhosted.org/packages/64/8a/745cfcae8e185d125989b6f426c8b75907583b682e25bae913f50a23ea9b/autest-1.10.6.tar.gz", hash = "sha256:4af75e29a61d930411c05dd51a80411fc4afbb6acbf767b52d33c5b3ef50fe9f", size = 95120, upload-time = "2026-02-21T22:53:50.566Z" } +sdist = { url = "https://files.pythonhosted.org/packages/52/aa/daefb1236e47561ca53f469f4832f625b38ad6db4e5c68e589dd72928d61/cdifflib-1.2.9.tar.gz", hash = "sha256:6286da08f72b7ddb5b40145dcb8f214ad913a86d72b1f62cc8d6cf7a92029590", size = 12323, upload-time = "2025-01-13T22:18:04.625Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/7e/18/6f6a846fa664fc57563ca1a00a46a9896b1bea52ccf9fc403fd6bacf29ca/autest-1.10.6-py3-none-any.whl", hash = "sha256:1b03c01116d853a7ca4cf830785441a5b14410b21e8349293af53e02a0884d73", size = 138644, upload-time = "2026-02-21T22:53:49.5Z" }, + { url = "https://files.pythonhosted.org/packages/0a/2a/12cc95269e666ac40a20662c865677e24363fdca4d5c72566ad36b14d24a/cdifflib-1.2.9-cp310-cp310-macosx_15_0_arm64.whl", hash = "sha256:24219193d1d298ead211d4b628ad2124ffa1c0676890cea8fbacdeaf66a2369b", size = 11044, upload-time = "2025-01-13T22:17:55.548Z" }, + { url = "https://files.pythonhosted.org/packages/ba/35/161f137709a77ae861dfdebb478a7dad323b3a7fd3c24b97799ccf48a2b7/cdifflib-1.2.9-cp311-cp311-macosx_15_0_arm64.whl", hash = "sha256:32c56f7895253b0734f42ba023a9c181b52d72f3d51afacc29d5ea8ee72e4643", size = 11046, upload-time = "2025-01-13T22:17:58.077Z" }, + { url = "https://files.pythonhosted.org/packages/cd/94/caf01d3efe4aa31086217d20538ad9a8ef8a925b49771d34d4dab0295de8/cdifflib-1.2.9-cp312-cp312-macosx_15_0_arm64.whl", hash = "sha256:75a81d8a5e2b0ca055d3f7850fd0a29b488b086c91445841fa3113ac328410e0", size = 11028, upload-time = "2025-01-13T22:17:59.168Z" }, + { url = "https://files.pythonhosted.org/packages/7c/05/5071e0757237e7aa79a6256c1ddddebebab500e1807a1603739f777f37b1/cdifflib-1.2.9-cp313-cp313-macosx_15_0_arm64.whl", hash = "sha256:c7113c018e1d8190ce6c00318ae5afe7e99c8e4e0b4b631ee79acde74949c2e8", size = 11039, upload-time = "2025-01-13T22:18:01.439Z" }, ] [[package]] @@ -282,12 +261,24 @@ wheels = [ ] [[package]] -name = "future" -version = "1.0.0" +name = "exceptiongroup" +version = "1.3.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "typing-extensions" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/50/79/66800aadf48771f6b62f7eb014e352e5d06856655206165d775e675a02c9/exceptiongroup-1.3.1.tar.gz", hash = "sha256:8b412432c6055b0b7d14c310000ae93352ed6754f70fa8f7c34141f91c4e3219", size = 30371, upload-time = "2025-11-21T23:01:54.787Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/8a/0e/97c33bf5009bdbac74fd2beace167cab3f978feb69cc36f1ef79360d6c4e/exceptiongroup-1.3.1-py3-none-any.whl", hash = "sha256:a7a39a3bd276781e98394987d3a5701d0c4edffb633bb7a5144577f82c773598", size = 16740, upload-time = "2025-11-21T23:01:53.443Z" }, +] + +[[package]] +name = "execnet" +version = "2.1.2" source = { registry = "https://pypi.org/simple" } -sdist = { url = "https://files.pythonhosted.org/packages/a7/b2/4140c69c6a66432916b26158687e821ba631a4c9273c474343badf84d3ba/future-1.0.0.tar.gz", hash = "sha256:bd2968309307861edae1458a4f8a4f3598c03be43b97521076aebf5d94c07b05", size = 1228490, upload-time = "2024-02-21T11:52:38.461Z" } +sdist = { url = "https://files.pythonhosted.org/packages/bf/89/780e11f9588d9e7128a3f87788354c7946a9cbb1401ad38a48c4db9a4f07/execnet-2.1.2.tar.gz", hash = "sha256:63d83bfdd9a23e35b9c6a3261412324f964c2ec8dcd8d3c6916ee9373e0befcd", size = 166622, upload-time = "2025-11-12T09:56:37.75Z" } wheels = [ - { url = "https://files.pythonhosted.org/packages/da/71/ae30dadffc90b9006d77af76b393cb9dfbfc9629f339fc1574a1c52e6806/future-1.0.0-py3-none-any.whl", hash = "sha256:929292d34f5872e70396626ef385ec22355a1fae8ad29e1a734c3e43f9fbc216", size = 491326, upload-time = "2024-02-21T11:52:35.956Z" }, + { url = "https://files.pythonhosted.org/packages/ab/84/02fc1827e8cdded4aa65baef11296a9bbe595c474f0d6d758af082d849fd/execnet-2.1.2-py3-none-any.whl", hash = "sha256:67fba928dd5a544b783f6056f449e5e3931a5c378b128bc18501f7ea79e296ec", size = 40708, upload-time = "2025-11-12T09:56:36.333Z" }, ] [[package]] @@ -382,17 +373,44 @@ wheels = [ ] [[package]] -name = "proxy-verifier-autests" +name = "iniconfig" +version = "2.3.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/72/34/14ca021ce8e5dfedc35312d08ba8bf51fdd999c576889fc2c24cb97f4f10/iniconfig-2.3.0.tar.gz", hash = "sha256:c76315c77db068650d49c5b56314774a7804df16fee4402c1f19d6d15d8c4730", size = 20503, upload-time = "2025-10-18T21:55:43.219Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/cb/b1/3846dd7f199d53cb17f49cba7e651e9ce294d8497c8c150530ed11865bb8/iniconfig-2.3.0-py3-none-any.whl", hash = "sha256:f631c04d2c48c52b84d0d0549c99ff3859c98df65b3101406327ecc7d53fbf12", size = 7484, upload-time = "2025-10-18T21:55:41.639Z" }, +] + +[[package]] +name = "packaging" +version = "26.3" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/7d/fa/3944b40b07da9ce895c0e6303a5ab7d53da063554f534556b134a54d6093/packaging-26.3.tar.gz", hash = "sha256:94edc256424af38762eb31306eed28beb9f0efc50a8837492c9d6fd6004aed79", size = 313412, upload-time = "2026-08-04T18:15:28.737Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/63/34/ba1c580383c9eada3711951fef0795c80b829a078d72188184bcab9dd527/packaging-26.3-py3-none-any.whl", hash = "sha256:d7193f7c8e4e93f444fde0262bf90af30e16fa0ad0ad44cb553c87339b23cd1c", size = 129956, upload-time = "2026-08-04T18:15:27.159Z" }, +] + +[[package]] +name = "pluggy" +version = "1.6.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/f9/e2/3e91f31a7d2b083fe6ef3fa267035b518369d9511ffab804f839851d2779/pluggy-1.6.0.tar.gz", hash = "sha256:7dcc130b76258d33b90f61b658791dede3486c3e6bfb003ee5c9bfb396dd22f3", size = 69412, upload-time = "2025-05-15T12:30:07.975Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/54/20/4d324d65cc6d9205fabedc306948156824eb9f0ee1633355a8f7ec5c66bf/pluggy-1.6.0-py3-none-any.whl", hash = "sha256:e920276dd6813095e9377c0bc5566d94c932c33b27a3e3945d8389c374dd4746", size = 20538, upload-time = "2025-05-15T12:30:06.134Z" }, +] + +[[package]] +name = "proxy-verifier-urtests" version = "0.1.0" source = { virtual = "." } dependencies = [ { name = "aioquic" }, - { name = "autest", version = "1.10.5", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version < '3.11'" }, - { name = "autest", version = "1.10.6", source = { registry = "https://pypi.org/simple" }, marker = "python_full_version >= '3.11'" }, { name = "eventlet" }, { name = "h2" }, { name = "psutil" }, { name = "pyopenssl" }, + { name = "pytest" }, + { name = "pytest-xdist" }, { name = "ruamel-yaml" }, ] @@ -400,19 +418,24 @@ dependencies = [ dev = [ { name = "pyflakes" }, ] +fast-diff = [ + { name = "cdifflib" }, +] [package.metadata] requires-dist = [ { name = "aioquic", specifier = "==1.3.0" }, - { name = "autest" }, + { name = "cdifflib", marker = "extra == 'fast-diff'", specifier = ">=1.2.9" }, { name = "eventlet" }, { name = "h2" }, { name = "psutil" }, { name = "pyflakes", marker = "extra == 'dev'" }, { name = "pyopenssl" }, + { name = "pytest", specifier = ">=8.0" }, + { name = "pytest-xdist", specifier = ">=3.6" }, { name = "ruamel-yaml" }, ] -provides-extras = ["dev"] +provides-extras = ["fast-diff", "dev"] [[package]] name = "psutil" @@ -460,6 +483,15 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/c2/2f/81d580a0fb83baeb066698975cb14a618bdbed7720678566f1b046a95fe8/pyflakes-3.4.0-py2.py3-none-any.whl", hash = "sha256:f742a7dbd0d9cb9ea41e9a24a918996e8170c799fa528688d40dd582c8265f4f", size = 63551, upload-time = "2025-06-20T18:45:26.937Z" }, ] +[[package]] +name = "pygments" +version = "2.21.0" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/49/2e/ced460408999b33da6b31b0021b0f37d329e202d4169aeb164493778f25b/pygments-2.21.0.tar.gz", hash = "sha256:610ca751c9bc2492b38eb9a38a7fbc93edbbb2d7182edaf34e66ae493dee5c8c", size = 5005329, upload-time = "2026-08-17T08:02:48.824Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/71/46/17f022dd3e953bf20a04a028a21ec746d942f8d2af30fa0f124fa0e6a684/pygments-2.21.0-py3-none-any.whl", hash = "sha256:2363c69b61c4a97c838da3b130dcd6468f4848992b21a82f2a63ec34377137d9", size = 1250147, upload-time = "2026-08-17T08:02:44.912Z" }, +] + [[package]] name = "pylsqpack" version = "0.3.24" @@ -492,6 +524,37 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/51/ad/2cf6d3fa2fae5c79e1ed9960c0d42badd0f94d81dd12b50604cdc839e648/pyopenssl-26.4.0-py3-none-any.whl", hash = "sha256:f0eb0cb2d581d3ad2b9c489468485e7f2ab6727d08401bcf9d824c3caddf3c1c", size = 56026, upload-time = "2026-08-01T19:50:48.94Z" }, ] +[[package]] +name = "pytest" +version = "9.1.1" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "colorama", marker = "sys_platform == 'win32'" }, + { name = "exceptiongroup", marker = "python_full_version < '3.11'" }, + { name = "iniconfig" }, + { name = "packaging" }, + { name = "pluggy" }, + { name = "pygments" }, + { name = "tomli", marker = "python_full_version < '3.11'" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/e4/47/b9efed96c114afcfa3c9d3fe98a76a1d14c74a9e266d397cf6eb64be5e01/pytest-9.1.1.tar.gz", hash = "sha256:1088fbde8f2b49d95a549a195707afa7a76a3ce9bcadc26b6d71f0ffda5fe313", size = 1636369, upload-time = "2026-06-19T10:58:32.857Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/24/25/1de2678b631f5a49215c6c96fff41ba892b0a34df68d6d80292b1b48aa7f/pytest-9.1.1-py3-none-any.whl", hash = "sha256:37a86b45efb9a47a61a36449063e8e18d0cab3161329fc099eb21783169c4f0c", size = 386536, upload-time = "2026-06-19T10:58:31.347Z" }, +] + +[[package]] +name = "pytest-xdist" +version = "3.8.0" +source = { registry = "https://pypi.org/simple" } +dependencies = [ + { name = "execnet" }, + { name = "pytest" }, +] +sdist = { url = "https://files.pythonhosted.org/packages/78/b4/439b179d1ff526791eb921115fca8e44e596a13efeda518b9d845a619450/pytest_xdist-3.8.0.tar.gz", hash = "sha256:7e578125ec9bc6050861aa93f2d59f1d8d085595d6551c2c90b6f4fad8d3a9f1", size = 88069, upload-time = "2025-07-01T13:30:59.346Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/ca/31/d4e37e9e550c2b92a9cbc2e4d0b7420a27224968580b5a447f420847c975/pytest_xdist-3.8.0-py3-none-any.whl", hash = "sha256:202ca578cfeb7370784a8c33d6d05bc6e13b4f25b5053c30a152269fd10f0b88", size = 46396, upload-time = "2025-07-01T13:30:56.632Z" }, +] + [[package]] name = "ruamel-yaml" version = "0.19.1" @@ -514,6 +577,60 @@ wheels = [ { url = "https://files.pythonhosted.org/packages/93/eb/2433e1af4ff903499144de4846569fb3300b816179ae99a03c2f011b666a/service_identity-26.1.0-py3-none-any.whl", hash = "sha256:68c32dadbb69135fb951077677e07cd7f6031020f3a8c8f47a28cda8a0742118", size = 11370, upload-time = "2026-05-30T12:04:53.911Z" }, ] +[[package]] +name = "tomli" +version = "2.4.1" +source = { registry = "https://pypi.org/simple" } +sdist = { url = "https://files.pythonhosted.org/packages/22/de/48c59722572767841493b26183a0d1cc411d54fd759c5607c4590b6563a6/tomli-2.4.1.tar.gz", hash = "sha256:7c7e1a961a0b2f2472c1ac5b69affa0ae1132c39adcb67aba98568702b9cc23f", size = 17543, upload-time = "2026-03-25T20:22:03.828Z" } +wheels = [ + { url = "https://files.pythonhosted.org/packages/f4/11/db3d5885d8528263d8adc260bb2d28ebf1270b96e98f0e0268d32b8d9900/tomli-2.4.1-cp311-cp311-macosx_10_9_x86_64.whl", hash = "sha256:f8f0fc26ec2cc2b965b7a3b87cd19c5c6b8c5e5f436b984e85f486d652285c30", size = 154704, upload-time = "2026-03-25T20:21:10.473Z" }, + { url = "https://files.pythonhosted.org/packages/6d/f7/675db52c7e46064a9aa928885a9b20f4124ecb9bc2e1ce74c9106648d202/tomli-2.4.1-cp311-cp311-macosx_11_0_arm64.whl", hash = "sha256:4ab97e64ccda8756376892c53a72bd1f964e519c77236368527f758fbc36a53a", size = 149454, upload-time = "2026-03-25T20:21:12.036Z" }, + { url = "https://files.pythonhosted.org/packages/61/71/81c50943cf953efa35bce7646caab3cf457a7d8c030b27cfb40d7235f9ee/tomli-2.4.1-cp311-cp311-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:96481a5786729fd470164b47cdb3e0e58062a496f455ee41b4403be77cb5a076", size = 237561, upload-time = "2026-03-25T20:21:13.098Z" }, + { url = "https://files.pythonhosted.org/packages/48/c1/f41d9cb618acccca7df82aaf682f9b49013c9397212cb9f53219e3abac37/tomli-2.4.1-cp311-cp311-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:5a881ab208c0baf688221f8cecc5401bd291d67e38a1ac884d6736cbcd8247e9", size = 243824, upload-time = "2026-03-25T20:21:14.569Z" }, + { url = "https://files.pythonhosted.org/packages/22/e4/5a816ecdd1f8ca51fb756ef684b90f2780afc52fc67f987e3c61d800a46d/tomli-2.4.1-cp311-cp311-musllinux_1_2_aarch64.whl", hash = "sha256:47149d5bd38761ac8be13a84864bf0b7b70bc051806bc3669ab1cbc56216b23c", size = 242227, upload-time = "2026-03-25T20:21:15.712Z" }, + { url = "https://files.pythonhosted.org/packages/6b/49/2b2a0ef529aa6eec245d25f0c703e020a73955ad7edf73e7f54ddc608aa5/tomli-2.4.1-cp311-cp311-musllinux_1_2_x86_64.whl", hash = "sha256:ec9bfaf3ad2df51ace80688143a6a4ebc09a248f6ff781a9945e51937008fcbc", size = 247859, upload-time = "2026-03-25T20:21:17.001Z" }, + { url = "https://files.pythonhosted.org/packages/83/bd/6c1a630eaca337e1e78c5903104f831bda934c426f9231429396ce3c3467/tomli-2.4.1-cp311-cp311-win32.whl", hash = "sha256:ff2983983d34813c1aeb0fa89091e76c3a22889ee83ab27c5eeb45100560c049", size = 97204, upload-time = "2026-03-25T20:21:18.079Z" }, + { url = "https://files.pythonhosted.org/packages/42/59/71461df1a885647e10b6bb7802d0b8e66480c61f3f43079e0dcd315b3954/tomli-2.4.1-cp311-cp311-win_amd64.whl", hash = "sha256:5ee18d9ebdb417e384b58fe414e8d6af9f4e7a0ae761519fb50f721de398dd4e", size = 108084, upload-time = "2026-03-25T20:21:18.978Z" }, + { url = "https://files.pythonhosted.org/packages/b8/83/dceca96142499c069475b790e7913b1044c1a4337e700751f48ed723f883/tomli-2.4.1-cp311-cp311-win_arm64.whl", hash = "sha256:c2541745709bad0264b7d4705ad453b76ccd191e64aa6f0fc66b69a293a45ece", size = 95285, upload-time = "2026-03-25T20:21:20.309Z" }, + { url = "https://files.pythonhosted.org/packages/c1/ba/42f134a3fe2b370f555f44b1d72feebb94debcab01676bf918d0cb70e9aa/tomli-2.4.1-cp312-cp312-macosx_10_13_x86_64.whl", hash = "sha256:c742f741d58a28940ce01d58f0ab2ea3ced8b12402f162f4d534dfe18ba1cd6a", size = 155924, upload-time = "2026-03-25T20:21:21.626Z" }, + { url = "https://files.pythonhosted.org/packages/dc/c7/62d7a17c26487ade21c5422b646110f2162f1fcc95980ef7f63e73c68f14/tomli-2.4.1-cp312-cp312-macosx_11_0_arm64.whl", hash = "sha256:7f86fd587c4ed9dd76f318225e7d9b29cfc5a9d43de44e5754db8d1128487085", size = 150018, upload-time = "2026-03-25T20:21:23.002Z" }, + { url = "https://files.pythonhosted.org/packages/5c/05/79d13d7c15f13bdef410bdd49a6485b1c37d28968314eabee452c22a7fda/tomli-2.4.1-cp312-cp312-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:ff18e6a727ee0ab0388507b89d1bc6a22b138d1e2fa56d1ad494586d61d2eae9", size = 244948, upload-time = "2026-03-25T20:21:24.04Z" }, + { url = "https://files.pythonhosted.org/packages/10/90/d62ce007a1c80d0b2c93e02cab211224756240884751b94ca72df8a875ca/tomli-2.4.1-cp312-cp312-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:136443dbd7e1dee43c68ac2694fde36b2849865fa258d39bf822c10e8068eac5", size = 253341, upload-time = "2026-03-25T20:21:25.177Z" }, + { url = "https://files.pythonhosted.org/packages/1a/7e/caf6496d60152ad4ed09282c1885cca4eea150bfd007da84aea07bcc0a3e/tomli-2.4.1-cp312-cp312-musllinux_1_2_aarch64.whl", hash = "sha256:5e262d41726bc187e69af7825504c933b6794dc3fbd5945e41a79bb14c31f585", size = 248159, upload-time = "2026-03-25T20:21:26.364Z" }, + { url = "https://files.pythonhosted.org/packages/99/e7/c6f69c3120de34bbd882c6fba7975f3d7a746e9218e56ab46a1bc4b42552/tomli-2.4.1-cp312-cp312-musllinux_1_2_x86_64.whl", hash = "sha256:5cb41aa38891e073ee49d55fbc7839cfdb2bc0e600add13874d048c94aadddd1", size = 253290, upload-time = "2026-03-25T20:21:27.46Z" }, + { url = "https://files.pythonhosted.org/packages/d6/2f/4a3c322f22c5c66c4b836ec58211641a4067364f5dcdd7b974b4c5da300c/tomli-2.4.1-cp312-cp312-win32.whl", hash = "sha256:da25dc3563bff5965356133435b757a795a17b17d01dbc0f42fb32447ddfd917", size = 98141, upload-time = "2026-03-25T20:21:28.492Z" }, + { url = "https://files.pythonhosted.org/packages/24/22/4daacd05391b92c55759d55eaee21e1dfaea86ce5c571f10083360adf534/tomli-2.4.1-cp312-cp312-win_amd64.whl", hash = "sha256:52c8ef851d9a240f11a88c003eacb03c31fc1c9c4ec64a99a0f922b93874fda9", size = 108847, upload-time = "2026-03-25T20:21:29.386Z" }, + { url = "https://files.pythonhosted.org/packages/68/fd/70e768887666ddd9e9f5d85129e84910f2db2796f9096aa02b721a53098d/tomli-2.4.1-cp312-cp312-win_arm64.whl", hash = "sha256:f758f1b9299d059cc3f6546ae2af89670cb1c4d48ea29c3cacc4fe7de3058257", size = 95088, upload-time = "2026-03-25T20:21:30.677Z" }, + { url = "https://files.pythonhosted.org/packages/07/06/b823a7e818c756d9a7123ba2cda7d07bc2dd32835648d1a7b7b7a05d848d/tomli-2.4.1-cp313-cp313-macosx_10_13_x86_64.whl", hash = "sha256:36d2bd2ad5fb9eaddba5226aa02c8ec3fa4f192631e347b3ed28186d43be6b54", size = 155866, upload-time = "2026-03-25T20:21:31.65Z" }, + { url = "https://files.pythonhosted.org/packages/14/6f/12645cf7f08e1a20c7eb8c297c6f11d31c1b50f316a7e7e1e1de6e2e7b7e/tomli-2.4.1-cp313-cp313-macosx_11_0_arm64.whl", hash = "sha256:eb0dc4e38e6a1fd579e5d50369aa2e10acfc9cace504579b2faabb478e76941a", size = 149887, upload-time = "2026-03-25T20:21:33.028Z" }, + { url = "https://files.pythonhosted.org/packages/5c/e0/90637574e5e7212c09099c67ad349b04ec4d6020324539297b634a0192b0/tomli-2.4.1-cp313-cp313-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:c7f2c7f2b9ca6bdeef8f0fa897f8e05085923eb091721675170254cbc5b02897", size = 243704, upload-time = "2026-03-25T20:21:34.51Z" }, + { url = "https://files.pythonhosted.org/packages/10/8f/d3ddb16c5a4befdf31a23307f72828686ab2096f068eaf56631e136c1fdd/tomli-2.4.1-cp313-cp313-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:f3c6818a1a86dd6dca7ddcaaf76947d5ba31aecc28cb1b67009a5877c9a64f3f", size = 251628, upload-time = "2026-03-25T20:21:36.012Z" }, + { url = "https://files.pythonhosted.org/packages/e3/f1/dbeeb9116715abee2485bf0a12d07a8f31af94d71608c171c45f64c0469d/tomli-2.4.1-cp313-cp313-musllinux_1_2_aarch64.whl", hash = "sha256:d312ef37c91508b0ab2cee7da26ec0b3ed2f03ce12bd87a588d771ae15dcf82d", size = 247180, upload-time = "2026-03-25T20:21:37.136Z" }, + { url = "https://files.pythonhosted.org/packages/d3/74/16336ffd19ed4da28a70959f92f506233bd7cfc2332b20bdb01591e8b1d1/tomli-2.4.1-cp313-cp313-musllinux_1_2_x86_64.whl", hash = "sha256:51529d40e3ca50046d7606fa99ce3956a617f9b36380da3b7f0dd3dd28e68cb5", size = 251674, upload-time = "2026-03-25T20:21:38.298Z" }, + { url = "https://files.pythonhosted.org/packages/16/f9/229fa3434c590ddf6c0aa9af64d3af4b752540686cace29e6281e3458469/tomli-2.4.1-cp313-cp313-win32.whl", hash = "sha256:2190f2e9dd7508d2a90ded5ed369255980a1bcdd58e52f7fe24b8162bf9fedbd", size = 97976, upload-time = "2026-03-25T20:21:39.316Z" }, + { url = "https://files.pythonhosted.org/packages/6a/1e/71dfd96bcc1c775420cb8befe7a9d35f2e5b1309798f009dca17b7708c1e/tomli-2.4.1-cp313-cp313-win_amd64.whl", hash = "sha256:8d65a2fbf9d2f8352685bc1364177ee3923d6baf5e7f43ea4959d7d8bc326a36", size = 108755, upload-time = "2026-03-25T20:21:40.248Z" }, + { url = "https://files.pythonhosted.org/packages/83/7a/d34f422a021d62420b78f5c538e5b102f62bea616d1d75a13f0a88acb04a/tomli-2.4.1-cp313-cp313-win_arm64.whl", hash = "sha256:4b605484e43cdc43f0954ddae319fb75f04cc10dd80d830540060ee7cd0243cd", size = 95265, upload-time = "2026-03-25T20:21:41.219Z" }, + { url = "https://files.pythonhosted.org/packages/3c/fb/9a5c8d27dbab540869f7c1f8eb0abb3244189ce780ba9cd73f3770662072/tomli-2.4.1-cp314-cp314-macosx_10_15_x86_64.whl", hash = "sha256:fd0409a3653af6c147209d267a0e4243f0ae46b011aa978b1080359fddc9b6cf", size = 155726, upload-time = "2026-03-25T20:21:42.23Z" }, + { url = "https://files.pythonhosted.org/packages/62/05/d2f816630cc771ad836af54f5001f47a6f611d2d39535364f148b6a92d6b/tomli-2.4.1-cp314-cp314-macosx_11_0_arm64.whl", hash = "sha256:a120733b01c45e9a0c34aeef92bf0cf1d56cfe81ed9d47d562f9ed591a9828ac", size = 149859, upload-time = "2026-03-25T20:21:43.386Z" }, + { url = "https://files.pythonhosted.org/packages/ce/48/66341bdb858ad9bd0ceab5a86f90eddab127cf8b046418009f2125630ecb/tomli-2.4.1-cp314-cp314-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:559db847dc486944896521f68d8190be1c9e719fced785720d2216fe7022b662", size = 244713, upload-time = "2026-03-25T20:21:44.474Z" }, + { url = "https://files.pythonhosted.org/packages/df/6d/c5fad00d82b3c7a3ab6189bd4b10e60466f22cfe8a08a9394185c8a8111c/tomli-2.4.1-cp314-cp314-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:01f520d4f53ef97964a240a035ec2a869fe1a37dde002b57ebc4417a27ccd853", size = 252084, upload-time = "2026-03-25T20:21:45.62Z" }, + { url = "https://files.pythonhosted.org/packages/00/71/3a69e86f3eafe8c7a59d008d245888051005bd657760e96d5fbfb0b740c2/tomli-2.4.1-cp314-cp314-musllinux_1_2_aarch64.whl", hash = "sha256:7f94b27a62cfad8496c8d2513e1a222dd446f095fca8987fceef261225538a15", size = 247973, upload-time = "2026-03-25T20:21:46.937Z" }, + { url = "https://files.pythonhosted.org/packages/67/50/361e986652847fec4bd5e4a0208752fbe64689c603c7ae5ea7cb16b1c0ca/tomli-2.4.1-cp314-cp314-musllinux_1_2_x86_64.whl", hash = "sha256:ede3e6487c5ef5d28634ba3f31f989030ad6af71edfb0055cbbd14189ff240ba", size = 256223, upload-time = "2026-03-25T20:21:48.467Z" }, + { url = "https://files.pythonhosted.org/packages/8c/9a/b4173689a9203472e5467217e0154b00e260621caa227b6fa01feab16998/tomli-2.4.1-cp314-cp314-win32.whl", hash = "sha256:3d48a93ee1c9b79c04bb38772ee1b64dcf18ff43085896ea460ca8dec96f35f6", size = 98973, upload-time = "2026-03-25T20:21:49.526Z" }, + { url = "https://files.pythonhosted.org/packages/14/58/640ac93bf230cd27d002462c9af0d837779f8773bc03dee06b5835208214/tomli-2.4.1-cp314-cp314-win_amd64.whl", hash = "sha256:88dceee75c2c63af144e456745e10101eb67361050196b0b6af5d717254dddf7", size = 109082, upload-time = "2026-03-25T20:21:50.506Z" }, + { url = "https://files.pythonhosted.org/packages/d5/2f/702d5e05b227401c1068f0d386d79a589bb12bf64c3d2c72ce0631e3bc49/tomli-2.4.1-cp314-cp314-win_arm64.whl", hash = "sha256:b8c198f8c1805dc42708689ed6864951fd2494f924149d3e4bce7710f8eb5232", size = 96490, upload-time = "2026-03-25T20:21:51.474Z" }, + { url = "https://files.pythonhosted.org/packages/45/4b/b877b05c8ba62927d9865dd980e34a755de541eb65fffba52b4cc495d4d2/tomli-2.4.1-cp314-cp314t-macosx_10_15_x86_64.whl", hash = "sha256:d4d8fe59808a54658fcc0160ecfb1b30f9089906c50b23bcb4c69eddc19ec2b4", size = 164263, upload-time = "2026-03-25T20:21:52.543Z" }, + { url = "https://files.pythonhosted.org/packages/24/79/6ab420d37a270b89f7195dec5448f79400d9e9c1826df982f3f8e97b24fd/tomli-2.4.1-cp314-cp314t-macosx_11_0_arm64.whl", hash = "sha256:7008df2e7655c495dd12d2a4ad038ff878d4ca4b81fccaf82b714e07eae4402c", size = 160736, upload-time = "2026-03-25T20:21:53.674Z" }, + { url = "https://files.pythonhosted.org/packages/02/e0/3630057d8eb170310785723ed5adcdfb7d50cb7e6455f85ba8a3deed642b/tomli-2.4.1-cp314-cp314t-manylinux2014_aarch64.manylinux_2_17_aarch64.manylinux_2_28_aarch64.whl", hash = "sha256:1d8591993e228b0c930c4bb0db464bdad97b3289fb981255d6c9a41aedc84b2d", size = 270717, upload-time = "2026-03-25T20:21:55.129Z" }, + { url = "https://files.pythonhosted.org/packages/7a/b4/1613716072e544d1a7891f548d8f9ec6ce2faf42ca65acae01d76ea06bb0/tomli-2.4.1-cp314-cp314t-manylinux2014_x86_64.manylinux_2_17_x86_64.manylinux_2_28_x86_64.whl", hash = "sha256:734e20b57ba95624ecf1841e72b53f6e186355e216e5412de414e3c51e5e3c41", size = 278461, upload-time = "2026-03-25T20:21:56.228Z" }, + { url = "https://files.pythonhosted.org/packages/05/38/30f541baf6a3f6df77b3df16b01ba319221389e2da59427e221ef417ac0c/tomli-2.4.1-cp314-cp314t-musllinux_1_2_aarch64.whl", hash = "sha256:8a650c2dbafa08d42e51ba0b62740dae4ecb9338eefa093aa5c78ceb546fcd5c", size = 274855, upload-time = "2026-03-25T20:21:57.653Z" }, + { url = "https://files.pythonhosted.org/packages/77/a3/ec9dd4fd2c38e98de34223b995a3b34813e6bdadf86c75314c928350ed14/tomli-2.4.1-cp314-cp314t-musllinux_1_2_x86_64.whl", hash = "sha256:504aa796fe0569bb43171066009ead363de03675276d2d121ac1a4572397870f", size = 283144, upload-time = "2026-03-25T20:21:59.089Z" }, + { url = "https://files.pythonhosted.org/packages/ef/be/605a6261cac79fba2ec0c9827e986e00323a1945700969b8ee0b30d85453/tomli-2.4.1-cp314-cp314t-win32.whl", hash = "sha256:b1d22e6e9387bf4739fbe23bfa80e93f6b0373a7f1b96c6227c32bef95a4d7a8", size = 108683, upload-time = "2026-03-25T20:22:00.214Z" }, + { url = "https://files.pythonhosted.org/packages/12/64/da524626d3b9cc40c168a13da8335fe1c51be12c0a63685cc6db7308daae/tomli-2.4.1-cp314-cp314t-win_amd64.whl", hash = "sha256:2c1c351919aca02858f740c6d33adea0c5deea37f9ecca1cc1ef9e884a619d26", size = 121196, upload-time = "2026-03-25T20:22:01.169Z" }, + { url = "https://files.pythonhosted.org/packages/5a/cd/e80b62269fc78fc36c9af5a6b89c835baa8af28ff5ad28c7028d60860320/tomli-2.4.1-cp314-cp314t-win_arm64.whl", hash = "sha256:eab21f45c7f66c13f2a9e0e1535309cee140182a9cdae1e041d02e47291e8396", size = 100393, upload-time = "2026-03-25T20:22:02.137Z" }, + { url = "https://files.pythonhosted.org/packages/7b/61/cceae43728b7de99d9b847560c262873a1f6c98202171fd5ed62640b494b/tomli-2.4.1-py3-none-any.whl", hash = "sha256:0d85819802132122da43cb86656f8d1f8c6587d54ae7dcaf30e90533028b49fe", size = 14583, upload-time = "2026-03-25T20:22:03.012Z" }, +] + [[package]] name = "typing-extensions" version = "4.16.0"