From 81ed2571773e96e7179c36a2081d07bdf16f36d3 Mon Sep 17 00:00:00 2001 From: Jarry Shaw Date: Mon, 26 Oct 2026 15:08:06 -0400 Subject: [PATCH] docs(tests): seventh whole-repo pass over the tests (#1103) Full read of every docstring and comment under tests/. - protocols, vendor, const, integration: stale claims (RFC sections, byte counts, the L2TP field mapping, vendor-extra installs, HIP_COPIES), the 24-bit PRI value, change-relative narratives and counts. - project: claims checked against the workflows, util scripts, docs and pyproject; the #1967 parity tests describe the inline shell form as the reference the scripts must match. - corekit, foundation, utilities, toolkit, dumpkit, interface, cli, root modules: stale claims, "without the fix" restatements, counts, issue references as #NNN, never-released names dropped from docstrings. Docstrings and comments only; test names, assertion messages and string literals are unchanged. --- tests/_dependency_gates.py | 16 +- tests/_tiers.py | 14 +- tests/cli/test_main.py | 6 +- tests/conftest.py | 6 +- tests/const/test_const_apptype_split_unit.py | 25 +- tests/const/test_const_enum_builtin_parity.py | 57 ++-- tests/const/test_const_enum_no_mint.py | 20 +- tests/const/test_const_ethertype_862_unit.py | 11 +- ...onst_ftp_command_get_key_type_1824_unit.py | 6 +- .../test_const_ftp_featcode_case_903_unit.py | 8 +- ...st_const_ipv6_extension_header_925_unit.py | 2 +- tests/const/test_const_linktype_209_unit.py | 36 +-- ...st_const_method_case_sensitive_896_unit.py | 19 +- ...test_const_method_value_lookup_908_unit.py | 38 +-- ...ster_validates_every_registry_1816_unit.py | 6 +- tests/const/test_const_registry_protocol.py | 33 +- .../const/test_const_str_payload_870_unit.py | 51 ++- tests/corekit/test_decorators.py | 2 +- .../corekit/test_decorators_path_1735_unit.py | 6 +- ..._enum_get_exception_provenance_923_unit.py | 7 +- .../test_fields_all_exports_2174_unit.py | 14 +- .../test_fields_byteorder_1519_unit.py | 8 +- .../test_fields_misc_packet_context.py | 2 +- ..._numbers_port_enum_field_peek_1774_unit.py | 2 +- .../test_fields_numbers_unassigned_enum.py | 15 +- tests/corekit/test_io.py | 33 +- ...o_seekable_reader_short_reads_1586_unit.py | 54 ++-- .../test_io_stream_proxies_1516_unit.py | 3 +- tests/corekit/test_sentinels_housing_unit.py | 12 +- tests/dumpkit/test_common_unit.py | 12 +- .../test_nameless_enum_rendering_unit.py | 25 +- .../engines/test_dpkt_parse_error_unit.py | 12 +- .../test_dpkt_pcapng_interfaces_unit.py | 12 +- .../test_dpkt_raw_null_linktypes_unit.py | 8 +- tests/foundation/engines/test_engine_base.py | 4 +- .../test_engine_verbose_callback_1874_unit.py | 9 +- tests/foundation/engines/test_pcap_backend.py | 3 +- .../test_pcap_backend_probe_once_2113_unit.py | 7 +- .../test_pcapng_spb_zero_snaplen_1558_unit.py | 10 +- .../test_pypcap_unusable_module_2132_unit.py | 6 +- .../foundation/engines/test_pyshark_engine.py | 26 +- .../engines/test_pyshark_non_packet_unit.py | 10 +- tests/foundation/engines/test_scapy_engine.py | 24 +- .../test_scapy_pcapng_sections_runtime.py | 10 +- .../engines/test_scapy_reader_hooks_unit.py | 2 +- .../test_scapy_reader_interfaces_runtime.py | 10 +- .../test_scapy_record_length_1717_runtime.py | 4 +- .../test_scapy_record_length_1717_unit.py | 3 +- .../foundation/reassembly/data/test_models.py | 9 +- tests/foundation/reassembly/test_ip.py | 22 +- ...est_ip_cut_non_final_fragment_1567_unit.py | 10 +- .../test_ip_last_fragment_offset_1565_unit.py | 10 +- .../test_ip_length_limit_1585_unit.py | 6 +- .../test_ip_oversized_datagram_1566_unit.py | 6 +- .../test_ip_reassembly_roundtrip_unit.py | 11 +- .../test_ip_tso_total_length_1555_runtime.py | 8 +- .../test_ip_tso_total_length_1555_unit.py | 2 +- ...pv6_cut_non_final_fragment_1567_runtime.py | 10 +- .../reassembly/test_reassembly_base.py | 4 +- tests/foundation/reassembly/test_tcp.py | 57 ++-- .../test_tcp_reassembly_roundtrip_unit.py | 14 +- tests/foundation/registry/test_foundation.py | 16 +- tests/foundation/registry/test_protocols.py | 18 +- .../test_registrar_code_check_unit.py | 7 +- .../test_registry_name_restore_unit.py | 11 +- .../registry/test_restore_builtin_unit.py | 16 +- tests/foundation/test_extraction.py | 39 +-- ...ion_constructor_closes_engine_2107_unit.py | 6 +- ...extraction_constructor_raises_2100_unit.py | 8 +- ..._extraction_engine_fallback_format_unit.py | 13 +- tests/foundation/test_extraction_no_eof.py | 37 +-- ...xtraction_non_seekable_stream_1586_unit.py | 53 ++-- tests/foundation/test_extraction_ownership.py | 12 +- ...raction_windows_pipe_seekable_2097_unit.py | 8 +- tests/foundation/traceflow/test_tcp.py | 7 +- .../traceflow/test_traceflow_base.py | 4 +- .../test_traceflow_label_unique_unit.py | 5 +- tests/integration/test_files_output_naming.py | 16 +- tests/integration/test_pcapng_end_to_end.py | 4 +- .../integration/test_reassembly_end_to_end.py | 28 +- .../test_reassembly_engine_parity.py | 13 +- tests/interface/test_core.py | 2 +- tests/interface/test_registry.py | 10 +- tests/project/test_actions_helper_2047.py | 4 +- tests/project/test_actions_pinned_1968.py | 2 +- tests/project/test_annotation_names.py | 48 ++- tests/project/test_apt_packages_1967.py | 25 +- .../test_ast_unparse_is_gated_1899_unit.py | 4 +- tests/project/test_benchmark_workflow_2125.py | 4 +- tests/project/test_build_leftovers_1773.py | 2 +- tests/project/test_build_only_dispatch.py | 12 +- tests/project/test_bump_version.py | 15 +- tests/project/test_capture_tracking.py | 25 +- tests/project/test_changelog_md.py | 23 +- tests/project/test_ci_timing.py | 13 +- .../test_class_guard_annotations_1857_unit.py | 8 +- tests/project/test_codeql_config_2130.py | 6 +- .../test_conda_intel_cli_upload_2044.py | 6 +- ...test_conda_legs_install_no_pcapkit_1854.py | 12 +- .../test_conda_native_platforms_1822.py | 2 +- .../test_conda_no_windows_steps_1967.py | 6 +- tests/project/test_conda_windows_leg_2060.py | 9 +- tests/project/test_conventions_doc_claims.py | 182 +++++------ tests/project/test_coverage_rcfile.py | 2 +- tests/project/test_cron_reports_2030.py | 6 +- tests/project/test_discussion_report_2030.py | 2 +- .../test_docs_aenum_formatter_1858_unit.py | 4 +- .../test_docs_ambiguous_xrefs_1775_unit.py | 23 +- .../test_docs_class_signature_1860_unit.py | 10 +- .../test_docs_enum_signature_1862_unit.py | 4 +- .../test_docs_epilog_links_1936_unit.py | 3 +- .../test_docs_internal_refs_1940_unit.py | 8 +- .../test_docs_internal_refs_1947_unit.py | 4 +- .../test_docs_missing_directives_2234_unit.py | 2 +- .../test_docs_preserve_defaults_1859_unit.py | 6 +- .../test_docs_table_wrapper_ids_1861_unit.py | 4 +- .../test_docs_typehints_pin_1856_unit.py | 6 +- .../test_docs_viewcode_builtins_1863_unit.py | 4 +- .../test_docs_warns_xrefs_2094_unit.py | 2 +- .../test_experimental_concurrency_1903.py | 4 +- .../test_ftp_featcode_doc_page_934_unit.py | 2 +- tests/project/test_graalpy_shards_1963.py | 7 +- tests/project/test_isort_clean.py | 7 +- .../test_ktlo_jobs_gate_nothing_1738.py | 2 +- .../project/test_ktlo_legs_stay_green_1672.py | 18 +- tests/project/test_lint_pages_reports_2030.py | 4 +- tests/project/test_module_isolation.py | 6 +- ...est_no_engine_agreement_prose_1708_unit.py | 5 +- ...test_no_moved_name_re_exports_1744_unit.py | 7 +- tests/project/test_no_setup_py_2061.py | 11 +- ...irectives_in_generated_tables_1884_unit.py | 6 +- tests/project/test_per_class_import_guard.py | 5 +- tests/project/test_pip_install_1967.py | 27 +- ...test_pr_template_commit_types_1102_unit.py | 4 +- tests/project/test_project_pins_once_1954.py | 6 +- tests/project/test_project_status.py | 14 +- tests/project/test_public_api.py | 15 +- .../test_pylint_pragma_names_1923_unit.py | 4 +- tests/project/test_pyshark_encap_map.py | 16 +- .../project/test_python_matrix_tiers_1633.py | 22 +- tests/project/test_python_versions_1660.py | 22 +- .../test_python_versions_update_1660.py | 13 +- tests/project/test_regenerate_samples_1967.py | 12 +- .../project/test_release_concurrency_1705.py | 2 +- .../test_release_conda_leg_smoke_1855.py | 11 +- .../test_release_conda_scripts_1967.py | 19 +- tests/project/test_release_gates.py | 298 +++++++++--------- ...st_release_notice_states_no_totals_1849.py | 8 +- .../project/test_release_reset_build_1967.py | 16 +- tests/project/test_release_scripts_1967.py | 18 +- .../test_release_status_scripts_1967.py | 42 +-- .../test_release_tags_the_run_sha_1634.py | 41 +-- ...test_release_version_check_scripts_1967.py | 42 +-- tests/project/test_resource_monitor_1740.py | 8 +- tests/project/test_rfc_anchor_fragments.py | 14 +- .../test_rfc_constants_one_home_1739_unit.py | 8 +- .../project/test_secret_environments_1968.py | 3 +- .../test_sentinels_doc_page_934_unit.py | 4 +- tests/project/test_setuptools_floor_2062.py | 2 +- .../test_stdlib_feature_probes_1709_unit.py | 2 +- tests/project/test_superseded_runs_1938.py | 10 +- .../test_superseded_summary_copy_2078.py | 10 +- .../test_sweeps_on_latest_only_1732.py | 6 +- tests/project/test_unit_tests_queue.py | 10 +- .../test_unit_tests_shellcheck_1967.py | 6 +- .../project/test_unittest_ordering_shards.py | 8 +- ...est_utilities_import_boundary_1735_unit.py | 6 +- .../test_vendor_comment_only_no_bump_1674.py | 4 +- .../test_vendor_commits_new_modules_1685.py | 25 +- tests/project/test_vendor_test_gate_1853.py | 6 +- .../test_whole_suite_on_latest_1958.py | 2 +- tests/project/test_workflow_apt_cache.py | 10 +- tests/project/test_workflow_apt_timeouts.py | 12 +- tests/project/test_workflow_hardening_1968.py | 11 +- tests/project/test_workflow_run_verdict.py | 2 +- tests/project/test_workflow_scripts_1967.py | 6 +- .../test_workflows_at_a_glance_1732.py | 6 +- tests/protocols/_edge_roundtrip.py | 4 +- tests/protocols/application/test_ftp_unit.py | 12 +- .../application/test_http2_roundtrip_unit.py | 4 +- tests/protocols/application/test_http_unit.py | 27 +- .../test_httpv2_payload_length_unit.py | 25 +- .../application/test_layer_placement_unit.py | 12 +- .../test_ospf_auth_dict_roundtrip_unit.py | 2 +- ..._ospf_cryptographic_authentication_unit.py | 2 +- tests/protocols/application/test_ospf_unit.py | 4 +- .../test_ext_chain_limit_rebuild_1619_unit.py | 20 +- .../test_hip_locator_set_length_unit.py | 6 +- .../test_hip_r1_counter_width_unit.py | 12 +- tests/protocols/internet/test_hip_unit.py | 12 +- ...t_ipv4_header_truncated_payload_runtime.py | 2 +- tests/protocols/internet/test_ipv4_unit.py | 34 +- .../internet/test_ipv6_ext_hint_unit.py | 2 +- .../protocols/internet/test_ipv6_ext_unit.py | 12 +- .../internet/test_ipv6_extension_unit.py | 6 +- .../internet/test_ipv6_reassembly_runtime.py | 2 +- .../test_length_hint_minimum_header_unit.py | 2 +- .../test_message_spelling_2171_unit.py | 2 +- .../internet/test_mh_flags_reserved_unit.py | 2 +- tests/protocols/internet/test_mh_unit.py | 25 +- .../test_smf_dpd_mode_rebuild_unit.py | 2 +- .../protocols/link/test_l2tp_version_unit.py | 14 +- tests/protocols/link/test_link_unit.py | 5 +- .../misc/pcap/test_frame_length_runtime.py | 6 +- .../misc/test_misc_short_read_1470_runtime.py | 15 +- .../misc/test_pcapng_roundtrip_unit.py | 2 +- tests/protocols/misc/test_pcapng_unit.py | 25 +- .../test_schema_metaclass_abc_cache_unit.py | 8 +- .../test_construction_keyword_check_unit.py | 6 +- .../test_declared_length_roundtrip_runtime.py | 2 +- .../test_dispatch_default_resolution_unit.py | 2 +- .../protocols/test_dispatch_registry_unit.py | 4 +- .../test_frame_layer_limit_1610_unit.py | 2 +- ...t_next_layer_hit_not_memoised_1559_unit.py | 2 +- .../test_option_generator_tcp_base_unit.py | 12 +- tests/protocols/test_option_roundtrip_unit.py | 8 +- .../test_package_reexports_1807_unit.py | 2 +- tests/protocols/test_pcapng_regression.py | 10 +- .../test_protocol_code_registration_unit.py | 4 +- .../test_short_read_roundtrip_unit.py | 2 +- .../test_trial_parse_warnings_1580_unit.py | 2 +- .../transport/test_mptcp_roundtrip_unit.py | 4 +- .../transport/test_sctp_stray_octets_unit.py | 2 +- tests/protocols/transport/test_sctp_unit.py | 5 +- .../transport/test_tcp_http_dispatch_unit.py | 7 +- .../transport/test_tcp_mptcp_addaddr_unit.py | 46 ++- .../test_tcp_mptcp_capable_length_unit.py | 12 +- .../test_tcp_mptcp_error_message_unit.py | 10 +- .../test_tcp_mptcp_join_flag_ordering_unit.py | 6 +- .../test_tcp_mptcp_length_arithmetic_unit.py | 6 +- .../transport/test_tcp_mptcp_subtype_unit.py | 5 +- .../protocols/transport/test_tcp_udp_unit.py | 20 +- .../transport/test_transport_unit.py | 2 +- tests/test_base_class_contract.py | 26 +- tests/test_docstring_contract.py | 29 +- tests/test_final_enforcement.py | 83 ++--- tests/test_support_helpers.py | 79 ++--- tests/test_tier_guard.py | 107 ++++--- tests/test_tier_guard_xdist.py | 20 +- tests/test_windows_posix_gates_2102_unit.py | 4 +- .../test_dpkt_reassembly_rawbytes_unit.py | 15 +- .../test_dpkt_timestamp_copy_1647_unit.py | 6 +- tests/toolkit/test_dpkt_unit.py | 17 +- .../toolkit/test_engine_pcap_trace_runtime.py | 3 +- tests/toolkit/test_pcap_ct_unit.py | 4 +- ...pcap_tcp_segment_ipv6_version_1666_unit.py | 11 +- .../test_pcap_tcp_segment_public_1570_unit.py | 9 +- tests/toolkit/test_pcap_unit.py | 4 +- tests/toolkit/test_pypcap_unit.py | 4 +- .../toolkit/test_pypcapfile_tcp_last_unit.py | 2 +- tests/toolkit/test_pypcapfile_unit.py | 41 ++- tests/toolkit/test_pyshark_unit.py | 49 +-- .../test_rfc_constants_readers_1739_unit.py | 2 +- tests/toolkit/test_scapy_linktype_unit.py | 8 +- tests/toolkit/test_scapy_unit.py | 26 +- .../test_tcp_carrier_walk_1606_1617_unit.py | 6 +- .../test_tunnelled_tcp_flow_key_1581_unit.py | 10 +- tests/utilities/test_compat.py | 20 +- tests/utilities/test_config_unit.py | 23 +- tests/utilities/test_exceptions_excepthook.py | 27 +- tests/utilities/test_logging.py | 8 +- tests/utilities/test_stacklevel.py | 12 +- .../test_utilities_datetime_module_unit.py | 4 +- tests/vendor/test_ftp_return_code_unit.py | 5 +- tests/vendor/test_ipx_packet_unit.py | 5 +- tests/vendor/test_request_prompt_unit.py | 3 +- tests/vendor/test_user_agent_unit.py | 5 +- tests/vendor/test_vendor_dest_path_unit.py | 9 +- ...t_vendor_ipv6_extension_header_925_unit.py | 4 +- .../test_vendor_snapshot_restore_unit.py | 21 +- .../vendor/test_vendor_variables_1726_unit.py | 10 +- 271 files changed, 1925 insertions(+), 1913 deletions(-) diff --git a/tests/_dependency_gates.py b/tests/_dependency_gates.py index 1420d37881..f1fbd06722 100644 --- a/tests/_dependency_gates.py +++ b/tests/_dependency_gates.py @@ -120,8 +120,8 @@ #: (#1732), and :file:`cron-vendor.yml` runs ``tests/const`` and ``tests/vendor`` #: over a freshly crawled tree, where a failure discards modules rather than #: failing the job (#1853); neither is read here. Other workflows install -#: ``.[all]`` somewhere -- a docs build, a conda recipe, the lint pass, and the -#: release packaging workflow -- and none of them invokes the suite; +#: ``.[all]`` somewhere -- a docs build, a conda recipe, and the lint pass -- +#: and none of them invokes the suite; #: :file:`python-compatibility.yml` installs a bare ``.`` and only compiles and #: imports, and :file:`codeql-analysis.yml` installs nothing explicitly at all #: (CodeQL's own autobuild step). That is exactly why this module keys on *jobs that @@ -621,7 +621,7 @@ class AmbiguousProvider(NamedTuple): def requirement_key(text: 'str') -> 'Requirement': """Split one requirement string into a :class:`Requirement`. - Deliberately not a PEP 508 parser: it handles the four things the extras in + Deliberately not a PEP 508 parser: it handles what the extras in :file:`pyproject.toml` actually use -- a name, optional bracketed extras, an optional version specifier, and an optional ``;`` marker -- and normalises the name per PEP 503 so ``pcap-ct``, ``pcap_ct`` and ``PCAP.CT`` compare @@ -1040,11 +1040,11 @@ def _flag_imports(path: 'pathlib.Path') -> 'dict[str, str]': def flag_requirements() -> 'dict[tuple[str, str], frozenset[str]]': """``(module, flag)`` -> the modules that flag needs importable. - Resolved per file rather than per flag name, which matters for exactly one - flag and matters a lot: ``HAS_RUNTIME`` names the core dependencies in - every module but one, and in + Resolved per file rather than per flag name, because one flag name can + probe different modules in different files: ``HAS_RUNTIME`` names the core + dependencies in most modules, and in :file:`tests/foundation/engines/test_runtime_engines.py` it names those - plus ``dpkt``, ``scapy`` and ``pyshark``. Keyed on the name alone, that one + plus ``dpkt``, ``scapy`` and ``pyshark``. Keyed on the name alone, one module's requirements would be demanded of every other module that gates on it, or they would excuse it. @@ -1420,7 +1420,7 @@ def job_reaches(job: 'Job', gate: 'Gate') -> 'bool': that gate skip, so one selected test is enough for the install line to matter. A node ID naming one method of a class therefore does reach a class-level gate on that class, even though the class's other methods run - elsewhere; :data:`Gap` counts gate decorators rather than methods, so nothing + elsewhere; :class:`Gap` counts gate decorators rather than methods, so nothing is double-counted by that. The strict reading -- "every test the gate guards" -- would be the unsafe one, because it would let a job that runs one gated test out of three off the hook entirely. diff --git a/tests/_tiers.py b/tests/_tiers.py index c75341b300..75d8d606fb 100644 --- a/tests/_tiers.py +++ b/tests/_tiers.py @@ -14,9 +14,9 @@ :file:`*_runtime.py`, :file:`*_regression.py` =================================== ========================================== -The unit tier has to pass on a fresh clone with nothing installed but -``pip install -e '.[test]'``, which is exactly how the ``test`` job of -:file:`.github/workflows/unit-tests.yml` runs it -- see +The unit tier has to pass on a fresh clone with no capture generated, which is +exactly how the ``test`` job of :file:`.github/workflows/unit-tests.yml` runs +it -- see :data:`UNIT_TIER_SELECTION` for the selection it uses. The fixture-dependent tier runs only after :file:`examples/generators/make_samples.py` has rebuilt :file:`examples/captures/`, so it may read any capture it likes. @@ -278,9 +278,11 @@ #: :func:`is_project_pin`. PROJECT_PINS_DIR = 'project' #: The project pins' selector, verbatim from the workflows: the ``project-tests`` -#: job runs what it prints, and every ``test`` and ``experimental`` leg passes each -#: path as an ``--ignore``. Asked at run time, as :data:`FIXTURE_TIER_SELECTOR` is, -#: so neither workflow can drift from :func:`is_project_pin`. +#: job runs what it prints, and the ``latest`` leg of ``test`` passes each path as +#: an ``--ignore``. Every other ``test`` leg and every ``experimental`` leg leaves +#: the pins out through :data:`LATEST_TIER_SELECTOR`, since every pin is +#: ``LATEST``. Asked at run time, as :data:`FIXTURE_TIER_SELECTOR` is, so no +#: workflow can drift from :func:`is_project_pin`. PROJECT_PINS_SELECTOR = ( 'python -c "from tests._tiers import project_pin_paths; ' 'print(\' \'.join(project_pin_paths()))"' diff --git a/tests/cli/test_main.py b/tests/cli/test_main.py index 8a115b4ed5..1385786d72 100644 --- a/tests/cli/test_main.py +++ b/tests/cli/test_main.py @@ -386,9 +386,9 @@ def test_help_strings_have_no_known_slips(self) -> None: def test_layer_and_protocol_reach_the_extractor(self) -> None: """``-L``/``-P`` are forwarded under the names ``Extractor`` reads. - The CLI half of GH-356. The forwarding was always correct -- it is the - core that dropped the limits -- so this pins the contract that made it - correct: ``Extractor`` takes ``layer=`` and ``protocol=``, and the CLI + The CLI half of GH-356. The defect there is the core dropping the + limits, not the forwarding, so this pins the contract the forwarding + rests on: ``Extractor`` takes ``layer=`` and ``protocol=``, and the CLI must not invent its own spelling for either. """ diff --git a/tests/conftest.py b/tests/conftest.py index ec2cb3725c..23f52fb299 100644 --- a/tests/conftest.py +++ b/tests/conftest.py @@ -77,7 +77,7 @@ _pinned_snapshot = None # type: Optional[dict[str, types.ModuleType]] #: What :func:`pytest_sessionstart` imports before the pin: the package, plus the -#: two app-type registries it loads on first use rather than with itself (GitHub +#: app-type registries it loads on first use rather than with itself (GitHub #: issue #1538). See that hook for why the registries cannot be left out. WARM_MODULES = ('pcapkit', 'pcapkit.const.reg.apptype.tcp', 'pcapkit.const.reg.apptype.udp') @@ -163,7 +163,7 @@ def pytest_sessionstart(session: 'pytest.Session') -> 'None': that imports the library and nothing else leaves the region exactly as it found it, so there is nothing to put back. - The two lazily loaded app-type registries in :data:`WARM_MODULES` are + The lazily loaded app-type registries in :data:`WARM_MODULES` are imported here too, and that is for correctness rather than speed. Left out of the pin, they were dropped from :data:`sys.modules` after whichever test first loaded them, while the package kept the classes it had already bound @@ -692,7 +692,7 @@ def _tier_guard_reader_hold() -> 'Iterator[None]': @pytest.fixture(autouse=True) def _tier_guard_reader_lock(request: 'pytest.FixtureRequest') -> 'Iterator[None]': - """Hold the probe/scan mutex shared, for every unit-tier test but the two + """Hold the probe/scan mutex shared, for every unit-tier test but those that self-manage it. See :func:`_tier_guard_lock` for the race this closes. Deciding by diff --git a/tests/const/test_const_apptype_split_unit.py b/tests/const/test_const_apptype_split_unit.py index 9de0237c56..6ec58f2fe6 100644 --- a/tests/const/test_const_apptype_split_unit.py +++ b/tests/const/test_const_apptype_split_unit.py @@ -43,8 +43,8 @@ def _dunder_sample(registry: 'type') -> 'list': """The members of ``registry`` the byte-identity sweep formats, in row order. - GitHub issue #1062. Sweeping every member ran a subtest per member, and - per-test coverage contexts found only a few distinct arc sets among them: + GitHub issue #1062. Sweeping every member runs a subtest per member, and + per-test coverage contexts find only a few distinct arc sets among them: ``repr`` and ``str``, each with and without an ``(aliases: ...)`` suffix -- the registry, the port and the service name never change which code runs. So the sample keeps every member that carries an alias (the suffix branch), @@ -144,8 +144,8 @@ def test_a_member_is_still_an_apptype(self) -> None: # One service on two transports is two members, not one shared object -- # they carry the same name and differ in which registry they belong to, - # which since GitHub issue #806 is what ``proto`` says. It named the whole - # IANA set on both of them before, so the two compared equal. + # which is what ``proto`` says (GitHub issue #806). Guards against it + # naming the whole IANA set on both, under which the two compare equal. self.assertIsNot(TCP.http, UDP.http) self.assertEqual(TCP.http.svc, UDP.http.svc) self.assertNotEqual(TCP.http.proto, UDP.http.proto) @@ -1271,8 +1271,8 @@ def test_no_lookup_call_site_in_the_library_builds_a_composite_proto(self) -> No apptype_registry.UDP.__proto__[65210] = previous def test_the_new_dunders_are_byte_identical_to_the_percent_form(self) -> None: - """GitHub issue #798: ``AppType``'s ``__new__``/``__repr__``/``__str__`` moved - from ``%`` formatting to f-strings, and ``__new__``'s format sets every + """GitHub issue #798: ``AppType``'s ``__new__``/``__repr__``/``__str__`` format + with f-strings rather than ``%``, and ``__new__``'s format sets every real member's underlying :class:`~aenum.StrEnum` value -- a far larger blast radius than an error path. It is checked on a deterministic sample rather than member by member, because one f-string formats every @@ -1297,7 +1297,7 @@ def test_the_new_dunders_are_byte_identical_to_the_percent_form(self) -> None: :meth:`~unittest.TestCase.addCleanup`, but the population check below is only meaningful against a registry no other test has touched. - GitHub issue #807 grew ``repr``/``str`` an ``(aliases: ...)`` suffix + ``repr``/``str`` carry an ``(aliases: ...)`` suffix (GitHub issue #807) for a member that shares its port with another service in the same registry, so ``old_repr``/``old_str`` below append the identical suffix through ``member.aliases`` before comparing. ``old_value`` @@ -1349,10 +1349,11 @@ def test_every_member_renders_its_own_registrys_transport_protocol(self) -> None ``proto.name`` is folded into the member's underlying :class:`~aenum.StrEnum` *value* by ``__new__``, not merely into its display, - so retyping ``proto`` rewrites ``TCP.http.value`` from - ``'http [80 - tcp|udp|sctp]'`` to ``'http [80 - tcp]'``. ``_value_`` is the - live key in ``_value2member_map_``, which is why the retype has to happen in - the generated source rather than at runtime -- a member whose ``_value_`` + so a single-transport ``proto`` makes ``TCP.http.value`` + ``'http [80 - tcp]'`` where the whole IANA set would make it + ``'http [80 - tcp|udp|sctp]'``. ``_value_`` is the live key in + ``_value2member_map_``, which is why ``proto`` is set in the generated + source rather than at runtime -- a member whose ``_value_`` were edited afterwards would no longer be reachable by its own value. Checked against the formula rather than against the member's own ``proto``: @@ -1369,7 +1370,7 @@ def test_every_member_renders_its_own_registrys_transport_protocol(self) -> None modules declare (see :func:`_declared`), not against counts a vendor update moves (GitHub issue #1820). - GitHub issue #807 grew ``str``/``repr`` an ``(aliases: ...)`` suffix + ``str``/``repr`` carry an ``(aliases: ...)`` suffix (GitHub issue #807) for a member sharing its port with another service in this registry, so ``value``/``shown`` below grow the identical suffix through ``member.aliases`` before comparing. The lookup round-trip at the diff --git a/tests/const/test_const_enum_builtin_parity.py b/tests/const/test_const_enum_builtin_parity.py index 968e31758f..8b4ec20fd9 100644 --- a/tests/const/test_const_enum_builtin_parity.py +++ b/tests/const/test_const_enum_builtin_parity.py @@ -25,7 +25,7 @@ :exc:`AttributeError` (``'int' object has no attribute 'upper'``) rather than :exc:`ValueError`. -All six carry a guard raising a bare :exc:`ValueError`, emitted by their +Each carries a guard raising a bare :exc:`ValueError`, emitted by their templates under :mod:`pcapkit.vendor` rather than hand-written -- the tree is generated, so a fix that is not in the template is reverted by the next crawl. @@ -74,7 +74,7 @@ #: same reason. UNRESOLVABLE = 1 << 70 -#: The six registries GitHub issue #647 converged, as ``(module, class name, +#: The registries GitHub issue #647 converged, as ``(module, class name, #: what the lookup did with an invalid value without the fix)``. Named individually as #: well as swept, so a regression reports *which* registry came apart rather #: than only that the sweep shrank. @@ -119,12 +119,8 @@ #: Each was rendering a registry that GitHub issue #647 found unguarded. #: #: Keyed to each template's own guard text rather than one literal shared by -#: all of them, kept as a dict rather than collapsed back to one shared literal: -#: GitHub issue #792 moved ``pcapkit.vendor.reg.apptype.apptype``'s copy to an -#: f-string first, deliberately leaving the others on ``%`` so its own diff over -#: the registry's very large generated file stayed reviewable, and GitHub issue -#: #798 swept the rest onto the same f-string form. They all agree, but the -#: dict stays: a future bespoke guard is not guaranteed to match this +#: all of them. They all use the same f-string form (GitHub issues #792 and +#: #798), but the dict stays: a future bespoke guard is not guaranteed to match this #: one's shape, and what #647 actually needs pinned is that each template #: still carries *a* guard rejecting an invalid value, not that every template #: shares one literal. @@ -143,7 +139,7 @@ ('pcapkit.vendor.http.method', 'Method', 'pcapkit.const.http.method'), ) -#: The three GitHub issue #818 finishes, as ``(vendor module, class name, +#: The registries GitHub issue #818 finishes, as ``(vendor module, class name, #: const module)`` -- the same shape as :data:`ISSUE_804_PAIR`, for the #: ``__repr__`` methods #804 deliberately left out of its own scope. #: @@ -155,7 +151,7 @@ #: :mod:`pcapkit.vendor.default` template) and which the issue's own last #: comment split back out, plus ``pcapkit.const.reg.apptype.apptype`` (an #: f-string, #792). What is left with an actual ``%``-formatted ``__repr__`` -#: is exactly these three -- each its own bespoke vendor template, like +#: is exactly these -- each its own bespoke vendor template, like #: :data:`ISSUE_804_PAIR`'s pair, so each gets a const half and a vendor half. ISSUE_818_TRIPLE = ( ('pcapkit.vendor.ftp.return_code', 'ReturnCode', 'pcapkit.const.ftp.return_code'), @@ -167,14 +163,14 @@ def percent_format_lines_in_repr(source: 'str') -> 'list[int]': """Every ``%``-formatting line inside a ``def __repr__`` body, and nowhere else. - Narrower than :func:`percent_format_lines`, which the four - :data:`BESPOKE_TEMPLATES` const modules cannot use for this: all three of - :data:`ISSUE_818_TRIPLE` still format their ``__str__`` and ``_missing_`` + Narrower than :func:`percent_format_lines`, which the + :data:`BESPOKE_TEMPLATES` const modules cannot use for this: every module of + :data:`ISSUE_818_TRIPLE` formats its ``__str__`` and ``_missing_`` with ``%``, deliberately -- GitHub issue #818 is about ``__repr__`` specifically, not the module-wide sweep #804 already finished for its own pair. Walking only the ``__repr__`` :class:`ast.FunctionDef` is what makes that distinction possible; a whole-module :func:`percent_format_lines` - would report the same nonzero count before and after this fix, because + would report the same nonzero count with or without the fix, because the ``__str__`` and ``_missing_`` sites it also matches are untouched by it either way. @@ -267,7 +263,7 @@ class _StdStrEnum(str, enum.Enum): """A built-in string enumeration. Spelled ``(str, enum.Enum)`` rather than :class:`enum.StrEnum`, which is - 3.11+, because CI still tests 3.8-3.10 (KTLO, #1633). + 3.11+, because CI tests 3.8-3.10 (KTLO, #1633). """ A = 'a' @@ -466,7 +462,7 @@ def test_an_out_of_range_value_is_rejected_unless_the_registry_registers_it(self f'{raised.__name__ if raised else None}') def test_the_registries_named_in_issue_647(self) -> None: - """The six outliers, by name, with what each did without the fix.""" + """The outliers, by name, with what each did without the fix.""" for module_name, class_name, before in ISSUE_647_OUTLIERS: with self.subTest(enum=f'{module_name}.{class_name}', before=before): obj = getattr(importlib.import_module(module_name), class_name) @@ -486,7 +482,7 @@ def test_every_new_guard_is_a_classmethod(self) -> None: :mod:`tests.const.test_const_enum_lookup` pins the fix -- but its sweep is ``_iter_const_int_enums``, which excludes :class:`~aenum.IntFlag`, and it never sees a :class:`~aenum.StrEnum` either. Every guard GitHub issue - #647 added is on one of those two kinds, so all six would have been + #647 added is on one of those two kinds, so every one of them would be outside it. """ checked = 0 @@ -502,7 +498,7 @@ def test_every_new_guard_is_a_classmethod(self) -> None: self.assertEqual(offenders, [], f'_missing_ without @classmethod (GitHub issue #492): {offenders}') - # The six of issue #647 specifically, since the sweep above would still + # The registries of issue #647 specifically, since the sweep above would still # pass if they had no ``_missing_`` at all -- which was the defect. # # ``TransportProtocol`` is the one deliberate exception: it has no @@ -524,7 +520,7 @@ def test_every_new_guard_is_a_classmethod(self) -> None: self.assertIsInstance(vars(obj)['_missing_'], classmethod) def test_a_string_registry_rejects_a_non_string_rather_than_crashing(self) -> None: - """The three ``StrEnum`` outliers reached ``value.upper()`` unguarded.""" + """Guards against the ``StrEnum`` outliers reaching ``value.upper()`` unguarded.""" from pcapkit.const.ftp.command import Command, FEATCode from pcapkit.const.http.method import Method @@ -643,7 +639,7 @@ def test_a_bounded_flag_matches_the_built_in_under_its_strict_boundary(self) -> class ConstEnumRegisterFallbackTests(unittest.TestCase): - """A once-sanctioned divergence, retired for these specific registries. + """The registering fallback, ruled out for these specific registries. The owner's design intent, recorded on GitHub issue #647, was for the const enums to mirror the built-in enum's behaviour with one exception: a missing @@ -659,9 +655,8 @@ class ConstEnumRegisterFallbackTests(unittest.TestCase): registry entry, and for these registries only IANA-registered values are legitimate -- ``get()`` does not have enough information to build one on its own. Stated for :class:`Method`, :class:`Command` and - :class:`FEATCode`, the three registries PR 1 converted, but the - reasoning is unconditional and PR 2 applies it to ``AppType`` - identically. Concretely, + :class:`FEATCode`, but the reasoning is unconditional and applies to + ``AppType`` identically. Concretely, :class:`Command` needs :attr:`~pcapkit.const.ftp.command.Command.feat`/ :attr:`~pcapkit.const.ftp.command.Command.desc`/ :attr:`~pcapkit.const.ftp.command.Command.type`/ @@ -912,7 +907,7 @@ def test_every_bespoke_template_carries_the_guard(self) -> None: def test_no_bespoke_const_module_still_uses_percent_formatting_for_its_guard(self) -> None: """The committed side of :data:`BESPOKE_TEMPLATES`, needing no ``requests``. - GitHub issue #798: the three vendor templates the previous test's + GitHub issue #798: the vendor templates the previous test's ``skipUnless`` can silently skip -- exactly how #792's regression reached seven CI legs -- so this checks the same guards through the committed :mod:`pcapkit.const` modules instead, which need no network @@ -1010,7 +1005,7 @@ def test_the_converted_reprs_render_exactly_what_percent_formatting_did(self) -> """Member by member, the ``__repr__`` output is byte-identical. The conversion is only safe if ``%s`` and an f-string's default - conversion agree on every operand these three dunders interpolate. + conversion agree on every operand these dunders interpolate. They do -- ``format(x, '')`` falls through to ``str(x)`` for both :class:`str` and :data:`None`, and ``Command.desc`` is the one ``Optional[str]`` among them -- but GitHub issue #796's text claimed @@ -1188,7 +1183,7 @@ def test_the_tcp_flags_template_renders_the_committed_module(self) -> None: """The whole file, character for character. Mirrors ``test_the_vendor_templates_render_the_committed_modules`` in - :mod:`tests.const.test_const_enum_lookup`, which does this for the four + :mod:`tests.const.test_const_enum_lookup`, which does this for the Mobility Header templates. Needs no network: the crawl supplies only the enumeration block, which is read back out of the committed module. @@ -1267,15 +1262,15 @@ def test_self_check_of_the_repr_percent_format_walk(self) -> None: self.assertEqual(len(percent_format_lines_in_repr(source)), expected) def test_no_percent_formatting_survives_in_the_issue_818_triples_repr(self) -> None: - """None of :data:`ISSUE_818_TRIPLE`'s three ``__repr__`` methods still format with ``%``. + """None of :data:`ISSUE_818_TRIPLE`'s ``__repr__`` methods formats with ``%``. Scoped to ``__repr__`` alone via :func:`percent_format_lines_in_repr`, - because unlike :data:`ISSUE_804_PAIR`'s pair, all three of these - modules keep formatting their ``__str__`` and ``_missing_`` guard with + because unlike :data:`ISSUE_804_PAIR`'s pair, each of these + modules keeps formatting its ``__str__`` and ``_missing_`` guard with ``%`` -- that is unrelated to GitHub issue #818, which is about ``__repr__`` specifically, and a whole-module check would never turn green. Without #818's conversion it reports one line apiece in the - three const modules. + const modules. The vendor half is checked separately, by :meth:`test_no_percent_formatting_survives_in_the_issue_818_triples_vendor_repr_template`, @@ -1320,7 +1315,7 @@ def test_the_issue_818_triples_reprs_render_exactly_what_percent_formatting_did( :meth:`test_the_converted_reprs_render_exactly_what_percent_formatting_did`, derived per member rather than assumed: ``%s`` and an f-string's default conversion agree for :class:`str` and :class:`int` operands, - which is all three of these interpolate (``__class__.__name__``, + which is all these interpolate (``__class__.__name__``, ``_value_``/``opt_name``, and -- for :class:`~pcapkit.const.pcapng .option_type.OptionType` -- ``opt_value``, an :class:`int` that ``%d`` and ``str()`` render identically for every non-negative value diff --git a/tests/const/test_const_enum_no_mint.py b/tests/const/test_const_enum_no_mint.py index f830773c36..c813a75a5c 100644 --- a/tests/const/test_const_enum_no_mint.py +++ b/tests/const/test_const_enum_no_mint.py @@ -55,8 +55,8 @@ ``_unregistered_member`` override reconstructing those attributes rather than the shared base's generic one, and :class:`StatusCode` and :class:`ReturnCode` use the base's ``get()`` rather than a hand-written one --- see :class:`BespokeGetReplacementTests` for the ``default == -1`` -> -``NO_DEFAULT`` behaviour change that implies. +-- see :class:`BespokeGetReplacementTests` for the ``NO_DEFAULT`` convention, +rather than ``default == -1``, that implies. :class:`OptionType` keeps its own ``get()`` (genuine multi-namespace dispatch the base does not replicate), and both its int/namespace path and its ``str`` path are ``_unregistered_member`` calls too -- the same @@ -2193,7 +2193,7 @@ class BespokeGetOverrideTests(unittest.TestCase): """:class:`~pcapkit.const.ftp.command.Command`, :class:`~pcapkit.const.http.method.Method` and :class:`~pcapkit.const.pcapng.option_type.OptionType` keep their own - hand-written ``get()`` in PR 1, because each does real dispatch the + hand-written ``get()``, because each does real dispatch the base's generic ``get()`` does not replicate. :class:`Command` resolves case-insensitively (GitHub issue #582 -- ``Command.get('abor')`` must return :attr:`Command.ABOR`, not raise), which the base's plain @@ -2217,7 +2217,7 @@ class BespokeGetOverrideTests(unittest.TestCase): :class:`OptionType`'s ``get()`` does its own multi-namespace dispatch via :attr:`__members_ns__` with no base equivalent at all. These are - regression guards, not new coverage. + regression guards. """ @@ -2264,13 +2264,13 @@ def test_optiontype_get_namespace_dispatch_is_unchanged(self) -> None: class AppTypeUnmintConvertedTests(unittest.TestCase): - """GitHub issue #860 step 2, PR 2 (the second and final PR of this step): - :class:`~pcapkit.const.reg.apptype.apptype.AppType` and its four + """GitHub issue #860 step 2, PR 2: + :class:`~pcapkit.const.reg.apptype.apptype.AppType` and its per-transport registries -- :class:`~pcapkit.const.reg.apptype.tcp.TCP`, :class:`~pcapkit.const.reg.apptype.udp.UDP`, :class:`~pcapkit.const.reg.apptype.sctp.SCTP`, - :class:`~pcapkit.const.reg.apptype.dccp.DCCP` -- brought onto - :class:`~pcapkit.corekit.enum.EnumRegistry` and stopped from minting. + :class:`~pcapkit.const.reg.apptype.dccp.DCCP` -- on + :class:`~pcapkit.corekit.enum.EnumRegistry`, minting nothing. Largest of the bespoke classes #860 step 2 converts: an AST walk over :meth:`AppType._missing_` finds range-bounded branches @@ -2355,8 +2355,8 @@ def test_apptype_family_carries_the_registry_protocol(self) -> None: def test_unassigned_range_resolves_without_minting(self) -> None: """226 sits in the base registry's 225-241 ``reserved`` span, which - names no transport protocol, so it answers every one of the four - per-transport registries identically -- the same shape as + names no transport protocol, so it answers every + per-transport registry identically -- the same shape as :class:`UnassignedRangeDoesNotMintTests` above, generalised to a registry whose ``_unregistered_member`` reconstructs three attributes rather than the generic zero.""" diff --git a/tests/const/test_const_ethertype_862_unit.py b/tests/const/test_const_ethertype_862_unit.py index ecccebd951..c2818078b7 100644 --- a/tests/const/test_const_ethertype_862_unit.py +++ b/tests/const/test_const_ethertype_862_unit.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- """``EtherType(0x0101)`` must resolve to Old Xerox, not IEEE802.3 Length Field. -GitHub issue #862: :meth:`pcapkit.const.reg.ethertype.EtherType._missing_` tested -``0x0000 <= value <= 0x05DC`` (IEEE802.3 Length Field) *before* +GitHub issue #862: guards against :meth:`pcapkit.const.reg.ethertype.EtherType._missing_` +testing ``0x0000 <= value <= 0x05DC`` (IEEE802.3 Length Field) *before* ``0x0101 <= value <= 0x01FF`` (Old Xerox Experimental values). The second range is wholly contained in the first, and the generated ``_missing_`` returns on the -first matching ``if``, so the Old Xerox branch was unreachable for every value -it covers -- ``EtherType(0x0101).name`` resolved to +first matching ``if``, so in that order the Old Xerox branch is unreachable for +every value it covers -- ``EtherType(0x0101).name`` resolves to ``'IEEE802_3_Length_Field_0x0101'`` instead of the Old Xerox name. Same shape as #844/#852 (``LinkType`` 209) and #841 (IPX socket ranges): the @@ -228,8 +228,7 @@ def test_nested_ranges_are_ordered_narrowest_first(self) -> None: on them, so they are not exercised below. So this test drives two orders, one representative of each - discriminating pair, rather than one order claimed (wrongly, in an - earlier revision) to catch both. + discriminating pair, since no single order catches both. """ from pcapkit.vendor.reg.ethertype import EtherType diff --git a/tests/const/test_const_ftp_command_get_key_type_1824_unit.py b/tests/const/test_const_ftp_command_get_key_type_1824_unit.py index 86817316d1..f10903d4b8 100644 --- a/tests/const/test_const_ftp_command_get_key_type_1824_unit.py +++ b/tests/const/test_const_ftp_command_get_key_type_1824_unit.py @@ -51,12 +51,12 @@ def _refused(self, key: 'object', **kwargs: 'object') -> 'BaseException': return caught.exception def test_a_bytes_key_raises_instead_of_returning_a_bytes_keyed_member(self) -> 'None': - """Was ```` -- a member, not an error.""" + """Guards against returning ```` -- a member, not an error.""" self._refused(b'\x01') self._refused(b'ABOR') def test_an_int_key_raises_the_in_library_error(self) -> 'None': - """Was a builtin :exc:`AttributeError` from ``key.upper()``.""" + """Guards against a builtin :exc:`AttributeError` from ``key.upper()``.""" error = self._refused(-1) self.assertNotIsInstance(error, AttributeError) self.assertIsInstance(error, ValueError) @@ -171,7 +171,7 @@ def test_the_crawler_template_carries_the_same_get(self) -> 'None': class FEATCodeUnaffectedTests(unittest.TestCase): - """``FEATCode.get`` already delegated a non-``str`` key, so it never had the defect.""" + """``FEATCode.get`` delegates a non-``str`` key to the base, so it has no such defect.""" def test_a_non_str_key_raises_the_in_library_error(self) -> 'None': from pcapkit.const.ftp.command import FEATCode diff --git a/tests/const/test_const_ftp_featcode_case_903_unit.py b/tests/const/test_const_ftp_featcode_case_903_unit.py index d20b771033..9b16c96eef 100644 --- a/tests/const/test_const_ftp_featcode_case_903_unit.py +++ b/tests/const/test_const_ftp_featcode_case_903_unit.py @@ -282,7 +282,7 @@ def test_command_keeps_its_own_rfc_959_fold(self) -> 'None': """:class:`~pcapkit.const.ftp.command.Command` shares this module and folds for a different reason -- RFC 959 Section 5.3's *"Upper and lower case alphabetic characters are to be treated identically"* -- - so it must be unaffected by the sibling class changing.""" + so it must be unaffected by the sibling class's fold.""" self.assertIs(Command.get('RETR'), Command.RETR) # type: ignore[attr-defined] self.assertIs(Command.get('retr'), Command.RETR) # type: ignore[attr-defined] @@ -315,9 +315,9 @@ def test_the_generated_module_imports_what_the_override_needs(self) -> 'None': """``NO_DEFAULT`` is the sentinel the override's own signature carries, so the template has to import it as well as emit the method. - ``EnumLookup`` joined the same import with GitHub issue #930, which - re-parented ``CommandType`` and ``ConformanceRequirement`` -- the last - two of #877's non-registry enumerations -- onto it. + ``EnumLookup`` is in the same import because GitHub issue #930 + re-parents ``CommandType`` and ``ConformanceRequirement`` -- #877's + remaining non-registry enumerations -- onto it. """ from pcapkit.vendor.ftp.command import LINE diff --git a/tests/const/test_const_ipv6_extension_header_925_unit.py b/tests/const/test_const_ipv6_extension_header_925_unit.py index d0e4e1f598..4f1174d3dd 100644 --- a/tests/const/test_const_ipv6_extension_header_925_unit.py +++ b/tests/const/test_const_ipv6_extension_header_925_unit.py @@ -9,7 +9,7 @@ does not have. :mod:`tests.vendor.test_vendor_ipv6_extension_header_925_unit` pins the crawler to the authoritative registry, and its ``test_context_matches_the_committed_const_file_byte_for_byte`` proves this -file is exactly what the fixed crawler produces from the fixture the issue's +file is exactly what the crawler produces from the fixture the issue's own investigation fetched. :meth:`pcapkit.protocols.internet.ipv6.IPv6._decode_next_layer` resolves diff --git a/tests/const/test_const_linktype_209_unit.py b/tests/const/test_const_linktype_209_unit.py index 381095f7de..e58501b553 100644 --- a/tests/const/test_const_linktype_209_unit.py +++ b/tests/const/test_const_linktype_209_unit.py @@ -76,9 +76,7 @@ #: parse its fixture. Spelled exactly as #: :data:`tests.vendor.test_request_prompt_unit.VENDOR_DEPS` so this file reaches #: the **existing** ``HAS_VENDOR_DEPS`` dependency gate rather than minting a new -#: one -- its entry in :data:`tests._dependency_gates.DEPENDENCY_GATE_EXCLUSIONS` -#: already accounts for that gate, with -#: ``dark={'test': ('html5lib',), 'gate': ('html5lib',)}``. +#: one, which :mod:`tests._dependency_gates` already maps. #: #: ``html5lib`` is the one that is actually missing on ``test``: ``requests`` and #: ``bs4`` are in the ``test`` extra (#507), but only @@ -92,9 +90,10 @@ #: rather than making the whole unit tier depend on the crawlers'. #: #: Consequence worth knowing: :class:`LinkTypeGeneratorLegacyOrderingTests` is the -#: root-cause pin, and it runs only where the ``vendor`` extra is installed, -#: which no ``unit-tests.yml`` job does (#849) -- so locally, and in -#: ``cron-vendor.yml`` only when a crawl changed a constant module (#1853). +#: root-cause pin, and it runs only where the ``vendor`` extra is installed: +#: in ``unit-tests.yml``, the ``latest`` leg of ``test`` and the ``gate`` job +#: (#1958), and in ``cron-vendor.yml`` only when a crawl changed a constant +#: module (#1853). #: :class:`LinkType209ConstResolutionTests` #: needs no parser and so still guards the reported symptom everywhere. HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS) @@ -141,8 +140,8 @@ def test_value_209_resolves_to_the_current_name(self) -> None: "'I2C_LINUX' one; see GitHub issue #844") def test_the_legacy_name_is_still_reachable_as_an_alias(self) -> None: - # The fix reorders which name is canonical for 209; it does not take - # the legacy name away -- both still name the same member by value. + # The current name is canonical for 209, and the legacy name is kept: + # both name the same member by value. from pcapkit.const.reg.linktype import LinkType self.assertEqual(LinkType['IPMB_LINUX'], LinkType['I2C_LINUX']) @@ -274,14 +273,14 @@ def _duplicate_pair_table_html(first: str, second: str) -> str: class LinkTypeGeneratorValueAwareTests(unittest.TestCase): """Item 1 of GitHub issue #852: the sink predicate must be value-aware. - :meth:`~pcapkit.vendor.reg.linktype.LinkType.process`'s sink rule - (added by #848) tested a row's notes for the word "legacy" alone, with no - check that the row's value is actually claimed by another row. Issue - #852 pointed out this can invert in the *other* direction: a future + Guards against :meth:`~pcapkit.vendor.reg.linktype.LinkType.process`'s + sink rule (#848) testing a row's notes for the word "legacy" alone, with + no check that the row's value is actually claimed by another row, which + #852 points out can invert in the *other* direction: a future *current* row whose notes happen to mention "legacy" about some unrelated code gets sunk even though nothing else shares its value -- silently reordering the generated file's *emission order* for a value that was - never ambiguous in the first place. (It also cannot, by itself, save a + never ambiguous in the first place. (Value-awareness cannot, by itself, save a row from being wrongly sunk when tcpdump's wording is attached to the wrong member of a *genuine* duplicate pair; that residual risk is documented at the ``dup_values`` computation in @@ -332,12 +331,11 @@ def test_non_duplicated_legacy_wording_does_not_reorder(self) -> None: def test_duplicate_pair_canonical_member_is_order_independent(self) -> None: """The *current* row must win regardless of which table position it is in. - Unlike an earlier version of this test, which fed only the - current-first ordering and left order-independence resting entirely - on :class:`LinkTypeGeneratorLegacyOrderingTests`'s separate, - legacy-first 209 fixture, this drives the *same* duplicate pair - through :meth:`process` in **both** orders in one test -- so the name - is earned here rather than borrowed from a sibling class. A predicate + This drives the *same* duplicate pair through :meth:`process` in + **both** orders in one test, rather than leaving order-independence + resting on :class:`LinkTypeGeneratorLegacyOrderingTests`'s separate, + legacy-first 209 fixture -- so the name is earned here rather than + borrowed from a sibling class. A predicate that regressed to counting only a positional prefix (e.g. "has this value already been appended to ``enum``") would still pass a single-order test, because that predicate is only diff --git a/tests/const/test_const_method_case_sensitive_896_unit.py b/tests/const/test_const_method_case_sensitive_896_unit.py index 5184719357..4ddbc9e479 100644 --- a/tests/const/test_const_method_case_sensitive_896_unit.py +++ b/tests/const/test_const_method_case_sensitive_896_unit.py @@ -29,9 +29,7 @@ ``default`` supplied, ``EnumRegistry.get`` raises :exc:`KeyError` -- it never builds an unregistered member the way ``Method._missing_`` does for the *constructor* path. Confirmed on :class:`~pcapkit.const.ftp.command.FEATCode`, -which ran the base unmodified when this was measured -- GitHub issue #903's -audit has since given it a case-insensitive ``get`` of its own per -:rfc:`5797#section-2`, and the measurement still stands, because that override +whose case-insensitive ``get`` (GitHub issue #903, per :rfc:`5797#section-2`) delegates to the base for any key it cannot match even after folding: ``FEATCode.get('totally-unknown-thing')`` raises ``KeyError``, while ``FEATCode('totally-unknown-thing')`` -- the constructor, reaching @@ -43,8 +41,8 @@ :mod:`tests.const.test_const_enum_no_mint` also require. So the fix keeps ``Method``'s own ``get`` override -- it still needs to build that unregistered member on a miss, which the base's ``get`` does not do for a ``str`` key -- and -only removes the ``.upper()`` folding from the *match* it performs before -falling through to that fallback. The ``_unregistered_member`` call it falls +does no ``.upper()`` folding in the *match* it performs before falling +through to that fallback. The ``_unregistered_member`` call it falls through to still canonicalises the *name* (the identifier) to upper case, exactly as :meth:`Method._missing_` does; only the *value* -- and the match against a *registered* name -- @@ -268,12 +266,11 @@ def test_vendor_template_matches_the_generated_module(self) -> None: # And the fix itself is actually present in both -- a byte-identical # comparison above already implies this, but names the exact shape - # so a failure here is legible without a diff. GitHub issue #908 - # moved ``get`` from testing ``_member_map_`` by hand to delegating - # to the base's own ``get`` (which also checks - # ``_value2member_map_``), so the markers pinned here moved with it - # -- see tests.const.test_const_method_value_lookup_908_unit for - # that change's own dedicated coverage. + # so a failure here is legible without a diff. ``get`` delegates to + # the base's own ``get`` (which also checks ``_value2member_map_``; + # GitHub issue #908), so the markers pinned here are that delegation + # -- see tests.const.test_const_method_value_lookup_908_unit for its + # own dedicated coverage. for label, source in (('template render', rendered), ('generated module', committed)): with self.subTest(rendering=label): self.assertIn('return super().get(key)', source) diff --git a/tests/const/test_const_method_value_lookup_908_unit.py b/tests/const/test_const_method_value_lookup_908_unit.py index c2fd8a1b36..8b2464ec89 100644 --- a/tests/const/test_const_method_value_lookup_908_unit.py +++ b/tests/const/test_const_method_value_lookup_908_unit.py @@ -5,14 +5,15 @@ only ``BASELINE_CONTROL`` (value ``'BASELINE-CONTROL'``) and ``VERSION_CONTROL`` (value ``'VERSION-CONTROL'``) have a member *name* that differs from their *value*, because a hyphen cannot appear in a Python -identifier. The override's own ``get`` checked only ``_member_map_`` (names) -and fell straight through to building an *unregistered* member on any miss, -so a caller spelling either method by its registered *value* got back a -freshly minted, hollowed-out member (``safe=False``, ``idempotent=False``) -instead of the real one the IANA registry describes (``idempotent=True`` for -both) -- even though the very same value already resolved correctly through -the constructor (:meth:`~pcapkit.const.http.method.Method._missing_`, which -this override never touched). +identifier. Guards against the override's own ``get`` checking only +``_member_map_`` (names) and falling straight through to building an +*unregistered* member on any miss, under which a caller spelling either method +by its registered *value* gets back a freshly minted, hollowed-out member +(``safe=False``, ``idempotent=False``) instead of the real one the IANA registry +describes (``idempotent=True`` for both) -- even though the very same value +resolves correctly through the constructor +(:meth:`~pcapkit.const.http.method.Method._missing_`, which the override does +not touch). **Why a capture reaches it.** ``pcapkit/protocols/application/http.py``'s ``_RE_METHOD = re.compile(rb"(?P[A-Z][A-Z-]*)\\Z")``, which @@ -27,11 +28,10 @@ **The fix, and the trap in the version the issue first proposed.** Delegate to :meth:`~pcapkit.corekit.enum.EnumLookup.get` and keep the override only -for the unregistered-member fallback -- but ``Method.get`` was a -:class:`staticmethod` while the base is a :class:`classmethod`, and a -zero-argument ``super()`` inside a :class:`staticmethod` has no first -argument to bind (``RuntimeError: super(): no arguments``). So the override -had to move to :class:`classmethod` as well, following the shape +for the unregistered-member fallback -- but the base is a +:class:`classmethod`, and a zero-argument ``super()`` inside a +:class:`staticmethod` has no first argument to bind (``RuntimeError: super(): +no arguments``). So the override is a :class:`classmethod` as well, following the shape PR #913's ``FEATCode.get`` sets for the same base method. **Two behaviours settled by earlier issues must survive unchanged**, each @@ -54,7 +54,7 @@ minting a new one carrying that string), even though nothing in this tree calls ``get`` with a non-``None`` ``default`` to notice -- confirmed by grepping every call site under ``pcapkit/`` and ``tests/``. -So the signature and the default's meaning are left exactly as they were; +So the signature and the default's meaning stay the override's own; :class:`DefaultSignatureTests` below pins that explicitly. """ @@ -223,15 +223,15 @@ def test_a_version_control_request_line_reports_the_registrys_idempotent_flag( class NonStrKeyTests(unittest.TestCase): - """The non-``str`` surface, which GitHub issue #908's fix moved. + """The non-``str`` surface of the delegation (GitHub issue #908). Delegating to the base routes a non-``str`` key through ``cls(key)`` and so ``_missing_``, which raises :exc:`ValueError`. The override catches only :exc:`KeyError` -- a failed *name* lookup -- so that - :exc:`ValueError` reaches the caller. Pinned because it is a - caller-visible change that the fix makes as a side effect rather than as - its purpose, and because the ``bytes`` case moved from *returning* a - member to raising. + :exc:`ValueError` reaches the caller. Pinned because it is + caller-visible behaviour the delegation has as a side effect rather than + as its purpose, and because without it the ``bytes`` case *returns* a + member rather than raising. """ def setUp(self) -> None: diff --git a/tests/const/test_const_register_validates_every_registry_1816_unit.py b/tests/const/test_const_register_validates_every_registry_1816_unit.py index de90c41a38..0c629d6f6f 100644 --- a/tests/const/test_const_register_validates_every_registry_1816_unit.py +++ b/tests/const/test_const_register_validates_every_registry_1816_unit.py @@ -25,9 +25,9 @@ :class:`staticmethod` backport that bypasses the base lookup. ``OptionType``'s answers every integer code with an unregistered member, so there the constructor is the oracle instead; ``OptionType``'s ``_missing_`` refusing anything above -``0xFFFF`` is what the issue cites. ``Command``'s was one too, calling -``key.upper()`` and so answering :class:`bytes`, until #1824 made it a -:class:`classmethod` that hands a non-``str`` key to the base. +``0xFFFF`` is what the issue cites. ``Command``'s is a :class:`classmethod` +that hands a non-``str`` key to the base (#1824); without that fix it is a +backport calling ``key.upper()``, and so answers :class:`bytes`. Each test imports the registries after :func:`~tests._support.reimport_once_per_class`, whose ``restore`` puts the class's diff --git a/tests/const/test_const_registry_protocol.py b/tests/const/test_const_registry_protocol.py index c6b3132c00..4a6644b6c1 100644 --- a/tests/const/test_const_registry_protocol.py +++ b/tests/const/test_const_registry_protocol.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- """Tests for :class:`pcapkit.corekit.enum.EnumRegistry`, tier 2 of issue #775. -Tier 1 (#838) removed the mint from the sites in +Tier 1 (#838) takes the mint out of the sites in :data:`pcapkit.vendor.default.LINE` that the default-template registries -inherit. It could not reach the crawlers that replace that template with -their own, because each of those carries a hand-copied ``get()`` -- and none of -them carries ``register``, ``register_alias`` or ``get_all`` at all. +inherit, which leaves out the crawlers that replace that template with their +own. The design ruling on #842 finalises the abstraction: ``get``, ``get_all``, ``register`` and ``register_alias`` should always exist on the const enums, so they -move to the base class and every registry answers the same four calls. The +live on the base class and every registry answers the same calls. The ``AppType`` sub-base class keeps its own overrides and dispatching logic, and its subclasses override further where their contracts differ. @@ -44,7 +43,7 @@ #: ``_name_`` and ``_value_`` -- builds a complete member for them. ``tcp/flags`` #: is one of them: ``grep -c 'def __new__'`` reports ``0`` for both #: ``pcapkit/vendor/tcp/flags.py`` and ``pcapkit/const/tcp/flags.py``, so it is -#: the same shape as the four ``mh/*_flag`` registries, not the templates below +#: the same shape as the ``mh/*_flag`` registries, not the templates below #: that "each attach extra attributes in ``__new__``". Its own ``_missing_`` #: does its own 16-bit range check ending in ``super()._missing_(value)``, #: whichever base it has: that call chain resolves through :mod:`aenum`'s own @@ -240,12 +239,12 @@ def _resolve_or_raise(cls: 'Any', value: 'Any') -> 'Any': class TCPFlagsConversionTests(unittest.TestCase): - """GitHub issue #775's own finding: the stated reason for excluding - ``tcp/flags`` from :data:`CONVERTED` -- "each attach extra attributes in - ``__new__``" -- does not hold for it. + """GitHub issue #775's own finding: the reason for excluding a bespoke + template from :data:`CONVERTED` -- "each attach extra attributes in + ``__new__``" -- does not hold for ``tcp/flags``. ``grep -c 'def __new__'`` reports ``0`` for both ``pcapkit/vendor/tcp/flags.py`` and ``pcapkit/const/tcp/flags.py``, the - same shape as the four ``mh/*_flag`` registries in :data:`CONVERTED`, so + same shape as the ``mh/*_flag`` registries in :data:`CONVERTED`, so it belongs there rather than among the templates excluded for actually defining one. """ @@ -659,8 +658,8 @@ def test_int_valued_registries(self) -> None: def test_str_valued_registries(self) -> None: """``cls._member_type_.__new__`` is what generalises this beyond ``int`` - -- the generated fragment it replaces hardcoded ``int.__new__``, so the - five :class:`~aenum.StrEnum` registries could not have shared it.""" + -- a generated fragment hardcoding ``int.__new__`` could not serve the + :class:`~aenum.StrEnum` registries.""" class _Str(EnumRegistry, StrEnum): known = 'known' @@ -673,8 +672,9 @@ class _Str(EnumRegistry, StrEnum): self.assertNotIn('unit_test_absent', _Str.__members__) -#: Sample of the default-template registries GitHub issue #775's tier 3 -#: moved onto :class:`~pcapkit.corekit.enum.EnumRegistry`, chosen to cover +#: Sample of the default-template registries on +#: :class:`~pcapkit.corekit.enum.EnumRegistry` (GitHub issue #775's tier 3), +#: chosen to cover #: the two shapes the census turned up in ``pcapkit/vendor/default.py``'s #: ``LINE`` template: a registry whose own ``_missing_`` mints directly via #: :func:`~aenum.extend_enum` for an unassigned value (``TransType``, @@ -901,8 +901,7 @@ def test_member_type_stays_int_not_enumregistry(self) -> None: class GeneratedMissingRangeParityTests(unittest.TestCase): """``_missing_``'s own bounded ranges are deliberately outside the - conversion onto the base, so this is a regression pin on that claim - rather than a test of new behaviour.""" + conversion onto the base, so this is a regression pin on that claim.""" def setUp(self) -> None: reimport_once_per_class(self, restore=True) @@ -1355,7 +1354,7 @@ def test_identity_survives_an_ordinary_second_import(self) -> None: would do that (see :class:`~pcapkit.corekit.sentinels.NoDefaultType`'s own docstring caveat, and :class:`~pcapkit.corekit.sentinels.NullType`'s before it), and nothing in this package reloads - :mod:`pcapkit.corekit.enum` after import.""" + :mod:`pcapkit.corekit.sentinels` after import.""" import importlib from pcapkit.corekit.sentinels import NO_DEFAULT as first_import diff --git a/tests/const/test_const_str_payload_870_unit.py b/tests/const/test_const_str_payload_870_unit.py index c49c14e09b..c8d84e8ee1 100644 --- a/tests/const/test_const_str_payload_870_unit.py +++ b/tests/const/test_const_str_payload_870_unit.py @@ -1,26 +1,24 @@ # -*- coding: utf-8 -*- """A registered ``StrEnum`` member's :class:`str` payload must be its value. -GitHub issue #870: :meth:`pcapkit.const.http.method.Method.__new__` built every -registered member with ``obj = str.__new__(cls)`` -- no second argument -- so -the underlying :class:`str` content was permanently empty regardless of the -member's own declared value. ``_value_`` was set correctly (``Method.GET.value -== 'GET'``), but the :class:`str` the member itself *is* was not: -``str(Method.GET) == ''``, ``len(str(Method.GET)) == 0`` and, most visibly, -``Method.GET == 'GET'`` was :data:`False` for every declared member, as -measured, and true since the class was first written -- GitHub -pull request #869 fixed the same inconsistency for an *unregistered* member's -own str payload +GitHub issue #870: guards against :meth:`pcapkit.const.http.method.Method.__new__` +building every registered member with ``obj = str.__new__(cls)`` -- no second +argument -- under which the underlying :class:`str` content is empty regardless +of the member's own declared value. ``_value_`` is still set correctly +(``Method.GET.value == 'GET'``), but the :class:`str` the member itself *is* is +not: ``str(Method.GET) == ''``, ``len(str(Method.GET)) == 0`` and, most visibly, +``Method.GET == 'GET'`` is :data:`False` for every declared member, as +measured. An *unregistered* member carries its value as its str payload (:meth:`~pcapkit.const.http.method.Method._unregistered_member` calls the base's :meth:`~pcapkit.corekit.enum.EnumRegistry._unregistered_member`, which -does pass the value along), which is what exposed this half: after #869, +passes the value along; GitHub pull request #869), so with the defect ``str(Method('frob')) == 'frob'`` while ``str(Method.GET) == ''`` -- a registered member and an unregistered one disagreeing about the one thing they are both supposed to be, a :class:`str`. The fix is ``obj = str.__new__(cls, value)``, mirroring :meth:`pcapkit.const.ftp.command.Command.__new__` -(``obj = str.__new__(cls, name)``), which never had this defect. Applied to +(``obj = str.__new__(cls, name)``), which has no such defect. Applied to both :mod:`pcapkit.const.http.method` (the generated module) and :mod:`pcapkit.vendor.http.method` (the generator template that produces it), since a fix only in the former is silently reverted by the next crawl. @@ -38,7 +36,7 @@ :meth:`~pcapkit.protocols.application.httpv1.HTTP._make_start_line` reads ``.value``; neither calls ``str()`` or ``==`` on the member itself), so the fix is observable only to external callers and to this module's own dunders -(``__repr__``, which reads ``_value_`` and was already correct). +(``__repr__`` reads ``_value_`` and is unaffected). The wider sweep below -- :class:`StrValuedRegistryPayloadTests` -- checks the same property, ``str(member) == member.value``, for every ``EnumRegistry`` @@ -46,10 +44,10 @@ ``__new__``: :class:`~pcapkit.const.ftp.command.Command`, :class:`~pcapkit.const.ftp.command.FEATCode`, :class:`~pcapkit.const.http.method.Method` and -:class:`~pcapkit.const.pcapng.option_type.OptionType`. ``Command`` already -passed ``name`` to ``str.__new__`` and ``FEATCode`` has no custom ``__new__`` -at all (the base :class:`~aenum.StrEnum` machinery handles it), so neither -carried this defect -- only ``Method`` did. ``OptionType`` is deliberately +:class:`~pcapkit.const.pcapng.option_type.OptionType`. ``Command`` passes +``name`` to ``str.__new__`` and ``FEATCode`` has no custom ``__new__`` +at all (the base :class:`~aenum.StrEnum` machinery handles it), so the defect +is ``Method``'s alone. ``OptionType`` is deliberately *not* swept the same way: its own :attr:`~pcapkit.const.pcapng.option_type. OptionType.value` is *itself* the formatted display string (``'%s [%d]' % (opt_name, opt_value)``, set by its own ``__new__``), not the @@ -102,10 +100,9 @@ def test_every_one_of_the_40_registered_members_carries_its_value_as_str_payload self.assertEqual(len(str(member)), len(member.value)) def test_registered_and_unregistered_members_both_carry_their_value(self) -> None: - """The inconsistency #870 reported: with #869's fix to the - unregistered half alone, ``str(Method('frob')) == 'frob'`` while - ``str(Method.GET) == ''``. With #870's fix to the registered half, - both carry real content.""" + """The inconsistency #870 reported: without the fix to the + registered half, ``str(Method('frob')) == 'frob'`` while + ``str(Method.GET) == ''``. Both carry real content.""" from pcapkit.const.http.method import Method registered = Method.GET @@ -117,9 +114,9 @@ def test_registered_and_unregistered_members_both_carry_their_value(self) -> Non self.assertNotEqual(str(unregistered), '') def test_repr_and_the_extra_attributes_are_unaffected(self) -> None: - """The fix touches only the :class:`str` payload -- ``__repr__`` (which - reads ``_value_``, already correct) and the ``safe``/``idempotent`` - attributes :meth:`__new__` also sets must be untouched.""" + """Only the :class:`str` payload is at stake -- ``__repr__`` (which + reads ``_value_``) and the ``safe``/``idempotent`` attributes + :meth:`__new__` also sets must be unaffected.""" from pcapkit.const.http.method import Method self.assertEqual(repr(Method.GET), '') @@ -139,8 +136,8 @@ class StrValuedRegistryPayloadTests(unittest.TestCase): :meth:`test_optiontype_is_exempt_but_its_own_contract_still_holds` instead. """ - #: The three registries this sweep actually checks. ``Command`` and - #: ``FEATCode`` never carried GitHub issue #870's defect (see the module + #: The registries this sweep checks. ``Command`` and + #: ``FEATCode`` do not carry GitHub issue #870's defect (see the module #: docstring's sibling survey); they are swept anyway so a regression in #: either would be caught here too, not only in ``Method``. SWEPT_REGISTRIES = ('Command', 'FEATCode', 'Method') @@ -174,7 +171,7 @@ def test_optiontype_is_exempt_but_its_own_contract_still_holds(self) -> None: """Not swept above because ``OptionType.value`` *is* the formatted display string already -- ``str(member) == member.value`` would pass trivially and check nothing about a raw wire payload, unlike the - other three. This pins what its ``__new__``/``__str__`` actually + others. This pins what its ``__new__``/``__str__`` actually promise instead: the value equals the display string built from ``opt_name``/``opt_value``, and the wire-facing ``opt_value`` is reachable separately via :attr:`~pcapkit.const.pcapng.option_type. diff --git a/tests/corekit/test_decorators.py b/tests/corekit/test_decorators.py index 71222aff0d..f7cc519ec9 100644 --- a/tests/corekit/test_decorators.py +++ b/tests/corekit/test_decorators.py @@ -170,7 +170,7 @@ def test_prepare_accepts_data_and_positional_length(self) -> None: self._assert_call_shape_result(DemoSchema.unpack(b'payload', 7)) def test_prepare_accepts_data_length_and_packet_positionally(self) -> None: - """``unpack(data, length, packet)`` -- the shape that never needed the fix.""" + """``unpack(data, length, packet)`` -- the one shape that works without the fix.""" DemoSchema = self._demo_schema_for_call_shapes() self._assert_call_shape_result(DemoSchema.unpack(b'payload', 7, {})) diff --git a/tests/corekit/test_decorators_path_1735_unit.py b/tests/corekit/test_decorators_path_1735_unit.py index 081e20f0ef..d91219dcf7 100644 --- a/tests/corekit/test_decorators_path_1735_unit.py +++ b/tests/corekit/test_decorators_path_1735_unit.py @@ -5,10 +5,10 @@ and ``prepare`` act on a protocol's file and the schema it unpacks, and ``beholder`` falls back to protocol classes, so they live in :mod:`pcapkit.corekit.decorators` rather than ``pcapkit.utilities.decorators``. -Per the ruling on #1715 nothing is left at the old path, and +Per the ruling on #1715 nothing is left at ``pcapkit.utilities.decorators``, and :mod:`pcapkit.utilities` does not export those names. This pins both halves of -that: the old path -and the old exports are gone, and every importer uses the new module. +that: neither the module nor the exports exist, and every importer uses +:mod:`pcapkit.corekit.decorators`. """ import importlib diff --git a/tests/corekit/test_enum_get_exception_provenance_923_unit.py b/tests/corekit/test_enum_get_exception_provenance_923_unit.py index c6b9ef5219..40281ab7aa 100644 --- a/tests/corekit/test_enum_get_exception_provenance_923_unit.py +++ b/tests/corekit/test_enum_get_exception_provenance_923_unit.py @@ -399,10 +399,9 @@ def test_the_override_carries_no_handler_or_raise_left(self) -> 'None': pass every assertion above. An :mod:`ast` walk rather than a substring search over the text: this - method's body is mostly comment, and two of those comments legitimately - contain the word *except* (``exception-compatible``) and *raise* - (``used to raise AttributeError``), so a textual check reports a handler - that is not there. + method's body is mostly comment, and those comments legitimately contain + the word *except* (``exception-compatible``) and *raise* + (``would raise``), so a textual check reports a handler that is not there. """ import pcapkit.const.reg.apptype.apptype as const_module diff --git a/tests/corekit/test_fields_all_exports_2174_unit.py b/tests/corekit/test_fields_all_exports_2174_unit.py index 3d54e31c0f..61076a8ed6 100644 --- a/tests/corekit/test_fields_all_exports_2174_unit.py +++ b/tests/corekit/test_fields_all_exports_2174_unit.py @@ -1,19 +1,19 @@ # -*- coding: utf-8 -*- """The field modules' :attr:`__all__` lists keep in step with one another. -GitHub issue #2174. :mod:`pcapkit.corekit.fields.misc` left +GitHub issue #2174 found :mod:`pcapkit.corekit.fields.misc` leaving :class:`~pcapkit.corekit.fields.misc.SchemaField` and :func:`~pcapkit.corekit.fields.misc.nested_packet_context` out of its -:attr:`__all__`, :mod:`pcapkit.corekit.fields.strings` left out +:attr:`__all__`, :mod:`pcapkit.corekit.fields.strings` leaving out :class:`~pcapkit.corekit.fields.strings.BytesField`, and :mod:`pcapkit.corekit.fields`, :mod:`pcapkit.corekit` and -:mod:`pcapkit.all` all left out +:mod:`pcapkit.all` all leaving out :class:`~pcapkit.corekit.fields.misc.SwitchField` and :class:`~pcapkit.corekit.fields.numbers.PortEnumField`, so ``import *`` from any of them dropped documented field classes. :mod:`tests.project.test_all_exports_unit` checks :mod:`pcapkit.all` against -:mod:`pcapkit.corekit`, but nothing checked the field package against its own -submodules, nor :mod:`pcapkit.corekit` against the field package it re-lists by +:mod:`pcapkit.corekit`; this module checks the field package against its own +submodules, and :mod:`pcapkit.corekit` against the field package it re-lists by hand. The same issue found :meth:`DeferredPacket.to_dict @@ -94,8 +94,8 @@ def test_exclusions_are_not_stale(self) -> None: def test_each_field_is_exported_by_its_own_module(self) -> None: # The converse of the check above: a field the package exports is - # also in the __all__ of the module defining it. strings.__all__ had - # dropped BytesField, and misc.__all__ SchemaField (#2174). + # also in the __all__ of the module defining it. Guards against + # strings.__all__ dropping BytesField and misc.__all__ SchemaField (#2174). for name in pcapkit.corekit.fields.__all__: home = getattr(pcapkit.corekit.fields, name).__module__ with self.subTest(name=name, module=home): diff --git a/tests/corekit/test_fields_byteorder_1519_unit.py b/tests/corekit/test_fields_byteorder_1519_unit.py index 21159ddc60..fa073785c4 100644 --- a/tests/corekit/test_fields_byteorder_1519_unit.py +++ b/tests/corekit/test_fields_byteorder_1519_unit.py @@ -11,9 +11,9 @@ This pins the helper, its ``__packet__`` fallback, and that both schemas use it. Per GitHub issue #1711 the helper lives in -:mod:`pcapkit.corekit.fields.byteorder`, with no re-export at the old -``pcapkit.protocols.schema.misc.byteorder`` path. This also pins that the old -path does not import, and that the module reaches no higher than +:mod:`pcapkit.corekit.fields.byteorder`, with no re-export at +``pcapkit.protocols.schema.misc.byteorder``. This also pins that importing it +fails, and that the module reaches no higher than :mod:`pcapkit.corekit`. """ @@ -103,7 +103,7 @@ def test_frame_header_reads_the_byte_order_it_is_given(self) -> None: self.assertEqual(header.pack(dict(packet)), raw) def test_the_schema_misc_byteorder_path_does_not_import(self) -> None: - """#1711: the move leaves no re-export at the old path.""" + """#1711: the move leaves no re-export at ``pcapkit.protocols.schema.misc.byteorder``.""" with self.assertRaises(ModuleNotFoundError) as ctx: importlib.import_module(OLD_PATH) self.assertEqual(ctx.exception.name, OLD_PATH) diff --git a/tests/corekit/test_fields_misc_packet_context.py b/tests/corekit/test_fields_misc_packet_context.py index d0ca780795..2da317b00a 100644 --- a/tests/corekit/test_fields_misc_packet_context.py +++ b/tests/corekit/test_fields_misc_packet_context.py @@ -177,7 +177,7 @@ def test_pcapng_byteorder_consumer_still_works_with_both_shapes(self) -> None: :func:`~pcapkit.corekit.fields.byteorder.packet_byteorder` is called both with a plain dict built by hand -- as several tests and - :func:`~pcapkit.foundation.engines.pcapng` construct -- and with what + :mod:`pcapkit.foundation.engines.pcapng` construct -- and with what ``SchemaField`` actually builds. Both must work: the site implements the fallback itself and does not route through :func:`~pcapkit.corekit.fields.misc.nested_packet_context`. diff --git a/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py b/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py index c68adf175c..49b4493e3d 100644 --- a/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py +++ b/tests/corekit/test_fields_numbers_port_enum_field_peek_1774_unit.py @@ -24,7 +24,7 @@ #: interpreter. __tier__ = 'ALL' -#: An unassigned port on all four transports, in none of ``_missing_``'s spans. +#: An unassigned port on every transport, in none of ``_missing_``'s spans. PORT = 50000 diff --git a/tests/corekit/test_fields_numbers_unassigned_enum.py b/tests/corekit/test_fields_numbers_unassigned_enum.py index 28163991a6..1c46b7e7e5 100644 --- a/tests/corekit/test_fields_numbers_unassigned_enum.py +++ b/tests/corekit/test_fields_numbers_unassigned_enum.py @@ -52,8 +52,8 @@ one of thousands of unassigned codes and a fix special-casing it would pass a test that used only it, so :meth:`UnassignedEnumFieldTests.test_no_value_of_any_width_escapes_the_field_layer` -walks every value a one- and two-octet field can carry, for four registries, and -asserts that nothing foreign comes out. +walks every value a one- and two-octet field can carry, for registries of each +width, and asserts that nothing foreign comes out. """ from __future__ import annotations @@ -208,7 +208,8 @@ def test_the_registry_own_guard_is_not_weakened(self) -> None: """The fallback is at the field layer and nowhere else. A registry rejecting a value is a statement about that registry, and - callers -- including the generated ``get()`` -- rely on it. The fallback + callers -- including :meth:`~pcapkit.corekit.enum.EnumLookup.get`, which every + generated registry inherits -- rely on it. The fallback lives in the field, which knows the value arrived in a field of a fixed width; it does not reach into the registry, so both of these go on raising. @@ -235,8 +236,8 @@ def test_an_in_library_rejection_is_not_absorbed(self) -> None: :mod:`pcapkit.const`, which is why the stand-ins below are built here rather than borrowed. A *generated* guard raises a bare, unlogged :exc:`ValueError` on purpose -- ``tests/const/test_const_enum_builtin_parity.py`` - pins exactly that, because each generated ``get()``'s ``except - ValueError`` fallback depends on it -- so no registry under + pins exactly that, because the ``except ValueError`` fallback in + :meth:`~pcapkit.corekit.enum.EnumLookup.get` depends on it -- so no registry under :mod:`pcapkit.const` exercises this branch, and none needs to: a generated guard that bounds its registry to a width rejects with that same bare :exc:`ValueError`, whether the registry is a bit-flag one such @@ -329,7 +330,7 @@ def test_no_value_of_any_width_escapes_the_field_layer(self) -> None: ``28`` is one of thousands of unassigned codes, so a fix that special-cased it would pass a test written around it. This walks the *entire* value space - a one- and two-octet field can carry, across four registries of different + a one- and two-octet field can carry, across registries of different shapes, and asserts that nothing leaves the field layer except a member. Without the fix this raises for the overwhelming majority of @@ -365,7 +366,7 @@ def test_no_value_of_any_width_escapes_the_field_layer(self) -> None: f'first {escaped[:3]}' ) - # NOTE: All four registries are swept before anything is asserted, so a + # NOTE: Every registry is swept before anything is asserted, so a # report names every one that failed rather than only the first. A bare # ``assertEqual`` inside the loop would abort at the first failing # registry and leave the rest unmeasured, which is the difference between diff --git a/tests/corekit/test_io.py b/tests/corekit/test_io.py index bc6e6e40b7..a140bb631b 100644 --- a/tests/corekit/test_io.py +++ b/tests/corekit/test_io.py @@ -636,16 +636,16 @@ def test_a_refused_seek_leaves_the_position_untouched(self) -> None: self.assertEqual(reader.read(13), b'ABCDEFGHIJKLM') with self.assertRaises(self.exceptions.SeekError): reader.seek(8) - self.assertEqual(reader.tell(), 13) # was 8, the rejected target - self.assertEqual(reader.read(1), b'N') # was b'L', the octet at 11 + self.assertEqual(reader.tell(), 13) # without the fix: 8, the rejected target + self.assertEqual(reader.read(1), b'N') # without the fix: b'L', the octet at 11 self._close_reader(reader) reader = self.SeekableReader(io.BytesIO(b'ABCDEFGHIJKLMNOPQRSTUVWXYZ'), buffer_size=4) self.assertEqual(reader.read(6), b'ABCDEF') with self.assertRaises(self.exceptions.SeekError): reader.seek(0) - self.assertEqual(reader.tell(), 6) # was 0 - self.assertEqual(reader.read(1), b'G') # was b'C', the octet at 2 + self.assertEqual(reader.tell(), 6) # without the fix: 0 + self.assertEqual(reader.read(1), b'G') # without the fix: b'C', the octet at 2 self._close_reader(reader) def test_seek_refuses_a_negative_absolute_position_under_every_whence(self) -> None: @@ -694,10 +694,10 @@ def test_seek_refuses_a_negative_position_with_a_saved_buffer_too(self) -> None: reader = self.SeekableReader(io.BytesIO(b'abcdefghij'), buffer_size=4, buffer_save=True, buffer_path=path) with self.assertRaises(self.exceptions.SeekError) as ctx: - reader.seek(-5, io.SEEK_CUR) # returned -5, no exception at all + reader.seek(-5, io.SEEK_CUR) # without the fix: returns -5, no exception at all self.assertEqual(str(ctx.exception), 'negative seek value -5') self.assertEqual(reader.tell(), 0) - self.assertEqual(reader.read(1), b'a') # was OSError: [Errno 22] + self.assertEqual(reader.read(1), b'a') # without the fix: OSError: [Errno 22] self._close_reader(reader) finally: if os.path.exists(path): @@ -741,7 +741,7 @@ def test_peek_does_not_move_what_the_next_read_returns(self) -> None: self.assertEqual(reader.tell(), 2) # the cursor is the position's other half, and a preview may not have moved it self.assertEqual(reader._buffer.tell(), reader._tell - reader._buffer_set) - self.assertEqual(reader.read(3), control) # was b'\x06\x07\x08' + self.assertEqual(reader.read(3), control) # without the fix: b'\x06\x07\x08' self._close_reader(reader) stream = self._pipe_stream(data) @@ -756,7 +756,7 @@ def test_peek_does_not_move_what_the_next_read_returns(self) -> None: reader.read(8) self.assertEqual(reader.peek(4), b'\x09\x0a\x0b\x0c') self.assertEqual(reader.tell(), 8) - self.assertEqual(reader.read(4), control) # was b'\x01\x02\x03\x04' + self.assertEqual(reader.read(4), control) # without the fix: b'\x01\x02\x03\x04' self._close_reader(reader) # ------------------------------------------------------------------ # @@ -789,10 +789,10 @@ def test_buffered_reads_return_every_octet_available_from_the_position(self) -> """ # the `- 1` alone: seek to the base, ask for more than the cap allowed for method, data, size, expected in [ - ('read', b'abcde', 5, b'abcde'), # was b'abce' - ('readline', b'abcde\nfghij\n', 6, b'abcde\n'), # was b'abce\n' - ('read1', b'abcde', 4, b'abcd'), # was b'abc' - ('peek', b'abcde', 4, b'abcd'), # was b'abc' + ('read', b'abcde', 5, b'abcde'), # without the fix: b'abce' + ('readline', b'abcde\nfghij\n', 6, b'abcde\n'), # without the fix: b'abce\n' + ('read1', b'abcde', 4, b'abcd'), # without the fix: b'abc' + ('peek', b'abcde', 4, b'abcd'), # without the fix: b'abc' ]: with self.subTest(half='count-less-one', method=method): reader = self.SeekableReader(io.BytesIO(data)) @@ -803,7 +803,7 @@ def test_buffered_reads_return_every_octet_available_from_the_position(self) -> # the wrong origin: seek strictly inside the buffered region, ask past its end for method, data, size, expected in [ - ('read', b'0123456789' + b'X' * 20, 8, b'56789XXX'), # was b'56789\x00\x00\x00' + ('read', b'0123456789' + b'X' * 20, 8, b'56789XXX'), # without the fix: b'56789\x00\x00\x00' ('readline', b'0123456789\n' + b'X' * 20, 8, b'56789\n'), ('read1', b'0123456789' + b'X' * 20, 8, b'56789'), ('peek', b'0123456789' + b'X' * 20, 8, b'56789'), @@ -844,14 +844,13 @@ def test_an_unbounded_buffered_readline_still_finishes_the_line(self) -> None: def test_the_window_base_and_content_pointer_track_the_stream_consumption(self) -> None: """``_buffer_set + _buffer_cur`` is how far the stream has been read, always. - This is the invariant a draft of #633 broke, turning a loud :exc:`ValueError` into - silent data corruption: :meth:`seek` reads the pair as the stream's consumption + Breaking this invariant turns a loud :exc:`ValueError` into silent data + corruption (#633): :meth:`seek` reads the pair as the stream's consumption point and reads ahead from it to fill a gap, so a pair that under-reports makes the next forward seek splice octets in from the wrong absolute offset without complaining. It is asserted directly here, against a stream that counts what has actually been taken off it, after *every* operation rather than at the end -- an - output assertion can pass while the bookkeeping behind it is already wrong, which - is exactly how that draft got as far as review. + output assertion can pass while the bookkeeping behind it is already wrong. Deriving the buffer's cursor from ``_tell`` at each point of use is what preserves the pair by construction: nothing clamps or adjusts either member to make a read fit, diff --git a/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py b/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py index eb9db6c023..f3de3b719c 100644 --- a/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py +++ b/tests/corekit/test_io_seekable_reader_short_reads_1586_unit.py @@ -2,19 +2,19 @@ """:class:`~pcapkit.corekit.io.SeekableReader` over a stream that answers short (#1586). One read of a pipe, or of a raw stream, may return fewer octets than were asked -for at any point; only an empty read means the stream has ended. The reader took -the first answer as all there was in three places, and the frame readers, which -measure what is left with ``seek(0, SEEK_END)``, took that for the end of the -capture: +for at any point; only an empty read means the stream has ended. Without the fix +the reader takes the first answer as all there is in each of these places, and +the frame readers, which measure what is left with ``seek(0, SEEK_END)``, take +that for the end of the capture: -* ``read(n)`` made one read of the stream, so it came back short mid-stream. -* ``seek(0, SEEK_END)`` was the buffer's far edge. Once the buffer had filled, - that is exactly how far the stream has been read, so nothing more was read, - and the first record ending there measured nothing left after it. -* A forward ``seek`` made one read too, and landed wherever that read ended -- - short of the target, or past it when the stream answered more than the gap. +* ``read(n)`` makes one read of the stream, so it comes back short mid-stream. +* ``seek(0, SEEK_END)`` is the buffer's far edge. Once the buffer has filled, + that is exactly how far the stream has been read, so nothing more is read, + and the first record ending there measures nothing left after it. +* A forward ``seek`` makes one read too, and lands wherever that read ends -- + short of the target, or past it when the stream answers more than the gap. -Each test here fails on ``e9f3340f7``, the parent of the fix. +Each test here fails without the fix. """ @@ -73,7 +73,7 @@ def reader(self, data: 'bytes' = DATA, chunk: 'int' = 3, size: 'int' = 16) -> 'o return reader def test_read_returns_every_octet_asked_for(self) -> None: - """``read(10)`` over three-octet answers returned three octets.""" + """Without the fix, ``read(10)`` over three-octet answers returns three octets.""" reader = self.reader() self.assertEqual(reader.read(10), DATA[:10]) # half from the buffer, half from the stream @@ -93,7 +93,8 @@ def test_read_comes_back_short_only_at_the_end_of_the_stream(self) -> None: self.assertEqual(reader.read(1), b'') def test_end_of_stream_is_a_buffer_past_the_position(self) -> None: - """Before the buffer fills, the end was where one three-octet read left it.""" + """Before the buffer fills, the end is a buffer past the position, not where one + three-octet read leaves it.""" reader = self.reader() self.assertEqual(reader.seek(0, io.SEEK_END), 16) self.assertEqual(reader.seek(0), 0) @@ -103,15 +104,15 @@ def test_end_of_stream_after_the_buffer_has_filled(self) -> None: """The #1586 cut-off: a full buffer's edge measured nothing left to read. This is what a frame reader does at the first octet of every record, so - the record after the one that filled the buffer was taken for the end of - the capture -- 870 of the 1,117 frames of ``http.pcap``. + without the fix the record after the one that fills the buffer is taken + for the end of the capture -- 870 of the 1,117 frames of ``http.pcap``. """ reader = self.reader(chunk=4096) self.assertEqual(reader.read(40), DATA[:40]) current = reader.tell() - self.assertEqual(reader.seek(0, io.SEEK_END) - current, 16) # was 0 + self.assertEqual(reader.seek(0, io.SEEK_END) - current, 16) # without the fix: 0 self.assertEqual(reader.seek(current), current) self.assertEqual(reader.read(16), DATA[40:56]) @@ -122,18 +123,19 @@ def test_end_of_stream_is_the_real_end_when_that_comes_first(self) -> None: self.assertEqual(reader.read(1), b'') def test_forward_seek_lands_on_its_target_over_short_answers(self) -> None: - """A seek past the octets read stopped at the end of one three-octet read.""" + """Without the fix, a seek past the octets read stops at the end of one three-octet read.""" reader = self.reader() - self.assertEqual(reader.seek(10), 10) # was 3 + self.assertEqual(reader.seek(10), 10) # without the fix: 3 self.assertEqual(reader.read(2), DATA[10:12]) def test_forward_seek_does_not_overshoot_its_target(self) -> None: - """The read that filled the gap asked for more than it, and the seek landed there.""" + """Without the fix, the read that fills the gap asks for more than it, and the + seek lands there.""" from pcapkit.corekit.io import SeekableReader # pylint: disable=import-outside-toplevel reader = SeekableReader(io.BytesIO(DATA), buffer_size=16, stream_closing=False) self.addCleanup(reader.close) - self.assertEqual(reader.seek(1), 1) # was 4 + self.assertEqual(reader.seek(1), 1) # without the fix: 4 self.assertEqual(reader.read(2), DATA[1:3]) def test_end_of_stream_reads_ahead_only_as_far_as_asked(self) -> None: @@ -146,7 +148,7 @@ def test_end_of_stream_reads_ahead_only_as_far_as_asked(self) -> None: reader = self.reader(DATA[:30], chunk=4096) reader._read_ahead_size = 10 # pylint: disable=protected-access self.assertEqual(reader.seek(0, io.SEEK_END), 10) - self.assertEqual(reader.raw.consumed, 10) # was 16: the whole buffer's worth + self.assertEqual(reader.raw.consumed, 10) # without the fix: 16, the whole buffer's worth self.assertEqual(reader.seek(0), 0) self.assertEqual(reader.read(30), DATA[:30]) @@ -154,15 +156,15 @@ def test_reading_ahead_past_the_buffer_grows_it(self) -> None: """A record longer than the buffer is held whole, from its first octet on. The frame readers measure a record from its first octet and then seek back to - it. Capped at the buffer, the read-ahead measured it short; read on, the record - slid out of the buffer, and the seek back failed with ``cannot seek before the + it. Capped at the buffer, the read-ahead measures it short; read on, the record + slides out of the buffer, and the seek back fails with ``cannot seek before the beginning of the buffer``. """ reader = self.reader(chunk=7) self.assertEqual(reader.read(8), DATA[:8]) reader._read_ahead_size = 40 # pylint: disable=protected-access - self.assertEqual(reader.seek(0, io.SEEK_END), 48) # was 24: 8 + 16 + self.assertEqual(reader.seek(0, io.SEEK_END), 48) # without the fix: 24, i.e. 8 + 16 self.assertEqual(reader.seek(8), 8) self.assertEqual(reader.read(40), DATA[8:48]) @@ -170,7 +172,7 @@ def test_reading_ahead_a_bogus_length_stops_at_the_real_end(self) -> None: """A length read from a damaged record is read as far as the stream goes.""" reader = self.reader(DATA[:50], chunk=4096) reader._read_ahead_size = 1 << 40 # pylint: disable=protected-access - self.assertEqual(reader.seek(0, io.SEEK_END), 50) # was 16 + self.assertEqual(reader.seek(0, io.SEEK_END), 50) # without the fix: 16 self.assertEqual(reader.seek(0), 0) self.assertEqual(reader.read(), DATA[:50]) @@ -188,7 +190,7 @@ def test_close_after_the_stream_was_closed(self) -> None: reader = SeekableReader(stream, buffer_size=16, stream_closing=False) self.assertEqual(reader.read(4), DATA[:4]) stream.close() - reader.close() # was ValueError + reader.close() # without the fix: ValueError self.assertTrue(reader.closed) def test_seek_past_a_stream_that_ended_inside_the_buffer_is_silent(self) -> None: diff --git a/tests/corekit/test_io_stream_proxies_1516_unit.py b/tests/corekit/test_io_stream_proxies_1516_unit.py index 550110a893..299e1e8ae8 100644 --- a/tests/corekit/test_io_stream_proxies_1516_unit.py +++ b/tests/corekit/test_io_stream_proxies_1516_unit.py @@ -5,8 +5,7 @@ ``peek`` one, for :class:`~pcapkit.foundation.extraction.Extractor` (#1506, #1509). :class:`~pcapkit.corekit.io.NamedStream` gives a stream the ``name`` :func:`pcapfile.savefile.load_savefile` reads, for the PyPCAPFile engine -(formerly the file-local ``_NamedStream`` in -:mod:`pcapkit.foundation.engines.pypcapfile`). Both live beside +(:mod:`pcapkit.foundation.engines.pypcapfile`). Both live beside :class:`~pcapkit.corekit.io.SeekableReader`. Every class is imported from :mod:`pcapkit.corekit.io` inside each test, after diff --git a/tests/corekit/test_sentinels_housing_unit.py b/tests/corekit/test_sentinels_housing_unit.py index 307073ccb8..881bd92aef 100644 --- a/tests/corekit/test_sentinels_housing_unit.py +++ b/tests/corekit/test_sentinels_housing_unit.py @@ -20,17 +20,16 @@ ``tests/project/test_no_moved_name_re_exports_1744_unit.py`` pins that. Without #922 the sentinels module does not exist, which is exactly what makes -this a regression test rather than a description: at ``d31c0aaf6``, a ``main`` -commit without #922, ``import pcapkit.corekit.sentinels`` itself -fails -- +this a regression test rather than a description: +``import pcapkit.corekit.sentinels`` itself fails -- .. code-block:: text ModuleNotFoundError: No module named 'pcapkit.corekit.sentinels' -- so every test below, including the module-level import at the top of this -file, fails before its body ever runs. Quoted from an actual run against that -commit, not inferred: +file, fails before its body ever runs. Quoted from an actual run against +``d31c0aaf6``, a ``main`` commit without #922, not inferred: .. code-block:: text @@ -254,7 +253,8 @@ def setUp(self) -> 'None': self.null = self.sentinels.NULL def test_a_freshly_written_payload_names_the_new_home(self) -> 'None': - """Every protocol names the new home, never the old one, and loads back.""" + """Every protocol names :mod:`pcapkit.corekit.sentinels`, never + ``pcapkit.corekit.module``, and loads back.""" for protocol in range(0, pickle.HIGHEST_PROTOCOL + 1): with self.subTest(protocol=protocol): blob = pickle.dumps(self.null, protocol=protocol) diff --git a/tests/dumpkit/test_common_unit.py b/tests/dumpkit/test_common_unit.py index 8e2f88a2a1..d3109140fb 100644 --- a/tests/dumpkit/test_common_unit.py +++ b/tests/dumpkit/test_common_unit.py @@ -267,8 +267,8 @@ def test_make_dumper_escapes_mapping_keys_for_plist_like_output(self) -> None: """GitHub issue #772, the half the hook above cannot reach. ``PLIST._append_dict`` in :mod:`dictdumper.plist` writes a key as - ``'{item}'.format(item=item)`` and calls ``_encode_value`` on - the *value* two lines down, never on the key -- so :meth:`object_hook` + ``'{item}'.format(item=item)`` and then calls ``_encode_value`` + on the *value*, never on the key -- so :meth:`object_hook` is handed every value the writer interpolates and no key at all, and the escaping proven above cannot reach one. Both branches that build a mapping therefore escape their own keys: @@ -307,9 +307,9 @@ def test_make_dumper_escapes_mapping_keys_for_plist_like_output(self) -> None: converted = [plist_dumper.object_hook(entry) for entry in plist_dumper.object_hook(multidict)] self.assertEqual(converted, [{'a & b <c>': 'value'}, {ESCAPED_BYTES_KEY: 'secret'}]) - # An enum-derived key is still escaped exactly once: the escaping moved - # out of this branch and into one shared helper, so escaping it twice is - # the shape of mistake that move could have made. + # An enum-derived key is escaped exactly once: this branch renders it + # with ``render_enum`` and escapes the result through the one shared + # ``escape_key`` helper, so escaping it twice is the mistake to rule out. unknown = enum.IntEnum('', {'': 1}) member = getattr(unknown, '') multidict = MultiDict() @@ -551,7 +551,7 @@ def make(number: int, packet: bytes) -> Frame: def test_pcap_dumper_cannot_serialise_a_mapping_frame(self) -> None: # Why Extractor substitutes a dict-capable trace format for the PyShark - # and PyPCAPFile engines (DPKT and Scapy left that guard in #1507): their + # and PyPCAPFile engines (DPKT and Scapy are outside it, #1507): their # flow-tracing adapters report each frame as a plain dict from # ``packet2dict``, and this dumper reads # ``value.packet`` and ``value.frame_info`` off a dissected Frame. Pinned diff --git a/tests/dumpkit/test_nameless_enum_rendering_unit.py b/tests/dumpkit/test_nameless_enum_rendering_unit.py index 977f2a9410..1b1ca8148a 100644 --- a/tests/dumpkit/test_nameless_enum_rendering_unit.py +++ b/tests/dumpkit/test_nameless_enum_rendering_unit.py @@ -22,7 +22,7 @@ class StdFlags(enum.IntFlag): - """A stdlib replica of :class:`pcapkit.const.tcp.flags.Flags`' declared bits. + """``ACK`` and ``SYN`` of :class:`pcapkit.const.tcp.flags.Flags`, as a stdlib flag enumeration. Present so the tests can show the nameless pseudo-member is not an :mod:`aenum` quirk. :mod:`enum` and :mod:`aenum` spell a wholly-undeclared @@ -78,13 +78,13 @@ class NamelessEnumRenderingTests(unittest.TestCase): """#648 -- a nameless flag member must not render as ``Type::None [n]``. :func:`~pcapkit.dumpkit.common.make_dumper`'s ``object_hook`` renders every - enumeration member as ``Type::name [value]``, and it interpolated - :attr:`~enum.Enum.name` unguarded at three separate places. A + enumeration member as ``Type::name [value]``, through + :func:`~pcapkit.dumpkit.common.render_enum` in each branch that meets one. A :class:`~enum.Flag` value composed entirely of undeclared bits has - ``name is None``, so all three put the literal four characters ``None`` into - the name half. + ``name is None``, so interpolating :attr:`~enum.Enum.name` unguarded puts + the literal four characters ``None`` into the name half. - The replacement is the value's own decimal spelling -- ``'Flags::0 [0]'``, + The fallback is the value's own decimal spelling -- ``'Flags::0 [0]'``, ``'Flags::8 [8]'`` -- chosen for reasons these tests pin: * It is what the enumeration libraries themselves already use for an @@ -185,8 +185,8 @@ def test_named_members_are_untouched(self) -> None: def test_multidict_key_path_renders_a_nameless_key(self) -> None: """The ``MultiDict``/``OrderedMultiDict`` *key* path. - A second, separate interpolation of the same shape. It builds the - dictionary key rather than the value, so a nameless key collapsed every + A separate branch rendering the same way. It builds the dictionary key + rather than the value, so an unguarded nameless key collapses every undeclared flag value onto the single key ``'Flags::None [n]'`` -- and, for two different nameless values, onto keys distinguished only by the bracketed half. @@ -216,15 +216,14 @@ def test_multidict_key_path_renders_a_nameless_key(self) -> None: self.assertEqual(dumper.object_hook(ordered), [{'Flags::0 [0]': 'first'}, {'Flags::0 [0]': 'second'}]) def test_addon_branch_renders_a_nameless_member(self) -> None: - """The third interpolation -- the ``'enum'`` key of the ``addon`` mapping. + """The ``'enum'`` key of the ``addon`` mapping, rendered the same way. Reached when the member carries public instance attributes, which for a pseudo-member takes a :meth:`~enum.Enum._missing_` override. No registry under :mod:`pcapkit.const` is both a flag enumeration and an annotated - one, so this branch is not reachable from a capture -- but the - interpolation is character-for-character the same as the other two, so - it is guarded with them rather than left as the one place the literal - ``None`` survives. + one, so this branch is not reachable from a capture -- but its + rendering is the same as the other branches', so it is guarded with them + rather than left as the one place the literal ``None`` survives. """ from pcapkit.dumpkit.common import make_dumper diff --git a/tests/foundation/engines/test_dpkt_parse_error_unit.py b/tests/foundation/engines/test_dpkt_parse_error_unit.py index fd49f4e539..771db9a2b6 100644 --- a/tests/foundation/engines/test_dpkt_parse_error_unit.py +++ b/tests/foundation/engines/test_dpkt_parse_error_unit.py @@ -3,8 +3,8 @@ GitHub issue #1351: dpkt 1.9.8 reads ``frag_off`` from whatever header follows an IPv6 Fragment header, so a Fragment header followed by ESP or Destination -Options raises :exc:`AttributeError` from ``dpkt/ip6.py``. The engine did not -catch it, so the third-party exception ended the whole extraction. +Options raises :exc:`AttributeError` from ``dpkt/ip6.py``. Guards against the +engine letting that third-party exception end the whole extraction. Every case writes its own capture to a temporary directory. Everything from :mod:`pcapkit` is imported inside each test, after @@ -68,10 +68,10 @@ def test_unparsable_frame_is_kept_as_raw_data(self) -> None: and 'cannot parse' in str(item.message) for item in caught)) def test_unparsable_frame_is_written_out(self) -> None: - # The engine writes every frame through packet2dict, which read the - # header fields a frame kept raw does not have, so AttributeError ended - # the extraction there (#1662). It maps to no fields, then the next frame - # is read. + # The engine writes every frame through packet2dict. Without the fix, + # packet2dict reads the header fields a frame kept raw does not have, so + # AttributeError ends the extraction there (#1662). Such a frame maps to + # no fields, then the next frame is read. import pcapkit from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.dpkt import packet2dict diff --git a/tests/foundation/engines/test_dpkt_pcapng_interfaces_unit.py b/tests/foundation/engines/test_dpkt_pcapng_interfaces_unit.py index 236d724e39..d90461107a 100644 --- a/tests/foundation/engines/test_dpkt_pcapng_interfaces_unit.py +++ b/tests/foundation/engines/test_dpkt_pcapng_interfaces_unit.py @@ -2,18 +2,18 @@ """The DPKT engine resolves every PCAP-NG packet against its own interface. GitHub issue #1379: :class:`dpkt.pcapng.Reader` reads only the first Interface -Description Block (IDB) of a file, so the DPKT engine gave every packet the -first interface's link type and ``if_tsresol`` -- a packet on a nanosecond -interface came out 1000 times too late, a raw-IP interface was dissected as -Ethernet, and a later section's interfaces were never read at all. +Description Block (IDB) of a file. Guards against the DPKT engine giving every +packet the first interface's link type and ``if_tsresol`` -- a packet on a +nanosecond interface coming out 1000 times too late, a raw-IP interface +dissected as Ethernet, and a later section's interfaces never read at all. Each case writes a capture built here to a temporary directory: two sections of opposite byte order, whose interfaces differ in link type, ``if_tsresol`` and ``if_tsoffset``. Everything from :mod:`pcapkit` is imported inside each test, after :func:`~tests._support.reimport_once_per_class`, not at module load. -GitHub issue #1520: the engine's reader skipped Simple Packet Blocks, so their -packets were missing and every later frame was numbered one short. +GitHub issue #1520: guards against the engine's reader skipping Simple Packet +Blocks, which loses their packets and numbers every later frame one short. """ diff --git a/tests/foundation/engines/test_dpkt_raw_null_linktypes_unit.py b/tests/foundation/engines/test_dpkt_raw_null_linktypes_unit.py index af3542a1a0..a842d4c0ca 100644 --- a/tests/foundation/engines/test_dpkt_raw_null_linktypes_unit.py +++ b/tests/foundation/engines/test_dpkt_raw_null_linktypes_unit.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """The DPKT engine reads raw-IP and BSD loopback frames by their version and address family. -GitHub issues #1502 and #1574. :meth:`DPKT._get_protocol -` knew Ethernet, IPv4 and -IPv6 only, so a ``LINKTYPE_RAW`` (101), ``LINKTYPE_NULL`` (0) or -``LINKTYPE_LOOP`` (108) frame was kept as raw data with a warning. Raw IP is +GitHub issues #1502 and #1574. Guards against :meth:`DPKT._get_protocol +` knowing Ethernet, IPv4 +and IPv6 only, which keeps a ``LINKTYPE_RAW`` (101), ``LINKTYPE_NULL`` (0) or +``LINKTYPE_LOOP`` (108) frame as raw data with a warning. Raw IP is read as IPv4 or IPv6 by its version nibble, and both loopback link types by the address family, as :class:`~pcapkit.protocols.link.loopback.Loopback` reads them, with the network layer where :mod:`pcapkit.toolkit.dpkt` looks for it. diff --git a/tests/foundation/engines/test_engine_base.py b/tests/foundation/engines/test_engine_base.py index 74eee16df1..9cb05272bd 100644 --- a/tests/foundation/engines/test_engine_base.py +++ b/tests/foundation/engines/test_engine_base.py @@ -126,8 +126,8 @@ def test_engine_subclass_rejects_unrecognised_keyword(self) -> None: Unguarded by version on purpose: ``protocol`` is not one of the four names that collide with :meth:`abc.ABCMeta.__new__` below Python 3.11, so - the guard is reached on every supported version. Verified against this - tree on 3.10.21, where it raises ``UnsupportedCall`` as it does on 3.14. + the guard is reached on every supported version. Verified on 3.10.21, + where it raises ``UnsupportedCall`` as it does on 3.14. ``name`` is an unrecognised keyword here too, and is checked separately below because it is the one typo whose exception *type* is diff --git a/tests/foundation/engines/test_engine_verbose_callback_1874_unit.py b/tests/foundation/engines/test_engine_verbose_callback_1874_unit.py index 180d801b2c..3a8d93f9d7 100644 --- a/tests/foundation/engines/test_engine_verbose_callback_1874_unit.py +++ b/tests/foundation/engines/test_engine_verbose_callback_1874_unit.py @@ -2,9 +2,10 @@ """A callback given as ``verbose`` reaches every frame on every engine (#1874). :class:`~pcapkit.foundation.extraction.Extractor` keeps a callable ``verbose`` -as its ``_vfunc``, but each third-party engine installed its own printer -whenever verbose output was on, so the caller's callback was replaced and never -called. An engine installs its printer only for ``verbose=True``. +as its ``_vfunc``. Guards against a third-party engine installing its own +printer whenever verbose output is on, which replaces the caller's callback so +that it is never called. An engine installs its printer only for +``verbose=True``. Each case runs a real extraction of :file:`in.pcap`, which git tracks, so the module is unit tier. It proves first that the requested engine ran: the @@ -136,7 +137,7 @@ def test_verbose_true_prints_the_engines_own_chain(self: 'Any') -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class DefaultEngineVerboseTests(_VerboseCases, unittest.TestCase): - """The built-in PCAP engine, which always kept the callback.""" + """The built-in PCAP engine, which keeps the callback too.""" ENGINE = 'default' diff --git a/tests/foundation/engines/test_pcap_backend.py b/tests/foundation/engines/test_pcap_backend.py index d15d9fa16f..cc918bf8cd 100644 --- a/tests/foundation/engines/test_pcap_backend.py +++ b/tests/foundation/engines/test_pcap_backend.py @@ -190,7 +190,8 @@ def test_finds_a_dist_info_without_reading_its_metadata(self) -> None: from pcapkit.foundation.engines import _pcap_backend # From Python 3.15 ``importlib.metadata.distribution`` parses every - # candidate's METADATA, which ran on each pcap_ct extraction (#2110). + # candidate's METADATA, which would run on each pcap_ct extraction + # (#2110). # Existence is all that is asked, so here a METADATA read is an error. def unread(self, filename): raise AssertionError(f'read {filename} of {self}') diff --git a/tests/foundation/engines/test_pcap_backend_probe_once_2113_unit.py b/tests/foundation/engines/test_pcap_backend_probe_once_2113_unit.py index ab0e26b75a..0b0263a0ee 100644 --- a/tests/foundation/engines/test_pcap_backend_probe_once_2113_unit.py +++ b/tests/foundation/engines/test_pcap_backend_probe_once_2113_unit.py @@ -1,9 +1,10 @@ # -*- coding: utf-8 -*- """An extraction probes the pcap backend once, not three times (#2113). -Engine selection asked ``unsupported_reason`` in the constructor and again in -``run``, and the engine's constructor probed a third time; each probe scans the -installed distributions. The probe is shared within one extraction only, so a +Engine selection asks ``unsupported_reason`` in the constructor and again in +``run``, and the engine's constructor asks once more; each probe scans the +installed distributions. Guards against each of those asks probing afresh. The +probe is shared within one extraction only, so a later extraction, or a direct ``unsupported_reason`` call, still asks afresh. """ diff --git a/tests/foundation/engines/test_pcapng_spb_zero_snaplen_1558_unit.py b/tests/foundation/engines/test_pcapng_spb_zero_snaplen_1558_unit.py index 822001e411..0e814c62f7 100644 --- a/tests/foundation/engines/test_pcapng_spb_zero_snaplen_1558_unit.py +++ b/tests/foundation/engines/test_pcapng_spb_zero_snaplen_1558_unit.py @@ -2,11 +2,11 @@ """GitHub issue #1558: a Simple Packet Block keeps its padding when the snaplen is 0. An SPB's packet data is ``min(original_len, snaplen)`` octets, with interface -0's snaplen, and a snaplen of 0 means no limit. The default engine handed -interface 0's snaplen to the parser as ``__packet__['snaplen']`` as it stood, so -a 0 read as a limit of no octets, and the block's padding was read as packet -data: 57 data octets came out as 60. The engine hands over no limit for a -zero snaplen, and :class:`~pcapkit.protocols.misc.pcapng.PCAPNG` reads a zero +0's snaplen, and a snaplen of 0 means no limit. Guards against the default +engine handing interface 0's snaplen to the parser as ``__packet__['snaplen']`` +as it stands, which reads a 0 as a limit of no octets and the block's padding as +packet data: 57 data octets come out as 60. The engine hands over no limit for +a zero snaplen, and :class:`~pcapkit.protocols.misc.pcapng.PCAPNG` reads a zero snaplen handed in as no limit too. Each case builds its capture here. Everything from :mod:`pcapkit` is imported diff --git a/tests/foundation/engines/test_pypcap_unusable_module_2132_unit.py b/tests/foundation/engines/test_pypcap_unusable_module_2132_unit.py index c1d0bcce2b..0602569e16 100644 --- a/tests/foundation/engines/test_pypcap_unusable_module_2132_unit.py +++ b/tests/foundation/engines/test_pypcap_unusable_module_2132_unit.py @@ -4,9 +4,9 @@ than :exc:`ImportError` -- ``OSError: Cannot find libpcap.so library`` with no system libpcap, for one. :meth:`Extractor.import_test ` lets that through, so the -engine's ``unsupported_reason()`` has to report it. ``PCAP_CT`` did and ``PyPCAP`` -did not, so ``extract(engine='pypcap')`` raised the loader's error where -``engine='pcap_ct'`` warned and fell back. Both ask +engine's ``unsupported_reason()`` has to report it. Guards against ``PyPCAP`` +not reporting it, which makes ``extract(engine='pypcap')`` raise the loader's +error where ``engine='pcap_ct'`` warns and falls back. Both ask :func:`pcapkit.foundation.engines._pcap_backend.engine_reason`. """ diff --git a/tests/foundation/engines/test_pyshark_engine.py b/tests/foundation/engines/test_pyshark_engine.py index 25da8c89a5..a2c024608d 100644 --- a/tests/foundation/engines/test_pyshark_engine.py +++ b/tests/foundation/engines/test_pyshark_engine.py @@ -247,7 +247,7 @@ def test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous(self) -> Non @unittest.skipUnless(HAS_PYSHARK, 'pyshark not installed') def test_a_config_ini_naming_an_off_path_tshark_is_not_read_as_missing(self) -> None: - """Regression for the cross-review's measured "2 failed". + """A tshark that only ``config.ini`` names is found, not reported missing. pyshark's own ``get_process_path()`` reads ``tshark_path`` from a ``config.ini`` on :func:`pathlib.Path.cwd` *before* it ever consults @@ -255,23 +255,21 @@ def test_a_config_ini_naming_an_off_path_tshark_is_not_read_as_missing(self) -> exact case that makes :func:`shutil.which` the wrong probe: a tshark stand-in that ``config.ini`` names but that sits in a directory never on ``PATH``, so ``which`` cannot resolve *that* path while pyshark finds it - anyway. A probe keyed on ``which`` -- what this file used before -- would - call this "missing" and fail both + anyway. A probe keyed on ``which`` would call this "missing" and fail both :meth:`test_the_reason_tracks_the_running_interpreter` and :meth:`test_this_host_really_has_no_tshark_so_the_check_is_not_vacuous`; :func:`_tshark_missing` and ``PyShark.unsupported_reason`` must not. - Deliberately host-independent, and that is itself the regression: - asserting ``shutil.which('tshark') is None``, i.e. that the *host* has - no tshark anywhere on ``PATH`` at all, is true on a machine without it - and false on every ``engine-tests`` CI leg once #755's own - ``apt-get install … tshark`` step runs, which is exactly the asymmetry - that escaped review. The claim this test actually needs is narrower: - that ``which`` cannot resolve *the stand-in specifically*, because its - directory was never put on ``PATH`` -- true regardless of whether some - unrelated ``tshark`` happens to sit on ``PATH`` elsewhere. Both worlds - are constructed and checked below rather than left to whatever the - running host happens to provide. + Deliberately host-independent, and that is itself the regression: asserting + ``shutil.which('tshark') is None``, i.e. that the *host* has no tshark + anywhere on ``PATH`` at all, is true on a machine without it and false on + every ``engine-tests`` CI leg once #755's own ``apt-get install … tshark`` + step runs. The claim this test actually needs is narrower: that ``which`` + cannot resolve *the stand-in specifically*, because its directory was never + put on ``PATH`` -- true regardless of whether some unrelated ``tshark`` + happens to sit on ``PATH`` elsewhere. Both worlds are constructed and + checked below rather than left to whatever the running host happens to + provide. """ import os diff --git a/tests/foundation/engines/test_pyshark_non_packet_unit.py b/tests/foundation/engines/test_pyshark_non_packet_unit.py index eb86763185..17238e8670 100644 --- a/tests/foundation/engines/test_pyshark_non_packet_unit.py +++ b/tests/foundation/engines/test_pyshark_non_packet_unit.py @@ -3,14 +3,14 @@ :program:`tshark` numbers every record wiretap hands it, and a PCAP-NG systemd Journal Export Block, Custom Block or Sysdig Event Block is one. The default -engine counts Enhanced, Simple and obsolete Packet Blocks only, so a capture -holding a journal block read as one frame more through ``pyshark``, and every -later frame was off by one. +engine counts Enhanced, Simple and obsolete Packet Blocks only. Guards against a +capture holding a journal block reading as one frame more through ``pyshark``, +with every later frame off by one. The same lookup serves the verbose handler. ``pyshark`` takes the second PDML ```` as ``frame_info``, and on a packet that carries a comment that is -``pkt_comment``, which has no ``protocols``. So ``verbose=True`` raised -:exc:`AttributeError` on frame 1 of :file:`examples/captures/test.pcapng`. +``pkt_comment``, which has no ``protocols``. Without the fix, ``verbose=True`` +raises :exc:`AttributeError` on frame 1 of :file:`examples/captures/test.pcapng`. The packets here are stand-ins shaped like what ``pyshark`` 0.6 builds from :program:`tshark` 4.6.9's PDML. The field names are the ones measured on diff --git a/tests/foundation/engines/test_scapy_engine.py b/tests/foundation/engines/test_scapy_engine.py index 9015f03686..015db59e05 100644 --- a/tests/foundation/engines/test_scapy_engine.py +++ b/tests/foundation/engines/test_scapy_engine.py @@ -11,22 +11,22 @@ dissects down as far as the loaded modules reach and then stops at ``Raw``. So this engine's *import statement* is load-bearing in a way an engine's usually -is not, and #406 is what happens when it is wrong: the engine imported only -:mod:`scapy.sendrecv`, and every frame of every capture came back as ``Raw`` -- -no exception, no :mod:`pcapkit` warning, and a stderr line from `Scapy`_ that a -library consumer never sees. ``follow_tcp_stream`` quietly reported no streams. +is not, and #406 is what happens when it is wrong: an engine that imports only +:mod:`scapy.sendrecv` returns every frame of every capture as ``Raw`` -- no +exception, no :mod:`pcapkit` warning, and a stderr line from `Scapy`_ that a +library consumer never sees. ``follow_tcp_stream`` quietly reports no streams. **Why these tests use a subprocess.** The tables live on `Scapy`_'s process-wide ``conf`` singleton, and once any module has populated them they stay populated: no public API resets them, and ``sys.modules`` surgery does not un-run a -``bind_layers`` call. So an in-process assertion here would pass whenever -something earlier in the run had imported a `Scapy`_ layer -- which is exactly -how #406 went unnoticed. :mod:`tests.toolkit.test_scapy_unit` imports -:mod:`scapy.layers.l2`, :mod:`~scapy.layers.inet` and :mod:`~scapy.layers.inet6` -to build its fixtures, so ``pytest tests/toolkit tests/interface/test_misc.py`` -saw correct dissection while ``pytest tests/interface/test_misc.py`` alone did -not. A fresh interpreter is the only place the engine's own import can be held -responsible for the outcome, so that is where it is asserted. +``bind_layers`` call. So an in-process assertion here would pass whenever something +earlier in the run had imported a `Scapy`_ layer -- which is exactly how #406 went +unnoticed. :mod:`tests.toolkit.test_scapy_unit` imports :mod:`scapy.layers.l2`, +:mod:`~scapy.layers.inet` and :mod:`~scapy.layers.inet6` to build its fixtures, so +under #406 ``pytest tests/toolkit tests/interface/test_misc.py`` sees correct +dissection while ``pytest tests/interface/test_misc.py`` alone does not. A fresh +interpreter is the only place the engine's own import can be held responsible for +the outcome, so that is where it is asserted. The end-to-end behaviour these guard is asserted through the public interface in :meth:`tests.interface.test_misc.FollowTCPStreamTests.test_scapy_engine_matches_the_default_engine`; diff --git a/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py b/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py index b2ef2bd83c..a446b2390a 100644 --- a/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py +++ b/tests/foundation/engines/test_scapy_pcapng_sections_runtime.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """The scapy engine starts each PCAP-NG section with its own interfaces (#1522). -An interface ID counts the Interface Description Blocks of its own section only, -but :class:`scapy.utils.RawPcapNgReader` appends every section's to one table, so -a later section's interface 0 resolved to the first section's, link type and -``if_tsresol`` included. :mod:`pcapkit.foundation.engines.scapy` resets the table -at each Section Header Block in its own reader subclass. +An interface ID counts the Interface Description Blocks of its own section only, but +:class:`scapy.utils.RawPcapNgReader` appends every section's to one table, so +without the fix a later section's interface 0 resolves to the first section's, link +type and ``if_tsresol`` included. :mod:`pcapkit.foundation.engines.scapy` resets the +table at each Section Header Block in its own reader subclass. Both captures are synthesised from :file:`tcp.pcap`, so the module belongs to the fixture-dependent tier. Each has two little-endian sections, the first on one diff --git a/tests/foundation/engines/test_scapy_reader_hooks_unit.py b/tests/foundation/engines/test_scapy_reader_hooks_unit.py index 440fc8091c..ea54acd705 100644 --- a/tests/foundation/engines/test_scapy_reader_hooks_unit.py +++ b/tests/foundation/engines/test_scapy_reader_hooks_unit.py @@ -9,7 +9,7 @@ * ``_check_interface_id`` returns whether the interface exists since Scapy 2.8, which then skips the block on a false result; before 2.8 it returns - :data:`None` and raises instead. An override that dropped the result made 2.8 + :data:`None` and raises instead. An override that drops the result makes 2.8 skip every packet block. * Scapy 2.5 keeps an interface as ``(linktype, snaplen, tsresol)`` and 2.7 as ``(linktype, snaplen, options)``, so the ``if_tsoffset`` of each interface is diff --git a/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py b/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py index d7a4207b79..16e489a2b4 100644 --- a/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py +++ b/tests/foundation/engines/test_scapy_reader_interfaces_runtime.py @@ -8,11 +8,11 @@ * #1503 -- a Simple Packet Block carries no timestamp. Scapy leaves the packet with the time it was built, i.e. the moment it was read; the default engine reads 0. -* #1517 -- the flow tracer looked the link type up by the name of the packet's - first layer, and a raw IPv4 packet's is ``IP``, which names no link type, so - tracing raised :exc:`~pcapkit.utilities.exceptions.MissingKeyError`. -* #1549 -- Scapy ignores an interface's ``if_tsoffset``, so each timestamp on it - was short by the offset. +* #1517 -- a flow tracer that looks the link type up by the name of the + packet's first layer finds ``IP`` for a raw IPv4 packet, which names no link + type, so tracing raises :exc:`~pcapkit.utilities.exceptions.MissingKeyError`. +* #1549 -- Scapy ignores an interface's ``if_tsoffset``, so each timestamp it + reads on that interface is short by the offset. The captures are synthesised from :file:`tcp.pcap`, whose frames 1, 2, 6 and 7 are one IPv4 flow and 3, 4 and 5 one IPv6 flow, so the module belongs to the diff --git a/tests/foundation/engines/test_scapy_record_length_1717_runtime.py b/tests/foundation/engines/test_scapy_record_length_1717_runtime.py index 7a4a608883..1fb6cc9938 100644 --- a/tests/foundation/engines/test_scapy_record_length_1717_runtime.py +++ b/tests/foundation/engines/test_scapy_record_length_1717_runtime.py @@ -3,8 +3,8 @@ :file:`examples/generators/options.py` writes HOPOPT's Jumbo Payload case as a genuine jumbogram, a 65,590-octet frame in a capture whose snaplen is 262,144. -The scapy engine handed over its first 65,535 octets, Scapy's ``MTU``, which -``recv()`` cuts every record to by default. +Guards against the scapy engine handing over only its first 65,535 octets, +Scapy's ``MTU``, which ``recv()`` cuts every record to by default. :file:`options-ipv6.pcap` is generated, so the module belongs to the fixture-dependent tier. diff --git a/tests/foundation/engines/test_scapy_record_length_1717_unit.py b/tests/foundation/engines/test_scapy_record_length_1717_unit.py index 4e9fcf3892..0c2367c63f 100644 --- a/tests/foundation/engines/test_scapy_record_length_1717_unit.py +++ b/tests/foundation/engines/test_scapy_record_length_1717_unit.py @@ -5,7 +5,8 @@ ``MTU`` of 65,535 octets, and every Scapy reader cuts a record to ``size`` -- Scapy 2.6's PCAP-NG reader to ``MTU`` whatever the ``size``. Scapy 2.8's PCAP reader also reads at most ``MTU * 4`` octets of a record, 262,140, and skips -the rest. So a record longer than either reached the engine's frames short. +the rest. So, without the fix, a record longer than either reaches the +engine's frames short. The reader subclass of :mod:`pcapkit.foundation.engines.scapy` reads it whole. The captures are synthesised, so the module belongs to the unit tier. diff --git a/tests/foundation/reassembly/data/test_models.py b/tests/foundation/reassembly/data/test_models.py index 1f353adc79..d7105b2cb0 100644 --- a/tests/foundation/reassembly/data/test_models.py +++ b/tests/foundation/reassembly/data/test_models.py @@ -87,11 +87,10 @@ def test_completion_is_a_string_that_reads_as_a_bool(self) -> None: def test_completion_values_are_pinned(self) -> None: """GitHub issue #884: pin every member's exact value and the member set. - :class:`~pcapkit.foundation.reassembly.data.data.Completion` was - converted from spelled-out literals to ``auto()`` -- which on a - :class:`~pcapkit.utilities.compat.StrEnum` returns - ``name.lower()`` and so is value-preserving here -- but a future - change to ``_generate_next_value_`` could silently alter these + :class:`~pcapkit.foundation.reassembly.data.data.Completion` spells its + values with ``auto()``, which on a + :class:`~pcapkit.utilities.compat.StrEnum` returns ``name.lower()``, so + a change to ``_generate_next_value_`` could silently alter these wire-visible strings without this test noticing unless every member and every value is asserted explicitly. diff --git a/tests/foundation/reassembly/test_ip.py b/tests/foundation/reassembly/test_ip.py index a1bed8b9ce..2f754db227 100644 --- a/tests/foundation/reassembly/test_ip.py +++ b/tests/foundation/reassembly/test_ip.py @@ -269,11 +269,10 @@ def test_conflict_is_recorded_even_when_a_later_fragment_completes_the_datagram( def test_conflict_stays_scoped_to_its_own_bufid(self) -> None: """Coverage gap named in #482's review: ``Buffer.conflict`` is created - fresh per BUFID, but "created fresh" is an argument, not a test -- - nothing exercised two BUFIDs actually in flight at once. This is the - more valuable of the two gaps: #478's boundary cases all shared a - single ACK bucket, and that is exactly the shape of gap that let a - genuine cross-bucket data-loss regression through undetected. + fresh per BUFID, but "created fresh" is an argument, not a test -- this + exercises two BUFIDs actually in flight at once. Cases that all share a + single ACK bucket, as #478's boundary cases do, cannot see a genuine + cross-bucket data-loss regression. So this interleaves two datagrams' fragments -- neither one completes before the other's next fragment arrives -- rather than finishing one @@ -309,10 +308,10 @@ def test_conflict_stays_scoped_to_its_own_bufid(self) -> None: def test_single_write_produces_two_conflict_runs_separated_by_a_match(self) -> None: """Coverage gap named in #482's review: the inner loop of :meth:`~pcapkit.foundation.reassembly.ip.IP._detect_conflicts` breaks a - conflict run on a match and resumes detection afterwards, but no - existing test drove a single fragment's comparison pass through two - separate, non-adjacent conflict runs -- only a leading match (or - mismatch) followed by one trailing run. + conflict run on a match and resumes detection afterwards. This drives a + single fragment's comparison pass through two separate, non-adjacent + conflict runs, rather than only a leading match (or mismatch) followed + by one trailing run. One arriving fragment overlaps a base fragment across three 8-octet blocks: octets 8-15 and 24-31 disagree with the base, octets 16-23 @@ -358,8 +357,9 @@ class DeferredAnalysisTests(unittest.TestCase): datagram is submitted for *every* frame -- ``pcapkit/toolkit/pcap.py`` dismisses an IPv4 frame only when its **DF** flag is set, so a frame with ``DF=0, MF=0, FO=0`` is not fragmented in any sense and still arrives here. - On ``http.pcap``, which holds no fragments at all, that was 1117 re-parses - per extraction and 86% of the cost of IP reassembly. + On ``http.pcap``, which holds no fragments at all, analysing at submit time + means 1117 re-parses per extraction, measured at 86% of the cost of IP + reassembly. What a caller sees must not change, which is why the assertions below are about *when* the analyser runs and not only about what it returns. diff --git a/tests/foundation/reassembly/test_ip_cut_non_final_fragment_1567_unit.py b/tests/foundation/reassembly/test_ip_cut_non_final_fragment_1567_unit.py index 0a9e90c59a..89f8d364e2 100644 --- a/tests/foundation/reassembly/test_ip_cut_non_final_fragment_1567_unit.py +++ b/tests/foundation/reassembly/test_ip_cut_non_final_fragment_1567_unit.py @@ -2,11 +2,11 @@ """A non-final IP fragment that ends mid-block leaves a hole, not zeros. GitHub issue #1567. The IPv6 adapters derive ``tl`` from the captured payload, -so a fragment the snapshot length cut declares exactly what was captured, and -the reassembler marked its partial last block received. A 3-fragment datagram -with its first fragment cut from 64 octets to 60 then came out ``COMPLETE``, -octets 60-63 zero-filled; cut to 52, strict mode's first run carried 4 -zero-filled octets. +so a fragment the snapshot length cut declares exactly what was captured. +Guards against the reassembler marking its partial last block received, with +which a 3-fragment datagram whose first fragment is cut from 64 octets to 60 +comes out ``COMPLETE``, octets 60-63 zero-filled, and, cut to 52, strict mode's +first run carries 4 zero-filled octets. Every fragment but the last carries a multiple of 8 octets (:rfc:`8200#section-4.5`, :rfc:`791#section-3.2`), so only the final fragment's diff --git a/tests/foundation/reassembly/test_ip_last_fragment_offset_1565_unit.py b/tests/foundation/reassembly/test_ip_last_fragment_offset_1565_unit.py index 31acd14435..0fa059c83b 100644 --- a/tests/foundation/reassembly/test_ip_last_fragment_offset_1565_unit.py +++ b/tests/foundation/reassembly/test_ip_last_fragment_offset_1565_unit.py @@ -1,12 +1,12 @@ # -*- coding: utf-8 -*- """IP reassembly buffers a fragment that reaches octet 65,528. -GitHub issue #1565. The data buffer holds 65,535 octets, but the received-bit -table ``RCVBT`` had only 8,191 entries, one per 8-octet block, which cover -octets 0 to 65,527. A fragment whose data reached octet 65,528 -- the last -fragment at the largest Fragment Offset, 8191, among them -- made +GitHub issue #1565. The data buffer holds 65,535 octets. Guards against a +received-bit table ``RCVBT`` of only 8,191 entries, one per 8-octet block, which +cover octets 0 to 65,527: a fragment whose data reaches octet 65,528 -- the last +fragment at the largest Fragment Offset, 8191, among them -- then makes :meth:`IP._detect_conflicts ` -index past the table, and the whole extraction raised :exc:`IndexError`. The +index past the table, and the whole extraction raises :exc:`IndexError`. The table has 8,192 entries, the partial last block included. Every case builds its fragments in memory and reads no capture. Classes are diff --git a/tests/foundation/reassembly/test_ip_length_limit_1585_unit.py b/tests/foundation/reassembly/test_ip_length_limit_1585_unit.py index 1b9c2d32df..88263b3e24 100644 --- a/tests/foundation/reassembly/test_ip_length_limit_1585_unit.py +++ b/tests/foundation/reassembly/test_ip_length_limit_1585_unit.py @@ -2,9 +2,9 @@ """An IP datagram longer than its length field can declare is never complete. GitHub issue #1585. The data buffer holds 65,535 octets of data, but IPv4's -Total Length counts the header as well, so fragments whose data covered octets -0-65,522 behind a 20-octet header made a 65,543-octet datagram, and it read -``COMPLETE``. Linux's ``ip_frag_reasm`` rejects it. The bound counts the +Total Length counts the header as well, so fragments whose data covers octets +0-65,522 behind a 20-octet header make a 65,543-octet datagram, which must not +read ``COMPLETE``. Linux's ``ip_frag_reasm`` rejects it. The bound counts the header as the family's length field does: IPv4's Total Length counts all of it, and IPv6's Payload Length counts the extension headers but not the fixed 40-octet header. A Total Length of 0 declares no length (#1555), so a datagram diff --git a/tests/foundation/reassembly/test_ip_oversized_datagram_1566_unit.py b/tests/foundation/reassembly/test_ip_oversized_datagram_1566_unit.py index 15b74c68cc..c60d378c3c 100644 --- a/tests/foundation/reassembly/test_ip_oversized_datagram_1566_unit.py +++ b/tests/foundation/reassembly/test_ip_oversized_datagram_1566_unit.py @@ -2,9 +2,9 @@ """An IP datagram longer than the data buffer is never complete. GitHub issue #1566. The data buffer holds 65,535 octets and ``RCVBT`` 8,192 -blocks. Both completion checks tested ``all(RCVBT[0:(TDL + 7) // 8])``, and the -slice stops silently at the table's end, so a final fragment declaring more data -than fits completed its datagram short. A datagram completes only if its +blocks. Guards against a completion check of ``all(RCVBT[0:(TDL + 7) // 8])``, +whose slice stops silently at the table's end, so a final fragment declaring +more data than fits completes its datagram short. A datagram completes only if its total data length fits the buffer. A fragment the buffer's end clips counts as cut there: its partial last block is diff --git a/tests/foundation/reassembly/test_ip_reassembly_roundtrip_unit.py b/tests/foundation/reassembly/test_ip_reassembly_roundtrip_unit.py index 51ea9e0a2b..56a10bc597 100644 --- a/tests/foundation/reassembly/test_ip_reassembly_roundtrip_unit.py +++ b/tests/foundation/reassembly/test_ip_reassembly_roundtrip_unit.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- """IP reassembly reports exactly the octets it received. -- GitHub issue #1357: a snaplen-truncated fragment shrank the data buffer and - marked its whole declared range received, so the datagram came out +- GitHub issue #1357: a snaplen-truncated fragment that shrinks the data buffer + and marks its whole declared range received makes the datagram come out ``COMPLETE`` with zeros in it and the rest shifted. -- GitHub issue #1358: ``strict`` runs copied whole 8-octet blocks past the - total data length, and dropped a run that reached the end of the bit table. +- GitHub issue #1358: ``strict`` runs must neither copy whole 8-octet blocks + past the total data length nor drop a run that reaches the end of the bit + table. - GitHub issue #1359: a datagram whose total data length is ``0`` never - completed, because the length was tested for truthiness. + completes if the length is tested for truthiness. Every case builds its fragments in memory and reads no capture. Classes are imported inside each test, after :func:`~tests._support.reimport_once_per_class`. diff --git a/tests/foundation/reassembly/test_ip_tso_total_length_1555_runtime.py b/tests/foundation/reassembly/test_ip_tso_total_length_1555_runtime.py index ee75c71d89..f4c857d38c 100644 --- a/tests/foundation/reassembly/test_ip_tso_total_length_1555_runtime.py +++ b/tests/foundation/reassembly/test_ip_tso_total_length_1555_runtime.py @@ -2,10 +2,10 @@ """A TSO capture reassembles to the same IPv4 datagrams as the unmodified one. GitHub issue #1555. With the four IPv4 frames of :file:`tcp.pcap` given Total -Length 0, as TCP segmentation offload leaves them, ``extract(ipv4=True, -reassembly=True)`` reported four ``PARTIAL`` datagrams with no payload under -every engine, against four ``COMPLETE`` ones of 44, 32, 244 and 32 octets on -the unmodified capture: the reassembler took the data length from ``tl - ihl``. +Length 0, as TCP segmentation offload leaves them, a reassembler that takes the +data length from ``tl - ihl`` makes ``extract(ipv4=True, reassembly=True)`` +report four ``PARTIAL`` datagrams with no payload under every engine, against +four ``COMPLETE`` ones of 44, 32, 244 and 32 octets on the unmodified capture. Wireshark does not reassemble such a frame at all -- it is one segment -- so the expected result is the unmodified capture's, the zeroed header aside. diff --git a/tests/foundation/reassembly/test_ip_tso_total_length_1555_unit.py b/tests/foundation/reassembly/test_ip_tso_total_length_1555_unit.py index f7966579e6..6894ea59ab 100644 --- a/tests/foundation/reassembly/test_ip_tso_total_length_1555_unit.py +++ b/tests/foundation/reassembly/test_ip_tso_total_length_1555_unit.py @@ -4,7 +4,7 @@ GitHub issue #1555. A capture taken on a host that offloads TCP segmentation records its IPv4 headers with Total Length 0, and the adapters hand over the rest of the captured frame as the payload (#1547). Taking the fragment's data -length from ``tl - ihl``, i.e. ``-20``, made every such datagram come out +length from ``tl - ihl``, i.e. ``-20``, makes every such datagram come out ``PARTIAL`` and empty. The reassembler takes the payload's own length instead, as Wireshark 4.6.9 does ("presumed TSO") before it reassembles anything. A non-final fragment's partial last block can then only be a snaplen cut, so it is diff --git a/tests/foundation/reassembly/test_ipv6_cut_non_final_fragment_1567_runtime.py b/tests/foundation/reassembly/test_ipv6_cut_non_final_fragment_1567_runtime.py index 137ad3829e..a4fd7473f0 100644 --- a/tests/foundation/reassembly/test_ipv6_cut_non_final_fragment_1567_runtime.py +++ b/tests/foundation/reassembly/test_ipv6_cut_non_final_fragment_1567_runtime.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """A snaplen-cut first IPv6 fragment leaves a hole under every engine. -GitHub issue #1567. A 3-fragment IPv6 datagram of 64, 64 and 72 octets, its -first fragment captured to 60 octets, came out ``COMPLETE`` with octets 60-63 -zero-filled under the default, dpkt and scapy engines, since every IPv6 adapter -passes ``tl`` as the captured length. The fix is in the reassembler they share, -so each engine is run over the same capture here and has to report the same +GitHub issue #1567. Without the fix, a 3-fragment IPv6 datagram of 64, 64 and 72 +octets, its first fragment captured to 60 octets, comes out ``COMPLETE`` with octets +60-63 zero-filled under the default, dpkt and scapy engines, since every IPv6 +adapter passes ``tl`` as the captured length. The fix is in the reassembler they +share, so each engine is run over the same capture here and has to report the same ``PARTIAL`` datagram. The captures are built in memory, through the engines, so the module belongs to diff --git a/tests/foundation/reassembly/test_reassembly_base.py b/tests/foundation/reassembly/test_reassembly_base.py index af10bbdd4d..87b7f69ee2 100644 --- a/tests/foundation/reassembly/test_reassembly_base.py +++ b/tests/foundation/reassembly/test_reassembly_base.py @@ -182,8 +182,8 @@ def test_reassembly_subclass_rejects_unrecognised_keyword(self) -> None: """A misspelled class keyword raises instead of being swallowed. Guards against it landing in ``**kwargs``, being dropped by a bare - ``super().__init_subclass__()``, and leaving the class registered under - its own class name -- no exception, no warning. + ``super().__init_subclass__()``, and, registration being opt-in, leaving + the class silently unregistered -- no exception, no warning. The keyword used here is deliberately *not* ``name``. Four class keyword names -- ``mcls``, ``name``, ``bases`` and ``namespace`` -- collide with diff --git a/tests/foundation/reassembly/test_tcp.py b/tests/foundation/reassembly/test_tcp.py index c37dfc639a..a56f065a80 100644 --- a/tests/foundation/reassembly/test_tcp.py +++ b/tests/foundation/reassembly/test_tcp.py @@ -258,22 +258,21 @@ class TCPReassemblyCoordinateTests(unittest.TestCase): TCP reassembly keeps its :rfc:`815` hole descriptor list in absolute sequence numbers, and each payload buffer indexed from its own initial - sequence number. Mixing the two produced three separate wrong answers, none - of which the rest of the suite could see, because they all need an initial - sequence number that is not zero: - - * a stream with a gap in it yielded **no datagram at all**, silently, since - every hole bound landed far past the end of a buffer a few kilobytes long; - * a stream captured without its handshake yielded only its *first* fragment, - since the completeness test counted hole descriptors instead of asking - whether any hole fell inside the data; - * every extracted fragment came out one octet too long, since a hole bound - was computed as the sequence number *after* the segment while the + sequence number. Mixing the two produces three separate wrong answers, none + of which shows without an initial sequence number that is not zero: + + * a stream with a gap in it yields **no datagram at all**, silently, since + every hole bound lands far past the end of a buffer a few kilobytes long; + * a stream captured without its handshake yields only its *first* fragment, + if the completeness test counts hole descriptors instead of asking + whether any hole falls inside the data; + * every extracted fragment comes out one octet too long, if a hole bound + is computed as the sequence number *after* the segment while the descriptor list treats its bounds as inclusive. A closely related error is pinned too: the sequence number a SYN spends - became a NUL octet at the head of the payload, so a complete datagram - did not compare equal to the octets that were sent. + must not become a NUL octet at the head of the payload, which would make a + complete datagram compare unequal to the octets that were sent. Every stream here is driven through :class:`~pcapkit.foundation.reassembly.tcp.TCP` with segment descriptors built exactly as the :mod:`pcapkit.toolkit` modules @@ -389,9 +388,9 @@ def test_incomplete_datagram_survives_a_realistic_initial_sequence_number(self) def test_the_answer_does_not_depend_on_the_initial_sequence_number(self) -> None: """The same stream reassembles the same way whatever ISN it uses. - This is the property the defect broke: at zero the two coordinate - systems coincide and the fragments came out one octet too long - each; anywhere else the datagram disappeared entirely. + This is the property the defect breaks: at zero the two coordinate + systems coincide and the fragments come out one octet too long + each; anywhere else the datagram disappears entirely. """ from pcapkit.foundation.reassembly.data.data import Completion @@ -518,9 +517,9 @@ def test_hole_descriptor_bounds_are_absolute_sequence_numbers(self) -> None: """The hole list is in sequence space; the payload buffer is not. Asserted on the live buffer rather than through a datagram, because this - is the invariant the defect broke: the list was seeded with + is the invariant the defect breaks: a list seeded with ``first=info.len`` -- a payload length -- and then extended with absolute - sequence numbers, so one descriptor held one of each. + sequence numbers leaves one descriptor holding one of each. """ from pcapkit.foundation.reassembly.data.tcp import HOLE_OPEN_BOUND @@ -583,9 +582,9 @@ def test_open_hole_bound_is_not_sys_maxsize(self) -> None: """Holes stay right above ``2 ** 31`` when :data:`sys.maxsize` is ``2 ** 31 - 1``. That is :data:`sys.maxsize` on GraalPy and on 32-bit builds, and it lies - inside the ``2 ** 32`` sequence space. Used as the open bound, it left + inside the ``2 ** 32`` sequence space. Used as the open bound, it leaves the open-ended hole with ``last`` below its own ``first`` once sequence - numbers passed ``2 ** 31`` (#1959). + numbers pass ``2 ** 31`` (#1959). """ from unittest import mock @@ -605,9 +604,10 @@ def test_open_hole_bound_is_not_sys_maxsize(self) -> None: def test_fin_completes_a_stream_above_2_31_whatever_sys_maxsize(self) -> None: """A whole stream is submitted at its FIN, with or without its handshake. - With :data:`sys.maxsize` at ``2 ** 31 - 1`` the open-ended hole above a - stream past ``2 ** 31`` survived the FIN, so the stream never counted as - whole and was held until the capture ended (#1959). + Guards against the open-ended hole above a stream past ``2 ** 31`` + surviving the FIN with :data:`sys.maxsize` at ``2 ** 31 - 1``, so that + the stream never counts as whole and is held until the capture ends + (#1959). """ from unittest import mock @@ -815,13 +815,12 @@ def test_a_conflict_in_one_ack_bucket_is_not_reported_in_another(self) -> None: :attr:`~pcapkit.foundation.reassembly.data.tcp.Fragment`, and this is what proves it rather than arguing it from where the field is declared. - Worth stating why this case in particular. The data loss this whole - mechanism exists to fix survived the boundary tests precisely because - every one of them used a *single* ACK bucket, so none could see state - leaking across buckets, and the + Worth stating why this case in particular: boundary tests that all use + a *single* ACK bucket cannot see state leaking across buckets, which is + where the data loss this whole mechanism exists to fix hides, and the + single-bucket tests of the :attr:`~pcapkit.foundation.reassembly.data.tcp.Fragment.gap` - representation came with more single-bucket tests -- the same blind - spot. + representation share that blind spot. Bucket 1000 takes a real conflicting retransmission over ``base..base+3`` and must report exactly that range. Bucket 2000 is interleaved with it, diff --git a/tests/foundation/reassembly/test_tcp_reassembly_roundtrip_unit.py b/tests/foundation/reassembly/test_tcp_reassembly_roundtrip_unit.py index 317bf7bd7c..a19148b416 100644 --- a/tests/foundation/reassembly/test_tcp_reassembly_roundtrip_unit.py +++ b/tests/foundation/reassembly/test_tcp_reassembly_roundtrip_unit.py @@ -4,14 +4,14 @@ GitHub issues #1354, #1355 and #1356, from the foundation round-trip audit (#1202): -* #1354 -- completion was judged from the buffer-wide hole list, which never - learnt of a gap opened *below* the first segment to arrive, and which another - acknowledgement number's payload buffer could clear. A zero-filled gap came +* #1354 -- completion judged from the buffer-wide hole list, which never + learns of a gap opened *below* the first segment to arrive, and which another + acknowledgement number's payload buffer can clear, brings a zero-filled gap back :attr:`~pcapkit.foundation.reassembly.data.data.Completion.COMPLETE`. -* #1355 -- sequence numbers were compared as plain integers, so a stream - crossing ``2 ** 32`` read as a gap of about 4 GiB and allocated it. -* #1356 -- a FIN submitted the buffer at once, so data reordered behind it was - split off into a buffer of its own. +* #1355 -- sequence numbers compared as plain integers make a stream crossing + ``2 ** 32`` read as a gap of about 4 GiB and allocate it. +* #1356 -- a FIN that submits the buffer at once splits data reordered behind + it off into a buffer of its own. Every case feeds hand-built segments and reads no capture. Classes are imported inside each test, after :func:`~tests._support.reimport_once_per_class`. diff --git a/tests/foundation/registry/test_foundation.py b/tests/foundation/registry/test_foundation.py index 5c26c6f43d..ed7cfdfb82 100644 --- a/tests/foundation/registry/test_foundation.py +++ b/tests/foundation/registry/test_foundation.py @@ -153,23 +153,23 @@ def test_registration_accepts_pcapkit_own_builtin_classes(self) -> None: This is the regression for GitHub issue #513. A test that mocks ``Extractor.register_*`` away never reaches the validation in - :file:`pcapkit/foundation/extraction.py` -- which is how the defect - survived: all three entry points rejected pcapkit's *own* engines, - reassembly and flow-tracing classes with + :mod:`pcapkit.foundation.extraction`, so it cannot catch the entry points + rejecting pcapkit's *own* engines, reassembly and flow-tracing classes with :exc:`~pcapkit.utilities.exceptions.RegistryError`. - The cause was that each check named the auto-registering public class - (``Engine``, ``Reassembly``, ``TraceFlow``) while every built-in subclasses + Guards against a check that names the auto-registering public class + (``Engine``, ``Reassembly``, ``TraceFlow``), while every built-in subclasses the ``*Base`` variant directly, specifically so that it is not - auto-registered. So ``issubclass(PCAP, Engine)`` was :data:`False` while - ``issubclass(PCAP, EngineBase)`` was :data:`True`. + auto-registered: ``issubclass(PCAP, Engine)`` is :data:`False` while + ``issubclass(PCAP, EngineBase)`` is :data:`True`. Deliberately does **not** mock, because the point is to exercise the gate. """ # NOTE: the public ``register_*`` guards accept ``Engine``/``Reassembly``/ # ``TraceFlow`` (the registration surface third parties extend), so the public - # door refuses the built-ins (#1016) -- see + # door refuses a built-in under any name other than the one it shipped with + # (#1016, #1363) -- see # ``test_public_registration_rejects_base_only_classes`` below. What #513 # guards -- that pcapkit's *own* classes are registrable through a real, # unmocked gate -- is checked through the internal diff --git a/tests/foundation/registry/test_protocols.py b/tests/foundation/registry/test_protocols.py index 4ad2e983a7..c9132109ac 100644 --- a/tests/foundation/registry/test_protocols.py +++ b/tests/foundation/registry/test_protocols.py @@ -598,7 +598,7 @@ def test_top_level_link_internet_and_transport_protocol_wrappers(self) -> None: # NOTE: a composite name such as ``'tcp|udp'`` raises for the same # reason the equivalent composite member is refused above: one call # registers under one transport protocol. On an ``IntFlag`` base, - # which #808 removed from ``TransportProtocol``, aenum's ``Flag`` + # which ``TransportProtocol`` does not have (#808), aenum's ``Flag`` # getitem parses a ``'|'``-joined name into a composite value on its # own, so a resolver built on it unguarded would accept this silently # instead of refusing it. @@ -671,14 +671,14 @@ def test_top_level_link_internet_and_transport_protocol_wrappers(self) -> None: udp_register.assert_not_called() register_protocol.assert_not_called() - # NOTE: further maintainer ruling on #815: ``class_`` is positional - # again, at the sibling position, and the swallow it once caused is - # disambiguated by ``type(module)`` alone, never by what the value - # looks like. Checked against the *real* ``TCP``/``UDP`` registries - # rather than mocks, because the branch lives inside the function - # body and a mock's ``call_args`` cannot distinguish "landed in - # ``transport`` and got registered" from "landed in ``class_`` and - # was silently dropped" as cleanly as the registry contents can. + # NOTE: further maintainer ruling on #815: ``class_`` is positional, at the + # sibling position, and whether the third positional is a class name or a + # transport is decided by ``type(module)`` alone, never by what the value + # looks like. Checked against the *real* ``TCP``/``UDP`` registries rather + # than mocks, because the branch lives inside the function body and a mock's + # ``call_args`` cannot distinguish "landed in ``transport`` and got + # registered" from "landed in ``class_`` and was silently dropped" as + # cleanly as the registry contents can. self._guard_registry(registry.TCP.__proto__, 65200) self._guard_registry(registry.UDP.__proto__, 65200) with mock.patch.object(registry, 'register_protocol'): diff --git a/tests/foundation/registry/test_registrar_code_check_unit.py b/tests/foundation/registry/test_registrar_code_check_unit.py index bdc9b1a631..a44da41c4e 100644 --- a/tests/foundation/registry/test_registrar_code_check_unit.py +++ b/tests/foundation/registry/test_registrar_code_check_unit.py @@ -2,8 +2,9 @@ """A registrar handed a ``code`` outside its enum changes nothing. C.f. #2186 and #2224. The registrars keyed on an enum code log ``code.name`` once the registration is -done, so an ``int`` or :obj:`None` failed there with :exc:`AttributeError` after -the protocol's table had been written. Each registrar rejects such a ``code`` +done, so without a check an ``int`` or :obj:`None` fails there with +:exc:`AttributeError` after the protocol's table has been written. Each +registrar rejects such a ``code`` with :exc:`~pcapkit.utilities.exceptions.RegistryError` before it touches anything. @@ -18,7 +19,7 @@ Every registrar, those four included, also rejects a member of an enum other than the one it is typed on (#2224). The registry enums compare equal to their values, so such a member keys the entry of the registrar's own member of that -value: ``register_linktype(TransType.ICMP, ...)`` replaced +value: without the check, ``register_linktype(TransType.ICMP, ...)`` replaces ``Frame.__proto__[LinkType.ETHERNET]``. Each wrong-enum case picks a member equal to one of the registrar's own where the value types allow, so the check has to be on the type. diff --git a/tests/foundation/registry/test_registry_name_restore_unit.py b/tests/foundation/registry/test_registry_name_restore_unit.py index 1ef5f67858..2b6f29ab2d 100644 --- a/tests/foundation/registry/test_registry_name_restore_unit.py +++ b/tests/foundation/registry/test_registry_name_restore_unit.py @@ -3,10 +3,11 @@ Every code registrar in :mod:`pcapkit.foundation.registry.protocols` -- ``register_tcp``, ``register_linktype`` and the rest -- ends by naming the class -it registered in :data:`pcapkit.protocols.__proto__`. Registering the shipped -entry back restored the code but never the name: ``register_tcp(80, Custom)`` -then ``register_tcp(80, shipped)`` left ``'CUSTOM'`` behind for good, and a -custom class named like a built-in left the built-in displaced. +it registered in :data:`pcapkit.protocols.__proto__`. Guards against registering +the shipped entry back restoring the code but never the name: +``register_tcp(80, Custom)`` then ``register_tcp(80, shipped)`` leaving +``'CUSTOM'`` behind for good, and a custom class named like a built-in leaving +the built-in displaced. :mod:`tests.foundation.registry.test_registry_symmetry_unit` covers the plain round trip for every registrar. These pin the cases it does not reach: a @@ -207,7 +208,7 @@ def test_any_sequence_restored_in_full_leaves_names_as_found(self) -> None: """Seeded fuzz: register at random, restore every code, compare with the start. Custom classes, some named like each other and some like built-ins, and the - built-ins themselves (``IPv4`` and the three ``HTTP`` classes), go through + built-ins themselves (``IPv4`` and each ``HTTP`` class), go through ``register_tcp``, ``register_udp`` and ``register_apptype`` at a handful of shipped ports, with restores mixed in. Every code is then restored to its shipped entry, in a shuffled order, and the names must be exactly as found. diff --git a/tests/foundation/registry/test_restore_builtin_unit.py b/tests/foundation/registry/test_restore_builtin_unit.py index 87b0286620..4e12c8e864 100644 --- a/tests/foundation/registry/test_restore_builtin_unit.py +++ b/tests/foundation/registry/test_restore_builtin_unit.py @@ -2,16 +2,16 @@ """A foundation registrar restores the built-in a name shipped with. GitHub issue #1363: the public engine, reassembly and flow tracing registrars -refuse pcapkit's own classes (#513, #1016), so once ``register_extractor_engine -('dpkt', MyEngine)`` overrode a built-in, handing ``DPKT`` back raised -:exc:`~pcapkit.utilities.exceptions.RegistryError` and the override could not be -undone. The owner ruled that a registrar accepts the shipped original for a name -it shipped with, and still refuses every other built-in. +refuse pcapkit's own classes (#513, #1016), so without the fix, once +``register_extractor_engine('dpkt', MyEngine)`` overrides a built-in, handing +``DPKT`` back raises :exc:`~pcapkit.utilities.exceptions.RegistryError` and the +override cannot be undone. The owner ruled that a registrar accepts the shipped +original for a name it shipped with, and still refuses every other built-in. The restore must leave each registry exactly as shipped: the same objects, which are :class:`~pcapkit.corekit.module.ModuleDescriptor` entries, not the -classes they name. That also covers the dumper half of GitHub issue #1364, where -handing a dumper registrar its own shipped entry warned and replaced the +classes they name. That also covers the dumper half of GitHub issue #1364: a +dumper registrar handed its own shipped entry must neither warn nor replace the descriptor with the class. Classes are imported inside each test, after @@ -36,7 +36,7 @@ def setUp(self) -> None: @contextlib.contextmanager def _registries(self): # type: ignore[no-untyped-def] - """Snapshot the four registries, and put them back afterwards.""" + """Snapshot the foundation registries, and put them back afterwards.""" from pcapkit.foundation.extraction import Extractor from pcapkit.foundation.traceflow.traceflow import TraceFlow diff --git a/tests/foundation/test_extraction.py b/tests/foundation/test_extraction.py index f5dec49c39..440e964925 100644 --- a/tests/foundation/test_extraction.py +++ b/tests/foundation/test_extraction.py @@ -91,11 +91,11 @@ def setUp(self) -> None: from pcapkit.foundation.extraction import Extractor - # Several tests here register into the four registries for real -- + # Several tests here register into the registries for real -- # overwriting ``json``, ``dpkt``, ``ipv4`` and ``tcp`` with stand-ins, # or adding ``unit-*`` names -- and the class shares one import, so each - # table is put back afterwards. Without that, a later extraction ran the - # stand-in engine and hung, and :mod:`tests.interface.test_core` found + # table is put back afterwards. Without that, a later extraction runs the + # stand-in engine and hangs, and :mod:`tests.interface.test_core` finds # engines it has no constant for (GitHub issue #1065). for table in (Extractor.__output__, Extractor.__engine__, Extractor.__reassembly__, Extractor.__traceflow__): @@ -181,10 +181,11 @@ def test_properties_success_and_unsupported_paths(self) -> None: def test_declared_engine_type_is_satisfied_by_the_built_in_engines(self) -> None: """``_exeng``, ``engine`` and ``record_header`` must name a type the engine holds. - Both built-in engines subclass :class:`~pcapkit.foundation.engines.engine.EngineBase` - directly (so that they are not auto-registered), hence neither is an + The built-in engines subclass + :class:`~pcapkit.foundation.engines.engine.EngineBase` directly (so that + they are not auto-registered), hence none is an :class:`~pcapkit.foundation.engines.engine.Engine`. Guards against the - three declarations saying ``Engine``, a gap ``cast`` calls can hide (#1022). + declarations saying ``Engine``, a gap ``cast`` calls can hide (#1022). """ import pcapkit.foundation.extraction as module from pcapkit.foundation.engines.pcap import PCAP @@ -428,10 +429,10 @@ def submit(self) -> tuple[object, ...]: def test_register_helpers_reject_a_non_class_with_registry_error(self) -> None: """GitHub issue #1021: a non-class raises ``RegistryError``, not ``TypeError``. - Each guard was a bare ``issubclass(x, Base)``, which refuses a non-class - itself -- ``register_dumper`` directly (``Dumper`` has a plain ``type`` - metaclass), the other three from inside ``abc`` -- so the documented - ``RegistryError`` was unreachable. A wrong *class* still reaches the + Guards against a bare ``issubclass(x, Base)`` guard, which refuses a + non-class itself -- in ``register_dumper`` directly (``Dumper`` has a plain + ``type`` metaclass), in the others from inside ``abc`` -- and so leaves the + documented ``RegistryError`` unreachable. A wrong *class* still reaches the subclass check and raises ``RegistryError``. """ from pcapkit.corekit.module import ModuleDescriptor @@ -466,10 +467,10 @@ def test_register_helpers_reject_a_non_class_with_registry_error(self) -> None: def test_register_engine_identity_guard(self) -> None: """GitHub issue #739: re-registering the same engine class is silent. - ``register_engine`` used a presence-only guard, so the *second* of two - calls registering the exact same class under the exact same name - warned about an overwrite that never happened. This mirrors the - identity guard GitHub issue #718 gave the code-keyed protocol + Guards against a presence-only guard in ``register_engine``, with which + the *second* of two calls registering the exact same class under the + exact same name warns about an overwrite that never happened. This + mirrors the identity guard GitHub issue #718 gave the code-keyed protocol registrars (see ``test_register_stays_quiet_on_same_object_reregistration`` in ``tests/protocols/test_protocol_base_unit.py``): the guard fires only when the incumbent differs from the replacement, and the @@ -620,7 +621,7 @@ def test_register_dumper_identity_guard(self) -> None: """GitHub issue #739: re-registering the same dumper class is silent. Same defect and fix as ``test_register_engine_identity_guard``, for - ``register_dumper``. Unlike the other three, ``__output__`` maps each + ``register_dumper``. Unlike the other registries, ``__output__`` maps each format to a ``(dumper, ext)`` pair and is a :class:`collections.defaultdict`, so this also pins that the identity check compares the incumbent *dumper* (not the pair, which @@ -682,7 +683,7 @@ def test_import_test_and_make_name_paths(self) -> None: # dot. ``Extractor.__output__`` spells its extensions *with* the dot # and ``make_name`` normalises it away, because ``_fext`` reaches the # engines, which each compose a per-frame name as - # ``f'{name}.{ext._fext}'`` -- leaving the dot on gave + # ``f'{name}.{ext._fext}'`` -- leaving the dot on gives # ``Frame 1..json`` on disk. The output filename below keeps exactly # one dot, which is the other half of the same contract. self.assertEqual( @@ -1215,9 +1216,9 @@ def _warned(warn: mock.Mock, needle: str) -> bool: def test_warns_bullet_for_pcap_trace_output_names_the_engines_its_guard_warns_for(self) -> None: # The bullet lists engines by name and the guard lists them again as a - # tuple, and nothing else ties the two: the bullet once went on naming - # DPKT and Scapy after #1507 took them out of the guard (#1719). Every - # engine is put through the guard -- ``_traced`` takes the requested + # tuple, and nothing else ties the two, so the bullet can go on naming + # an engine the guard does not hold (#1719). Every engine is put + # through the guard -- ``_traced`` takes the requested # engine as the one that runs -- and the set that warns is compared # with the set the bullet names. from pcapkit.foundation.extraction import Extractor diff --git a/tests/foundation/test_extraction_constructor_closes_engine_2107_unit.py b/tests/foundation/test_extraction_constructor_closes_engine_2107_unit.py index ee89eee261..54207e48fb 100644 --- a/tests/foundation/test_extraction_constructor_closes_engine_2107_unit.py +++ b/tests/foundation/test_extraction_constructor_closes_engine_2107_unit.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """An ``Extractor`` whose constructor raises after choosing an engine closes it (#2107). -#2100 closed the input such a constructor owns, but not the engine, so an -engine's own resources -- PyShark's ``tshark`` process and temporary file -- were -left for collection. Closing it must not replace the error that stopped the +Guards against closing the input such a constructor owns (#2100) but not the engine, +which leaves an engine's own resources -- PyShark's ``tshark`` process and temporary +file -- for collection. Closing it must not replace the error that stopped the constructor, even when the close raises too. The constructor does that through ``_cleanup(failed=True)``, and ``__exit__`` diff --git a/tests/foundation/test_extraction_constructor_raises_2100_unit.py b/tests/foundation/test_extraction_constructor_raises_2100_unit.py index 12cb15c5d2..3af3b76fa4 100644 --- a/tests/foundation/test_extraction_constructor_raises_2100_unit.py +++ b/tests/foundation/test_extraction_constructor_raises_2100_unit.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """An ``Extractor`` whose constructor raises has closed the input it opened (#2100). -Only a ``KeyboardInterrupt`` reached ``_cleanup``, so a ``FormatError`` out of -``__init__`` left the file open until collection, and the exception holds the -frame that keeps it alive. Windows then refuses to delete the file -(``WinError 32``); POSIX hides it, so the handles are recorded here instead. +Guards against only a ``KeyboardInterrupt`` reaching ``_cleanup``, which leaves the +file open until collection after a ``FormatError`` out of ``__init__``, and the +exception holds the frame that keeps it alive. Windows then refuses to delete the +file (``WinError 32``); POSIX hides it, so the handles are recorded here instead. """ diff --git a/tests/foundation/test_extraction_engine_fallback_format_unit.py b/tests/foundation/test_extraction_engine_fallback_format_unit.py index f23f2deebe..ca58522723 100644 --- a/tests/foundation/test_extraction_engine_fallback_format_unit.py +++ b/tests/foundation/test_extraction_engine_fallback_format_unit.py @@ -1,12 +1,13 @@ # -*- coding: utf-8 -*- """PCAP output survives a fallback from an unavailable engine to the default one. -GitHub issue #1264: :class:`~pcapkit.foundation.extraction.Extractor` replaced -``format='pcap'`` with ``'json'`` from the *requested* engine name, before -:meth:`~pcapkit.foundation.extraction.Extractor.run` found the engine missing -and ran the default :class:`~pcapkit.foundation.engines.pcap.PCAP` engine -- -which writes PCAP -- so ``out.json`` appeared where ``out.pcap`` was asked for. -The trace format was replaced the same way. +GitHub issue #1264: guards against +:class:`~pcapkit.foundation.extraction.Extractor` replacing ``format='pcap'`` +with ``'json'`` from the *requested* engine name, before +:meth:`~pcapkit.foundation.extraction.Extractor.run` finds the engine missing +and runs the default :class:`~pcapkit.foundation.engines.pcap.PCAP` engine -- +which writes PCAP -- so that ``out.json`` appears where ``out.pcap`` was asked +for, and against the trace format being replaced the same way. Each engine is made unavailable by patching its own check, so the result does not depend on what this host has installed: ``unsupported_reason`` covers the diff --git a/tests/foundation/test_extraction_no_eof.py b/tests/foundation/test_extraction_no_eof.py index 0645a7ec87..ea9cfd93da 100644 --- a/tests/foundation/test_extraction_no_eof.py +++ b/tests/foundation/test_extraction_no_eof.py @@ -1,17 +1,17 @@ # -*- coding: utf-8 -*- """``no_eof`` has to stop eventually -- #620. -``extract(..., no_eof=True)`` never returned. End of stream was detected -correctly, and ``ExtractionWarning: EOF reached`` fired, but the three loops that -handle it all read +Without the fix, ``extract(..., no_eof=True)`` never returns. End of stream is +detected correctly, and ``ExtractionWarning: EOF reached`` fires, but every loop +that handles it reads .. code-block:: python if self._flag_n: continue -with nothing else to stop them, so an exhausted input was retried forever. The -flag suppressed the error that *ended* the loop without supplying any other +with nothing else to stop it, so an exhausted input is retried forever. The +flag suppresses the error that *ends* the loop without supplying any other ending. The flag is not pointless, which is why it gets a termination condition rather @@ -96,8 +96,9 @@ first extraction, which imports more modules and is otherwise cold code: about 21 CPU-seconds before the case under test begins, after which every case here takes 0.02-0.06s. CPython takes 0.9s in all. A single 30s timeout over the -whole child therefore failed four of eleven GraalPy runs on ``main``, the other -exhausted-input cases in each finishing at 25-30s, and called it #620. So the +whole child is therefore not enough: measured on ``main``, it failed four of +eleven GraalPy runs, the other exhausted-input cases in each finishing at +25-30s, and reported them as #620. So the child warms up with one plain extraction and prints :data:`READY`; that phase gets :data:`STARTUP`, and only what follows counts against :data:`DEADLINE`. @@ -319,8 +320,8 @@ def run_bounded(self, body: 'str') -> 'str': class ChildAllowances(unittest.TestCase): """:func:`run_child` charges warm-up to :data:`STARTUP`, not :data:`DEADLINE`. - #2085: one timeout over the whole child failed GraalPy runs whose extraction - had not even begun, and reported them as #620. These children are bare + #2085: one timeout over the whole child fails GraalPy runs whose extraction + has not even begun, and reports them as #620. These children are bare interpreters that sleep, so the cases need no capture and no runtime dependencies. @@ -374,11 +375,11 @@ def test_a_child_that_dies_before_warming_up_reports_its_status(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class NoEOFTerminatesOnAnExhaustedInput(ChildBoundedTestCase): - """The reproduction from #620, over all three loops that handle end of stream. + """The reproduction from #620, over every loop that handles end of stream. ``record_frames`` (the ``auto=True`` path), ``__next__`` and ``__call__`` each - carried their own copy of the unbounded retry, so each needs its own case: - the issue reproduced the first two and this covers the third as well. + handle end of stream themselves, so each needs its own case: the issue + reproduces the first two and this covers the third as well. Every case here parses a real capture, so every case runs in a child process -- see the module docstring for why an in-process deadline does not hold. @@ -456,10 +457,10 @@ def test_a_pipe_whose_writer_closed_terminates(self) -> None: """The ``pcapkit -`` case: a non-seekable input, exhausted. ``pcapkit/__main__.py`` sets ``no_eof`` precisely for ``fin='-'``, so this - is the flag's own documented use. #620 does not mention it, but it hung - too: a pipe reports end of stream once its writer has closed, and that was - retried forever like any other. A ``BufferedReader`` over a non-seekable - raw stream is what ``sys.stdin.buffer`` is. + is the flag's own documented use. #620 does not mention it, but without the + fix it hangs too: a pipe reports end of stream once its writer has closed, + and that is retried forever like any other. A ``BufferedReader`` over a + non-seekable raw stream is what ``sys.stdin.buffer`` is. """ self.run_bounded(f""" @@ -514,8 +515,8 @@ class NoEOFOverAGrowingFileStopsAtWhatIsThere(ChildBoundedTestCase): written down here. The case below sequences the append on the extractor's own end of stream, not - on that 0.6s timer: a loaded runner took longer than 0.6s to get there, read - the sixth frame, and failed (#1583). + on that 0.6s timer: a loaded runner can take longer than 0.6s to get there, + read the sixth frame, and fail (#1583). """ diff --git a/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py b/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py index 7c851e04ad..23012f73d7 100644 --- a/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py +++ b/tests/foundation/test_extraction_non_seekable_stream_1586_unit.py @@ -2,23 +2,24 @@ """Extracting from a pipe or another non-seekable stream drops no frame (#1586). ``examples/captures/http.pcap`` holds 1,117 frames. Read by path the default -engine gets all of them; piped in -- ``cat http.pcap | ...`` with ``fin`` set to -``sys.stdin.buffer``, an :func:`os.pipe` written in 4,096-octet chunks, or a raw -stream answering short -- it got 870, silently, with ``no_eof`` or without. -Frame 870 is the first to end past the window +engine gets all of them. Without the fix, piped in -- ``cat http.pcap | ...`` +with ``fin`` set to ``sys.stdin.buffer``, an :func:`os.pipe` written in +4,096-octet chunks, or a raw stream answering short -- it gets 870, silently, +with ``no_eof`` or without. Frame 870 is the first to end past the window :class:`~pcapkit.corekit.io.SeekableReader` keeps over such a stream, 131,072 -octets on CPython 3.14. Cut into seven-octet reads it got none at all, and -one-octet reads failed on the magic number. - -The frame readers measure what is left with ``seek(0, SEEK_END)``, and the -reader answered that with its window's far edge -- once the window has filled, -exactly how far the stream has been read, so the next record measured nothing -left. ``read`` and a forward ``seek`` took one short answer from the stream as -all there was, and ``Extractor`` took a short ``peek`` for a short file. See +octets on CPython 3.14. Cut into seven-octet reads it gets none at all, and +one-octet reads fail on the magic number. + +The frame readers measure what is left with ``seek(0, SEEK_END)``. Guards +against the reader answering that with its window's far edge -- once the window +has filled, exactly how far the stream has been read, so the next record +measures nothing left -- against ``read`` and a forward ``seek`` taking one short +answer from the stream as all there was, and against ``Extractor`` taking a +short ``peek`` for a short file. See :mod:`tests.corekit.test_io_seekable_reader_short_reads_1586_unit` for the -reader itself. Reading a buffer's worth ahead to measure each record then held a -live capture's frames back until that much more had arrived, so the engines read -ahead only as far as the record they are about to parse. +reader itself. Reading a buffer's worth ahead to measure each record would hold +a live capture's frames back until that much more has arrived, so the engines +read ahead only as far as the record they are about to parse. The captures here are built in memory, past two windows of the default buffer, so the window fills and slides whatever :data:`io.DEFAULT_BUFFER_SIZE` the @@ -323,10 +324,10 @@ def write() -> None: def test_a_record_longer_than_the_buffer_reads_as_from_a_file(self) -> None: """A record the buffer cannot hold is read whole, as it is from a file. - The read-ahead was capped at the buffer, so a PCAP-NG block longer than it was - measured short and parsed as cut, and a PCAP record slid out of the buffer - before its reader sought back to it, which failed with ``cannot seek before - the beginning of the buffer``. + Guards against a read-ahead capped at the buffer, with which a PCAP-NG + block longer than it is measured short and parsed as cut, and a PCAP + record slides out of the buffer before its reader seeks back to it, which + fails with ``cannot seek before the beginning of the buffer``. """ for kind in ('pcap', 'epb', 'shb'): @@ -341,9 +342,9 @@ def test_dpkt_reads_a_section_header_longer_than_the_buffer(self) -> None: """The dpkt engine seeks back to the first octet once the section header is read. That is :meth:`Extractor.record_header - `, which failed with - ``cannot seek before the beginning of the buffer: 0`` once the section header - had slid out of it. + `, which without the + fix fails with ``cannot seek before the beginning of the buffer: 0`` once + the section header has slid out of it. """ data = oversize('shb') @@ -356,9 +357,9 @@ def test_pcapng_blocks_parsed_past_their_end_read_as_from_a_file(self) -> None: The PCAP-NG engine reads a stream ahead only as far as the next block, but two kinds of block are parsed past that: one shorter than its own fixed fields, and a Decryption Secrets Block whose Secrets Length runs past it. Read only as - far as the block, the first warned ``packet length < 0`` where the file did - not, and the second was kept as cut where the file read the blocks after it - as secrets and failed. + far as the block, the first warns ``packet length < 0`` where the file does + not, and the second is kept as cut where the file reads the blocks after it + as secrets and fails. """ import pcapkit.utilities.exceptions as errors # pylint: disable=import-outside-toplevel @@ -382,7 +383,7 @@ def test_pcapng_blocks_parsed_past_their_end_read_as_from_a_file(self) -> None: self.assertEqual(got, expected) def test_a_packet_block_before_any_interface_fails_as_from_a_file(self) -> None: - """The engine's look at the next block type was a ``peek``, short on a pipe.""" + """The engine's look at the next block type is not cut short on a pipe.""" import pcapkit.utilities.exceptions as errors # pylint: disable=import-outside-toplevel data = make_pcapng() diff --git a/tests/foundation/test_extraction_ownership.py b/tests/foundation/test_extraction_ownership.py index c44941571e..36e555444a 100644 --- a/tests/foundation/test_extraction_ownership.py +++ b/tests/foundation/test_extraction_ownership.py @@ -301,9 +301,9 @@ def test_non_seekable_stream_survives_its_seekable_wrapper(self) -> None: A non-seekable input is wrapped in a :class:`~pcapkit.corekit.io.SeekableReader`, which ``Extractor`` constructed and so may close -- but ``stream_closing`` has to keep the - caller's stream underneath it open. That argument was - ``not self._flag_s``, i.e. always :data:`True` here, so the stream went - down with the wrapper. + caller's stream underneath it open. Guards against that argument being + ``not self._flag_s``, i.e. always :data:`True` here, which takes the + stream down with the wrapper. """ import pcapkit @@ -343,9 +343,9 @@ def test_non_seekable_stream_survives_its_seekable_wrapper(self) -> None: class ContextExitFollowsOwnership(unittest.TestCase): """#1999: leaving a ``with`` block closes only what ``Extractor`` owns. - ``__exit__`` closed the input unconditionally, so scoping an extraction with - ``with`` closed a stream the caller had passed in and still held, which the - rule of #610 says is the caller's to close. + Guards against ``__exit__`` closing the input unconditionally, which makes + scoping an extraction with ``with`` close a stream the caller passed in and + still holds, which the rule of #610 says is the caller's to close. """ diff --git a/tests/foundation/test_extraction_windows_pipe_seekable_2097_unit.py b/tests/foundation/test_extraction_windows_pipe_seekable_2097_unit.py index 1e3542545d..8e34bc42a8 100644 --- a/tests/foundation/test_extraction_windows_pipe_seekable_2097_unit.py +++ b/tests/foundation/test_extraction_windows_pipe_seekable_2097_unit.py @@ -2,10 +2,10 @@ """A pipe that says it can seek and does not still yields every frame (#2097). On Windows before CPython 3.16 a pipe's ``seekable()`` is :data:`True` and its -``seek`` silently does nothing (CPython gh-86768). ``Extractor`` took that at -its word, skipped :class:`~pcapkit.corekit.io.SeekableReader`, and lost the -octets its look at the magic number read. :class:`LyingPipe` reproduces such a -pipe on any platform, over a real :func:`os.pipe`. +``seek`` silently does nothing (CPython gh-86768). Guards against ``Extractor`` +taking that at its word, skipping :class:`~pcapkit.corekit.io.SeekableReader`, and +losing the octets its look at the magic number read. :class:`LyingPipe` reproduces +such a pipe on any platform, over a real :func:`os.pipe`. """ diff --git a/tests/foundation/traceflow/test_tcp.py b/tests/foundation/traceflow/test_tcp.py index 634f3e4c2b..390c755c58 100644 --- a/tests/foundation/traceflow/test_tcp.py +++ b/tests/foundation/traceflow/test_tcp.py @@ -555,9 +555,10 @@ def test_tcp_trace_ipv6_label_and_no_extension_output_path(self) -> None: def test_an_empty_extension_reports_the_written_file_open_or_finalised(self) -> None: """An empty extension still writes a file, and every report names it (#2237). - The flow file is named after the label alone. :meth:`TCP.submit` reported - :data:`None` for a flow still open while :meth:`TCP._finalise` reported the - path, so one flow's ``fpout`` depended on when it was read. + The flow file is named after the label alone. Without the fix, + :meth:`TCP.submit` reports :data:`None` for a flow still open while + :meth:`TCP._finalise` reports the path, so one flow's ``fpout`` depends on + when it is read. """ from dictdumper import JSON diff --git a/tests/foundation/traceflow/test_traceflow_base.py b/tests/foundation/traceflow/test_traceflow_base.py index 456a9e886d..fb7d9285dd 100644 --- a/tests/foundation/traceflow/test_traceflow_base.py +++ b/tests/foundation/traceflow/test_traceflow_base.py @@ -230,8 +230,8 @@ def test_traceflow_subclass_rejects_unrecognised_keyword(self) -> None: """A misspelled class keyword raises instead of being swallowed. Guards against it landing in ``**kwargs``, being dropped by a bare - ``super().__init_subclass__()``, and leaving the class registered under - its own class name -- no exception, no warning. + ``super().__init_subclass__()``, and, registration being opt-in, leaving + the class silently unregistered -- no exception, no warning. The keyword used here is deliberately *not* ``name``; see the sibling test in ``tests/foundation/reassembly/test_reassembly_base.py`` for why diff --git a/tests/foundation/traceflow/test_traceflow_label_unique_unit.py b/tests/foundation/traceflow/test_traceflow_label_unique_unit.py index f0a0bce7cc..218c8015e3 100644 --- a/tests/foundation/traceflow/test_traceflow_label_unique_unit.py +++ b/tests/foundation/traceflow/test_traceflow_label_unique_unit.py @@ -3,8 +3,9 @@ GitHub issue #1360: the label is built from the packet that opens a flow, and the output file is named after it and opened -- truncating it -- when the flow starts. -Two flows opened on the same endpoints at the same capture timestamp got one label -and one file, and the later flow erased the earlier one's frames. That happens +Without the fix, two flows opened on the same endpoints at the same capture +timestamp get one label and one file, and the later flow erases the earlier one's +frames. That happens unidirectionally when a direction resumes after its FIN, and bidirectionally when a new connection reuses a torn-down conversation's endpoints. diff --git a/tests/integration/test_files_output_naming.py b/tests/integration/test_files_output_naming.py index a89aec0a84..1b3ad44252 100644 --- a/tests/integration/test_files_output_naming.py +++ b/tests/integration/test_files_output_naming.py @@ -29,17 +29,11 @@ #: Output formats that ``files=True`` can be driven with, and the extension each #: is expected to land on. #: -#: Three registered formats are deliberately absent, each because it raises -#: before any file is named -- so none of them can say anything about how a name -#: is composed, and all three are separate defects from #358: -#: -#: * ``'pcap'`` and ``'cap'`` -- ``PCAPIO.__init__`` wants a ``protocol`` -#: keyword that the extractor never supplies, so they fail with -#: ``TypeError: PCAPIO.__init__() missing 1 required keyword-only argument``. -#: * ``'text'`` -- :attr:`Extractor.__output__ -#: ` maps it to -#: ``dictdumper.Text``, which :mod:`dictdumper` does not export, so it fails -#: with ``AttributeError: module 'dictdumper' has no attribute 'Text'``. +#: ``'pcap'`` and ``'cap'`` are deliberately absent: ``PCAPIO.__init__`` wants a +#: ``protocol`` keyword that the extractor never supplies, so they fail with +#: ``TypeError: PCAPIO.__init__() missing 1 required keyword-only argument`` +#: before any file is named, and so cannot say anything about how a name is +#: composed -- a separate defect from #358. FORMATS = { 'json': '.json', 'plist': '.plist', diff --git a/tests/integration/test_pcapng_end_to_end.py b/tests/integration/test_pcapng_end_to_end.py index 89e2d992fe..7a053cc11c 100644 --- a/tests/integration/test_pcapng_end_to_end.py +++ b/tests/integration/test_pcapng_end_to_end.py @@ -5,8 +5,8 @@ :file:`dhcp.pcapng` to a tree report and checked nothing. :file:`tests/protocols/test_pcapng_regression.py` already asserts that each PCAP-NG fixture extracts with ``length > 0`` and ``nofile=True``; this module -pins the exact block inventory *and* dumps a real report file, which is the part -that was never covered. +pins the exact block inventory *and* dumps a real report file, which that module +does not cover. The fixtures and what each is for are documented in the module docstring of :file:`examples/generators/pcapng.py`, which also records the parser defects diff --git a/tests/integration/test_reassembly_end_to_end.py b/tests/integration/test_reassembly_end_to_end.py index e90d620e54..23a1e4158e 100644 --- a/tests/integration/test_reassembly_end_to_end.py +++ b/tests/integration/test_reassembly_end_to_end.py @@ -285,23 +285,23 @@ class IPv6FragmentPayloadTests(EndToEndTestCase): def test_the_four_fragments_reassemble_into_the_whole_datagram(self) -> None: """The reassembled payload is the four fragments, in order. - This is a regression test for two fixed defects, which is why it asserts - the exact octet count rather than only that the payload is non-empty. - - The first was the fragment offset scaling. - ``pcapkit/protocols/internet/ipv6_frag.py`` passed the IPv6 fragment - offset through unscaled where the IPv4 path scaled it into octets, and - ``pcapkit/toolkit/pcap.py`` then handed it to the reassembly machinery as - ``fo``, which is an octet offset. The four fragments landed at octets 0, - 181, 362 and 543 instead of 0, 1448, 2896 and 4344, overwrote one - another, and the datagram came out 977 octets long -- ``543 + 434`` -- + This guards against two defects, which is why it asserts the exact + octet count rather than only that the payload is non-empty. + + The first is the fragment offset scaling. With + ``pcapkit/protocols/internet/ipv6_frag.py`` passing the IPv6 fragment + offset through unscaled where the IPv4 path scales it into octets, and + ``pcapkit/toolkit/pcap.py`` handing it to the reassembly machinery as + ``fo``, which is an octet offset, the four fragments land at octets 0, + 181, 362 and 543 instead of 0, 1448, 2896 and 4344, overwrite one + another, and the datagram comes out 977 octets long -- ``543 + 434`` -- while still reporting ``completed=True``. That is what the assertion on ``len(datagram.payload)`` pins. - The second was the buffer identifier. ``pcapkit/toolkit/pcap.py`` keyed - the reassembly buffer on the IPv6 header's flow label rather than on the - fragment header's identification, so ``datagram.id.id`` came out 0 where - this fixture's identification is 110308. One fragmented datagram cannot + The second is the buffer identifier. With ``pcapkit/toolkit/pcap.py`` + keying the reassembly buffer on the IPv6 header's flow label rather than + on the fragment header's identification, ``datagram.id.id`` comes out 0 + where this fixture's identification is 110308. One fragmented datagram cannot show the merging that the wrong key causes, but it does show the wrong value, so the identification is asserted here. diff --git a/tests/integration/test_reassembly_engine_parity.py b/tests/integration/test_reassembly_engine_parity.py index e4c6adb0c4..85b463d3d1 100644 --- a/tests/integration/test_reassembly_engine_parity.py +++ b/tests/integration/test_reassembly_engine_parity.py @@ -3,9 +3,9 @@ Each of :mod:`pcapkit.toolkit.pcap`, :mod:`pcapkit.toolkit.pcapng`, :mod:`pcapkit.toolkit.dpkt` and :mod:`pcapkit.toolkit.scapy` builds a -:term:`reasm.ipv6.packet` from whatever object model its engine hands it, and -each was only ever tested against itself. That is how #415 survived: for -``ipv6.pcap`` frame 13 the four reported ``ihl`` 48/48/40/40, ``len(header)`` +:term:`reasm.ipv6.packet` from whatever object model its engine hands it. +Tested only against itself, each can drift from the RFC unnoticed, as in +#415: for ``ipv6.pcap`` frame 13 the adapters reported ``ihl`` 48/48/40/40, ``len(header)`` 48/48/40/40 and ``tl`` 1496/1496/1488/1496, where the RFC calls for 40, 40 and 1488. @@ -277,9 +277,10 @@ def test_no_engine_advertises_a_fragment_header_on_the_datagram(self) -> None: def test_every_engine_reassembles_the_captured_payload(self) -> None: """The datagram itself, not only its header. - ``scapy``'s ``tl`` counted the Fragment header while its ``ihl`` did not, - so the reassembly machinery wrote each fragment's payload over a span 8 - octets too wide and the datagram came out 4786 octets instead of 4778 -- + Guards against ``scapy``'s ``tl`` counting the Fragment header while its + ``ihl`` does not, under which the reassembly machinery writes each + fragment's payload over a span 8 octets too wide and the datagram comes + out 4786 octets instead of 4778 -- eight stray zeroes per fragment boundary. Each engine's datagram is the fragments' payloads as captured, read past the Ethernet, IPv6 and Fragment headers of frames 13 to 16. diff --git a/tests/interface/test_core.py b/tests/interface/test_core.py index 81862047ac..7493dedf18 100644 --- a/tests/interface/test_core.py +++ b/tests/interface/test_core.py @@ -260,7 +260,7 @@ def test_every_shipped_engine_has_a_constant(self) -> None: 'the engine constants and the engine registry have diverged') def test_constants_are_reachable_from_the_package_root(self) -> None: - # ``pcapkit.DPKT`` has always worked, so ``pcapkit.PyPCAP`` has to as well -- + # ``pcapkit.DPKT`` works, so ``pcapkit.PyPCAP`` has to as well -- # a constant that exists only in the submodule is a constant nobody finds. # Both re-export lists name their symbols explicitly, so neither picks a new # one up on its own. diff --git a/tests/interface/test_registry.py b/tests/interface/test_registry.py index 4b84cf85df..ec3e199d13 100644 --- a/tests/interface/test_registry.py +++ b/tests/interface/test_registry.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """Unit tests for :mod:`pcapkit.interface.registry`. -GitHub issue #1519: the registry-entry helpers the registrars share were private -functions of :mod:`pcapkit.foundation.extraction`, imported from there by -:mod:`pcapkit.foundation.traceflow.traceflow`. The owner ruled that they become -public and move to a module of their own under :mod:`pcapkit.interface`. The -registrars' end-to-end behaviour is covered by +The registry-entry helpers that the registrars of +:mod:`pcapkit.foundation.extraction` and +:mod:`pcapkit.foundation.traceflow.traceflow` share are public, in a module of +their own under :mod:`pcapkit.interface`, as the owner ruled on GitHub issue +#1519. The registrars' end-to-end behaviour is covered by ``tests/foundation/registry/test_restore_builtin_unit.py``; these tests pin each helper's own contract. diff --git a/tests/project/test_actions_helper_2047.py b/tests/project/test_actions_helper_2047.py index 906747328b..c76d2d7b5b 100644 --- a/tests/project/test_actions_helper_2047.py +++ b/tests/project/test_actions_helper_2047.py @@ -166,7 +166,7 @@ def test_an_annotation_names_a_level(self) -> None: _actions.annotation('debug', 'm') def test_the_title_escapes_as_the_scripts_did(self) -> None: - """``leg_summary.py`` escaped only ``,`` in a leg's name; for the legs the matrices name, that is all.""" + """For the legs the matrices name, a title escapes only the ``,`` in the leg's name.""" for leg in ('Python 3.8 (KTLO, non-blocking)', 'Python 3.14t (experimental, non-blocking)', 'Python pypy-3.10', 'Python graalpy-25.0'): with self.subTest(leg=leg): @@ -234,7 +234,7 @@ def test_a_carriage_return_takes_the_multiline_form_too(self) -> None: self.assertIsNotNone(found, text) def test_an_unset_file(self) -> None: - """A ``required`` write raises, as ``os.environ[...]`` did; any other writes nothing.""" + """A ``required`` write raises :exc:`KeyError`, as ``os.environ[...]`` does; any other writes nothing.""" for env in ({}, {'GITHUB_OUTPUT': '', 'GITHUB_STEP_SUMMARY': ''}): with self.subTest(env=env): self.assertFalse(_actions.set_output('code', 'true', required=False, environ=env)) diff --git a/tests/project/test_actions_pinned_1968.py b/tests/project/test_actions_pinned_1968.py index 90bc2426c3..017495584b 100644 --- a/tests/project/test_actions_pinned_1968.py +++ b/tests/project/test_actions_pinned_1968.py @@ -182,7 +182,7 @@ def test_the_root_job_skips_the_conda_requirements(self) -> None: dependabot-core's ``SharedFileFetcher.req_files_for_dir`` fetches them, and ``FileFetchers::Base.filter_excluded`` drops an entry whose path from the - repository root matches ``exclude-paths``; without it the job checked + repository root matches ``exclude-paths``; without it the job checks :file:`conda/requirements.txt`'s cryptography pin. """ self.assertIn('conda/requirements.txt', updates()['pip'].get('exclude-paths') or []) diff --git a/tests/project/test_annotation_names.py b/tests/project/test_annotation_names.py index c2ca2bd04d..bb1f128ff2 100644 --- a/tests/project/test_annotation_names.py +++ b/tests/project/test_annotation_names.py @@ -16,9 +16,8 @@ This module is the same check without mypy: cheap enough to run on every commit, and it does not depend on a type checker being installed or on which version of -one. Run against the tree that carried those defects it reports exactly -those, name for name and line for line, which is what says it is measuring -the right thing. +one. Without the #642 fix it reports exactly those, name for name and line +for line, which is what says it is measuring the right thing. A deliberate non-goal: this says nothing about whether the annotations resolve at *runtime*. They do not, and not because of anything this catches -- @@ -63,8 +62,8 @@ def annotation_strings(tree: 'ast.AST') -> 'Iterator[tuple[str, int]]': These are the places a string is a type rather than a value, and each is used in this package: a variable annotation, a parameter annotation, a return annotation, and the first argument of :func:`typing.cast`. The ``cast`` case - is the one that is easiest to miss and is exactly where - the ``Optional`` in ``RPL.post_process`` of ``ipv6_route.py`` hid -- :func:`typing.cast` never + is the one that is easiest to miss and is exactly where #642 found + the ``Optional`` in ``RPL.post_process`` of ``ipv6_route.py`` -- :func:`typing.cast` never evaluates that argument, so an unresolvable name there is invisible to every test that runs the line. @@ -126,17 +125,16 @@ def bound_names(tree: 'ast.AST') -> 'set[str]': is not trying to reimplement Python's scoping rules, because the failure it exists to catch is a name bound *nowhere in the file at all*. - A stricter version was tried and reverted, and the reason is worth recording - because the strict version looks obviously better. ``get_type_hints`` resolves + A stricter version looks obviously better, and the reason it is wrong is + worth recording. ``get_type_hints`` resolves a *parameter or return* annotation against module and class scope only, never against a function's locals, so narrowing to those scopes catches a real defect: a module that binds ``Optional`` as an ordinary local somewhere and then writes ``'Optional[int]'`` without importing it. But the same narrowing - false-positived on real code here, because a :func:`typing.cast` string is - resolved *lexically*, in the function containing the call -- until #1652, - :file:`pcapkit/toolkit/scapy.py` cast to ``'IPv6ExtHdrFragment'``, imported - six lines above it inside the same function, which is correct and which mypy - accordingly did not flag. Distinguishing the two would mean tracking a + false-positives on correct code, because a :func:`typing.cast` string is + resolved *lexically*, in the function containing the call: a function that + imports a name and then casts to it is correct, and mypy accordingly does not + flag it. Distinguishing the two would mean tracking a binding set per scope and knowing which construct resolves in which, i.e. reimplementing the type checker this deliberately is not. @@ -181,9 +179,9 @@ def referenced_names(source: str) -> 'Optional[set[str]]': A string *inside* the annotation is descended into, because a nested forward reference is still a type reference: ``'list["Nested"]'`` needs ``Nested`` in scope just as ``'list[Nested]'`` does, and the typing machinery resolves it the - same way. Walking only the :class:`ast.Name` nodes missed that entirely -- a + same way. Walking only the :class:`ast.Name` nodes misses that entirely -- a nested reference parses as an :class:`ast.Constant`, invisible to the walk -- - so an unresolvable name one level of quoting down went unreported. + so an unresolvable name one level of quoting down would go unreported. Which is why the descent below is explicit rather than :func:`ast.walk`: the two places a string inside an annotation is a *value* and not a type have to be @@ -356,9 +354,9 @@ def test_a_nested_forward_reference_is_followed(self) -> None: ``'list["Nested"]'`` parses to a :class:`ast.Constant` inside the subscript, so a walk that looks only at :class:`ast.Name` nodes never sees - ``Nested`` at all. This was a real hole, found by review rather than by the - self-check above, and it is the shape most likely to recur because it looks - exactly like the annotations that *are* checked. + ``Nested`` at all. The self-check above does not exercise this shape, and + it is the one most likely to recur because it looks exactly like the + annotations that *are* checked. """ findings = self._findings_for( @@ -374,8 +372,8 @@ def test_a_literal_member_is_not_mistaken_for_a_type(self) -> None: to know what it is descending into: inside ``Literal`` a string is a *value*, and this package writes many of them. Following those indiscriminately reports every literal member as an unresolvable name -- a - false positive on real code, which is worse than the missed defect it was - meant to fix. ``Annotated``'s trailing metadata is the same case. + false positive on real code, which is worse than the missed defect the + descent exists to catch. ``Annotated``'s trailing metadata is the same case. """ self.assertEqual( @@ -395,13 +393,11 @@ def test_a_literal_member_is_not_mistaken_for_a_type(self) -> None: def test_a_cast_to_a_function_local_import_is_not_a_finding(self) -> None: """The case that keeps :func:`bound_names` scope-blind. - Until #1652, :file:`pcapkit/toolkit/scapy.py` imported ``IPv6ExtHdrFragment`` - inside a function and cast to it six lines later in the same function. A - :func:`typing.cast` string resolves lexically, so that is correct code and - mypy does not flag it -- and a check narrowed to module and class scope - reports it, which is how the stricter version of :func:`bound_names` was - discovered to be wrong. Pinned here so that reintroducing the narrowing - fails a test rather than a real module. + A function that imports a name and casts to it later in the same function + is correct code: a :func:`typing.cast` string resolves lexically, and mypy + does not flag it. A check narrowed to module and class scope reports it, + which is why :func:`bound_names` stays scope-blind. Pinned here so that + introducing the narrowing fails a test rather than a real module. """ self.assertEqual( diff --git a/tests/project/test_apt_packages_1967.py b/tests/project/test_apt_packages_1967.py index 9a257b63ec..2e1c5a6db7 100644 --- a/tests/project/test_apt_packages_1967.py +++ b/tests/project/test_apt_packages_1967.py @@ -4,11 +4,12 @@ * :class:`TestPlan`, :class:`TestInstall` and :class:`TestCollect` run each subcommand in this process with :func:`subprocess.run` stubbed, so every command it would run, and every failure of one, is checked without ``sudo`` or apt. -* :class:`TestTheFormerShellAgrees` runs the shell the steps held before the script, - and then the steps' own ``run:``, against the same stand-in ``sudo``, ``timeout``, - ``apt-get``, ``dpkg``, ``dpkg-deb``, ``dpkg-query`` and ``debconf-set-selections``, - and requires the same status, output, commands and files. The seed steps, which - #1967 put back inline as one copy and a count, run their own ``run:`` there. +* :class:`TestTheFormerShellAgrees` runs the steps' inline shell form, the + reference the script must match (#1967), and then the steps' own ``run:``, against the same stand-in ``sudo``, + ``timeout``, ``apt-get``, ``dpkg``, ``dpkg-deb``, ``dpkg-query`` and + ``debconf-set-selections``, and requires the same status, output, commands and + files. The seed steps, whose shell is inline as one copy and a count, run their + own ``run:`` there. * :class:`TestTheStepsRunTheScript` pins each apt step to the script, or the seed steps to their shell, with its ``id``, ``if:``, ``continue-on-error`` and ``timeout-minutes``, the KTLO twin to the same steps, and the AppArmor step @@ -401,7 +402,7 @@ def test_refused(self) -> None: self.assertEqual(raised.exception.code, 2) -#: The steps' shell before the script, each with the runner paths it named. +#: The steps' inline shell form, the reference the script must match, each with the runner paths it names. FORMER_PLAN = textwrap.dedent(r''' set -eu for attempt in 1 2; do @@ -421,7 +422,7 @@ def test_refused(self) -> None: echo "key=${prefix}$(printf '%s\n' "$plan" | sha256sum | cut -c1-16)" >> "$GITHUB_OUTPUT" fi ''').lstrip() # noqa: E501 -#: The seed steps' ``run:``, which a ``seed`` subcommand held for a while (#1967). +#: The seed steps' ``run:``, inline rather than a ``seed`` subcommand (#1967). SEED = textwrap.dedent(r''' set -eu sudo cp "$RUNNER_TEMP"/apt-debs/*.deb /var/cache/apt/archives/ @@ -506,10 +507,10 @@ def test_refused(self) -> None: #: cache to plan or collect for, as (workflow, step name). Each is pinned by its #: workflow's own test, as :mod:`tests.project.test_benchmark_workflow_2125`. ELSEWHERE = {('benchmark.yml', 'Install libpcap headers and tshark')} -#: Each step's former shell, by the subcommand that replaced it. +#: Each subcommand's inline shell form, the reference it must match, by subcommand. FORMER = {'plan': FORMER_PLAN, 'collect': FORMER_COLLECT} APPARMOR = 'Let tshark read the checkout past its AppArmor profile' -#: Text only the apt steps' former shell held. +#: Text only inline apt shell holds. INLINE = ('apt-get', 'dpkg', 'debconf-set-selections', 'apt-mirrors', '/var/cache/apt', 'apt_install') #: Stand-ins, each logging its call to ``$CALLS``. ``apt-get``'s first ``$FAIL_UPDATES`` @@ -555,7 +556,7 @@ def test_refused(self) -> None: 'find', 'wc', 'head', 'cp', 'env')), 'needs bash and the coreutils the former shell ran') class TestTheFormerShellAgrees(unittest.TestCase): - """Each step's ``run:`` writes, runs and leaves what its former shell did, on the same stand-ins.""" + """Each step's ``run:`` writes, runs and leaves what its inline shell form does, on the same stand-ins.""" def setUp(self) -> None: steps(WORKFLOW, 'engine-tests') # skips the test, not each subtest, without PyYAML @@ -595,7 +596,7 @@ def read(path: 'pathlib.Path') -> 'Optional[str]': def both(self, job: 'str', step: 'str', former: 'str', prepare: 'Callable[[], None]', **env: 'str') -> 'tuple[tuple[object, ...], tuple[object, ...]]': - """``(status, output, *state)`` of the former shell, then of the step's ``run:``, each from ``prepare``.""" + """``(status, output, *state)`` of ``former``, then of the step's ``run:``, each from ``prepare``.""" run = next(item for item in steps(WORKFLOW, job) if item.get('name') == step)['run'] command = run.split(f'{SCRIPT} ', 1)[1].split()[0] option = {'install': '--mirrors', 'plan': None}.get(command, '--archives') @@ -627,7 +628,7 @@ def reset(self) -> None: self.calls.write_text('', encoding='utf-8') def scenarios(self) -> 'Iterator[tuple[str, str, str, dict[str, str]]]': - """``(job, step, former shell, env)`` for every apt step but the installs.""" + """``(job, step, former, env)`` for every apt step but the installs, ``former`` its inline shell form.""" for job, sites in SITES.items(): for name, _, _, _, _, run in sites: if SCRIPT not in run: diff --git a/tests/project/test_ast_unparse_is_gated_1899_unit.py b/tests/project/test_ast_unparse_is_gated_1899_unit.py index c77bcbbabc..f903874b3c 100644 --- a/tests/project/test_ast_unparse_is_gated_1899_unit.py +++ b/tests/project/test_ast_unparse_is_gated_1899_unit.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """No test calls :func:`ast.unparse` outside a version gate (#1899). -:func:`ast.unparse` is 3.9+, and the unit tier also runs on the +:func:`ast.unparse` is 3.9+, and the ``ALL`` test tier (#1958) also runs on the ``Python 3.8 (KTLO)`` leg, where the call raises :class:`AttributeError`. That leg reads green even when a test fails (#1679), so the failure shows only in its annotations and log, which is how the #1878 engine-stub tests broke there @@ -29,7 +29,7 @@ if TYPE_CHECKING: from typing import Iterator -#: Directory that is scanned: every module of it runs on the 3.8 leg. +#: Directory that is scanned, whatever each module's test tier: an ``ALL`` one runs on the 3.8 leg. TESTS = ROOT / 'tests' diff --git a/tests/project/test_benchmark_workflow_2125.py b/tests/project/test_benchmark_workflow_2125.py index fcdca17a68..d29c4bba81 100644 --- a/tests/project/test_benchmark_workflow_2125.py +++ b/tests/project/test_benchmark_workflow_2125.py @@ -246,8 +246,8 @@ def test_one_leg_on_the_chosen_runner(self) -> None: def test_the_timeout_is_sized_from_a_measured_run(self) -> None: """Run 40062653380 took about 38 minutes on the default runner with everything. - The placeholder was 300, which let a hung extraction hold a runner for five - hours. 90 leaves over twice the measured time, and the comment names the run. + A bound of 300 would let a hung extraction hold a runner for five hours. + 90 leaves over twice the measured time, and the comment names the run. """ self.assertEqual(job(WORKFLOW, 'benchmark')['timeout-minutes'], 90) diff --git a/tests/project/test_build_leftovers_1773.py b/tests/project/test_build_leftovers_1773.py index dc0b02410e..0fe01fcf2d 100644 --- a/tests/project/test_build_leftovers_1773.py +++ b/tests/project/test_build_leftovers_1773.py @@ -62,7 +62,7 @@ def test_make_profile_resolves(self) -> None: class ManifestTests(unittest.TestCase): - """What :file:`MANIFEST.in` names exists, and the examples' READMEs still ship.""" + """What :file:`MANIFEST.in` names exists, and the examples' READMEs ship.""" def test_every_prune_names_a_tracked_or_ignored_directory(self) -> None: files = tracked() diff --git a/tests/project/test_build_only_dispatch.py b/tests/project/test_build_only_dispatch.py index afd0c80991..fb8a21fa31 100644 --- a/tests/project/test_build_only_dispatch.py +++ b/tests/project/test_build_only_dispatch.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """A scheduled workflow can be started by hand, and a manual run publishes nothing. -The maintainer asked for "build-only workflow triggers for those scheduled jobs -so that we can easily debug without waiting". So: +The maintainer asked for build-only manual triggers on the scheduled workflows, +so that one can be debugged without waiting for its schedule. So: * :class:`TestEveryScheduledWorkflowCanBeStarted`: each workflow with a ``schedule:`` has ``workflow_dispatch:``, except those in :data:`EXEMPT`, @@ -22,9 +22,9 @@ * :class:`TestCreateReleaseIgnoresAManualVendorUpdate`: Create Release, which every completed Vendor Update raises, releases nothing for a manual one. -The workflows that had ``workflow_dispatch`` before, Lint, Project Status, CI -Timing and Python Versions, keep the runs they had, and are not in -:data:`BUILD_ONLY`. +The other scheduled workflows with ``workflow_dispatch``, Lint, Project Status, +CI Timing, Python Versions and Engine Benchmark, run in full on a manual run, and +are not in :data:`BUILD_ONLY`. """ @@ -284,7 +284,7 @@ def test_the_conda_chain_builds_without_tagging(self) -> None: self.assertFalse(job_runs(jobs, 'conda-tag', context)) self.assertTrue(job_runs(jobs, 'conda-dist', context)) self.assertFalse(job_runs(jobs, 'discussion', context)) - # The schedule is unchanged: no pushed bump, no build. + # On the schedule, no pushed bump means no build. context = conda_context(event_name='schedule', unit_tests='success', verdict='skipped', verified=NULL, files_changed='false', pushed=NULL, conda_tag='skipped') self.assertFalse(job_runs(self.jobs['cron-conda.yml'], 'conda-dist', context)) diff --git a/tests/project/test_bump_version.py b/tests/project/test_bump_version.py index 5a6c5d7f7b..a6d2af78c6 100644 --- a/tests/project/test_bump_version.py +++ b/tests/project/test_bump_version.py @@ -367,7 +367,7 @@ class AbsentCitationFileTests(CitationTreeMixin, unittest.TestCase): """The absent-file case, which is a reported skip rather than a failure. Not fatal on purpose. :file:`.github/workflows/cron-vendor.yml` runs the script - under ``bash -e`` *before* its ``git commit``, so a non-zero exit there discards + in its ``Bump Version`` step, *before* ``Commit changes``, so a non-zero exit there discards the whole vendor crawl the run existed to produce and the release that would have followed. Announced rather than swallowed, because a bump that quietly skips a file it maintains is how the staleness starts. @@ -523,12 +523,13 @@ def test_the_fallback_still_complains_when_there_is_no_version(self) -> None: class RepositoryCitationTests(unittest.TestCase): """A gate: the committed :file:`CITATION.cff` must name the packaged version. - This is the half the script cannot guarantee on its own. Of the 159 commits - that have moved ``__version__`` on ``main``, 40 are hand-authored and so never - ran :file:`util/bump_version.py` -- a quarter over the project's life, and 11 - of the most recent 25, which is where ``1.4.0``, ``1.4.1``, ``1.5.0a1`` and the - ``1.5.0b1`` through ``b3`` run all came from. Without a check here the citation - metadata would still go stale through exactly those releases. + This is the half the script cannot guarantee on its own. Measured at + ``a41137716`` (#625): of the 159 commits that had moved ``__version__`` on + ``main``, 40 were hand-authored and so never ran :file:`util/bump_version.py` + -- a quarter over the project's life, and 11 of the latest 25, which is where + ``1.4.0``, ``1.4.1``, ``1.5.0a1`` and the ``1.5.0b1`` through ``b3`` run all + came from. Without a check here the citation metadata goes stale through + exactly such releases. """ diff --git a/tests/project/test_capture_tracking.py b/tests/project/test_capture_tracking.py index dc82b55d33..57a8591d27 100644 --- a/tests/project/test_capture_tracking.py +++ b/tests/project/test_capture_tracking.py @@ -9,17 +9,16 @@ inputs -- :file:`out.json`, :file:`out.plist`, :file:`out.txt` and :file:`pcapng.txt`, via ``make fixtures`` there. -Those reports were tracked once, and GitHub issue #685 is why they are not. A -tracked report has no reader: no test asserts on one, and the documentation -only quotes them as prose. So a parser change that alters how a -frame renders leaves the committed copy wrong with nothing anywhere to say so -- -:file:`pcapng.txt` sat recording ``packet -> NIL`` for blocks that had -started carrying their captured octets, and it was found by somebody reading the -file rather than by anything failing. - -Untracking them fixed that instance. This module is what keeps it fixed, because -the mistake is one commit away in either direction and neither direction looks -interesting in a diff review: +GitHub issue #685 is why those reports are not tracked. A tracked report has no +reader: no test asserts on one, and the documentation only quotes them as prose. +So a parser change that alters how a frame renders leaves a committed copy wrong +with nothing anywhere to say so -- #685 found :file:`pcapng.txt` recording +``packet -> NIL`` for blocks that carried their captured octets, by somebody +reading the file rather than by anything failing. + +This module keeps the reports out of the index, because the mistake is one +commit away in either direction and neither direction looks interesting in a +diff review: * ``git add -f examples/captures/out.txt`` on a freshly rebuilt report puts the staleness straight back, :file:`.gitignore` notwithstanding -- and so would a @@ -45,7 +44,7 @@ #: Reports rendered by :file:`examples/legacy_smoke/`, none of which git tracks. #: Named explicitly, rather than derived as "everything without a capture -#: suffix", because these are the ones that were tracked and the ones any +#: suffix", because these are the ones #685 is about and the ones any #: regression would most likely reinstate. RENDERED_REPORTS = ('out.json', 'out.plist', 'out.txt', 'pcapng.txt') #: The captures that must stay committed, because nothing regenerates them: @@ -138,7 +137,7 @@ def test_tracked_inputs_are_present_on_disk(self) -> None: class ReportReadTests(unittest.TestCase): """That no test module reads a rendered report as though it were a capture. - This is the regression untracking invites, and it is *not* covered by the + This is the regression an untracked report invites, and it is *not* covered by the tier guard's own audit. That audit only flags a **unit-tier** module reading a capture git does not track. These reports are worse than untracked: nothing rebuilds them as part of fixture generation either, because diff --git a/tests/project/test_changelog_md.py b/tests/project/test_changelog_md.py index 151afaf20a..bf15735908 100644 --- a/tests/project/test_changelog_md.py +++ b/tests/project/test_changelog_md.py @@ -7,13 +7,13 @@ Markdown without a second copy of the history to keep in step by hand. The conversion is a set of mechanical rules over a small reStructuredText subset, -and there is one test per rule. Each asserts both halves -- that the Markdown form +and each rule has tests of its own. Each asserts both halves -- that the Markdown form arrived *and* that the reStructuredText form is gone -- because a rule that fires in the wrong place and a rule that does not fire at all both leave markup in a published release body, and only checking for the new spelling would miss the second. -The rest cover the two things that are easy to get wrong and invisible when they +Others cover two things that are easy to get wrong and invisible when they are: which entry counts as "newest" (the toctree's first line, not a sort of the version strings), and ``--check``, which is what a CI gate calls and is worthless if it cannot fail. @@ -160,10 +160,10 @@ def test_rule_2_rfc_role_becomes_a_datatracker_link(self) -> None: self.assertNotIn(':rfc:', markdown) def test_rule_2_rfc_role_with_a_section_anchor_becomes_a_deep_link(self) -> None: - # #592: ``(\d+)`` between the backticks accepted digits and nothing else, - # so the anchored spelling Sphinx also accepts fell past rule 2 and was - # rejected by the guard -- which reported the *role* as uncovered when only - # the spelling ever was. The fragment has to reach the target, or the link + # #592: a ``(\d+)`` between the backticks accepts digits and nothing else, + # so the anchored spelling Sphinx also accepts falls past rule 2 to the + # guard -- which then reports the *role* as uncovered when only the + # spelling is. The fragment has to reach the target, or the link # lands at the top of a 100-page RFC. markdown = self.convert(ENTRY.replace(':rfc:`4303`', ':rfc:`4303#section-2.1`')) @@ -194,7 +194,7 @@ def test_rule_2_anchor_titles_follow_sphinx(self) -> None: f'rule 2 mis-rendered :rfc:`{target}`') def test_rule_2_leaves_an_anchor_it_cannot_read_to_the_guard(self) -> None: - # The widened pattern is deliberately not "anything between the backticks". + # Rule 2's pattern is deliberately not "anything between the backticks". # An anchor it cannot read has to reach the guard and be reported, rather # than being carried into a link whose text nobody checked. markdown = self.convert(ENTRY + '\nSee :rfc:`6554#section 3` for more.\n') @@ -611,7 +611,7 @@ def test_dash_and_tilde_sub_headings_become_atx_headings(self) -> None: # converter that handles ``=`` alone this does not reach the assertions at # all -- ``render()`` raises ``ResidualMarkupError`` on the joined # ``A module --------`` first, which is what makes this a real check - # rather than a restatement of current behaviour. + # rather than a restatement of whatever the converter emits. index = self.make_tree( entry=ENTRY + '\nA module\n--------\n\nAdded\n~~~~~\n\nSome prose.\n') @@ -683,8 +683,8 @@ class ComplaintLocationTests(ChangelogTreeMixin, unittest.TestCase): The guard blocks merges, so its message is the whole of the author's experience of it. These do not check the wording; they check that a cited line - read back out of the entry holds the construct it was cited for, which the - numbers counted against the converted body could not do. + read back out of the entry holds the construct it was cited for, which a + number counted against the converted body cannot do. No ``subTest`` here on purpose: under this repository's pytest a failing subtest gets a ``SUBFAILED`` line of its own while its parent's line still @@ -772,8 +772,7 @@ def test_complaints_arrive_in_the_entrys_order(self) -> None: def test_an_unreached_double_backtick_literal_is_located_too(self) -> None: # Rule 3's ``[^`]+`` cannot cross a backtick, so a literal holding one is - # copied through and only the guard catches it. That branch of the guard had - # no test of its own. + # copied through and only the guard catches it. entry = padded_entry('an ``a`b`` literal rule 3 cannot reach.') complaints = self.complaints(entry) diff --git a/tests/project/test_ci_timing.py b/tests/project/test_ci_timing.py index c28277dc42..51ac899b65 100644 --- a/tests/project/test_ci_timing.py +++ b/tests/project/test_ci_timing.py @@ -26,7 +26,7 @@ * :class:`TestTimingArtifacts` and :class:`TestReviewWorkflow` -- what the script relies on in :file:`unit-tests.yml` (artifact names, the coverage leg, upload steps that cannot fail a required leg) and the review workflow's own triggers and - permissions, which have to stay read-only. + permissions, which have to stay read-only but for posting its report (#2030). """ @@ -63,8 +63,8 @@ raise unittest.SkipTest(NO_TOMLLIB) #: Whether lxml, which the script parses JUnit with (#1894), is installed. The -#: test extra installs it everywhere but GraalPy, which has no wheel; with no -#: laxer parser to fall back to, what reads a JUnit artifact skips there. +#: test extra installs it on CPython alone; with no laxer parser to fall back +#: to, what reads a JUnit artifact skips elsewhere. HAS_LXML = importlib.util.find_spec('lxml') is not None NO_LXML = 'lxml, from the test extra, is not installed' @@ -84,7 +84,8 @@ PAGE = 2 PR_RUN, CANCELLED_RUN, PUSH_RUN, TIMEOUT_RUN = 38001768735, 38002005234, 37998639834, 37323913186 #: The coverage leg, and the legs paired with it, when the fixtures were recorded. -#: #1661 moved coverage to 3.15, which no fixture run has. +#: The script's own are ``latest``'s and the supported tier's (#1661), legs no +#: fixture run has. FIXTURE_COVERAGE_JOB = 'Python 3.14' FIXTURE_PAIRED_JOBS = ('Python 3.11', 'Python 3.12', 'Python 3.13') JUNIT_ARTIFACT_ID = 11648726901 @@ -317,8 +318,8 @@ def test_every_other_workflow_reads(self) -> 'None': self.assertEqual(ci_timing.match_job(compat, 'Compat Python 3.10 (KTLO)')['id'], 'compatibility') self.assertEqual(ci_timing.match_job(compat, 'Compat Python 3.12')['id'], 'compatibility') - # #1633 retired the literal ``Compat Python 3.15 (scheduled)`` job this used - # to read, so the literal-over-template rule is checked on a stand-in. + # python-compatibility.yml has no literal job name beside its template + # (#1633), so the literal-over-template rule is checked on a stand-in. both = ci_timing.parse_workflow( 'jobs:\n matrixed:\n name: Compat Python ${{ matrix.python-version }}\n' ' pinned:\n name: Compat Python 3.15 (scheduled)\n') diff --git a/tests/project/test_class_guard_annotations_1857_unit.py b/tests/project/test_class_guard_annotations_1857_unit.py index 315b758bd2..3d92852741 100644 --- a/tests/project/test_class_guard_annotations_1857_unit.py +++ b/tests/project/test_class_guard_annotations_1857_unit.py @@ -7,10 +7,10 @@ {})``. That is the module's globals with an empty local namespace, so neither the class body nor a class-level guarded import is in scope. :file:`docs/source/conf.py` binds the names of each module's *module-level* ``if TYPE_CHECKING:`` block first, -but not those of a class-level one. ``__flags__: 'Flags'`` named a nested class -bare, and each third-party engine's ``_expkg`` named a module imported only inside -the class guard. Each raised :exc:`NameError`, which fails the whole class, so -autodoc rendered every annotation of ``FrameType``, its frame schemas and those +but not those of a class-level one. A ``__flags__: 'Flags'`` naming a nested class +bare, or an engine's ``_expkg`` naming a module imported only inside the class +guard, raises :exc:`NameError`, which fails the whole class: #1857 found autodoc +rendering every annotation of ``FrameType``, its frame schemas and the third-party engines as raw strings. This module makes the same evaluation without Sphinx: diff --git a/tests/project/test_codeql_config_2130.py b/tests/project/test_codeql_config_2130.py index 118cf0931c..00bc801436 100644 --- a/tests/project/test_codeql_config_2130.py +++ b/tests/project/test_codeql_config_2130.py @@ -4,10 +4,10 @@ ``py/clear-text-logging-sensitive-data`` and ``py/clear-text-storage-sensitive-data`` call a value secret from its name alone. The tests that build a pcapng Decryption Secrets Block call its bytes -``secrets``, and those bytes reached ``logger`` calls in :mod:`pcapkit`, as a +``secrets``, and those bytes reach ``logger`` calls in :mod:`pcapkit`, as a capture's stream, its name, or its magic number, and a buffer file's write. -Each dismissal was lost when a -pull request moved the line the alert named. Python is not built, so a path the +A dismissal is lost when a +pull request moves the line the alert names. Python is not built, so a path the config ignores is not extracted and cannot be a flow source. * :class:`TestTheInitStep` checks ``Initialize CodeQL`` in diff --git a/tests/project/test_conda_intel_cli_upload_2044.py b/tests/project/test_conda_intel_cli_upload_2044.py index b153d8bd68..48d2a8c8ee 100644 --- a/tests/project/test_conda_intel_cli_upload_2044.py +++ b/tests/project/test_conda_intel_cli_upload_2044.py @@ -6,7 +6,7 @@ the action cannot upload from an Intel leg of ``conda-dist`` in :file:`.github/workflows/cron-conda.yml` or of ``conda`` in :file:`.github/workflows/create-release.yml`. The step "Upload conda packages to -Anaconda" therefore skips the Intel leg, and "Upload conda packages to Anaconda +Anaconda" therefore skips the Intel leg, as it does the Windows one (#2060), and "Upload conda packages to Anaconda (Intel)" runs there alone, making the action's own ``anaconda --token … upload --user …`` call from conda-forge's anaconda-client, with ``--skip-existing``. @@ -116,7 +116,7 @@ def action_arguments(inputs: 'dict[str, str]', token: 'str', packages: 'str', la class TestOnlyTheIntelLegUsesTheCLI(unittest.TestCase): - """The action on every leg but ``macos-26-intel``, the CLI step there alone.""" + """The action on every leg but ``macos-26-intel`` and Windows (#2060), the CLI step on Intel alone.""" def test_the_action_step_is_still_the_action(self) -> None: for name, job_id in JOBS: @@ -143,7 +143,7 @@ def test_every_other_leg_runs_the_action_only(self) -> None: self.assertFalse(runs(step(name, job_id, CLI_STEP), os)) def test_a_published_release_leg_runs_neither(self) -> None: - """#888's per-leg check still gates both release uploads.""" + """#888's per-leg check gates both release uploads.""" for os in legs('create-release.yml', 'conda'): with self.subTest(os=os): self.assertFalse(runs(step('create-release.yml', 'conda', ACTION_STEP), os, published='true')) diff --git a/tests/project/test_conda_legs_install_no_pcapkit_1854.py b/tests/project/test_conda_legs_install_no_pcapkit_1854.py index a9af379342..54224d216f 100644 --- a/tests/project/test_conda_legs_install_no_pcapkit_1854.py +++ b/tests/project/test_conda_legs_install_no_pcapkit_1854.py @@ -2,9 +2,9 @@ """The conda legs do not install pcapkit into setup-python's interpreter (#1854). ``conda-dist`` in :file:`.github/workflows/cron-conda.yml` and ``conda`` in -:file:`.github/workflows/create-release.yml` ran ``pip install -e .[all]``, and -the release leg ``pip install pathlib2 typing_extensions`` too, for no later -step: ``pip download`` needs pip alone, ``conda build`` installs into its own +:file:`.github/workflows/create-release.yml` need no ``pip install -e .[all]``, +nor ``pathlib2`` and ``typing_extensions``, for no later step uses them: +``pip download`` needs pip alone, ``conda build`` installs into its own host environment, the version comes from job outputs, and ``--smoke`` runs its probe under ``-I -S``. On ``macos-26-intel`` the extras resolve a cryptography with no Intel wheel, which pip has to compile or fail on. The jobs are read @@ -33,9 +33,9 @@ WORKFLOWS = pathlib.Path(__file__).resolve().parents[2] / '.github' / 'workflows' #: The conda jobs, as (workflow, job). JOBS = (('cron-conda.yml', 'conda-dist'), ('create-release.yml', 'conda')) -#: What each install step still upgrades: ``pip download`` runs on this pip. +#: What each install step upgrades: ``pip download`` runs on this pip. BUILD_TOOLS = ['python', '-m', 'pip', 'install', '-U', 'pip', 'setuptools', 'wheel'] -#: The backports #1821 removed from ``cron-conda.yml``. +#: The backports the conda legs do not install (#1821). BACKPORTS = {'pathlib2', 'typing_extensions', 'typing-extensions'} #: A step running pcapkit from setup-python's interpreter, which would need it installed. USES_PCAPKIT = re.compile(r'\bimport\s+pcapkit\b|-m\s+pcapkit\b|\bpcapkit-(?:cli|vendor)\b') @@ -94,7 +94,7 @@ def test_each_install_step_upgrades_the_build_tools_alone(self) -> None: self.assertEqual(installs(step), [BUILD_TOOLS]) def test_no_step_runs_pcapkit_from_that_interpreter(self) -> None: - """What would need the install back: an import, ``-m pcapkit`` or an entry point.""" + """What would need the install: an import, ``-m pcapkit`` or an entry point.""" for name, job_id, step in self.steps(): with self.subTest(workflow=name, job=job_id, step=step.get('name')): self.assertFalse([line for line in commands(step) if USES_PCAPKIT.search(line)]) diff --git a/tests/project/test_conda_native_platforms_1822.py b/tests/project/test_conda_native_platforms_1822.py index c5abc6e2a5..1a95e7fc0a 100644 --- a/tests/project/test_conda_native_platforms_1822.py +++ b/tests/project/test_conda_native_platforms_1822.py @@ -69,7 +69,7 @@ #: as read on 2026-10-16; the test below checks the snapshot of the image table too. LEGS = {'macos-latest': 'osx-arm64', 'macos-26-intel': 'osx-64', 'ubuntu-latest': 'linux-64', 'windows-latest': 'win-64'} -#: Each job's legs, alike for both (#2060 gave the release job its Windows one). +#: Each job's legs, alike for both, Windows included (#2060). JOB_LEGS = {('cron-conda.yml', 'conda-dist'): LEGS, ('create-release.yml', 'conda'): LEGS} #: ``platform.machine()``, as the runner-images table names the architecture. ARCH = {'x86_64': 'x64', 'arm64': 'arm64', 'AMD64': 'x64'} diff --git a/tests/project/test_conda_no_windows_steps_1967.py b/tests/project/test_conda_no_windows_steps_1967.py index 76276ef0e8..7b98561c73 100644 --- a/tests/project/test_conda_no_windows_steps_1967.py +++ b/tests/project/test_conda_no_windows_steps_1967.py @@ -9,13 +9,13 @@ * :class:`TestNoDeadRunnerCode` pins that no step's ``if:`` or ``continue-on-error`` names a runner label the job's matrix lacks, no ``case`` - arm maps one, :file:`util/release_conda.py`, which both jobs' steps run since - #1967, maps no label that neither job has a leg on, and, with no Windows leg, + arm maps one, :file:`util/release_conda.py`, which both jobs' steps run + (#1967), maps no label that neither job has a leg on, and, with no Windows leg, no step is Windows-only. So a Windows leg comes with its steps added deliberately, rather than reviving ones nobody has run, as #2060 added both jobs' Windows legs and steps. * :class:`TestTheCheckSeesDeadCode` feeds :func:`dead` the jobs with such dead - Windows code rebuilt into them from the current ones, so the pin cannot pass by + Windows code rebuilt into them from the real ones, so the pin cannot pass by reading nothing, and ``conda-dist`` without its Windows leg, whose steps must then read as dead. diff --git a/tests/project/test_conda_windows_leg_2060.py b/tests/project/test_conda_windows_leg_2060.py index 1be988ec96..f11b7c559f 100644 --- a/tests/project/test_conda_windows_leg_2060.py +++ b/tests/project/test_conda_windows_leg_2060.py @@ -17,15 +17,16 @@ * :class:`TestTheWindowsClient`: ``release_conda.py upload`` runs base's ``Scripts\\anaconda.exe`` on Windows. * :class:`TestBldBat`: ``conda/pypcapkit/bld.bat`` does what ``build.sh`` does: - one install of every downloaded file, then the project, each checked. Its - previous ``FOR`` loop wrote ``%A``, a syntax error in a batch file. + one install of every downloaded file, then the project, each checked. The + check refuses a ``FOR`` loop writing ``%A``, a syntax error in a batch file. * :class:`TestPortableExecutable` and :class:`TestTheWindowsLayout`: :file:`util/conda_vendor.py` reads a ``.pyd``'s PE machine, and ``--smoke`` takes a Windows package's ``Lib/site-packages``, or from Python 3.15 its ``lib/python/site-packages`` (conda-forge's CFEP-27), whose path names no Python. * :class:`TestTheEnvironmentWithoutEnvironb`: the scripts read their variables - where :data:`os.environb` is missing, as on Windows (#2120), where - ``release_conda.py build`` died reading ``PCAPKIT_CONDA_OS``. + where :data:`os.environb` is missing, as on Windows (#2120), where reading + it raises :exc:`AttributeError`, so ``release_conda.py build`` would die + reading ``PCAPKIT_CONDA_OS``. Nothing here runs on Windows; what a real run shows is the rest. diff --git a/tests/project/test_conventions_doc_claims.py b/tests/project/test_conventions_doc_claims.py index dcd0e46cde..0505b19da0 100644 --- a/tests/project/test_conventions_doc_claims.py +++ b/tests/project/test_conventions_doc_claims.py @@ -23,14 +23,14 @@ an anchor's file whole. * :class:`PhaseTwoRemainderTests` -- what the page says about `#877 `__'s phase 2 and the - population its audit covers, measured rather than remembered. The page said the - phase *"has not happened yet"* for as long as it did because nothing contradicted - it once #921 landed. The page states no counts (#1103's ruling, #1811), so what is - checked is each claim about the whole population. + population its audit covers, measured rather than remembered: unchecked, a claim + such as the phase *"has not happened yet"* outlives the work that falsifies it + (#921), because nothing contradicts it. The page states no counts (#1103's ruling, + #1811), so what is checked is each claim about the whole population. * :class:`ExtensionHeaderClassificationTests` -- the page's bases-per-header table against the declarations themselves. ``tests/protocols/internet/test_ipv6_ext_unit.py`` - already pins the *code* to the ruling; nothing pinned the *page* to the code, which is - the half that goes stale when a new header lands. + pins the *code* to the ruling; this pins the *page* to the code, which is the half + that goes stale when a new header lands. * :class:`FailedLookupExceptionTests` -- the worked example the page gives for a name miss, which names :exc:`~pcapkit.utilities.exceptions.EnumKeyError`, not the true but unspecific :exc:`KeyError` (#918). @@ -223,7 +223,7 @@ def _every_enumeration() -> 'dict[type, str]': continue # Not guarded: every module under pcapkit imports cleanly, and swallowing an # ImportError here would let this walk go quietly partial -- which is the one - # failure mode that would make every count below pass vacuously. + # failure mode that would make every check below pass vacuously. importlib.import_module(module.name) found = {} # type: dict[type, str] @@ -398,7 +398,7 @@ def setUp(self) -> 'None': self.note = ' '.join(_page('registry-protocol').split()) def test_the_walk_found_something_to_count(self) -> 'None': - """Guards every count below from passing on an empty discovery.""" + """Guards every check below from passing on an empty discovery.""" self.assertGreater(len(self.enumerations), 100, 'the enumeration walk collapsed; the counts below would ' 'pass vacuously') @@ -422,7 +422,7 @@ def test_the_page_says_phase_two_is_complete_and_the_walk_agrees(self) -> 'None' 'enumerations outside EnumLookup, which the page says none are') def test_the_page_names_every_enumeration_outside_the_hierarchy(self) -> 'None': - """The count alone would pass on a wrong list of the right length. + """The page names each enumeration outside the hierarchy. Vacuous while :attr:`self.outside` is empty -- GitHub issue #930 emptied it, and an empty dict gives the loop below nothing to iterate, so this @@ -659,11 +659,10 @@ class AenumRoleExclusionTests(unittest.TestCase): ``docs/source/conf.py`` excludes ``aenum`` from ``intersphinx_mapping`` deliberately -- its ``objects.inv`` carries no ``py:`` objects, so no - ``:mod:``/``:class:``/``:func:``/etc. role naming it could ever resolve. Part - B converted every such role this page carried to plain double-backtick - literals rather than leaving them promising a link that can never exist. A - forbidden role needs a test, or a later edit reintroduces one without - noticing. + ``:mod:``/``:class:``/``:func:``/etc. role naming it could ever resolve. The + pages write it as a plain double-backtick literal instead, rather than as a + role promising a link that can never exist. A forbidden role needs a test, + or a later edit reintroduces one without noticing. The other half of the same fix is pinned alongside it: the references to ``AbsentType``, ``NoValueType`` and ``ABSENT`` that part B qualified to their @@ -817,9 +816,9 @@ def test_a_delegating_override_is_a_classmethod(self) -> 'None': 'as the surviving staticmethod') # Execute the outcomes rather than assert the page names them. A grep - # for the error string cannot tell a wrong one -- the page once claimed + # for the error string cannot tell a wrong one -- a page claiming # `RuntimeError: super(): no arguments` for a shape that actually raises - # `TypeError`, and a prose-only assertion passed it. + # `TypeError` passes a prose-only assertion. class _Base: @classmethod def get(cls, key, default=None): # noqa: D102 @@ -863,15 +862,14 @@ def get(): # type: ignore[override] # noqa: D102 # Each wording pinned by a needle unique to it. The shared substring # ``instance or subtype of type`` occurs more than once on the page -- in both # quoted errors and in the sentence explaining what they share -- so asserting it - # pinned neither: corrupting the 3.13+ quote to something false still passed. + # pins neither: a 3.13+ quote corrupted to something false still passes. # - # **A recurring defect shape**, after ``labels: bug`` (a bare token matched - # elsewhere), a bare figure (matched by the page's own ``wc -l`` output) and - # ``quiet=True`` (several occurrences). The rule, stated once here - # because it kept being rediscovered: **an assertIn whose needle appears more - # than once on the page pins nothing** -- a later occurrence satisfies it and - # the claim it guards can be inverted freely. Count occurrences before - # asserting, and anchor to something unique. + # **A recurring defect shape**, shared with a bare ``labels: bug`` token matched + # elsewhere and a ``quiet=True`` with several occurrences. The rule, stated once + # here: **an assertIn whose needle appears more than once on the page pins + # nothing** -- a later occurrence satisfies it and the claim it guards can be + # inverted freely. Count occurrences before asserting, and anchor to something + # unique. self.assertIn('is not an instance or subtype of type', flat, 'the page no longer shows the 3.13+ wording of what a ' 'staticmethod delegation raises') @@ -941,9 +939,9 @@ def _restore() -> 'None': # Anchored to the headline sentence, not the bare token. `quiet=True` occurs # more than once on the page, so `assertIn('``quiet=True``')` is satisfied by a # later mention -- inverting the ruling itself to `quiet=False` would fail - # nothing. The same shape as the `labels: bug` assertion and the bare figure, - # which passed the same way, so the rule is explicit -- never assert a token - # that appears more than once on the page it is meant to pin. + # nothing. The same shape as a bare `labels: bug` assertion, so the rule is + # explicit -- never assert a token that appears more than once on the page it + # is meant to pin. self.assertIn('Raise the way the base raises, which means** ``quiet=True``', flat, 'the page no longer states the ruling as its headline, so a ' 'later mention of quiet=True is doing the work of pinning it') @@ -955,7 +953,7 @@ def test_the_mh_helpers_define_no_get_override(self) -> 'None': neither class defines ``get``, the module defines none at all, and no ``[override]``/``arguments-differ`` pair, which such signatures need, is left behind. Scoped to that exact pair rather than to ``arguments-differ`` alone, - which mh.py still carries for ``read`` and ``__post_init__`` -- + which mh.py carries for ``read`` and ``__post_init__`` -- unrelated to any ``get``, and measured before asserting on it. """ @@ -992,14 +990,14 @@ def test_the_mh_helpers_define_no_get_override(self) -> 'None': 'which #940 made false') def test_a_non_string_key_raises_a_value_miss_on_every_helper(self) -> 'None': - """The divergence #940's deletion closed, which is the page's worked reason. + """No divergence between the helpers (#940), which is the page's worked reason. The base branches on ``isinstance(key, str)`` and treats everything else as - a value, so ``get(None)`` is a *value* miss. The deleted overrides branched - on :class:`int` and fell through to the name path, making it a ``KeyError`` - on those two and a ``ValueError`` on the other helpers. The page names the - helpers rather than counting them (#1811), so each name is checked against - the derived population below. + a value, so ``get(None)`` is a *value* miss. Without #940's deletion, the two + mh overrides branch on :class:`int` and fall through to the name path, making + it a ``KeyError`` on those two and a ``ValueError`` on the other helpers. The + page names the helpers rather than counting them (#1811), so each name is + checked against the derived population below. """ from pcapkit.utilities.exceptions import EnumValueError @@ -1052,27 +1050,21 @@ def test_a_non_string_key_raises_a_value_miss_on_every_helper(self) -> 'None': def _optional_dependencies() -> 'dict[str, list[str]]': """``pyproject.toml``'s ``[project.optional-dependencies]``, without a TOML library. - :mod:`tomllib` is 3.11+ and CI still tests 3.8-3.10 (#1633) -- a first version used it - and failed the ``Python 3.10`` leg with ``ModuleNotFoundError`` while passing locally - on 3.14. ``tomli``, the usual backport, is not a dependency here either + :mod:`tomllib` is 3.11+ and CI tests 3.8-3.10 (#1633), where importing it fails + with ``ModuleNotFoundError`` although a local run on 3.11 or later passes. + ``tomli``, the usual backport, is not a dependency here either (``grep -n tomli pyproject.toml`` finds nothing), so there is nothing to fall back to and adding one for a docs-claims test is not worth it. **Verified against** :mod:`tomllib` **rather than asserted.** Matching arrays - with a non-greedy bracket pattern, which stops at the first literal ``]``, was - measured to mis-parse several extras, silently: - - * ``vendor`` came back **empty**, because the ``]`` inside ``"requests[socks]"`` - closed the match early. - * ``dev`` silently dropped its trailing entries for the same reason. - * ``test`` was truncated at a ``]`` inside a *comment* and then picked up a quoted - example from the prose, **inventing a dependency called** ``9 skipped``. - - No shipped assertion read those keys, so no test gave a wrong verdict -- but - ``dev`` and ``vendor`` are both described in the page's own prose, so the next - assertion to check either would have got wrong data with nothing raised. That is - precisely the "renders fine and fails nothing" failure this suite exists to close, - which is why it is fixed rather than documented around. + with a non-greedy bracket pattern, which stops at the first literal ``]``, + mis-parses an extra silently: ``vendor`` comes back **empty**, because the ``]`` + inside ``"requests[socks]"`` closes the match early, and ``dev`` drops its trailing + entries for the same reason; a ``]`` inside a *comment* truncates an extra the same + way, and a quoted example in the comment prose then reads as a dependency. + ``dev`` and ``vendor`` are both described in the page's own prose, so an assertion + checking either would get wrong data with nothing raised -- precisely the "renders + fine and fails nothing" failure this suite exists to close. So: strip ``#`` comments, then track bracket **depth** rather than matching to the first ``]``. Nested brackets inside requirement strings and comments containing ``]`` @@ -1136,9 +1128,9 @@ class ProcessConventionTests(unittest.TestCase): Each check pins the *claim the page makes* next to the *fact behind it*, and deliberately pins neither against the owner's own phrasing. Quoting him is what the page is forbidden to do here -- his instruction on GitHub issue #918 was to - paraphrase -- and asserting a quoted sentence is separately a trap this module has - already been bitten by: pinning an off-hand reply of his as a literal string makes - a sentence typed into a GitHub thread a CI build dependency (GitHub issue #949). + paraphrase -- and asserting a quoted sentence is separately a trap: pinning an + off-hand reply of his as a literal string makes a sentence typed into a GitHub + thread a CI build dependency (GitHub issue #949). Attribution lives in the issue number the page cites; the tests check substance. """ @@ -1148,7 +1140,7 @@ class ProcessConventionTests(unittest.TestCase): #: The changelog entry file the changelog ruling is about. Named rather than #: discovered: this is the 1.5.0 cycle's file, which is the one GitHub pull - #: request #657 accumulates into and the one the page cites. + #: request #657 accumulated into and the one the page cites. CHANGELOG = ROOT / 'docs' / 'source' / 'changelog' / '1.5.0.rst' #: ``.github/PULL_REQUEST_TEMPLATE.md``'s commit-type tickbox list is the tree's @@ -1165,11 +1157,11 @@ def setUp(self) -> 'None': def test_the_extras_reader_agrees_with_tomllib(self) -> 'None': """:func:`_optional_dependencies` matches a real TOML parser, key for key. - The reader exists because :mod:`tomllib` is 3.11+ and CI still tests 3.8-3.10 + The reader exists because :mod:`tomllib` is 3.11+ and CI tests 3.8-3.10 (#1633's KTLO legs), so the assertions above cannot use it. That makes the reader itself an - unverified dependency of every extras claim on the page -- and its second - version mis-parsed several extras silently, inventing a requirement - called ``9 skipped`` out of comment prose. + unverified dependency of every extras claim on the page -- and a reader that + matches to the first ``]`` mis-parses several extras silently, reading a + quoted example in comment prose as a requirement. So wherever a real parser *is* available -- which is every interpreter from 3.11 up, including the one this suite usually runs on -- the two are compared @@ -1232,7 +1224,7 @@ def test_the_page_keeps_the_engine_and_installability_exclusions_apart(self) -> #910 excluded the third-party engines **by kind** -- they are not core addons -- where ``PyPCAP`` and ``PCAP_CT`` were already out for *installability*, which the ruling did not touch. Reading those as one reason - is what let the list drift the first time, so the page has to state both. The + is what lets the list drift, so the page has to state both. The tree half checks the engine extras still exist to be excluded from, since a prose distinction about extras that no longer exist is not a distinction. @@ -1269,7 +1261,7 @@ def test_the_page_denies_one_changelog_entry_per_commit(self) -> 'None': The claim is narrow on purpose: entries are grouped by topic and are **not** one per commit. The grouping scheme is ruled too -- a section per top-level module, with the kind headings nested inside -- so the page records the scheme - and this checks for it. What is still unruled is narrower: which module an + and this checks for it. What is unruled is narrower: which module an entry spanning several belongs under. """ @@ -1293,8 +1285,8 @@ def test_the_changelog_file_is_not_shaped_one_entry_per_commit(self) -> 'None': """The tree half: the file already groups, and already merges issues. Structural rather than counted. An entry count would be pinned to whatever - GitHub pull request #657 had accumulated on the day, and would fail on its - next merge for no reason a reader could act on -- so what is checked is that + the file held on the day, and would fail on the next entry for no reason a + reader could act on -- so what is checked is that the kind headings are all present and that at least one entry cites two or more issues, which is the property that makes "one entry per commit" false of the file rather than merely discouraged. @@ -1343,17 +1335,16 @@ def test_the_page_names_every_commit_type_the_template_ticks(self) -> 'None': def test_the_page_names_what_the_tree_has(self) -> 'None': """Every claim the page makes that has local ground truth. - A cross-review once corrupted a batch of claims on these pages at once and - the suite stayed green, which is the honest measure of how much of the prose - was decorative. This closes the subset that has local ground truth: a path - the page cites, a module it names. The page states no entry count (#1103's - ruling, #1811), so none is pinned. + A batch of claims on these pages can be corrupted at once with the suite + green wherever no check reads them. This closes the subset that has local + ground truth: a path the page cites, a module it names. The page states no + entry count (#1103's ruling, #1811), so none is pinned. **What stays unpinned, and why, so the gap is stated rather than implied.** Every claim whose ground truth is a GitHub query -- the label count, which default labels are in live use, the ``breaking`` census, the issue and pull request numbers -- cannot be checked here, because this repository's CI has no - network. Those are why the page gives the *command* alongside the figure: the + network. Those are why the page gives the *command* instead of a figure: the command is the pin, run by a reader rather than by CI. Inverting the page's live-use claim about GitHub's default labels still passes this suite, measured, and no offline test can change that. @@ -1361,9 +1352,9 @@ def test_the_page_names_what_the_tree_has(self) -> 'None': """ import re as _re - # The module list the page prints as the by-module target. The tree half was - # already checked; this is the page half, which a corruption inserting a - # non-existent module survived. + # The module list the page prints as the by-module target. The tree half is + # test_the_page_denies_one_changelog_entry_per_commit's; this is the page half, + # which catches a non-existent module inserted into the listing. packages = sorted(d.name for d in (ROOT / 'pcapkit').iterdir() if d.is_dir() and not d.name.startswith('__')) for name in packages: @@ -1373,7 +1364,7 @@ def test_the_page_names_what_the_tree_has(self) -> 'None': 'among the modules the changelog groups by') # Only the ``ls -d`` comment block, not the whole page: elsewhere the page # legitimately writes the *distribution* name (``pip install - # pypcapkit[Scapy]``), which is not a module and which a looser sweep flagged. + # pypcapkit[Scapy]``), which is not a module and which a looser sweep flags. listing = _re.search(r'ls -d pcapkit/\*/.*?(?=\n\n\S)', _page(self.ANCHOR), _re.DOTALL) self.assertIsNotNone( @@ -1416,9 +1407,9 @@ def test_the_page_says_the_labels_are_set_by_hand(self) -> 'None': 'process.rst no longer states that the labels are applied by ' 'hand, which is the correction it was asked to carry') - # The issue templates are the second automated path, and the first version of - # this test could not see it: it read only dependabot.yml, so it passed while - # the page claimed dependabot was the *single* exception. Every template that + # The issue templates are the second automated path: a check reading only + # dependabot.yml passes while the page claims dependabot is the *single* + # exception. Every template that # sets a label in its front matter has to be named on the page, or the page # understates how many labels arrive without judgement behind them. templated = {} @@ -1427,12 +1418,11 @@ def test_the_page_says_the_labels_are_set_by_hand(self) -> 'None': if line.startswith('labels:'): templated[template.name] = line.split(':', 1)[1].strip() - # The dependabot attribution: unpinned, the page once named ``github_actions`` - # as dependabot-applied while .github/dependabot.yml configured only ``pip``. - # It configures ``github-actions`` too (#1968), so the label is dependabot's on - # a workflow bump and hand-applied everywhere else. Getting either half wrong - # is this section's own point stated backwards, and the places the page - # mentioned it once disagreed with each other about ``python``. + # The dependabot attribution: .github/dependabot.yml configures ``pip`` and + # ``github-actions`` (#1968), so ``github_actions`` is dependabot's on a + # workflow bump and hand-applied everywhere else. Getting either half wrong is + # this section's own point stated backwards, and so is the page's mentions of + # it disagreeing with each other about ``python``. dependabot = (ROOT / '.github' / 'dependabot.yml').read_text(encoding='utf-8') ecosystems = re.findall(r'package-ecosystem:\s*"([^"]+)"', dependabot) @@ -1447,13 +1437,12 @@ def test_the_page_says_the_labels_are_set_by_hand(self) -> 'None': # issue #949 asked to be looked at. Every sentence pairing the tool with the # label is checked instead, so the phrasing does not matter. # - # Proximity is not the test, and was measured wrong on #949: the section then - # stated more than once, deliberately, that ``github_actions`` was *not* - # dependabot's, and each of those sentences named the tool and the label - # together. Distinguishing an attribution from a denial by looking for a - # negation nearby also failed -- the attributing bullet's own sentence ended - # "so it never opens a workflow bump here", so the negation was present in the - # sentence that made the claim as well as in the two that denied it. + # Proximity is not the test (#949): a sentence stating, deliberately, that a + # label is *not* dependabot's names the tool and the label together too. + # Distinguishing an attribution from a denial by looking for a negation nearby + # fails as well -- an attributing sentence can carry one, such as one ending + # "so it never opens a workflow bump here", so the negation is present in the + # sentence that makes the claim as well as in those that deny it. # # So the page's attribution is *parsed* instead: the one clause that says what # dependabot puts on its pull requests is located, the labels named inside it are @@ -1468,7 +1457,7 @@ def test_the_page_says_the_labels_are_set_by_hand(self) -> 'None': # `findall`, not `search`: a first-match-only read checks one attributing # clause and lets a second, contradicting one through. That is the same - # walk-past-the-check defect this whole rewrite exists to remove, so every + # walk-past-the-check defect the parse exists to remove, so every # clause matching the shape is required to name the same set. clauses = re.findall(r'\*\*dependabot\*\* puts (.+?) on its own pull requests', self.flat) @@ -1502,10 +1491,9 @@ def test_the_page_says_the_labels_are_set_by_hand(self) -> 'None': f'{name} applies a label from its front matter, but the ' 'page does not name it among the automated paths') # Anchored to the template's own sentence, not a bare substring. - # `assertIn('bug', flat)` passed against a page saying `labels: + # `assertIn('bug', flat)` passes against a page saying `labels: # defect`, because the word `bug` occurs elsewhere ("alongside ``bug``, - # ``enhancement``") -- the same coincidence trap the changelog-runs - # test already guards against. + # ``enhancement``"). self.assertIn(f'``labels: {label}``', self.flat, f'{name} applies {label!r} automatically, but the page ' 'does not state that label next to the template that ' @@ -1515,9 +1503,9 @@ def test_every_issue_link_number_matches_its_own_url(self) -> 'None': """A ``#NNN`` label and the issue or pull number in its own URL must agree. Purely local, needing no network, and it closes the largest single class of - unpinned claim on these pages: a cross-review corrupted a run of link - numbers across these files and not one was caught, because nothing compared the - displayed number against the target. A mismatch is invisible to a reader -- + unpinned claim on these pages: a run of link numbers corrupted across these + files goes uncaught unless something compares the displayed number against the + target. A mismatch is invisible to a reader -- the text says #918 and the link goes to #919 -- and Sphinx cannot warn, since both halves are well-formed. @@ -1540,7 +1528,7 @@ def test_every_issue_link_number_matches_its_own_url(self) -> 'None': # A floor, because `assertEqual([], [])` is what an emptied page produces. This # check disables itself silently on any citation-style change -- a - # single-underscore named reference, or a further move away from the roles, + # single-underscore named reference, or any form other than these two, # takes both patterns to zero while the docstring goes on claiming it closes # the largest class of unpinned claim -- the shape the module's other floors # guard against too. diff --git a/tests/project/test_coverage_rcfile.py b/tests/project/test_coverage_rcfile.py index 9e417f73c1..302d1fb39f 100644 --- a/tests/project/test_coverage_rcfile.py +++ b/tests/project/test_coverage_rcfile.py @@ -5,7 +5,7 @@ ``--rcfile=.github/coverage.toml``, and coverage reads exactly one config file. So the ``[tool.coverage.*]`` tables in :file:`pyproject.toml` are *not* read on that run; the rcfile has to repeat them, plus the keys only the CI run needs -(``parallel``, ``patch`` and ``core``). A setting changed in :file:`pyproject.toml` alone +(``parallel``, ``patch``, ``sigterm`` and ``core``). A setting changed in :file:`pyproject.toml` alone would silently not apply in CI, which is the drift asserted against here. """ diff --git a/tests/project/test_cron_reports_2030.py b/tests/project/test_cron_reports_2030.py index 0e6e5ac904..c2d7d14dcb 100644 --- a/tests/project/test_cron_reports_2030.py +++ b/tests/project/test_cron_reports_2030.py @@ -6,9 +6,9 @@ :mod:`tests.project.test_discussion_report_2030` tests on its own. * :class:`TestTheWorkflowsPost` pins both workflows: the ``discussion`` job - alone holds ``discussions: write``, every other job's permissions as they - were, first-party actions only, the mode each event maps to, the report - steps and their guards, cron-conda's job running on a failed gate but not on + alone holds ``discussions: write``, every other job's permissions as + :data:`PERMISSIONS` lists them, first-party actions only, the mode each event + maps to, the report steps and their guards, cron-conda's job running on a failed gate but not on a deliberate skip, and no ``workflow_run`` consumer of either. * :class:`TestThePinRefresh` runs cron-conda's ``Refresh the platform pins`` with a stand-in ``python``: the log it keeps, and the exit status it keeps. diff --git a/tests/project/test_discussion_report_2030.py b/tests/project/test_discussion_report_2030.py index 65890280eb..98ba1a4f7f 100644 --- a/tests/project/test_discussion_report_2030.py +++ b/tests/project/test_discussion_report_2030.py @@ -424,7 +424,7 @@ def test_a_missing_token_or_repository_warns_before_any_call(self) -> None: self.assertEqual(self.calls(), []) def test_a_bad_command_line_exits_2(self) -> None: - """A missing or unknown mode or status, or a removed flag.""" + """A missing or unknown mode or status, or a flag the script does not take.""" base = [sys.executable, str(SCRIPT), '--category', CATEGORY, '--title', TITLE, '--report', str(self.report)] for args in ((), ('--mode', 'scheduled'), ('--status', 'success'), ('--mode', 'nightly', '--status', 'success'), ('--mode', 'push', '--status', 'passed'), ('--mode', 'push', '--status', 'success', '--always'), diff --git a/tests/project/test_docs_aenum_formatter_1858_unit.py b/tests/project/test_docs_aenum_formatter_1858_unit.py index 5845455c82..42badf8059 100644 --- a/tests/project/test_docs_aenum_formatter_1858_unit.py +++ b/tests/project/test_docs_aenum_formatter_1858_unit.py @@ -2,8 +2,8 @@ """Pins the docs build's rendering of :mod:`aenum` classes in types. GitHub issue #1858: ``typehints_fully_qualified`` is off and :mod:`aenum` has no -inventory, so a parameter annotated ``StdlibIntEnum | AenumIntEnum`` rendered as -``IntEnum | IntEnum``. ``format_class`` in :file:`docs/source/conf.py`, the +inventory, so by default a parameter annotated ``StdlibIntEnum | AenumIntEnum`` +renders as ``IntEnum | IntEnum``. ``format_class`` in :file:`docs/source/conf.py`, the build's ``typehints_formatter``, names an :mod:`aenum` class ``aenum.IntEnum``, as a literal since there is nothing to link to (#1920), and leaves the classes of pcapkit and the standard library to the default rendering. diff --git a/tests/project/test_docs_ambiguous_xrefs_1775_unit.py b/tests/project/test_docs_ambiguous_xrefs_1775_unit.py index 8aee5fcd1b..63d33ea467 100644 --- a/tests/project/test_docs_ambiguous_xrefs_1775_unit.py +++ b/tests/project/test_docs_ambiguous_xrefs_1775_unit.py @@ -1,23 +1,22 @@ # -*- coding: utf-8 -*- """Pins the fixes for the docs build's ambiguous cross-references. -GitHub issue #1775: the docs build gave ``more than one target found for -cross-reference`` warnings, and every one of those links landed on the wrong -object. They had these causes, each fixed where it starts rather than at the -references: +GitHub issue #1775: ``more than one target found for cross-reference`` warnings +in the docs build, each a link landing on the wrong object. These are their +causes, each fixed where it starts rather than at the references: -* A bare builtin name in a type -- ``dict``, ``type``, ``set`` -- was +* A bare builtin name in a type -- ``dict``, ``type``, ``set`` -- is suffix-matched to registry members such as ``TCP.dict``. ``resolve_builtins_exactly`` in :file:`docs/source/conf.py` stops it. -* Napoleon read a URL's colon in a registry member's docstring as Google's inline - ``type: description`` form, and the bogus type was then parsed as references -- +* Napoleon reads a URL's colon in a registry member's docstring as Google's inline + ``type: description`` form, and the bogus type is then parsed as references -- ``http``, ``Echo``. ``save_enum_member_docstring`` and ``restore_enum_member_docstring`` undo it. -* Hand-written ``.. data::`` and ``.. function::`` directives spelled out their - full dotted name under a ``.. module::`` that already supplied it, so each was - registered as ``..``. The exact lookup for a TypeVar such - as ``_AT`` then failed, and the fuzzy one found every ``_AT`` in the package -- - the remaining warnings. +* A hand-written ``.. data::`` or ``.. function::`` directive spelling out its + full dotted name under a ``.. module::`` that already supplies it is registered + as ``..``. The exact lookup for a TypeVar such as ``_AT`` + then fails, and the fuzzy one finds every ``_AT`` in the package. + :class:`DirectiveNameTests` keeps every directive to the bare name. The hook tests drive the hooks with the nodes Sphinx's own producers build and with napoleon itself, and check that ``setup`` connects each one where the build diff --git a/tests/project/test_docs_class_signature_1860_unit.py b/tests/project/test_docs_class_signature_1860_unit.py index b8a4fe0501..ae8381523c 100644 --- a/tests/project/test_docs_class_signature_1860_unit.py +++ b/tests/project/test_docs_class_signature_1860_unit.py @@ -8,8 +8,8 @@ class defines or inherits, before ``__init__``. ``sphinx-autodoc-typehints`` 3.1 :class:`~pcapkit.corekit.infoclass.Info`, :class:`~pcapkit.protocols.schema.schema.Schema` and the reassembly and flow-tracing bases each define a ``__new__`` that only passes ``*args`` and -``**kwargs`` through, so their classes rendered that catch-all, and their -class docstrings lost the types of the parameters they list. +``**kwargs`` through, so by default their classes render that catch-all, and +their class docstrings lose the types of the parameters they list. ``restore_class_signature`` in :file:`docs/source/conf.py` gives such a class its ``__init__``'s parameters, and ``constructor_for_types`` has the library read the @@ -43,7 +43,7 @@ class docstrings lost the types of the parameters they list. #: The event the signature hook answers. EVENT = 'autodoc-process-signature' -#: The catch-all the build rendered for these classes. +#: The catch-all the build renders for these classes by default. CATCH_ALL = '(*args, **kwargs)' @@ -145,7 +145,7 @@ def restore(self, name: 'str', what: 'str' = 'class') -> 'Any': return self.conf.restore_class_signature(self.app, what, name, resolve(name), {}, CATCH_ALL, None) def test_classes_get_their_init_parameters(self) -> 'None': - """Each rendered ``(*args, **kwargs)``, or ``PCAPIO`` its base's ``(fname, **kwargs)``.""" + """Each renders ``(*args, **kwargs)`` by default, or ``PCAPIO`` its base's ``(fname, **kwargs)``.""" for name, expected in RESTORED.items(): with self.subTest(name=name): self.assertEqual(self.restore(name), (expected, None)) @@ -215,7 +215,7 @@ def setUpClass(cls) -> 'None': cls.conf = load_conf() def test_init_behind_a_pass_through_new_is_read(self) -> 'None': - """Each class docstring lists the ``__init__`` parameters, which lost their types.""" + """Each class docstring lists the ``__init__`` parameters, which otherwise lose their types.""" for name in ('pcapkit.foundation.reassembly.ipv4.IPv4', 'pcapkit.foundation.traceflow.tcp.TCP', 'pcapkit.corekit.infoclass.Info', 'pcapkit.corekit.infoclass.MultiInfo'): cls = resolve(name) diff --git a/tests/project/test_docs_enum_signature_1862_unit.py b/tests/project/test_docs_enum_signature_1862_unit.py index cb8305ba14..818a78f7db 100644 --- a/tests/project/test_docs_enum_signature_1862_unit.py +++ b/tests/project/test_docs_enum_signature_1862_unit.py @@ -7,7 +7,7 @@ :class:`~pcapkit.const.reg.apptype.apptype.AppType`, :class:`~pcapkit.const.pcapng.filter_type.FilterType` and :class:`FrameType.Flags ` -rendered it. ``restore_class_signature`` in :file:`docs/source/conf.py` gives +render it by default. ``restore_class_signature`` in :file:`docs/source/conf.py` gives every enumeration the ``(*values)`` an enumeration with members renders. The tests call the hook directly and replay the event through Sphinx's own @@ -34,7 +34,7 @@ #: on one interpreter. __tier__ = 'LATEST' -#: The memberless enumerations that rendered the functional-API form. +#: The memberless enumerations that render the functional-API form by default. MEMBERLESS = ( 'pcapkit.const.reg.apptype.apptype.AppType', 'pcapkit.const.pcapng.filter_type.FilterType', diff --git a/tests/project/test_docs_epilog_links_1936_unit.py b/tests/project/test_docs_epilog_links_1936_unit.py index 117feaca11..e89deef45b 100644 --- a/tests/project/test_docs_epilog_links_1936_unit.py +++ b/tests/project/test_docs_epilog_links_1936_unit.py @@ -15,7 +15,8 @@ docstring defines an epilog name again. Only the text Sphinx renders is read: ``.rst`` pages under :file:`docs/` apart -from the changelog, and docstrings and ``#:`` comments under :file:`pcapkit/`. +from the changelog, which only the redefinition check reads, and docstrings and +``#:`` comments under :file:`pcapkit/`. """ diff --git a/tests/project/test_docs_internal_refs_1940_unit.py b/tests/project/test_docs_internal_refs_1940_unit.py index a4ab682eeb..0d263b35f6 100644 --- a/tests/project/test_docs_internal_refs_1940_unit.py +++ b/tests/project/test_docs_internal_refs_1940_unit.py @@ -2,9 +2,9 @@ """Pins library-internal references in rendered text as roles with a documented target (#1940). A library-internal name written as a double-backtick literal in rendered prose -never links, and neither does a role whose target has no documented entry. #1940 -converts those literals to Python-domain roles wherever the prose means the -object, and every object such a reference names has an entry -- the public +never links, and neither does a role whose target has no documented entry. So +rendered prose writes such a name as a Python-domain role wherever it means the +object (#1940), and every object such a reference names has an entry -- the public classes, and the private helpers and special methods that rendered text refers to -- so that each reference has a target. @@ -90,7 +90,7 @@ ('pcapkit/vendor/reg.rst', 'pcapkit.vendor.reg.ethertype.EtherType._insert_range'), ) -#: ``(source, literal, the role that replaced it)``. The first two are the +#: ``(source, literal, the role written instead)``. The first two are the #: examples #1940 names. CONVERTED = ( ('docs/source/contributing/conventions/registry-protocol.rst', '``AppType.get``', diff --git a/tests/project/test_docs_internal_refs_1947_unit.py b/tests/project/test_docs_internal_refs_1947_unit.py index 0d44f679ab..89c2dec605 100644 --- a/tests/project/test_docs_internal_refs_1947_unit.py +++ b/tests/project/test_docs_internal_refs_1947_unit.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """Pins the internal references and special-method docstrings of the AppType and OptionType templates (#1947). -#1940 converts library-internal literals to roles and gives every special -method that an API page names an entry; the AppType and OptionType templates, +Library-internal literals are written as roles, and every special method that +an API page names has an entry (#1940); the AppType and OptionType templates, and the const modules generated from them, are the part #1947 covers. * :class:`ConvertedReferenceTests` -- the references in those templates and diff --git a/tests/project/test_docs_missing_directives_2234_unit.py b/tests/project/test_docs_missing_directives_2234_unit.py index c369637a27..1ff4eff11b 100644 --- a/tests/project/test_docs_missing_directives_2234_unit.py +++ b/tests/project/test_docs_missing_directives_2234_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Pins the API entries #2234 adds for members that had a docstring but no directive. +"""Pins the API entries of members whose docstring no directive would otherwise render (#2234). * ``Protocol.context`` -- a public property of every protocol, listed on the ``Protocol`` autoclass and in the ``ProtocolBase`` property group beside its diff --git a/tests/project/test_docs_preserve_defaults_1859_unit.py b/tests/project/test_docs_preserve_defaults_1859_unit.py index fb790efe34..f27eba4807 100644 --- a/tests/project/test_docs_preserve_defaults_1859_unit.py +++ b/tests/project/test_docs_preserve_defaults_1859_unit.py @@ -2,11 +2,11 @@ """Pins the docs build's signature defaults to their source text. GitHub issue #1859: autodoc renders a default's ``repr()`` unless -``autodoc_preserve_defaults`` is on, so every ``byteorder=sys.byteorder`` showed -the build host's ``'little'``, and the ``corekit.fields`` pages showed +``autodoc_preserve_defaults`` is on, so with it off every ``byteorder=sys.byteorder`` +shows the build host's ``'little'``, and the ``corekit.fields`` pages show ``default=`` and ``callback=>``. :file:`docs/source/conf.py` turns -the setting on. Source text then exposed the private aliases the protocol +the setting on. Source text then exposes the private aliases the protocol modules import their registries under, ``next=Enum_TransType.UDP``, so ``name_aliased_defaults`` names each such class by its own name. diff --git a/tests/project/test_docs_table_wrapper_ids_1861_unit.py b/tests/project/test_docs_table_wrapper_ids_1861_unit.py index ebf39f8123..24f9a4fcfb 100644 --- a/tests/project/test_docs_table_wrapper_ids_1861_unit.py +++ b/tests/project/test_docs_table_wrapper_ids_1861_unit.py @@ -3,8 +3,8 @@ GitHub issue #1861: furo wraps each table in a ``table-wrapper`` container and copies every attribute of the table onto it, ``ids`` included, so a titled -``list-table`` -- which Sphinx gives an id, to number it -- carried its id on -the wrapper as well as on the table on ``const/reg.html``. ``DropTableWrapperIds`` in +``list-table`` -- which Sphinx gives an id, to number it, as on ``const/reg.html`` +-- carries its id on the wrapper as well as on the table. ``DropTableWrapperIds`` in :file:`docs/source/conf.py` takes the table's ids off the wrapper after furo's transform has run. diff --git a/tests/project/test_docs_typehints_pin_1856_unit.py b/tests/project/test_docs_typehints_pin_1856_unit.py index e870e3679e..be00af4c27 100644 --- a/tests/project/test_docs_typehints_pin_1856_unit.py +++ b/tests/project/test_docs_typehints_pin_1856_unit.py @@ -4,7 +4,7 @@ GitHub issue #1856: :file:`docs/source/conf.py` imports ``resolve_type_guarded_imports`` from a private module of ``sphinx-autodoc-typehints``, and 3.14.0 replaced that module, ``_resolver``, -with the ``_resolve`` package, so a dependency bump to such a release failed the +with the ``_resolve`` package, so a dependency bump to such a release fails the docs build at import. The file is written against a pinned release rather than branching on the @@ -14,8 +14,8 @@ everywhere. 3.14.0 also stopped binding a module's ``if TYPE_CHECKING:`` names into the -module, so Sphinx's own signature introspection lost them again: the schema -class signatures fell back to a bare ``_VT``, which matched another module's +module, so on its own Sphinx's signature introspection loses them: the schema +class signatures fall back to a bare ``_VT``, which matches another module's TypeVar as well. ``bind_guarded_names`` binds them itself. The tests that load :file:`conf.py` need Sphinx and the pinned release, and skip diff --git a/tests/project/test_docs_viewcode_builtins_1863_unit.py b/tests/project/test_docs_viewcode_builtins_1863_unit.py index d820094156..d89ffff393 100644 --- a/tests/project/test_docs_viewcode_builtins_1863_unit.py +++ b/tests/project/test_docs_viewcode_builtins_1863_unit.py @@ -3,8 +3,8 @@ GitHub issue #1863: each ``BufferID`` alias is a subscripted ``Tuple``, which is the builtin :class:`tuple` on every Python the docs build on, so it reports -``__module__ == 'builtins'``. viewcode followed that, and its module index linked -a ``builtins.html`` that is never written. ``follow_builtins_alias`` in +``__module__ == 'builtins'``. Left to itself, viewcode follows that, and its module +index links a ``builtins.html`` that is never written. ``follow_builtins_alias`` in :file:`docs/source/conf.py` keeps viewcode on the module that binds such a name. The tests call the hook directly and replay the event through Sphinx's own diff --git a/tests/project/test_docs_warns_xrefs_2094_unit.py b/tests/project/test_docs_warns_xrefs_2094_unit.py index 096b18641c..b908bc80b4 100644 --- a/tests/project/test_docs_warns_xrefs_2094_unit.py +++ b/tests/project/test_docs_warns_xrefs_2094_unit.py @@ -4,7 +4,7 @@ GitHub issue #2094: napoleon turns a ``Raises:`` entry into a ``:raises:`` field, which the Python domain links to the exception, but writes a ``Warns:`` entry into a plain field with the class name in bold, linking nothing -- and a -``~`` prefix was printed as it stands. ``parse_warns_section`` in +``~`` prefix is printed as it stands. ``parse_warns_section`` in :file:`docs/source/conf.py`, which ``setup`` installs over napoleon's own parser, writes each warning class as an ``exc`` role that searches the way the ``:raises:`` field does. diff --git a/tests/project/test_experimental_concurrency_1903.py b/tests/project/test_experimental_concurrency_1903.py index b4c82f755e..eb60f3d51e 100644 --- a/tests/project/test_experimental_concurrency_1903.py +++ b/tests/project/test_experimental_concurrency_1903.py @@ -5,8 +5,8 @@ wave, holding runners the open pull requests' CI then waits behind. The owner chose option 1: in experimental.yml only, a push to ``main`` is superseded by the next one, as a pull request's run is. Nothing waits on these legs (#1732), -so the newest commit's run is enough. #1938 keeps that rule and drops the -cancelling: experimental.yml has no group (:class:`TestExperimentalHasNoGroup`), +so the newest commit's run is enough. Under #1938 that rule holds without +cancelling anything: experimental.yml has no group (:class:`TestExperimentalHasNoGroup`), and its gate and watcher skip or stop a superseded run instead, which :mod:`tests.project.test_superseded_runs_1938` pins. diff --git a/tests/project/test_ftp_featcode_doc_page_934_unit.py b/tests/project/test_ftp_featcode_doc_page_934_unit.py index 84978bca4f..707500b531 100644 --- a/tests/project/test_ftp_featcode_doc_page_934_unit.py +++ b/tests/project/test_ftp_featcode_doc_page_934_unit.py @@ -44,7 +44,7 @@ def test_page_exists(self) -> 'None': self.assertTrue(PAGE.is_file(), f'{PAGE} does not exist') def test_page_documents_the_featcode_class(self) -> 'None': - """The page declares an ``autoclass`` for the class conventions.rst names. + """The page declares an ``autoclass`` for the class registry-protocol.rst names. Without this directive, ``:class:`~pcapkit.const.ftp.command.FEATCode``` in :file:`docs/source/contributing/conventions/registry-protocol.rst` diff --git a/tests/project/test_graalpy_shards_1963.py b/tests/project/test_graalpy_shards_1963.py index 3475e52969..d715b88dd9 100644 --- a/tests/project/test_graalpy_shards_1963.py +++ b/tests/project/test_graalpy_shards_1963.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """#1963: GraalPy's leg runs its tier in shards, so no xdist worker outlives its heap. -#1753 gives every GraalPy process a 2 GiB heap and an exit once it is spent, and a +#1740 gives every GraalPy process a 2 GiB heap and #1753 an exit once it is spent, and a worker's live heap grows with every test it runs. Over the curated ``ALL`` tier that took job 118912680486's workers to 2.5-3.0 GB RSS and nine of them down, each failing the test it ran, though each such test passes alone. So ``Run unit tests`` @@ -11,8 +11,9 @@ pytest once. :class:`TestTheStep` reads the step, :class:`TestTheStepRuns` runs it with -``sudo`` and ``python`` stubbed on ``PATH``, and :class:`TestTheScript` runs the -script alone. +``sudo`` and ``python`` stubbed on ``PATH``, :class:`TestTheShardsPartitionTheTier` +collects the tier through the script, and :class:`TestTheScript` runs the script +alone. """ diff --git a/tests/project/test_isort_clean.py b/tests/project/test_isort_clean.py index 9c3e3c5572..bf1ef58a8c 100644 --- a/tests/project/test_isort_clean.py +++ b/tests/project/test_isort_clean.py @@ -70,8 +70,8 @@ check. The install line is the other half. ``isort`` is a ``test`` extra requirement, -so the pytest jobs whose selection collects this module -- ``test`` and -``gate``, not ``engine-tests`` (#849) -- run it for real rather than reporting +so the pytest jobs whose selection collects this module -- ``project-tests`` +and ``gate``, not ``engine-tests`` (#849) -- run it for real rather than reporting a skip. That is the opposite resolution from ``mypy``'s, whose :data:`~tests._dependency_gates.DEPENDENCY_GATE_EXCLUSIONS` entry declines the install line, and the difference is not taste: :file:`lint.yml` already runs @@ -82,8 +82,7 @@ on a schedule rather than as a check. So declining the install line here would leave ``make isort``'s verdict checked nowhere at all, which is what #766 objected to. It is cheap enough to be uncontroversial, too: the lines below -take about 1.5s together, the "costs nothing per leg" category the test job's -own install comment puts ``DPKT`` in rather than the ``Scapy`` one. +take about 1.5s together. ``isort`` is in ``Pipfile``'s ``[dev-packages]`` too (#810) -- that being what ``make isort``'s own ``pipenv run`` prefix resolves against, so the target does diff --git a/tests/project/test_ktlo_jobs_gate_nothing_1738.py b/tests/project/test_ktlo_jobs_gate_nothing_1738.py index ede6693360..56558e8220 100644 --- a/tests/project/test_ktlo_jobs_gate_nothing_1738.py +++ b/tests/project/test_ktlo_jobs_gate_nothing_1738.py @@ -7,7 +7,7 @@ the job cap -- would still fail the job, and so ``Required checks passed``, which reads one ``needs..result`` per job, and skip ``unittest-ordering``, which needs ``test``. So they are ``test-ktlo``, ``integration-ktlo`` and -``pypcap-parity-ktlo``, and this module pins the three things that make that hold: +``pypcap-parity-ktlo``, and this module pins what makes that hold: * no job that can fail the run -- one without job-level ``continue-on-error`` -- ``needs:`` a KTLO job, directly or through another job; diff --git a/tests/project/test_ktlo_legs_stay_green_1672.py b/tests/project/test_ktlo_legs_stay_green_1672.py index a5cfd8138e..ab5ba37bec 100644 --- a/tests/project/test_ktlo_legs_stay_green_1672.py +++ b/tests/project/test_ktlo_legs_stay_green_1672.py @@ -21,15 +21,16 @@ step's failure is tried on every leg instead of read off the text. Expressions go through :mod:`tests.project.test_release_gates`'s evaluator, and the scripts that write annotations are executed. Both summaries run :file:`util/leg_summary.py` -(#1967), :file:`create-release.yml`'s ``pypi``'s too, whose former inline copy -of its shell is run against the script on the same steps. +(#1967), :file:`create-release.yml`'s ``pypi``'s too, and the script is run +against :data:`FORMER_RELEASE_SUMMARY`, the step's inline shell form and the +reference it must match, on the same steps. The experimental tier, free-threaded CPython and GraalPy (#1660), is a job of its own, ``experimental``, which nothing gating needs (#1732). They are non-blocking by the same mechanics, every step that can fail guarded, and report through ``Experimental summary``, that job's last step: -:class:`TestAnExperimentalLegIsNonBlockingTheSameWay`. Their setup-python, install -and test steps have caps of their own, so a slow or hung leg fails a step rather +:class:`TestAnExperimentalLegIsNonBlockingTheSameWay`. Their setup-python, install, +watcher setup and test steps have caps of their own, so a slow or hung leg fails a step rather than being cancelled with its job: :class:`TestASlowExperimentalLegTimesOutAStepNotTheJob`. @@ -150,7 +151,8 @@ #: The ``KTLO summary`` whose job keeps no copy of its summary: no CI report reads #: Create Release's runs (#2030). UNCOPIED_SUMMARY = ('create-release.yml', 'pypi') -#: ``pypi``'s ``KTLO summary`` before it ran :data:`SUMMARY_RUN` (#1967). +#: ``pypi``'s ``KTLO summary`` in its inline shell form, the reference +#: :data:`SUMMARY_RUN`'s script must match (#1967). FORMER_RELEASE_SUMMARY = '''\ failed=$(jq -r 'to_entries[] | select(.value.outcome == "failure") | .key' <<< "$STEPS") if [ -z "$failed" ]; then @@ -563,7 +565,7 @@ def test_the_summary_is_quiet_when_nothing_failed(self) -> None: self.assertIn(f'{LEG_KTLO}: no step failed.', result.stdout) def test_the_former_release_copy_reports_as_the_script_does(self) -> None: - """``pypi``'s ``KTLO summary`` shell from before #1967 and the script report alike.""" + """``pypi``'s ``KTLO summary`` in its inline shell form and the script report alike (#1967).""" if shutil.which('jq') is None: # pragma: no cover self.skipTest('jq, on every Ubuntu runner image, is needed to run the former KTLO summary') self.assertEqual(summary_step(job(*UNCOPIED_SUMMARY))['run'], SUMMARY_RUN) @@ -674,7 +676,7 @@ def test_every_failure_still_fails_a_supported_leg(self) -> None: self.assertFalse(continue_on_error(by_id[gated], context), gated) def test_no_summary_runs_on_a_supported_leg(self) -> None: - """``test`` has one, ``KTLO summary``; ``Experimental summary`` left it with its legs (#1732).""" + """``test`` has one, ``KTLO summary``; ``Experimental summary`` is the ``experimental`` job's (#1732).""" spec = job('unit-tests.yml', 'test') self.assertNotIn(EXPERIMENTAL_SUMMARY, [label(step) for step in spec['steps']]) position = len(spec['steps']) - 1 - len(JOB_SUMMARY_STEPS) @@ -737,7 +739,7 @@ def test_every_failure_it_masks_reaches_the_experimental_summary(self) -> None: # One script with the KTLO summary's, so a fix to either reaches both (#1967). self.assertEqual(self.summary['run'], EXPERIMENTAL_SUMMARY_RUN) self.assertEqual(summary_step(job('unit-tests.yml', 'test'))['run'], SUMMARY_RUN) - # #2060: no `conda-windows` tier since the win-64 conda leg blocks as the others do. + # #2060: the win-64 conda leg blocks as the others do, so it has no tier of its own. self.assertEqual(REASONS, {'ktlo': KTLO_REASON, 'experimental': EXPERIMENTAL_REASON}) def test_each_step_names_the_leg_by_its_tier(self) -> None: diff --git a/tests/project/test_lint_pages_reports_2030.py b/tests/project/test_lint_pages_reports_2030.py index a3191366d5..21bd71bf6b 100644 --- a/tests/project/test_lint_pages_reports_2030.py +++ b/tests/project/test_lint_pages_reports_2030.py @@ -6,8 +6,8 @@ :mod:`tests.project.test_discussion_report_2030` tests on its own. * :class:`TestTheWorkflowsPost` pins both workflows: the ``discussion`` job - alone holds ``discussions: write``, every other job's permissions as they - were, first-party actions only, the command each posts with, the mode each + alone holds ``discussions: write``, every other job's permissions as + :data:`PERMISSIONS` lists them, first-party actions only, the command each posts with, the mode each event maps to, Lint's report steps and their guards, and when each job runs: never on a pull request, and for GitHub Pages never on a verdict for another commit. diff --git a/tests/project/test_module_isolation.py b/tests/project/test_module_isolation.py index 07c9138adb..27aee47f5f 100644 --- a/tests/project/test_module_isolation.py +++ b/tests/project/test_module_isolation.py @@ -71,9 +71,9 @@ its own sake. Why not simply move ``test_protochain.py`` so it sorts after its victims: because -that is what was already happening by accident. It sorts last inside -``tests/corekit/``, so no sibling healed it, and the full CI selection passed only -because unrelated directories sort between it and ``tests/project/``. A fix that +sort order is what hid #660. The file sorted last inside ``tests/corekit/``, so no +sibling healed it, and the full CI selection passed only because unrelated +directories sort between it and ``tests/project/``. A fix that rearranges the collection order leaves the defect in place and re-hides it, and the next file added anywhere between the two re-exposes it. diff --git a/tests/project/test_no_engine_agreement_prose_1708_unit.py b/tests/project/test_no_engine_agreement_prose_1708_unit.py index e1e24f3aa6..555d56d0cd 100644 --- a/tests/project/test_no_engine_agreement_prose_1708_unit.py +++ b/tests/project/test_no_engine_agreement_prose_1708_unit.py @@ -6,8 +6,7 @@ to one, and protocol rules belong in the default engine only. So prose that says an adapter does something "as the default engine does", "as every other engine's adapters do", or "stops at the same depth" states a contract nobody -keeps. Issue #1708 removed the tests and the code behind those claims, then the -claims themselves. +keeps. This scans every module of :mod:`pcapkit` and every page and module under :file:`docs/source` but the changelog for the shapes in :data:`PATTERNS`. The @@ -178,7 +177,7 @@ def test_every_exemption_still_matches(self) -> None: self.assertIn(phrase, [found for (_, _, found) in claims(text)]) def test_the_issue_1708_shapes_are_matched(self) -> None: - """Each claim issue #1708 removed is seen, wrapped or not, so a pass is not vacuous.""" + """Each claim of a shape #1708 names is seen, wrapped or not, so a pass is not vacuous.""" source = ( ' """Reassembly and flow tracing are handed a plain float of it, as by\n' ' every other engine (#1501). The float nearest the exact timestamp, as\n' diff --git a/tests/project/test_no_moved_name_re_exports_1744_unit.py b/tests/project/test_no_moved_name_re_exports_1744_unit.py index 754a765f08..d97c357893 100644 --- a/tests/project/test_no_moved_name_re_exports_1744_unit.py +++ b/tests/project/test_no_moved_name_re_exports_1744_unit.py @@ -2,10 +2,9 @@ """A moved name keeps no re-export at its old path (GitHub issue #1744). The owner ruled on #1715 and #1103 that a moved name or module keeps no -re-export at its old path, ``TLSKeyLabel`` included, and #1744 swept the ones -that survived. Under ruling D1 on #1744 only an *explicit* re-export goes: an -alias assignment, a name in a non-home module's ``__all__``, or a name -documented as importable there. A module that still uses a name it imports from +re-export at its old path, ``TLSKeyLabel`` included. Under ruling D1 on #1744 +only an *explicit* re-export goes: an alias assignment, a name in a non-home +module's ``__all__``, or a name documented as importable there. A module that still uses a name it imports from the new home keeps the import, so the old path resolving through it is expected. These checks pin that: diff --git a/tests/project/test_no_setup_py_2061.py b/tests/project/test_no_setup_py_2061.py index aaffcc0cbc..39bd9fd7d0 100644 --- a/tests/project/test_no_setup_py_2061.py +++ b/tests/project/test_no_setup_py_2061.py @@ -1,15 +1,14 @@ # -*- coding: utf-8 -*- """#2061: there is no :file:`setup.py`, and the benchmark image copies only files that exist. -``pyproject.toml`` declares everything, and the 3.8 floor (#2051) left -:file:`setup.py` a Python 2 guard and a bare ``setup()``, so it went. +``pyproject.toml`` declares everything; under the 3.8 floor (#2051) a +:file:`setup.py` would hold only a Python 2 guard and a bare ``setup()``. -* :class:`TestNoSetupPy` fails if one comes back at the repository root. +* :class:`TestNoSetupPy` fails on one at the repository root. * :class:`TestBenchmarkDockerfileCopies` checks every source a ``COPY`` in :file:`examples/benchmark/Dockerfile` names is in the build context, the - repository root that ``run.sh`` passes. Its ``COPY`` of the packaging files - named :file:`setup.py` too, and a missing source fails only at ``docker - build``, which no CI job runs. + repository root that ``run.sh`` passes. A ``COPY`` of a missing source, such + as :file:`setup.py`, fails only at ``docker build``, which no CI job runs. """ diff --git a/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py b/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py index 1f744af10f..6a0ed27ac6 100644 --- a/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py +++ b/tests/project/test_no_version_directives_in_generated_tables_1884_unit.py @@ -18,7 +18,7 @@ * :class:`TestGeneratedModules` scans every module under :mod:`pcapkit.const`. * :class:`TestVendorTemplates` scans the template text of every crawler, and checks on a sample module which literals count as template text. -* :class:`TestDocsPages` pins the notes #1884 moved onto the docs pages. +* :class:`TestDocsPages` pins the notes the docs pages carry for the tables (#1884). This module is unit-tier and reads no capture. @@ -52,7 +52,7 @@ 'tcp/flags.py', ) -#: The notes #1884 moved from a template onto a docs page, as +#: The notes a docs page carries for its generated tables (#1884), as #: ``(page, directive, text the directive's body must hold)``. MOVED_NOTES = ( ('ftp.rst', 'versionchanged:: 1.5.0', ':issue:`860`'), @@ -245,7 +245,7 @@ def test_a_directive_spaced_wider_is_matched(self) -> 'None': class TestDocsPages(unittest.TestCase): - """The notes moved off the templates are on the hand-written docs pages.""" + """The notes the templates leave out are on the hand-written docs pages.""" def test_each_moved_note_is_on_its_page(self) -> 'None': for page, directive, text in MOVED_NOTES: diff --git a/tests/project/test_per_class_import_guard.py b/tests/project/test_per_class_import_guard.py index 53a11e113e..9bb0018f4e 100644 --- a/tests/project/test_per_class_import_guard.py +++ b/tests/project/test_per_class_import_guard.py @@ -8,8 +8,7 @@ ``tearDown`` that also purges :mod:`pcapkit` empties :data:`sys.modules` before that cleanup reads it, so the class keeps nothing and every test pays the full re-import again (about 0.67s and 329 modules, by that helper's docstring) while -the ``setUp`` reads as if it did not. Twelve classes in seven files did this -on ``main`` after #1070 converted their ``setUp`` and left the ``tearDown``. +the ``setUp`` reads as if it did not. A class that needs a fresh import for every test should say so by purging in ``setUp`` *instead of* calling :func:`~tests._support.reimport_once_per_class`, @@ -44,7 +43,7 @@ ALLOWED = {} # type: dict[tuple[str, str], str] #: Fewest classes the scan must see calling the helper, so that a scan that -#: silently matches nothing cannot pass. It counted 384 when this was written. +#: silently matches nothing cannot pass. It counted 384 under #1538. MIN_CLASSES = 300 diff --git a/tests/project/test_pip_install_1967.py b/tests/project/test_pip_install_1967.py index a4f64e8cf5..42161f6fd5 100644 --- a/tests/project/test_pip_install_1967.py +++ b/tests/project/test_pip_install_1967.py @@ -4,8 +4,10 @@ * :class:`TestPipInstall` runs the script against a stand-in ``pip``: a clean install, a failed one with and without an ``ERROR:`` line, a failed upgrade, and the arguments and levels it refuses. -* :class:`TestTheFormerShellAgrees` runs the shell the steps held before the - script against the same stand-in and requires the same output, log and status; +* :class:`TestTheFormerShellAgrees` runs the steps' inline shell form, + :data:`FORMER_ANNOTATED` and :data:`FORMER_BARE`, the reference the script + must match, against the same stand-in and requires the same output, log and + status; for the steps whose shell wrote no annotation, the same except for the annotation the script writes for a failed install; and for an indented ``ERROR:`` line, the same except for the reason the shell missed (#2041). @@ -74,7 +76,7 @@ }) #: The KTLO jobs, which run their twin's steps by alias (#1738). TWINS = {'test-ktlo': 'test', 'integration-ktlo': 'integration', 'pypcap-parity-ktlo': 'pypcap-parity'} -#: Text only the former inline install held. +#: Text only the inline install holds. INLINE = ('pip-install.log', 'did not install on', "s/^ERROR: //p") #: The covered steps outside ``unit-tests.yml``, as ``(workflow, job)``, each step whole, #: and its job's ``ANNOTATION_LEVEL``. The experimental legs are never blocking, so @@ -136,8 +138,9 @@ sys.exit(status) ''') -#: The ``run:`` of ``test``'s and ``integration``'s install before the script, -#: under ``shell: bash``; ``SPEC`` stands for each job's extras. +#: ``test``'s and ``integration``'s install in its inline shell form, under +#: ``shell: bash``, the reference the script must match; ``SPEC`` stands for each +#: job's extras. FORMER_ANNOTATED = textwrap.dedent('''\ python -m pip install -U pip setuptools wheel if ! python -m pip install -e 'SPEC' 2>&1 | tee "$RUNNER_TEMP/pip-install.log"; then @@ -147,7 +150,7 @@ exit 1 fi ''').replace('SPEC', SPEC) # noqa: E501 -#: The ``run:`` of the other covered steps before the script, under the default shell. +#: The other covered steps' install in its inline shell form, under the default shell. FORMER_BARE = textwrap.dedent('''\ python -m pip install -U pip setuptools wheel python -m pip install -e 'SPEC' @@ -157,7 +160,7 @@ FAILED = ['out:Collecting pytest>=8', 'err:ERROR: Ignored the following versions: 8.0.0 Requires-Python >=3.8', 'out:INFO: pip is looking at multiple versions', 'err:ERROR: No matching distribution found for pytest>=8'] #: The tail of a failed wheel build, as pip printed it on ``Python pypy3.10 (KTLO)``: -#: its one ``ERROR:`` line is indented, so the former shell found none (#2041). +#: its one ``ERROR:`` line is indented, so the inline shell finds none (#2041). WHEEL_FAILED = ['out: note: This error originates from a subprocess, and is likely not a problem with pip.', 'out: ERROR: Failed building wheel for cryptography', 'out:Successfully built pypcapkit', 'out:Failed to build cryptography', 'out:', @@ -309,10 +312,10 @@ def test_a_missing_variable_fails_before_pip_runs(self) -> None: @unittest.skipIf(any(shutil.which(tool) is None for tool in ('bash', 'tee', 'sed', 'tail')), 'needs bash, tee, sed and tail') class TestTheFormerShellAgrees(_Scratch): - """The script prints, logs and exits as the steps' former shell did.""" + """The script prints, logs and exits as the steps' inline shell form does.""" def former(self, shell: 'str', env: 'dict[str, str]') -> 'subprocess.CompletedProcess[str]': - """``shell`` as the step ran it: ``shell: bash`` for the annotated steps, ``bash -e`` for the rest.""" + """``shell`` as a step runs it: ``shell: bash`` for the annotated steps, ``bash -e`` for the rest.""" bin_dir = self.tmp / 'bin' bin_dir.mkdir(exist_ok=True) python = bin_dir / 'python' @@ -325,7 +328,7 @@ def former(self, shell: 'str', env: 'dict[str, str]') -> 'subprocess.CompletedPr capture_output=True, text=True, check=False, timeout=120) def both(self, shell: 'str', level: 'str', **pip: 'object') -> 'tuple[tuple[object, ...], tuple[object, ...]]': - """``(status, stdout, log, pip's calls)`` of the former shell, then of the script, each with its stderr.""" + """``(status, stdout, log, pip's calls)`` of the inline shell, then of the script, each with its stderr.""" found = [] for run in (lambda env: self.former(shell, env), lambda env: self.script('-e', SPEC, env=env)): result = run(self.env(level, **pip)) @@ -344,7 +347,7 @@ def test_every_annotated_outcome_matches(self) -> None: self.assertEqual(new, old) def test_an_indented_error_line_departs_on_purpose(self) -> None: - """The former shell's ``^ERROR: `` missed an indented line and fell back; the script names it + """The inline shell's ``^ERROR: `` misses an indented line and falls back; the script names it (#2041). Everything else still matches, and so does an outcome where both find the same line.""" for level in ('error', 'warning'): with self.subTest(level=level): @@ -451,7 +454,7 @@ def test_the_listed_steps_still_install(self) -> None: class TestTheOtherWorkflowsRunTheScript(unittest.TestCase): - """The experimental and Compat installs run the script, guards unchanged; nothing else there keeps the shell.""" + """The experimental and Compat installs run the script, with their guards; nothing else there keeps the shell.""" def test_each_site_is_the_script_with_its_guards(self) -> None: for (name, job_id), (expected, _) in ELSEWHERE.items(): diff --git a/tests/project/test_pr_template_commit_types_1102_unit.py b/tests/project/test_pr_template_commit_types_1102_unit.py index 584c14071f..1d5714bd25 100644 --- a/tests/project/test_pr_template_commit_types_1102_unit.py +++ b/tests/project/test_pr_template_commit_types_1102_unit.py @@ -3,8 +3,8 @@ :file:`CONTRIBUTING.md` lists the valid ``type`` prefixes of a commit subject, and :file:`.github/PULL_REQUEST_TEMPLATE.md` asks the contributor to tick the one their -subject carries. The template had no ``release`` box although ``release`` is a listed -type with commits in the history, so a version-bump pull request had nothing to tick. +subject carries. Without a ``release`` box, though ``release`` is a listed type with +commits in the history, a version-bump pull request has nothing to tick. """ diff --git a/tests/project/test_project_pins_once_1954.py b/tests/project/test_project_pins_once_1954.py index 350a6db459..39784bbd60 100644 --- a/tests/project/test_project_pins_once_1954.py +++ b/tests/project/test_project_pins_once_1954.py @@ -41,7 +41,11 @@ def step(where: 'tuple[str, str]', name: 'str') -> 'dict[str, object]': class TestThePinsJob(unittest.TestCase): - """``project-tests`` runs the pins on every diff but ``gate-only``'s, on ``latest``, and gates.""" + """``project-tests`` runs on every diff but ``gate-only``'s, on ``latest``, and gates. + + Its pins step skips a docs-only diff. + + """ def test_it_runs_on_every_diff_but_not_under_gate_only(self) -> None: spec = job(*JOB) diff --git a/tests/project/test_project_status.py b/tests/project/test_project_status.py index 28cf673f98..23eb3cb949 100644 --- a/tests/project/test_project_status.py +++ b/tests/project/test_project_status.py @@ -2,8 +2,8 @@ """Tests for :file:`util/project_status.py` and the workflow that runs it. :file:`.github/workflows/project-status.yml` sets each item's *Status* on the -project board from its state labels. Three things about it are checkable from the -tree, and each has its own class: +project board from its state labels. What is checkable about it from the tree +has a class of its own: * :class:`TestMapping` -- :func:`status_for` gives the documented status for every row of the table, precedence included. @@ -263,8 +263,8 @@ def scan(text: 'str') -> 'list[tuple[tuple[str, ...], str]]': It *fails closed*: any line outside that subset raises :exc:`UnsupportedYAML` rather than being read as something it is not. A value the scan misreads is a - ``run:`` script the safety tests never see -- a flow-mapping step, ``run :``, - ``run: |2``, or ``run: *alias`` all were (#1060) -- and a run without PyYAML + ``run:`` script the safety tests never see -- as a flow-mapping step, ``run :``, + ``run: |2`` or ``run: *alias`` is to a laxer scan (#1060) -- and a run without PyYAML has nothing to catch the disagreement, so refusing is the only answer that cannot pass vacuously. @@ -411,7 +411,7 @@ def test_event_payload_only_in_env(self) -> None: def test_the_env_exemption_is_the_value_s_own_mapping(self) -> None: # A job or step key merely *under* something named ``env`` is not in an - # ``env:`` block; ``'env' in path`` exempted both (#1060). + # ``env:`` block; an ``'env' in path`` test would exempt both (#1060). text = (f'jobs:\n env:\n runs-on: {_TITLE}\n' f' steps:\n - name: x\n if: {_TITLE}\n' f' env:\n T: {_TITLE}\n') @@ -463,8 +463,8 @@ def test_nothing_is_cancelled(self) -> None: _TITLE = '${{ github.event.issue.title }}' _STEPS = 'jobs:\n j:\n steps:\n' -#: Workflows that splice :data:`_TITLE` into a ``run:`` script in a form the old -#: line scan read as something else (#1060). Each must make :func:`scan` raise. +#: Workflows that splice :data:`_TITLE` into a ``run:`` script in a form a laxer +#: line scan reads as something else (#1060). Each must make :func:`scan` raise. BYPASSES = { 'flow-mapping step': f' - {{name: x, run: "echo {_TITLE}"}}\n', 'space before the colon': f' - name: x\n run : echo {_TITLE}\n', diff --git a/tests/project/test_public_api.py b/tests/project/test_public_api.py index d1069827ab..e51f8e72d3 100644 --- a/tests/project/test_public_api.py +++ b/tests/project/test_public_api.py @@ -48,8 +48,7 @@ No import is wrapped in a skip, either. That is deliberate and also measured: with ``dpkt``, ``scapy``, ``emoji``, ``cryptography`` and ``pycrate`` all absent, every -non-vendor module there was when this module was written still -imported, because each optional dependency is guarded at its import site -- e.g. +non-vendor module imported in that measurement, because each optional dependency is guarded at its import site -- e.g. ``pcapkit/toolkit/scapy.py`` catches :exc:`ModuleNotFoundError` and degrades to ``scapy = None`` with a warning. The vendor modules need nothing past the ``test`` extra at import time: the NGAP crawlers import ``pycrate`` only inside the @@ -99,8 +98,8 @@ instead of in three years. Further assertions cover the rest of the family, because a name can fail to be -exported in more ways than by being misspelled. Each of them failed against the -tree this module was written on: +exported in more ways than by being misspelled. Each of them guards against a +defect the package had: * :meth:`~PublicAPISurfaceTests.test_no_misspelled_all` -- ``pcapkit/const/pcapng/__init__.py`` spelled it ``___all__``, with *three* @@ -287,8 +286,8 @@ class PublicAPISurfaceTests(unittest.TestCase): def test_public_modules_were_actually_found(self) -> None: # A bug in the walk above would turn every other test in this class into # a silent pass over an empty list, so pin the shape of what it returns. - # The floor is deliberately far below the population found when this was - # written: it catches "found nothing", not "the library gained a module". + # The floor is deliberately far below the tree's module population: it + # catches "found nothing", not "the library gained a module". # The vendor packages are pinned because excluding the subtree hid # __all__ gaps in each (#1772). modules = public_modules() @@ -465,8 +464,8 @@ def test_the_non_export_allowlist_is_tight(self) -> None: # satisfied at all, so it gets an assertion of its own. Any of these kinds of # staleness turns an entry into an exclusion nobody decided, silently # excusing the next real omission of that name: the package is gone, the - # attribute is gone, or the name has since been added to __all__ and the - # entry now excuses nothing while looking as though it does. + # attribute is gone, or the name is in __all__ and the entry excuses + # nothing while looking as though it does. packages = {name for name, is_package in public_modules() if is_package} self.assertEqual(sorted(set(DELIBERATE_NON_EXPORTS) - packages), [], diff --git a/tests/project/test_pylint_pragma_names_1923_unit.py b/tests/project/test_pylint_pragma_names_1923_unit.py index c85ee3d60f..452f2ee938 100644 --- a/tests/project/test_pylint_pragma_names_1923_unit.py +++ b/tests/project/test_pylint_pragma_names_1923_unit.py @@ -1,10 +1,10 @@ # -*- coding: utf-8 -*- """Every ``# pylint:`` pragma in :mod:`pcapkit` names a real message (#1923). -A pragma that misspells its message, as ``import-outside-top-level`` did for +A pragma that misspells its message, as ``import-outside-top-level`` does for ``import-outside-toplevel``, suppresses nothing. pylint reports ``W0012 unknown-option-value`` on it and still reports the message it was meant to -silence, but Lint's pylint step is advisory, so nothing noticed. +silence, but Lint's pylint step is advisory, so nothing fails. :data:`KNOWN_MESSAGES` pins the names the package's pragmas use, and every pragma name has to be on it, so that check runs on every leg with nothing diff --git a/tests/project/test_pyshark_encap_map.py b/tests/project/test_pyshark_encap_map.py index 3b147eab7f..af25c3ef3b 100644 --- a/tests/project/test_pyshark_encap_map.py +++ b/tests/project/test_pyshark_encap_map.py @@ -15,8 +15,8 @@ Most of this is unit-tier: it exercises the parsing and formatting helpers against fabricated PDML/help-text fragments and a fabricated measurement list, -never shelling out to :program:`editcap`/:program:`tshark`. Two classes do shell -out, both gated on :data:`HAS_WIRESHARK` -- a real ``HAS_*`` flag, deliberately, +never shelling out to :program:`editcap`/:program:`tshark`. The classes that do +shell out are gated on :data:`HAS_WIRESHARK` -- a real ``HAS_*`` flag, deliberately, rather than a same-effect :func:`shutil.which` check under an unprefixed name. The latter rests on the theory that a name ``tests/_dependency_gates.py``'s AST scan does not recognise cannot need an entry there -- true, but the wrong @@ -98,7 +98,7 @@ def _load_generator(): #: Whether Wireshark is on ``PATH`` at all. Named with the ``HAS_`` prefix #: deliberately: ``tests/_dependency_gates.py``'s AST scan keys on exactly that #: prefix inside a ``skipUnless(...)`` call, and it needs to *see* this gate -#: darkening two classes on every CI leg -- see the module docstring for why +#: darkening the real-sweep classes on every CI leg -- see the module docstring for why #: an unprefixed same-effect name was the wrong fix. Declared in #: :data:`~tests._dependency_gates.NON_DISTRIBUTION_FLAGS` rather than #: :data:`~tests._dependency_gates.MODULE_PROVIDERS`, since a Wireshark binary @@ -106,8 +106,8 @@ def _load_generator(): HAS_WIRESHARK = _EDITCAP is not None and _TSHARK is not None #: Whether lxml, which the generator parses PDML with (#1894), is installed. The -#: test extra installs it everywhere but GraalPy, which has no wheel; with no -#: laxer parser to fall back to, everything here that parses PDML skips there. +#: test extra installs it on CPython only, as PyPy and GraalPy have no wheel; with +#: no laxer parser to fall back to, everything here that parses PDML skips there. HAS_LXML = importlib.util.find_spec('lxml') is not None NO_LXML = 'lxml, from the test extra, is not installed' @@ -164,9 +164,9 @@ class ParseParenthesisedNumberTests(unittest.TestCase): NULL/Loopback (15)"``. This generator never does that extraction at all; a key comes from ``frame.encap_type``'s number directly, and a filter name comes from the PDML ```` attribute, never parsed out - of prose. Reverting :data:`~pyshark_encap_map._PAREN_NUMBER` to #850's - original naive shape, ``r'(\\w+) \\((\\d+)\\)'`` used with - :func:`re.search`, leaves every test below still passing: ``.search`` is + of prose. #850's original naive shape, ``r'(\\w+) \\((\\d+)\\)'`` used with + :func:`re.search` in place of :data:`~pyshark_encap_map._PAREN_NUMBER`, + passes every test below as well: ``.search`` is unanchored, so ``\\w+`` matches ``Loopback`` and steps straight over the ``/`` -- confirmed by trying it. diff --git a/tests/project/test_python_matrix_tiers_1633.py b/tests/project/test_python_matrix_tiers_1633.py index 80bb38dba3..69c73822e6 100644 --- a/tests/project/test_python_matrix_tiers_1633.py +++ b/tests/project/test_python_matrix_tiers_1633.py @@ -26,7 +26,7 @@ No leg runs below 3.8, the ``requires-python`` floor pip enforces at install (#2051). A KTLO leg that cannot install or compile still fails fast and says why, at warning level, and :class:`TestAKTLOLegFailsFast` pins that -- the -install and verify steps are executed with ``python`` stubbed, and each +install and verify steps are executed with ``python``, or its ``pip``, stubbed, and each annotation is parsed as the runner parses it. The tiers are declared once, in :file:`.github/python-versions.toml` (#1660), @@ -271,7 +271,7 @@ def test_the_supported_tier_is_what_the_classifiers_list(self) -> None: self.assertEqual(listed, list(SUPPORTED)) def test_requires_python_is_the_oldest_ktlo_version(self) -> None: - """#2051 raised the floor to 3.8, the oldest version a KTLO leg runs.""" + """``requires-python`` is ``>=3.8`` (#2051), the oldest version a KTLO leg runs.""" text = (ROOT / 'pyproject.toml').read_text(encoding='utf-8') self.assertEqual(re.findall(r'(?m)^requires-python = (.+)$', text), ['">=3.8"']) self.assertEqual(min(KTLO, key=lambda v: tuple(map(int, v.split('.')))), '3.8') @@ -519,7 +519,7 @@ def test_no_conda_leg_is_ktlo_and_315_leaves_base_python_alone(self) -> None: ('unit-tests.yml', 'pypcap-parity', 'Install package, test and generator dependencies'), ('python-compatibility.yml', 'compatibility', 'Install package'), ) -#: The install steps of :data:`KTLO_STEPS`, each running util/pip_install.py (#1967). +#: The install steps of :data:`KTLO_STEPS`' jobs, each running util/pip_install.py (#1967). INSTALLS_KTLO = (KTLO_STEPS[0], KTLO_STEPS[1], INSTALLS[3]) @@ -643,10 +643,11 @@ def test_a_leg_that_cannot_install_stops_there(self) -> None: """The install needs setup-python's interpreter, and every later step needs the install. ``outcome``, not ``conclusion``: ``continue-on-error`` turns a failed step's - conclusion into ``success``, so only its outcome still says it failed. #1672 - brought Compat and ``pypcap-parity`` in; ``KTLO summary`` is the one step - that has to run after a failed install, to report it, with the two after - it that keep that report for the run's Discussions post (#2030). + conclusion into ``success``, so only its outcome still says it failed. Compat + and ``pypcap-parity`` follow it too (#1672). ``KTLO summary`` is the one step + that has to run after a failed install, to report it, with ``Name the job + summary`` and ``Upload the job summary`` after it, which keep that report for + the run's Discussions post (#2030). """ for name, job_id, install in INSTALLS: @@ -796,10 +797,9 @@ def prose(path: 'pathlib.Path') -> 'str': class TestTheProseFollowsTheTiers(unittest.TestCase): """What the docs and comments say about the matrices, read against the matrices. - Nothing executes these, so a tier change leaves them stale without failing - anything else: a first pass at #1633 left the workflows page miscounting the - ``test`` legs, and comments naming 3.10 as CI's floor or 3.15 as allowed to - fail. + Nothing executes these, so a tier change can leave them stale without failing + anything else: the workflows page miscounting the ``test`` legs, or comments + naming 3.10 as CI's floor or 3.15 as allowed to fail (#1633). """ diff --git a/tests/project/test_python_versions_1660.py b/tests/project/test_python_versions_1660.py index 98dc1bb049..6c7ffbb108 100644 --- a/tests/project/test_python_versions_1660.py +++ b/tests/project/test_python_versions_1660.py @@ -13,8 +13,10 @@ on the ``python3`` of every runner that runs it. * **Nothing moved.** The tiers as #1660 introduced them expand to exactly the legs, in the same order, that ``main`` spelt out at :data:`MAIN`, apart - from the one deliberate move: the coverage leg and the ``gate`` from 3.14 to - ``latest``, 3.15. Job-name templates are compared too, since rulesets match + from the deliberate moves: the coverage leg and the ``gate`` from 3.14 to + ``latest``, 3.15 (#1661); PyPy's legs in ``test`` and ``integration`` (#1958); + the conda jobs' Intel macOS and Windows legs (#1822, #2060); and unit-tests.yml's + KTLO legs in jobs of their own (#1738). Job-name templates are compared too, since rulesets match check names literally. Where git still has :data:`MAIN`, the recorded matrices are read back against it. * **Every version comes from the file.** No workflow pins a Python version -- @@ -334,7 +336,7 @@ def with_pypy(name: 'str', job_id: 'str', expected: 'list[dict[str, Any]]') -> ' def render(template: 'str', leg: 'dict[str, Any]') -> 'str': - """A matrix job's name for one leg, for the three expression shapes these names use.""" + """A matrix job's name for one leg, for the expression shapes these names use.""" name = template.replace("${{ matrix.ktlo && ' (KTLO)' || '' }}", ' (KTLO)' if leg.get('ktlo') is True else '') name = name.replace("${{ matrix.experimental && ' (experimental)' || '' }}", ' (experimental)' if leg.get('experimental') is True else '') @@ -578,7 +580,8 @@ def test_without_tomllib_the_tests_that_read_the_file_skip(self) -> None: """On 3.8-3.10 these modules skip, or their tests do, rather than fail to import. Run in a fresh interpreter whose ``import tomllib`` fails, as it does there, - where the two scripts that read the file cannot load at all. That + where :file:`util/python_versions.py` and :file:`util/ci_timing.py`, which + read the file, cannot load at all. That interpreter may already hold :mod:`tomllib`: on the coverage leg, ``patch = subprocess`` starts coverage in it, which reads its TOML config first. So the module is dropped and its entry set to ``None``, @@ -632,7 +635,7 @@ def test_without_tomllib_the_tests_that_read_the_file_skip(self) -> None: class TestNothingMovedButLatest(unittest.TestCase): - """The file reproduces every matrix ``main`` spelt out, but for the coverage leg.""" + """The file reproduces every matrix ``main`` spelt out, but for the deliberate moves.""" @classmethod def setUpClass(cls) -> None: @@ -640,7 +643,10 @@ def setUpClass(cls) -> None: cls.values = script().outputs(script().load(write_tiers(pathlib.Path(tmp), TIERS_AT_MAIN))) def test_every_matrix_expands_to_mains_legs(self) -> None: - """Same legs, same order; the coverage leg moves from 3.14 to ``latest`` (#1661), and PyPy's join (#1958). + """Same legs, same order; the coverage leg moves from 3.14 to ``latest`` (#1661), PyPy's join (#1958). + + The conda jobs' ``os`` axis adds Intel macOS and Windows (#1822, #2060), and + the KTLO legs of each job in :data:`KTLO_TWINS` are its twin's (#1738). Where the clone holds :data:`MAIN`, :data:`MAIN_MATRICES` is read back against it first, so the comparison is with what ``main`` really ran. @@ -1013,7 +1019,7 @@ def test_the_required_contexts_table_is_the_supported_tier(self) -> None: self.assertEqual(re.findall(r'\* - ``Compat Python (3\.\d+)``', page), self.supported) def test_the_workflows_page_cites_each_version_by_its_key(self) -> None: - """The required contexts' rows, and the two tiers' spans under them, cite the file's keys (#2165). + """The required contexts' rows, and the supported and KTLO spans under them, cite the file's keys (#2165). A key names its entry; a line number has to be looked up, so none is cited. @@ -1054,7 +1060,7 @@ def test_each_claim_about_the_latest_leg_names_latest(self) -> None: self.assertEqual(re.findall(pattern, prose(ROOT / path)), [self.latest]) -#: The two builds as setup-python spells them, at actions/setup-python 3892123fb: a +#: The experimental builds as setup-python spells them, at actions/setup-python 3892123fb: a #: free-threaded CPython as ``3.14t`` (src/find-python.ts's ``desugarFreeThreadedVersion``, #: ``/^(\d+\.\d+(\.\d+)?)(t)$/``), and GraalPy as ``graalpy-`` #: (src/find-graalpy.ts's ``parseGraalPyVersion``; README.md's "Basic usage", diff --git a/tests/project/test_python_versions_update_1660.py b/tests/project/test_python_versions_update_1660.py index e07f967ebb..7e42138870 100644 --- a/tests/project/test_python_versions_update_1660.py +++ b/tests/project/test_python_versions_update_1660.py @@ -550,7 +550,7 @@ def test_an_end_of_life_version_the_file_does_not_list_is_not_added(self) -> Non self.assertNotIn('3.5', result.entries) def test_a_dropped_version_with_builds_left_is_not_re_added(self) -> None: - """#2051 dropped 3.7 by hand, below ``requires-python``, while setup-python still builds it.""" + """3.7 is below ``requires-python`` (#2051) and off the file by hand, while setup-python still builds it.""" sources = add_build(as_of_the_file(snapshot()), '3.7.17', True, (('linux', '24.04', 'x64'),)) current = {version: entry for version, entry in frozen()['versions']['cpython'].items() if version != '3.7'} @@ -696,7 +696,7 @@ def test_every_claim_and_rewriter_matches_once_on_main(self) -> None: self.assertEqual(rewritten, text) def test_a_claim_rewrites_each_group_and_never_spans_a_break(self) -> None: - """#1811 retired the count claims and their spelt-out words, #2165 the line ones: a claim states versions.""" + """A claim states versions: never a count or its spelt-out word (#1811), nor a line number (#2165).""" module = updater() claim = module.Claim(r'(3\.\d+) to (3\.\d+)', lambda facts: ('3.12', '3.16')) self.assertEqual(module.apply_claims('Tests 3.11 to 3.15.', [claim], {}), ('Tests 3.12 to 3.16.', [])) @@ -829,8 +829,7 @@ def run_prose_audit(root: 'pathlib.Path') -> 'unittest.TestResult': """The audit's prose class, reading ``root`` instead of the checkout. The prose the script rewrites is what :class:`TestTheProseFollowsTheFile` reads; - the leg-count check :mod:`tests.project.test_python_matrix_tiers_1633` added to - it went with the counts (#1811). + none of it states a leg count (#1811). """ # pylint: disable=import-outside-toplevel @@ -854,7 +853,7 @@ def run_prose_audit(root: 'pathlib.Path') -> 'unittest.TestResult': ############################################################################### -#: Two lines of ``python -m unittest -v`` output for a failed and an errored test. +#: ``python -m unittest -v`` output naming a failed and an errored test. AUDIT_LOG = '''\ test_x (tests.project.test_python_matrix_tiers_1633.TestPython37IsKTLO.test_x) ... FAIL ====================================================================== @@ -1440,7 +1439,7 @@ def test_the_notes(self) -> None: "::error title=Cannot update the Python version tiers::KeyError: 'other'\n") def test_the_proposal_branch_and_the_outputs(self) -> None: - """What ``--proposal-branch`` finds, as its annotation, and ``--github-output``'s three outputs.""" + """What ``--proposal-branch`` finds, as its annotation, and ``--github-output``'s outputs.""" afresh = (f'::notice title=Proposing afresh::{ODD_DATA} has no open pull request, so what it holds was ' 'merged or closed; publish recreates it from main.\n') already = (f'::notice title=Already proposed::{ODD_DATA} already proposes these tiers ({FINGERPRINT}); ' @@ -1604,7 +1603,7 @@ def test_version_check_treats_its_merge_like_any_other(self) -> None: self.assertEqual(set(on), {'push', 'workflow_run'}) self.assertEqual(on['push'], {'tags': ['v*']}) self.assertEqual(on['workflow_run']['workflows'], ['Vendor Update']) - # #1748 added the triggering Vendor Update's own reading, which names no branch either. + # The triggering Vendor Update's own reading (#1748) names no branch either. self.assertEqual(' '.join(spec['jobs']['version_check']['if'].split()), "${{ !cancelled() && (github.event_name != 'workflow_run' || " "(github.event.workflow_run.conclusion == 'success' && " diff --git a/tests/project/test_regenerate_samples_1967.py b/tests/project/test_regenerate_samples_1967.py index 21783aec7f..d5f1112c8d 100644 --- a/tests/project/test_regenerate_samples_1967.py +++ b/tests/project/test_regenerate_samples_1967.py @@ -4,8 +4,9 @@ * :class:`TestRegenerateSamples` runs the script against a stand-in generator: a clean run, a run on stand-in captures, a failed run at either level, and a bad level. -* :class:`TestTheFormerShellAgrees` runs the shell the steps held before the - script against the same stand-ins and requires the same output, log and status. +* :class:`TestTheFormerShellAgrees` runs the step's inline shell form, + :data:`FORMER_SHELL`, against the same stand-ins and requires the same output, + log and status. * :class:`TestTheStepsRunTheScript` pins the step in the ``integration``, ``pypcap-parity`` and ``gate`` jobs to the script, and keeps an inline form of it out of every workflow. @@ -58,8 +59,9 @@ ('err', 'urlopen failed'), ('out', 'other.pcapng: synthesised (download unavailable)')) -#: The ``run:`` the ``integration`` and ``pypcap-parity`` steps held before the -#: script; ``gate``'s differed only in a literal ``error`` for ``${ANNOTATION_LEVEL}``. +#: The step's inline shell form, the reference the script must match, as the +#: ``integration`` and ``pypcap-parity`` jobs spell it; ``gate``'s differs only in a +#: literal ``error`` for ``${ANNOTATION_LEVEL}``. FORMER_SHELL = textwrap.dedent('''\ if ! python examples/generators/make_samples.py 2>&1 | tee "$RUNNER_TEMP/make-samples.log"; then echo "::${ANNOTATION_LEVEL} title=Sample fixture generation failed::examples/generators/make_samples.py could not rebuild examples/captures/. This is a fixture-generation failure, not a test failure -- the test suite has not run." @@ -173,7 +175,7 @@ def test_it_takes_no_arguments(self) -> None: @unittest.skipIf(shutil.which('bash') is None or shutil.which('tee') is None or shutil.which('grep') is None, 'needs bash, tee and grep') class TestTheFormerShellAgrees(_Scratch): - """The script prints, logs and exits as the steps' former shell did, under ``shell: bash``.""" + """The script prints, logs and exits as :data:`FORMER_SHELL` does, under ``shell: bash``.""" def former(self, level: 'str') -> 'subprocess.CompletedProcess[str]': # `python` is the leg's interpreter on the runner; here, this one's. diff --git a/tests/project/test_release_concurrency_1705.py b/tests/project/test_release_concurrency_1705.py index 5fa50d5026..18a11993ae 100644 --- a/tests/project/test_release_concurrency_1705.py +++ b/tests/project/test_release_concurrency_1705.py @@ -115,7 +115,7 @@ def test_skipped_vendor_updates_cannot_displace_run_38499150441(self) -> None: self.assertFalse(first.startswith('create-release-refs/'), first) def test_runs_that_may_release_still_wait_for_each_other(self) -> None: - """The double-publish guard the group was for: never per run, never cancelling.""" + """The double-publish guard the group is for: never per run, never cancelling.""" self.assertEqual(self.cancel, 'false') self.assertEqual(self.group(run_context('34894022730')), self.group(run_context('34894646592'))) diff --git a/tests/project/test_release_conda_leg_smoke_1855.py b/tests/project/test_release_conda_leg_smoke_1855.py index 6deaa5b24a..7958e5f1dd 100644 --- a/tests/project/test_release_conda_leg_smoke_1855.py +++ b/tests/project/test_release_conda_leg_smoke_1855.py @@ -1,11 +1,12 @@ # -*- coding: utf-8 -*- """The release conda leg smoke-tests its package as Conda Update does (#1855). -#1821 added ``util/conda_vendor.py --smoke`` to ``conda-dist`` in -:file:`.github/workflows/cron-conda.yml`, after ``conda build`` and before any -upload. ``conda`` in :file:`.github/workflows/create-release.yml`, which -publishes the release packages, had no such step, and its build step did not -write the ``host_package`` output the step reads. The jobs are read with +``conda-dist`` in :file:`.github/workflows/cron-conda.yml` runs +``util/conda_vendor.py --smoke`` after ``conda build`` and before any upload +(#1821). ``conda`` in :file:`.github/workflows/create-release.yml`, which +publishes the release packages, runs the same step, and its build step writes +the ``host_package`` output the step reads; without either, a release package +is published untested. The jobs are read with :func:`tests._workflows.steps`, and each build step is run with a stand-in ``conda``, as ``test_conda_native_platforms_1822.py`` runs it. diff --git a/tests/project/test_release_conda_scripts_1967.py b/tests/project/test_release_conda_scripts_1967.py index 5e6f85bc1a..0df4019fc5 100644 --- a/tests/project/test_release_conda_scripts_1967.py +++ b/tests/project/test_release_conda_scripts_1967.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """#1967: :file:`util/release_conda.py`, run as both conda jobs' steps run it. -* :class:`TestTheFormerShellAgrees` runs the shell each step held before the - scripts -- ``conda``'s "Check whether this leg is already on Anaconda", both +* :class:`TestTheFormerShellAgrees` runs each step's inline shell form, the + reference the script must match -- ``conda``'s "Check whether this leg is already on Anaconda", both conda jobs' "Build conda packages" and "Upload conda packages to Anaconda (Intel)" -- and the script, on the cases below, against stand-in ``curl``, ``conda`` and ``anaconda``, and requires the same status, standard output and error, step outputs and calls. @@ -39,7 +39,7 @@ if TYPE_CHECKING: from typing import Optional -#: Test tier (#1958): runs the tree's own scripts and the workflows' former shell. +#: Test tier (#1958): runs the tree's own scripts and the steps' inline shell forms. __tier__ = 'LATEST' ROOT = pathlib.Path(__file__).resolve().parents[2] @@ -100,7 +100,8 @@ def read_outputs(text: 'str') -> 'list[tuple[str, str]]': exit "${ANACONDA_FAIL:-0}" ''' -#: "Check whether this leg is already on Anaconda" in ``conda``, before :file:`util/release_conda.py`. +#: "Check whether this leg is already on Anaconda" in ``conda``, as inline shell: what +#: :file:`util/release_conda.py`'s ``leg`` must match. FORMER_LEG = textwrap.dedent(r''' set -euo pipefail @@ -152,8 +153,8 @@ def read_outputs(text: 'str') -> 'list[tuple[str, str]]': fi ''').lstrip() -#: "Build conda packages" in ``conda-dist``, before :file:`util/release_conda.py`; -#: ``conda``'s was the same but for the ``windows-latest`` arm (:data:`FORMER_RELEASE_BUILD`). +#: "Build conda packages" in ``conda-dist``, as inline shell: what ``build`` must match; +#: ``conda``'s is the same but for the ``windows-latest`` arm (:data:`FORMER_RELEASE_BUILD`). FORMER_BUILD = textwrap.dedent(r''' set -euo pipefail @@ -213,7 +214,7 @@ def read_outputs(text: 'str') -> 'list[tuple[str, str]]': ''').lstrip() FORMER_RELEASE_BUILD = FORMER_BUILD.replace(' windows-latest)\n target_platform="win-64"\n ;;\n', '') -#: "Upload conda packages to Anaconda (Intel)" in both jobs, before :file:`util/release_conda.py`. +#: "Upload conda packages to Anaconda (Intel)" in both jobs, as inline shell: what ``upload`` must match. FORMER_UPLOAD = textwrap.dedent(r''' set -euo pipefail @@ -418,7 +419,7 @@ def upload_env(self, case: 'tuple[object, ...]') -> 'dict[str, str]': @unittest.skipIf(not BASH, 'needs bash, which ran the former shell') class TestTheFormerShellAgrees(_Stand): - """Each script exits, prints, writes and calls exactly as the shell it replaced, on every case.""" + """Each script exits, prints, writes and calls exactly as the step's inline shell form, on every case.""" def agree(self, shell: 'str', run: 'str', env: 'dict[str, str]', label: 'str' = '', python: 'str' = '') -> 'tuple[object, ...]': @@ -496,7 +497,7 @@ def test_a_published_leg_is_announced(self) -> None: 'https://api.anaconda.org/release/jarryshaw/pypcapkit/1.5.0\n') def test_only_a_200_is_read(self) -> None: - """As ``[ "$status" = "200" ]`` was: a 201 holding this very leg reads ``false``, uncounted.""" + """As :data:`FORMER_LEG`'s ``[ "$status" = "200" ]``: a 201 holding this very leg reads ``false``, uncounted.""" case = next(case for case in LEG_CASES if case[0] == 'a 2xx other than 200 is not read') status, stdout, stderr, outputs, calls = self.script(LEG_RUN, self.leg_env(case)) self.assertEqual((status, stdout, stderr), (0, '', '')) diff --git a/tests/project/test_release_gates.py b/tests/project/test_release_gates.py index 8ef673f9a2..8b94cb5df7 100644 --- a/tests/project/test_release_gates.py +++ b/tests/project/test_release_gates.py @@ -1,24 +1,25 @@ # -*- coding: utf-8 -*- """Tests that nothing in the workflows that publish does so ungated or unpublished. -Two files: :file:`.github/workflows/create-release.yml`, which everything up to +Over :file:`.github/workflows/create-release.yml`, which everything up to :class:`TestYAMLAgreesWithTheScanner` is about, and :file:`.github/workflows/cron-conda.yml`, whose conda build bumps #1632 found neither tagged nor uploaded. They share the scanner, the expression evaluator and the cascade model below; nothing else. -#641: the ``pypi`` job's ``environment: release`` was commented out while the -``id-token: write`` from the same upstream snippet had been re-added live below it, -and the ``conda`` job never had an ``environment:`` at all. A scheduled vendor crawl -that bumps the version is enough to enter that workflow, so both package indexes -were reachable with no human in the path. +#641: guards against a publishing job with no ``environment:`` -- a ``pypi`` +job whose ``environment: release`` is commented out while the ``id-token: write`` +from the same upstream snippet sits live below it, or a ``conda`` job with no +``environment:`` at all. A scheduled vendor crawl that bumps the version is enough +to enter that workflow, so both package indexes are then reachable with no human +in the path. -What makes that worth a test rather than just a fix is how quietly it came about. +What makes that worth a test rather than just a fix is how quietly it arises. Nothing about a missing ``environment:`` line fails, warns, or shows up in a run's -output -- the job simply runs -- and the line that was missing sat inside a -commented block that read as though it had been disabled deliberately and as a -unit. So the regression is invisible by construction, and the only thing that can -notice it is an assertion over the file. +output -- the job simply runs -- and a line missing from inside a commented block +reads as though it had been disabled deliberately and as a unit. So the +regression is invisible by construction, and the only thing that can notice it is +an assertion over the file. The rule asserted here is not a copy of the current file: **any job in that workflow which runs a publishing action or pushes to git must declare an @@ -51,7 +52,7 @@ ``pypi`` and ``conda`` are both matrix jobs. That is a property of the Actions runner, observable only from a real run -- and moot for both, which have no required reviewers (#887). The question would resurface - only if a matrix job were gated by its own environment again. + only if a matrix job were gated by its own environment. :mod:`yaml` comes only with the ``test`` extra, so the scanner in this module is hand-rolled and dependency-free, and :class:`TestYAMLAgreesWithTheScanner` checks @@ -74,33 +75,29 @@ form these conditions take -- checkable the same coarse, textual way the rest of this module already checks ``environment:`` and ``needs:``. -A cross-review of the #888 change (PR #905) found two defects that every test -above -- being a string assertion over the YAML -- was structurally unable to -catch, because none of them executed anything the workflow itself executes: - -* **``release_status``'s branch logic demanded uniformity across all four - release jobs**, reading a self-heal -- ``github``/``tag`` legitimately - skipping because their artefacts already exist, ``pypi``/``conda`` running - and succeeding because theirs did not -- as the stranded-partial shape #888 - is about, and erroring on the very run that fixed it. - :class:`TestReleaseStatusScriptExecutesCorrectly` actually runs the - extracted shell script, over a table of the shapes a real run can take, - and checks which branch each one takes -- the uniformity bug fails exactly - the self-heal row. -* **The ``python3 -c`` snippets computing PyPI/Anaconda evidence had their - source indented**, which only ``compile()``s on Python 3.14 -- every - interpreter from 3.9 through 3.13 raises ``IndentationError: unexpected - indent`` on the leading whitespace the surrounding YAML block scalar's own - indentation forces onto it, measured directly against each one. Two of the - three snippets run before ``actions/setup-python`` even installs a chosen - version, so ``python3`` there is whatever the runner image ships by - default -- 3.12 on ``ubuntu-latest`` when #905 measured it, never 3.14. - :class:`TestPythonSnippetsSurviveOlderInterpreters` extracts each snippet - and actually runs it under an interpreter older than 3.14 when one can be - found on ``PATH``, skipping otherwise -- ``compile()`` under whatever - interpreter happens to be running this test suite proves nothing when that - interpreter is 3.14 itself, which is exactly how both defects shipped - despite every earlier test in this module passing. +#905's cross-review of #888 found two defects no string assertion over the +YAML can catch, because neither shows until something the workflow executes +runs: + +* **``release_status`` branch logic demanding uniformity across the release + jobs** reads a self-heal -- ``github``/``tag`` legitimately skipping because + their artefacts already exist, ``pypi``/``conda`` running and succeeding + because theirs did not -- as the stranded-partial shape #888 is about, and + errors on the very run that fixes it. + :class:`TestReleaseStatusScriptExecutesCorrectly` actually runs the step's + script, over a table of the shapes a real run can take, and checks which + branch each one takes -- the uniformity bug fails exactly the self-heal row. +* **``python3`` source indented by the surrounding YAML block scalar** only + ``compile()``s on Python 3.14 -- every interpreter from 3.9 through 3.13 + raises ``IndentationError: unexpected indent`` on that leading whitespace, + measured directly against each one. A snippet that runs before + ``actions/setup-python`` installs a chosen version gets whatever ``python3`` + the runner image ships by default -- 3.12 on ``ubuntu-latest`` when #905 + measured it, never 3.14. :class:`TestPythonSnippetsSurviveOlderInterpreters` + runs each snippet under an interpreter older than 3.14 when one can be found + on ``PATH``, skipping otherwise -- ``compile()`` under whatever interpreter + happens to be running this test suite proves nothing when that interpreter is + 3.14 itself. #962: the same argument, one layer up, applies to the ``if:`` expressions themselves. Review rounds on #961 produced mutation after mutation of a single @@ -124,11 +121,11 @@ operands are the exact opposite -- they do not change the truth table at all, so only the text layer has anything to say about them. -#967: a truth table is only as discriminating as its rows, and #962's could not -tell `` == 'false'`` from `` != 'true'``. Every row held the -evidence outputs at a literal ``'true'`` or ``'false'``, where the two spellings -agree, and the text layer never looks at *which* comparison is made against an -output at all -- so the edit was invisible to both layers at once. They diverge +#967: a truth table is only as discriminating as its rows, and rows holding +every evidence output at a literal ``'true'`` or ``'false'`` cannot tell +`` == 'false'`` from `` != 'true'``: the two spellings agree +there, and the text layer never looks at *which* comparison is made against an +output at all -- so the edit is invisible to both layers at once. They diverge only on an output that is neither, and for a publishing job that is the dangerous direction: ``!= 'true'`` runs where ``== 'false'`` skips, so evidence nothing established becomes a publish attempt. :func:`gate_rows` therefore @@ -169,10 +166,10 @@ :file:`cron-conda.yml` the cascade is evaluated rather than only asserted about. That workflow runs on ``workflow_run`` (#731) and skips its own ``unit-tests`` gate on that event; ``conda-update`` escapes the skipped -ancestor with ``!cancelled()`` and commits a build bump, while ``conda-tag`` -and ``conda-dist`` called no status-check function at all -- so Actions' -implicit ``success()`` skipped both, and the five bumps #1632 lists were -committed and then neither tagged nor uploaded. Measured on run 38325136710: +ancestor with ``!cancelled()`` and commits a build bump, and a ``conda-tag`` +or ``conda-dist`` calling no status-check function at all is skipped behind it +by Actions' implicit ``success()`` -- which is how the five bumps #1632 lists +were committed and then neither tagged nor uploaded. Measured on run 38325136710: ``Update requirements.txt`` succeeded with its Bump, Commit and Push steps all green (pushing ``f4f94bcad``), and both downstream jobs reported ``skipped``. @@ -187,9 +184,9 @@ condition text #1632 found, with no status-check call, so the model itself cannot drift into agreeing with whatever the file happens to say. -Getting past the cascade exposed a second defect: two runs can compute the same -bump from one checkout, and the second reports the bump the first already -pushed, so ``conda-tag`` would have re-created an existing tag and failed. Run +Past the cascade lies a second hazard: two runs can compute the same bump from +one checkout, and the second reports the bump the first already pushed, so a +``conda-tag`` reading that report re-creates an existing tag and fails. Run 38325153882 was one: raised by ``Unit Tests`` 38323026585 for 01151b13f, 12 s after 38325136710 was raised by 38323045571 for 9083163f5, both on ``main``'s tip 2e941bbb7 -- two runs in a merge wave, not one run's ``workflow_run`` firing @@ -209,7 +206,7 @@ The ``unit-tests`` gate and the ``verdict`` job are the jobs of :file:`cron-head.yml` (#1967), which ``cron-conda.yml`` calls as its ``head`` -job. The rows still say whether each of those two runs, and :data:`HEAD_JOBS` +job. The rows say whether each of those jobs runs, and :data:`HEAD_JOBS` are decided against that file; the jobs of ``cron-conda.yml`` read ``head``'s result and its ``verified`` output. @@ -299,9 +296,9 @@ #: obligation for whoever adds one, not something this scan infers. _NEEDS = re.compile(r'^ {4}needs:[ \t]*\[([^\]]*)\][ \t]*(?:#.*)?$') #: A job-level ``if:`` key, either inline (``if: ${{ ... }}``) or opening a -#: folded block scalar (``if: >-``, continued below at six spaces). #888 -#: introduced the second form, since ``tag``/``pypi``/``conda``'s conditions -#: grew past one comfortable line; :func:`declared_if` handles both. +#: folded block scalar (``if: >-``, continued below at six spaces), the form +#: ``tag``/``pypi``/``conda``'s conditions take since they run past one +#: comfortable line (#888); :func:`declared_if` handles both. _IF = re.compile(r'^ {4}if:[ \t]*(.*)$') @@ -325,9 +322,9 @@ def job_blocks(text: 'str', source: 'Optional[pathlib.Path]' = None) -> 'dict[st :class:`TestYAMLAgreesWithTheScanner` is what stops the two from drifting. ``source`` only names the file in the failure message, and defaults to - :data:`WORKFLOW` for the callers that predate there being a second one - (#1632) -- a message naming ``create-release.yml`` for a scan of - ``cron-conda.yml`` would send the reader to the wrong file. + :data:`WORKFLOW` for the callers scanning that one -- a message naming + ``create-release.yml`` for a scan of ``cron-conda.yml`` (#1632) would send + the reader to the wrong file. """ lines = text.splitlines() @@ -405,8 +402,8 @@ def declared_if(block: 'str') -> 'str': Handles both the single-line ``if: ${{ ... }}`` form used elsewhere in this file and the folded block-scalar form (``if: >-``, continued below at six - spaces) #888 introduced for ``tag``, ``pypi`` and ``conda``, whose - conditions grew past one line. + spaces) that ``tag``, ``pypi`` and ``conda`` use for conditions running + past one line (#888). Folding here is "join each continuation line with a single space", which is what YAML does to a ``>``/``>-`` scalar whose content lines all sit at the @@ -1049,8 +1046,8 @@ def evaluate_condition(condition: 'str', context: 'dict[str, object]') -> 'bool' JOB_RESULTS = ('success', 'failure', 'cancelled', 'skipped') #: ``{job: output}``, naming the ``version_check`` output each gated job reads -#: as its own evidence. #888's whole point is that these are four different -#: questions rather than four spellings of one -- see +#: as its own evidence. #888's whole point is that these are different +#: questions rather than spellings of one -- see #: :class:`TestEvidenceBasedGating`. The mapping is what lets a scenario row #: set one output to ``'true'`` and the rest to ``'false'``, so a gate reading #: somebody else's evidence reads the opposite answer and the row fails. @@ -1064,7 +1061,7 @@ def evaluate_condition(condition: 'str', context: 'dict[str, object]') -> 'bool' #: ``{job: predecessors}``, naming the *publishing* predecessors each gated job #: holds to the ``== 'success' || == 'skipped'`` equality pair. #: ``version_check`` is deliberately absent: it is a direct -#: dependency of all three and is held to the stricter ``== 'success'`` alone, +#: dependency of each of them and is held to the stricter ``== 'success'`` alone, #: which is the distinction #960 found the workflow's own comment had blurred. #: ``test_the_table_covers_every_declared_dependency`` is what stops this from #: going stale against the file's real ``needs:``. @@ -1121,7 +1118,7 @@ def release_context(version_check: 'str' = 'success', (``'true'``); every other output says ``'false'``. Naming the true ones rather than passing a whole mapping is what makes the cross-evidence trap free: a row that marks only ``PCAPKIT_PYPI_COMPLETE`` complete leaves the - other three incomplete, so a gate reading the wrong one reads the opposite + others incomplete, so a gate reading the wrong one reads the opposite answer. ``unknown`` names the outputs that say *neither*, i.e. that hold @@ -1203,7 +1200,7 @@ def gate_rows(job: 'str') -> 'list[tuple[str, dict[str, object], bool]]': that admitted one would publish untested. * **Each publishing predecessor over all four results.** ``success`` and a legitimate ``skipped`` both run -- that is #888's escape hatch, and the - ``skipped`` row is what stops it being quietly removed again -- while + ``skipped`` row is what stops it being quietly removed -- while ``failure`` and ``cancelled`` do not. ``cancelled`` is the one ``!= 'failure'`` would admit. * **A cancelled run**, twice: once with everything else running, and once @@ -1211,7 +1208,7 @@ def gate_rows(job: 'str') -> 'list[tuple[str, dict[str, object], bool]]': alone catches ``!cancelled()`` becoming ``always()``; the second also catches a ``true ||`` prefix on a row where every inner clause is true. * **All four corners of the ``startsWith(github.ref_name, 'v') || - == 'false'`` disjunction**, with the other three evidence + == 'false'`` disjunction**, with the other evidence outputs inverted so that a gate reading the wrong one fails the row. * **This job's own evidence at ``null``**, on a non-``v`` ref and with ``version_check`` otherwise successful. #967: the one shape on which @@ -1220,12 +1217,13 @@ def gate_rows(job: 'str') -> 'list[tuple[str, dict[str, object], bool]]': literal, where the two spellings agree; the ``its outputs never published`` rows do hold them at ``null``, but only alongside a non-``success`` ``version_check``, where the gate is already false before - the evidence clause is reached. The other three outputs stay ``'false'``, + the evidence clause is reached. The other outputs stay ``'false'``, so a gate reading somebody else's evidence runs where this row demands a skip, and the row keeps the cross-evidence trap the rows above set. - * **#1748's three: the version tagged at another commit**, on ``main`` + * **#1748's rows: the version tagged at another commit**, on ``main`` (skip) and on a ``v*`` push (run, into #1634's check), with this job's own - evidence still incomplete so that the new clause alone decides; and + evidence still incomplete so that the released-elsewhere clause alone + decides; and ``PCAPKIT_RELEASED_ELSEWHERE`` at ``null``, which runs -- the one row that tells ``!= 'true'`` from ``== 'false'`` for it. @@ -1235,14 +1233,14 @@ def gate_rows(job: 'str') -> 'list[tuple[str, dict[str, object], bool]]': The ``null`` row guards a future change rather than a present bug, and is kept on those terms. Neither curl-based evidence check can produce an empty - output as the workflow stands: ``PCAPKIT_PYPI_COMPLETE`` and - ``PCAPKIT_CONDA_COMPLETE`` both default ``complete=false`` and only then set - ``true``, under ``set -euo pipefail``. ``PCAPKIT_TAG_EXISTS`` and - ``PCAPKIT_CONDA_TAG_EXISTS`` are the ones not in this repository's hands: + output as the workflow stands: :file:`util/release_published.py` writes + ``PCAPKIT_PYPI_COMPLETE`` and ``PCAPKIT_CONDA_COMPLETE`` as ``true`` or + ``false`` alone, ``false`` for a count it cannot read. ``PCAPKIT_TAG_EXISTS`` + and ``PCAPKIT_CONDA_TAG_EXISTS`` are the ones not in this repository's hands: they come from ``mukunku/tag-exists-action@v1.7.0``, in ``version_check``'s ``check_tag`` and ``check_conda_tag`` steps, and ``tag``'s gate reads the - second of them. So for one of the three jobs the shape is a third party's - to produce, and for the other two it is one rewritten shell step away. + second of them. So for ``tag`` the shape is a third party's to produce, and + for ``pypi`` and ``conda`` it is one rewritten step away. """ evidence = GATE_EVIDENCE[job] @@ -1318,10 +1316,10 @@ def gate_violations(condition: 'str', job: 'str') -> 'list[str]': def text_gate_violations(condition: 'str', job: 'str') -> 'list[str]': """The existing text layer for a gated job, as a list rather than as assertions. - A deliberate mirror of the three assertions - :class:`TestEvidenceBasedGating` already makes about these conditions -- - the evidence it reads, its treatment of each publishing predecessor, and - ``version_check`` having strictly succeeded. The mirror exists so that + A deliberate mirror of the assertions + :class:`TestEvidenceBasedGating` makes about these conditions -- the + evidence it reads, its treatment of each publishing predecessor, and each of + :data:`STRICT_PREDECESSORS` having strictly succeeded. The mirror exists so that :class:`TestGateMutationsChangeTheTruthTable` can report *which* layer rejects each mutation; #962 settled that the text assertions themselves stay as #961 wrote them, so they are copied here rather than refactored @@ -1329,9 +1327,9 @@ def text_gate_violations(condition: 'str', job: 'str') -> 'list[str]': ``test_the_real_conditions_violate_neither_layer`` is what keeps the mirror from drifting away from the original. - Only the three downstream gates, since those are the three the mirrored + Only the downstream gates, since those are the ones the mirrored assertions are about; ``github`` is the deliberate asymmetry they exist to - preserve rather than a fourth case of it. + preserve rather than another case of it. """ violations = [] # type: list[str] @@ -1819,8 +1817,8 @@ def conda_context(unit_tests: 'str' = 'skipped', # state, which `TestCondaPushReportsWhatLanded` reproduces. With # `CONDA_CHANGED` mapped to `files_changed` instead, `conda-tag` runs here # and fails re-creating `conda-1.5.0b9+3`. 38325153882 itself was raised by - # a different `Unit Tests` run from 38325136710's, for 01151b13f, so since - # #1643 its `verdict` refuses it -- the row after this one. + # a different `Unit Tests` run from 38325136710's, for 01151b13f, so its + # `verdict` refuses it (#1643) -- the row after this one. ('a second run on a tip another run already bumped (38325153882\'s steps)', conda_context(pushed='false', conda_tag='skipped', conda_dist='skipped'), {'unit-tests': False, 'conda-update': True, 'conda-tag': False, 'conda-dist': False}), @@ -1951,8 +1949,8 @@ def test_every_publishing_job_declares_an_environment(self) -> None: An ``environment:`` is what lets Actions hold a job for a human, but only ``github-release``'s actually does (#887) -- ``conda-tag``, - ``pypi`` and ``anaconda`` kept their environments with no reviewer on - them, and are held instead by depending on ``github`` through + ``pypi`` and ``anaconda`` are environments with no reviewer on them, + and their jobs are held instead by depending on ``github`` through ``needs:``. This test only checks that the line is present, not that it still carries a reviewer or that the dependency exists; :class:`TestGatedJobsDependOnGithub` below is what checks the latter. @@ -1989,16 +1987,16 @@ def test_each_gated_job_has_its_own_environment(self) -> None: """Approval is granted to an environment, so sharing one merges two gates. A single ``release`` environment approved once would release PyPI - *and* Anaconda together, which is the outcome #641's fix was meant to - prevent rather than rename. Different credentials reach the two + *and* Anaconda together, which is the outcome #641 is about, renamed + rather than prevented. Different credentials reach the two indexes -- OIDC trusted publishing and ``ANACONDA_TOKEN`` -- and ``tag``'s write to ``main`` uses a third, so sharing one environment would still merge those blast radii even though only ``github-release`` carries a reviewer of its own (#887); the other gated jobs are held by depending on it instead. That each *does* depend on it is :class:`TestGatedJobsDependOnGithub`'s check, not - this one -- this test only stops the four gates from being collapsed - back into one. + this one -- this test only stops the gates from being collapsed into + one. """ environments = {} # type: dict[str, list[str]] @@ -2024,17 +2022,15 @@ def test_pypi_and_anaconda_are_separate(self) -> None: class TestGatedJobsDependOnGithub(WorkflowMixin, unittest.TestCase): - """#887: with only ``github-release`` still holding a required reviewer, - the other gated jobs have to reach it through ``needs:`` instead, - or removing their own reviewer left them ungated in every sense that - matters. + """#887: with only ``github-release`` holding a required reviewer, the + other gated jobs have to reach it through ``needs:`` instead, or they run + with no reviewer of their own, ungated in every sense that matters. Nothing above this class reads ``needs:`` at all -- :class:`WorkflowMixin` aside, every test in :class:`TestPublishingJobsAreGated` and - :class:`TestEnvironmentsAreNotShared` passes identically if ``tag``'s - ``needs:`` is reverted to ``[ version_check ]``, which is the exact - regression #887 fixed. This is the one test in the module that would - catch it. + :class:`TestEnvironmentsAreNotShared` passes identically with ``tag``'s + ``needs:`` as ``[ version_check ]``, which is the exact shape #887 is + about. This is the one test in the module that catches it. """ @@ -2066,9 +2062,9 @@ def test_every_gated_job_depends_on_github(self) -> None: def gate_coverage_violations(gate: 'str', jobs: 'dict[str, str]') -> 'list[str]': """Every evidence state in which a publisher would run and the gate would not. - Walks both refs, every ``'true'``/``'false'``/``null`` assignment of the four + Walks both refs, every ``'true'``/``'false'``/``null`` assignment of the evidence outputs and of ``PCAPKIT_RELEASED_ELSEWHERE`` (#1748), and - ``github``/``tag`` at ``success`` and at ``skipped`` -- 1944 contexts -- with + ``github``/``tag`` at ``success`` and at ``skipped``, with ``version_check`` and ``unit-tests`` themselves at ``success``, since the question is what the gate's own ``if:`` decides. ``github``'s condition is evaluated as declared; its implicit ``success()`` @@ -2080,8 +2076,7 @@ def gate_coverage_violations(gate: 'str', jobs: 'dict[str, str]') -> 'list[str]' gate_tree = parse_condition(gate) publishers = {name: parse_condition(declared_if(jobs[name])) for name in GATE_EVIDENCE} violations = [] # type: list[str] - # #1748: and ``PCAPKIT_RELEASED_ELSEWHERE`` over the same three values, - # 1944 contexts in all. + # #1748: and ``PCAPKIT_RELEASED_ELSEWHERE`` over the same three values. for ref_name in ('main', 'v1.5.0'): for states in itertools.product(('true', 'false', 'null'), repeat=len(names)): for elsewhere in ('true', 'false', 'null'): @@ -2216,7 +2211,7 @@ def test_a_version_tagged_elsewhere_releases_nothing_off_a_v_tag(self) -> None: def test_dropping_any_gate_clause_is_caught(self) -> None: """The coverage check above has to be able to fail. - Each of the gate's five disjuncts is removed in turn from a scratch + Each of the gate's disjuncts is removed in turn from a scratch copy; every removal must leave some state in which a publisher runs and the gate does not. @@ -2276,7 +2271,7 @@ def test_conda_gates_on_its_own_aggregate_evidence(self) -> None: self.assertNotIn('PCAPKIT_TAG_EXISTS ==', condition) def test_evidence_gated_jobs_survive_a_legitimately_skipped_predecessor(self) -> None: - """The half of the fix that has nothing to do with *which* evidence is read. + """The half of the gating that has nothing to do with *which* evidence is read. ``tag``, ``pypi`` and ``conda`` each depend (directly) on ``github``, and ``conda`` also on ``tag``. GitHub Actions skips a job whose @@ -2298,7 +2293,8 @@ def test_evidence_gated_jobs_survive_a_legitimately_skipped_predecessor(self) -> ``cancelled``, and a cancelled ``tag`` would send ``conda`` into its check that ``conda-+0`` names this run's commit (#1634), for a tag ``tag`` never pushed -- a failed step standing in for a clean skip, - found on the same cross-review as the two blocking defects above. + which #905's cross-review found beside the defects the module docstring + lists. """ direct_deps = { @@ -2341,10 +2337,9 @@ def test_version_check_must_strictly_succeed(self) -> None: cancelled ``version_check`` must stop them rather than being waved through the way a legitimately-skipped ``github`` or ``tag`` is. - #960: the comment above ``jobs:`` once read as though the equality - pair applied to "each of their own direct dependencies", which is - false for ``version_check`` specifically -- a direct dependency of - all three, required to have *strictly* succeeded, with no + #960: the equality pair does not apply to "each of their own direct + dependencies" -- not to ``version_check``, a direct dependency of each + of them, required to have *strictly* succeeded, with no ``|| 'skipped'``. An ``assertNotIn`` over known-bad spellings cannot pin this, because a @@ -2403,7 +2398,7 @@ def test_the_release_test_gate_must_strictly_succeed(self) -> None: ``version_check`` runs first and the gate skips whenever nothing is publishable, so a ``skipped`` gate is routine and its place in the chain - carries it to no publisher -- and these three open with + carries it to no publisher -- and these jobs open with ``!cancelled()``, which opts them out of the implicit check that would otherwise stop them. Hence an explicit top-level ``needs.unit-tests.result == 'success'`` conjunct, referenced @@ -2456,7 +2451,7 @@ def test_a_predicted_skip_is_announced(self) -> None: (ROOT / 'util' / 'release_conda.py').read_text(encoding='utf-8')) def test_version_check_exposes_the_new_evidence_outputs(self) -> None: - """The three outputs the jobs above actually read have to exist for + """The outputs the jobs above actually read have to exist for them to read anything at all -- a typo'd output name fails silently (an empty string, not an error), so this is worth asserting directly. @@ -2511,8 +2506,8 @@ class TestGateExpressionsEvaluateCorrectly(WorkflowMixin, unittest.TestCase): #: ``(row name, context, whether github must run)``, for the one gate that #: is deliberately asymmetric: ``github``'s own artefact *is* the ``v*`` - #: tag ``PCAPKIT_TAG_EXISTS`` answers for, so unlike the three jobs below - #: it that output is the right question for it -- see + #: tag ``PCAPKIT_TAG_EXISTS`` answers for, so unlike the jobs below it + #: that output is the right question for it -- see #: ``test_github_still_gates_on_the_v_tag_it_creates``. #: #: Its condition calls no status-check function, so Actions additionally @@ -2525,8 +2520,9 @@ class TestGateExpressionsEvaluateCorrectly(WorkflowMixin, unittest.TestCase): #: ``version_check published no outputs at all`` carries more weight than #: its name suggests. It holds every output at ``null`` while leaving #: ``version_check.result`` at ``success``, which makes it the one row here - #: that tells ``== 'false'`` from ``!= 'true'`` -- the #967 hole the three - #: downstream tables did have. Both halves of it are load-bearing, and + #: that tells ``== 'false'`` from ``!= 'true'`` -- the #967 hole the + #: downstream tables close with a ``null`` row of their own. Both halves of + #: it are load-bearing, and #: ``test_githubs_own_table_tells_equal_false_from_not_equal_true`` is what #: records that rather than leaving it to be rediscovered. GITHUB_ROWS = [ @@ -2819,18 +2815,17 @@ class TestGateMutationsChangeTheTruthTable(WorkflowMixin, unittest.TestCase): #: ``(name, substring, replacement, the layers that must reject it)``. #: ```` stands for the job's own evidence output, so one row - #: covers all three jobs. Substitution rather than a rewrite because the + #: covers every job. Substitution rather than a rewrite because the #: mutations have to be the *small* edits a review round produces -- a #: regenerated condition would differ in ways nobody would have typed. #: #: The mutations #962 names come first, then this module's own, covering #: the clauses the issue only points at -- the publishing predecessors' #: equality pair, the ``startsWith`` short-circuit, and reading somebody - #: else's evidence. The ``!= 'true'`` row is #967's, and is the only row - #: here that #962's table did not catch at all: it is rejected by exactly - #: one scenario -- the ``null`` evidence row :func:`gate_rows` grew for - #: it -- and by no text assertion. The rows after it are #1052's, for the - #: release test gate conjunct that issue added, and the last rows are + #: else's evidence. The ``!= 'true'`` row is #967's: it is rejected by + #: exactly one scenario -- the ``null`` evidence row :func:`gate_rows` + #: carries for it -- and by no text assertion. The rows after it are + #: #1052's, for the release test gate conjunct, and the last rows are #: #1748's, for its released-elsewhere conjunct. MUTATIONS = [ ('the && before the version_check clause becomes ||', @@ -2996,10 +2991,10 @@ def test_githubs_own_table_tells_equal_false_from_not_equal_true(self) -> None: Both layers are downstream-only -- :func:`gate_rows` keys on :data:`PUBLISHING_PREDECESSORS`, which has no ``github`` entry, and :func:`text_gate_violations` says in its own docstring why ``github`` - is not a fourth case of the assertions it mirrors. So the same question + is not another case of the assertions it mirrors. So the same question has to be asked of ``github``'s own table separately, and the answer - measured rather than assumed: the hole #967 found in the three - downstream tables is *not* in this one. + measured rather than assumed: the hole #967 is about is *not* in this + one. ``TestGateExpressionsEvaluateCorrectly.GITHUB_ROWS`` already discriminates, via ``version_check published no outputs at all`` -- which holds every output at ``null`` while leaving @@ -3237,7 +3232,7 @@ def test_it_depends_directly_on_every_other_job(self) -> None: ``needs..result`` and ``needs..outputs`` are only readable for jobs actually named in this job's own ``needs:``; a transitive dependency (the shortcut :class:`TestGatedJobsDependOnGithub` allows - for the other four jobs) would leave this one unable to see, say, + for the gated jobs) would leave this one unable to see, say, ``pypi``'s result at all. """ @@ -3295,14 +3290,13 @@ class TestReleaseStatusScriptExecutesCorrectly(WorkflowMixin, unittest.TestCase) over a table of the shapes a real run can take: the step's ``python3 util/release_status.py`` (#1967), on this interpreter. - A script demanding the same outcome (all four release jobs ``skipped``, or - all four ``success``) before treating a run as complete reads a self-heal + A script demanding the same outcome (every release job ``skipped``, or + every one ``success``) before treating a run as complete reads a self-heal -- ``github``/``tag`` skipping because their artefacts already exist, ``pypi``/``conda`` running and succeeding because theirs did not -- as the - #888 shape itself, erroring on the run that just fixed it, as a - cross-review of #888 found. Every other test in this module is a string - assertion over the YAML, so none of them executes the script or could - catch that. + #888 shape itself, erroring on the run that just fixed it, as #905's + cross-review of #888 found. A string assertion over the YAML executes + nothing, so it cannot catch that. """ @@ -3633,22 +3627,22 @@ def test_each_scenario_takes_the_right_branch(self) -> None: class TestPythonSnippetsSurviveOlderInterpreters(WorkflowMixin, unittest.TestCase): - """A cross-review of the #888 change found that the ``python3 -c`` - snippets computing PyPI/Anaconda evidence had their source indented by - the surrounding YAML block scalar, which only ``compile()``s on Python - 3.14 -- 3.9 through 3.13 all raise ``IndentationError: unexpected - indent``, measured directly. Two of the three run before - ``actions/setup-python`` installs a chosen version, so ``python3`` there - is the runner image's default (3.12 on ``ubuntu-latest`` when #905 measured it), - never 3.14. #905's fix feeds one line per ``printf`` argument instead, so - no line in the actual Python source is indented that Python did not - itself put there -- this class is what actually runs each snippet under - an older interpreter to check that claim, rather than trusting it. + """The ``python3 -`` source computing PyPI/Anaconda evidence carries no + indentation Python did not itself put there (#905). + + Source indented by a surrounding YAML block scalar only ``compile()``s on + Python 3.14 -- 3.9 through 3.13 all raise ``IndentationError: unexpected + indent``, measured directly -- and ``conda``'s per-leg check runs before + ``actions/setup-python`` installs a chosen version, so ``python3`` there is + the runner image's default (3.12 on ``ubuntu-latest`` when #905 measured + it), never 3.14. This class runs each snippet under an older interpreter to + check that claim, rather than trusting it. The PyPI and Anaconda evidence checks in ``version_check`` (#1967) feed - ``python3 -`` from :file:`util/release_published.py` instead, and ``conda``'s + ``python3 -`` from :file:`util/release_published.py`, and ``conda``'s per-leg check from :file:`util/release_conda.py`, so this runs those - snippets, as those scripts build them. + snippets, as those scripts build them, and any ``printf``-fed one the + workflow holds. """ @@ -3994,7 +3988,7 @@ def test_a_committed_bump_is_tagged_and_uploaded(self) -> None: ) def test_removing_the_status_check_reproduces_the_defect(self) -> None: - """The headline test has to be able to fail, and this is how it failed. + """The headline test has to be able to fail, and this is how it fails. Both conditions evaluate **true** on that row with or without the mutation -- asserted here, because it is the whole reason the test goes @@ -4023,11 +4017,11 @@ def test_removing_the_status_check_reproduces_the_defect(self) -> None: ) def test_a_failed_or_cancelled_predecessor_still_stops_the_chain(self) -> None: - """The other half of the fix: escaping the cascade must not escape the + """The other half of the gating: escaping the cascade must not escape the verdict. ``!cancelled()`` on its own would publish a tag off a half-finished - update, which is worse than the skip it replaced -- so each job + update, which is worse than the cascade's skip -- so each job checks its predecessors' results explicitly, and every non-``success`` result has to stop it. ``skipped`` included: a skipped ``conda-tag`` means no ``conda-+`` tag exists, and ``conda-dist`` @@ -4180,12 +4174,12 @@ def test_conda_tag_checks_out_and_tags_the_commit_push_changes_pushed(self) -> N 'Create Tag does not tag `$CONDA_COMMIT` by name') def test_cron_conda_really_does_publish(self) -> None: - """The claim #1632 found wrong in a comment of this module's. + """Against the claim #1632 found in a comment of this module's. - :data:`WORKFLOW`'s comment read that ``cron-conda.yml`` "build[s] and - commit[s] but upload[s] nothing", which is how this chain came to have - no test of any kind. It uploads to anaconda.org and attaches the same - packages to a GitHub Release. + That ``cron-conda.yml`` "build[s] and commit[s] but upload[s] nothing" + is the claim that left this chain with no test of any kind (#1632). It + uploads to anaconda.org and attaches the same packages to a GitHub + Release. """ block = self.jobs['conda-dist'] @@ -4817,7 +4811,7 @@ def _tag_on_origin(self, target: 'str') -> 'str': def test_the_tag_names_the_pushed_commit_after_main_moves_on(self) -> None: """#1643's review: a merge landing between the push and this job is not what was bumped, and has no verdict; tagging ``main``'s tip after a - ``git pull`` tagged it, and ``conda-dist`` would have uploaded it. + ``git pull`` tags it, and ``conda-dist`` then uploads it. """ outputs, pushed = self._pushed() diff --git a/tests/project/test_release_notice_states_no_totals_1849.py b/tests/project/test_release_notice_states_no_totals_1849.py index 49f9072756..1e9f2b83e4 100644 --- a/tests/project/test_release_notice_states_no_totals_1849.py +++ b/tests/project/test_release_notice_states_no_totals_1849.py @@ -2,10 +2,10 @@ """The steady-state "Nothing to release" notice states no file totals (#1849). ``release_status`` in :file:`.github/workflows/create-release.yml` posts it when -every target reconciled by skipping. It named fixed PyPI and Anaconda totals, -while ``version_check`` derives both at release time from the tag's own tier file, -so the notice went wrong once a supported version was added or retired, and for an -older tag. The step is read with :func:`tests._workflows.steps` and its script, +every target reconciled by skipping. Guards against it naming fixed PyPI and +Anaconda totals: ``version_check`` derives both at release time from the tag's own +tier file, so a fixed total goes wrong once a supported version is added or +retired, and for an older tag. The step is read with :func:`tests._workflows.steps` and its script, :file:`util/release_status.py` (#1967), run in that state, so the notice checked is the one a run prints. diff --git a/tests/project/test_release_reset_build_1967.py b/tests/project/test_release_reset_build_1967.py index cd4eab2f56..4b5c5386b9 100644 --- a/tests/project/test_release_reset_build_1967.py +++ b/tests/project/test_release_reset_build_1967.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """#1967: :file:`util/release_reset_build.py`, run as Create Release's "Reset Build Number" runs it. -* :class:`TestTheFormerShellAgrees` runs the shell the step held before the - script, kept here verbatim, and the script, each against a fresh scratch +* :class:`TestTheFormerShellAgrees` runs the step's inline shell form, the + reference the script must match, kept here verbatim, and the script, each against a fresh scratch repository built the same way at the same path, and requires the same status, standard output and error, the refs ``origin`` is left with, and the checkout's ``HEAD`` and ``conda/build``. A stand-in ``git`` in front of the real one @@ -11,10 +11,10 @@ shell ends. * :class:`TestTheStepRunsTheScript` pins the step to its script and its ``env:``, after a whole checkout of the repository, its ``git`` calls to ones - that never pull, merge, rebase, reset or force, and the script to Python 3.8's + that never pull, merge, rebase, reset or force-push, and the script to Python 3.8's syntax. -The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and +The shell form runs as a step with no ``shell:`` runs on the runner, ``bash -e``, and ``bash`` prefixes each of its own errors with the script's path and line, which :meth:`TestTheFormerShellAgrees.former` strips. The step's runs against the scenarios of #1634, after "Create Tag", are @@ -42,7 +42,7 @@ if TYPE_CHECKING: from typing import Callable, Optional -#: Test tier (#1958): runs the tree's own script and the workflow's former shell. +#: Test tier (#1958): runs the tree's own script and the step's inline shell form. __tier__ = 'LATEST' ROOT = pathlib.Path(__file__).resolve().parents[2] @@ -57,7 +57,7 @@ VERSION = '1.5.0b9' -#: The step's ``run:`` before the script, verbatim. +#: The step's inline shell form, the reference the script must match, verbatim. FORMER_RESET = textwrap.dedent(r''' set -euo pipefail @@ -349,7 +349,7 @@ def no_git(scratch: 'Scratch') -> 'str': @unittest.skipIf(not (BASH and GIT), 'needs bash, which ran the former shell, and git') class TestTheFormerShellAgrees(unittest.TestCase): - """The script exits, prints and leaves the repositories exactly as the shell it replaced, on every case.""" + """The script exits, prints and leaves the repositories exactly as the inline shell form, on every case.""" def setUp(self) -> None: tmp = tempfile.TemporaryDirectory(prefix='pcapkit-1967-reset-') # pylint: disable=consider-using-with @@ -387,7 +387,7 @@ def read(*args: 'str') -> 'str': build.read_bytes() if build.is_file() else None) def former(self, build: 'Callable[[Scratch], str]', env: 'dict[str, Optional[str]]') -> 'tuple[object, ...]': - """The former shell as the step ran it, ``bash -e``, its own errors' ``: line N:`` dropped.""" + """The inline shell form as a step runs it, ``bash -e``, its own errors' ``: line N:`` dropped.""" scratch, run = self.scratch(build) result = subprocess.run([BASH, '-e', str(self.shell)], cwd=scratch.work, # nosec: B603 env=self.environ(scratch, run, env), capture_output=True, text=True, diff --git a/tests/project/test_release_scripts_1967.py b/tests/project/test_release_scripts_1967.py index 8be384b48c..d63af7e7a1 100644 --- a/tests/project/test_release_scripts_1967.py +++ b/tests/project/test_release_scripts_1967.py @@ -6,8 +6,8 @@ commit" and of "Check which commit the version's tag names". * :class:`TestReleaseUpload` runs ``release_upload.py``, "Upload package to GitHub Release", against a stand-in ``gh``. -* :class:`TestTheFormerShellAgrees` runs the shell those steps held before the - scripts against the same stand-ins and requires the same status, standard +* :class:`TestTheFormerShellAgrees` runs those steps' inline shell form, the + reference the scripts must match, against the same stand-ins and requires the same status, standard output and error, ``$GITHUB_OUTPUT`` and calls. * :class:`TestTheStepsRunTheScripts` pins each step to its script, after a whole checkout of the repository, and the scripts to Python 3.8's syntax. @@ -92,7 +92,8 @@ ''' #: "Check the release tags name this run's commit" in ``github``, ``pypi`` and -#: ``conda``, as each held it before :file:`util/release_tags.py`. +#: ``conda``, in its inline shell form, the reference :file:`util/release_tags.py` +#: must match. FORMER_CHECK = textwrap.dedent(r''' set -euo pipefail @@ -125,8 +126,8 @@ echo "PCAPKIT_RELEASED_ELSEWHERE=${elsewhere}" >> "$GITHUB_OUTPUT" ''').lstrip() -#: "Upload package to GitHub Release" in ``pypi`` and ``conda``, before -#: :file:`util/release_upload.py`. +#: "Upload package to GitHub Release" in ``pypi`` and ``conda``, in its inline +#: shell form, the reference :file:`util/release_upload.py` must match. FORMER_UPLOAD = textwrap.dedent(r''' set -euo pipefail @@ -437,7 +438,7 @@ def test_it_takes_no_arguments(self) -> None: @unittest.skipIf(not BASH, 'needs bash, which ran the former shell') class TestTheFormerShellAgrees(_Stand): - """Each script exits, prints, writes and calls exactly as the shell it replaced, on every case.""" + """Each script exits, prints, writes and calls exactly as its step's inline shell form, on every case.""" def both(self, shell: 'str', command: 'list[str]', env: 'dict[str, str]', prepare: 'Callable[[], None]') -> 'tuple[tuple[object, ...], tuple[object, ...]]': @@ -472,7 +473,8 @@ def test_the_upload(self) -> None: self.assertEqual(new, old) def test_every_case_was_reached(self) -> None: - """The cases cover both exits of each script, so agreement is not agreement on one path.""" + """The check and upload cases end each shell in success, in refusal and with ``git``'s or ``gh``'s + own failing status, so agreement is not agreement on one path.""" statuses = set() for shell, cases in ((FORMER_CHECK, CHECK_CASES), (FORMER_UPLOAD, UPLOAD_CASES)): for case in cases: @@ -489,7 +491,7 @@ def test_every_case_was_reached(self) -> None: class TestTheStepsRunTheScripts(unittest.TestCase): - """Each step runs its script from a whole checkout, with the shell it had.""" + """Each step runs its script from a whole checkout.""" def test_each_step_runs_its_script(self) -> None: for (job, name), run in sorted(STEPS.items()): diff --git a/tests/project/test_release_status_scripts_1967.py b/tests/project/test_release_status_scripts_1967.py index 84cc64db78..2cd9bcc4d2 100644 --- a/tests/project/test_release_status_scripts_1967.py +++ b/tests/project/test_release_status_scripts_1967.py @@ -1,23 +1,23 @@ # -*- coding: utf-8 -*- """#1967: :file:`util/release_status.py`, and ``vendor_update``'s inline shell, run as Create Release runs them. -* :class:`TestTheFormerShellAgrees` runs the shell ``release_status``'s "Report - why this run did or did not release anything" held before the script, on the - cases below, and the script on the same, and requires the same status, +* :class:`TestTheFormerShellAgrees` runs ``release_status``'s "Report why this + run did or did not release anything" in its inline shell form, the reference + the script must match, on the cases below, and the script on the same, and requires the same status, standard output and error, ``$GITHUB_OUTPUT``, ``$GITHUB_STEP_SUMMARY`` and - ``gh`` calls. The former body is kept here verbatim, and the case table is - checked to reach every branch of it. The one sentence the shell never had, - the Anaconda remedy #2140 added, is dropped from the script's output first; + ``gh`` calls. That form is kept here verbatim, and the case table is + checked to reach every branch of it. The one sentence the shell form lacks, + the Anaconda remedy (#2140), is dropped from the script's output first; :mod:`tests.project.test_release_gates` pins it. * :class:`TestTheVendorUpdateStaysInline` pins ``vendor_update``'s "Read whether - its vendor-update job ran", which #1967 moved to a script and then put back - inline as too plain to need one, to its shell verbatim, with no checkout, and - runs that ``run:`` on its own case table, which reaches each of its branches. + its vendor-update job ran", inline as too plain to need a script (#1967), to + its shell verbatim, with no checkout, and runs that ``run:`` on its own case + table, which reaches each of its branches. * :class:`TestTheStepsRunTheScripts` pins ``release_status``'s step to its - script, after a checkout of :file:`util/` alone, and the script to the - standard library and Python 3.8's syntax. + script, after a checkout of :file:`util/` alone, and the script to + :file:`util/`, the standard library and Python 3.8's syntax. -The shell ran as a step with no ``shell:`` runs on the runner, ``bash -e``, and +The shell form runs as a step with no ``shell:`` runs on the runner, ``bash -e``, and ``bash`` prefixes each of its own errors with the script's path and line, which :meth:`_Stand.former` strips; the rest of standard error is compared whole. What the steps report, case by case, is @@ -48,7 +48,7 @@ if TYPE_CHECKING: from typing import Callable, Optional -#: Test tier (#1958): runs the tree's own scripts and the workflow's former shell. +#: Test tier (#1958): runs the tree's own scripts and the step's inline shell form. __tier__ = 'LATEST' ROOT = pathlib.Path(__file__).resolve().parents[2] @@ -74,8 +74,8 @@ cat "$GH_REPLY" ''' -#: "Report why this run did or did not release anything" in ``release_status``, as -#: it held before :file:`util/release_status.py`. +#: "Report why this run did or did not release anything" in ``release_status``, in +#: its inline shell form, the reference :file:`util/release_status.py` must match. FORMER_STATUS = textwrap.dedent(r''' set -euo pipefail @@ -213,7 +213,7 @@ ''').lstrip() #: "Read whether its vendor-update job ran" in ``vendor_update``: its ``run:``, -#: which a script held for a while (#1967). +#: inline rather than a script (#1967). VENDOR_SHELL = textwrap.dedent(r''' set -uo pipefail @@ -260,7 +260,7 @@ 'PCAPKIT_RELEASED_AT': RUN, 'PCAPKIT_RELEASED_ELSEWHERE': 'false', 'PCAPKIT_PYPI_KTLO_310': '', 'PCAPKIT_PYPI_KTLO_PYPY310': '', # The Vendor Update's own event. Only a manual one reads differently, a - # branch the former shell never had; test_build_only_dispatch.py pins it. + # branch the shell form lacks; test_build_only_dispatch.py pins it. 'PCAPKIT_WORKFLOW_RUN_EVENT': 'push', } #: Every publisher skipped, and each one's evidence. @@ -273,8 +273,8 @@ 'PCAPKIT_RELEASED_AT': OTHER} #: A value standing for a variable left unset. UNSET = None -#: The sentence #2140 appended to the unfinished warning where Anaconda files are -#: missing, which the former shell never printed. +#: The sentence ending the unfinished warning where Anaconda files are missing +#: (#2140), which the shell form never prints. ANACONDA_REMEDY = re.compile(r"(?m) Its Anaconda files are the exception: while main's version is still .*?, " r"dispatch Conda Update with publish on main, .*? only if that run's own conda legs " r'can still succeed\.$') @@ -346,7 +346,7 @@ ('the run sha unset where it is named', {**ELSEWHERE, **ALL_DONE, 'PCAPKIT_RUN_SHA': UNSET}), ) -#: Each way the former ``release_status`` shell ends: ``name -> (status, its last +#: Each way the ``release_status`` shell form ends: ``name -> (status, its last #: line of standard output, as a pattern)``. Every case matches one, and every one #: is matched. STATUS_BRANCHES = { @@ -517,7 +517,7 @@ def sampled_status_cases(count: 'int' = 150) -> 'list[tuple[str, dict[str, Optio @unittest.skipIf(not BASH, 'needs bash, which ran the former shell') class TestTheFormerShellAgrees(_Stand): - """Each script exits, prints, writes and calls exactly as the shell it replaced, on every case.""" + """Each script exits, prints, writes and calls exactly as its step's inline shell form, on every case.""" def status_env(self, overrides: 'dict[str, Optional[str]]') -> 'dict[str, str]': base = dict.fromkeys(status_env_names(), '') diff --git a/tests/project/test_release_tags_the_run_sha_1634.py b/tests/project/test_release_tags_the_run_sha_1634.py index e62dfcf45b..4dc7fdac69 100644 --- a/tests/project/test_release_tags_the_run_sha_1634.py +++ b/tests/project/test_release_tags_the_run_sha_1634.py @@ -297,7 +297,8 @@ def test_the_scan_found_every_checkout(self) -> None: self.assertEqual(found, dict.fromkeys(CHECKOUT_JOBS, 1)) def test_every_checkout_pins_the_run_sha(self) -> None: - """``tag``'s used no ``ref:``, ``pypi``'s ``v``, ``conda``'s ``conda-+0``.""" + """Guards against ``tag``'s with no ``ref:``, ``pypi``'s at ``v`` and ``conda``'s at + ``conda-+0``.""" for job, steps in sorted(self.checkouts().items()): for step in steps: with self.subTest(job=job): @@ -316,7 +317,7 @@ def releases(self) -> 'list[tuple[str, dict[str, Any]]]': if step.get('uses', '').startswith('softprops/action-gh-release@')] def test_only_github_runs_the_release_action(self) -> None: - """``pypi`` and ``conda`` used it to upload, which re-checked the target. + """Guards against ``pypi`` and ``conda`` uploading with it, which re-checks the target. ``action-gh-release`` 3.0.3 calls Update a release on an existing release before uploading anything (``release`` in ``src/github.ts``), passing @@ -324,8 +325,8 @@ def test_only_github_runs_the_release_action(self) -> None: GitHub's REST docs apply the workflow-file rule there to "the new value of target_commitish if you are changing it, otherwise the existing target". On ``workflow_run`` that target is ``github.sha``, so by the - docs a workflow change reaching ``main`` after ``github`` ran would have - failed these uploads after PyPI and anaconda.org had published. + docs a workflow change reaching ``main`` after ``github`` ran fails such + an upload after PyPI and anaconda.org have published. """ self.assertEqual([(job, label(step)) for job, step in self.releases()], [('github', 'Create Release')]) @@ -544,7 +545,7 @@ def test_the_conda_tag_names_the_run_commit(self) -> None: self.assertTrue(tagging[0].rstrip().endswith('"$run_commit"'), tagging[0]) def test_nothing_before_the_tag_reads_main(self) -> None: - """The defect was a fast-forward onto ``main`` *before* ``git tag`` ran.""" + """Guards against a fast-forward onto ``main`` *before* ``git tag`` runs (#1634).""" upto = self.index('tag', 'Create Tag') for step in self.steps['tag'][:upto + 1]: with self.subTest(step=label(step)): @@ -585,7 +586,7 @@ def test_the_check_is_one_script(self) -> None: self.assertEqual(len(set(scripts.values())), 1, 'the three copies have drifted apart') def test_the_check_blocks_every_leg(self) -> None: - """#1633's KTLO legs carry ``continue-on-error`` on every other step; this one must not. + """#1633's KTLO legs carry ``continue-on-error`` on every step after this one; this one must not. It fails alike on every leg, and with ``continue-on-error`` a KTLO leg would run its later steps and publish under a tag naming another commit. @@ -1184,9 +1185,9 @@ class TestTheCountsAreOfTheReleasedTiers(ScratchRepository): ``pre`` is tagged before the tier file existed, ``added`` adds it as #1677 did, and ``grown`` merges the first weekly proposal, pypy3.12; the run is on - ``grown``. Counting ``grown``'s tiers for a version tagged earlier read the - finished release as incomplete, ran the gate and ``pypi``, and failed the - tag check above on every run. + ``grown``. Guards against counting ``grown``'s tiers for a version tagged + earlier, which reads the finished release as incomplete, runs the gate and + ``pypi``, and fails the tag check above on every run. """ @@ -1299,7 +1300,8 @@ def test_a_tag_pushed_after_the_checkout_is_fetched(self) -> None: self.assertEqual(self.released(exists=True)[1], self.values[self.added]) def test_a_tag_older_than_the_tier_file_counts_the_tiers_it_was_added_with(self) -> None: - """As ``v1.5.0b12`` (``2025da5e5``) is: tagged before #1677 (``727c0fc60``) added the file.""" + """As ``v1.5.0b12`` (``2025da5e5``) is: tagged at a commit older than ``727c0fc60``, which added the + file (#1677).""" self.push(self.pre, f'refs/tags/v{self.VERSION}') self.checkout(self.grown) result, written = self.released(exists=True) @@ -1351,9 +1353,9 @@ class TestTheCountsAreOfTheReleasedPlatforms(ScratchRepository): ``pre`` is tagged before util/release_published.py existed, as every tag up to v1.5.0b14 is; ``added`` adds it with the platforms #1967 transcribed from the step's shell; ``windows`` adds win-64, as #2060 did; the run is on - ``windows``. Counting this run's platforms for a version tagged earlier read - v1.5.0b14 as lacking win-64, and Release status warned that it was unfinished - on every run. + ``windows``. Guards against counting this run's platforms for a version + tagged earlier, which reads v1.5.0b14 as lacking win-64 and has Release + status warn that it is unfinished on every run. """ @@ -1435,7 +1437,8 @@ def test_a_tag_before_win_64_does_not_need_it(self) -> None: self.assertEqual(self.count(True, 'linux-64 osx-arm64')[0], 'false') def test_a_tag_older_than_the_script_counts_the_platforms_it_was_added_with(self) -> None: - """As v1.5.0b14 (``6a9b666a1``) is: tagged before #1967 (``79934ddd9``) added the script.""" + """As v1.5.0b14 (``6a9b666a1``) is: tagged at a commit older than ``79934ddd9``, which added the + script (#1967).""" self.push(self.pre, f'refs/tags/v{self.VERSION}') self.checkout(self.windows) state, log = self.count(True, self.BEFORE) @@ -1492,9 +1495,9 @@ def test_the_apps_token_refused_names_its_permissions(self) -> None: #: A stand-in for ``gh``. ``$GH_ASSETS`` holds the release's assets as #: ``namestate`` lines; absent, there is no release, and ``release view`` #: fails as the real one does ("release not found", exit 1, measured with gh -#: 2.102.0). It answers the three ``--json``/``--jq`` pairs the workflows ask, -#: the bare-name one being the script's before ``state`` was read, and -#: ``isDraft`` from ``$GH_DRAFT``. ``release upload`` appends the file as +#: 2.102.0). It answers the ``--json``/``--jq`` pairs the workflows ask -- +#: :data:`ASSETS_JQ`, and ``isDraft`` from ``$GH_DRAFT`` -- and a bare-name +#: ``assets`` pair no workflow asks. ``release upload`` appends the file as #: ``uploaded`` and, like the real one, refuses a name already there unless #: ``--clobber`` deletes it first. ``$GH_FAIL`` fails any call. Anything else #: exits 96-99. @@ -1641,7 +1644,7 @@ def test_a_new_release_gets_every_file_in_order(self) -> None: self.assertEqual(self.calls(), self.expected_calls(*paths)) def test_a_pattern_matching_nothing_fails(self) -> None: - """A change from the action, which only warned: ``fail_on_unmatched_files`` + """Unlike the action, which only warns: ``fail_on_unmatched_files`` is unset here, so 3.0.3's ``run`` in ``src/run.ts`` logs "🤔 Pattern '…' does not match any files." and carries on.""" self.published() @@ -1651,7 +1654,7 @@ def test_a_pattern_matching_nothing_fails(self) -> None: self.assertEqual(self.calls(), self.expected_calls()) def test_a_missing_release_fails_before_any_upload(self) -> None: - """A change from the action, which made one: with no release for the tag, + """Unlike the action, which makes one: with no release for the tag, 3.0.3 calls ``createRelease`` (from ``release`` in ``src/github.ts``) with no notes and ``target_commitish`` unset (``parseConfig`` in ``src/util.ts``), which Create a release defaults to "the repository's default branch". Making diff --git a/tests/project/test_release_version_check_scripts_1967.py b/tests/project/test_release_version_check_scripts_1967.py index cc3b840f42..486fea5784 100644 --- a/tests/project/test_release_version_check_scripts_1967.py +++ b/tests/project/test_release_version_check_scripts_1967.py @@ -1,10 +1,11 @@ # -*- coding: utf-8 -*- """#1967: ``version_check``'s :file:`util/` scripts, run as Create Release runs them. -* :class:`TestTheFormerShellAgrees` runs the shell ``version_check``'s "Read - the released Python versions", "Check PyPI publish completeness" and "Check - Anaconda publish completeness" held before :file:`util/release_tiers.py` and - :file:`util/release_published.py`, on the cases below, and each script on the +* :class:`TestTheFormerShellAgrees` runs ``version_check``'s "Read the released + Python versions", "Check PyPI publish completeness" and "Check Anaconda + publish completeness" in their inline shell form, the reference + :file:`util/release_tiers.py` and :file:`util/release_published.py` must + match, on the cases below, and each script on the same, against stand-in ``python3``, ``git``, ``mktemp`` and ``curl``, and requires the same status, standard output and error, ``$GITHUB_OUTPUT``, calls and scratch files. The step bodies are kept here @@ -13,7 +14,7 @@ * :class:`TestAPlantedDivergenceIsCaught` changes each script in a copy, one plant at a time, and requires the cases to tell it from the shell. * :class:`TestTheStepsRunTheScripts` pins each step to its script, and the - scripts to the standard library and Python 3.8's syntax. + scripts to :file:`util/`, the standard library and Python 3.8's syntax. The steps run with no ``shell:``, so on the runner's ``bash -e``, and ``bash`` prefixes each of its own errors with the script's path and line, which @@ -44,7 +45,7 @@ if TYPE_CHECKING: from typing import Optional, Union -#: Test tier (#1958): runs the tree's own scripts and the workflow's former shell. +#: Test tier (#1958): runs the tree's own scripts and the steps' inline shell form. __tier__ = 'LATEST' ROOT = pathlib.Path(__file__).resolve().parents[2] @@ -99,7 +100,8 @@ def bash_version() -> 'str': #: A ``git`` answering as "Read the released Python versions" calls it, from #: the tags in ``$GIT_TAGS`` and ``$GIT_REMOTE_TAGS`` (`` `` lines), #: the ``:`` blobs in ``$GIT_FILES``, ``$GIT_LOG_REPLY``'s bytes, -#: and ``$GIT_HEAD``; each ``*_FAIL`` fails its command with that status. +#: and ``$GIT_HEAD``; ``$SHOW_FAIL`` fails ``show`` for a path ending in it, +#: and each other ``*_FAIL`` fails its command with that status. FAKE_GIT = r'''#!/bin/sh printf 'git %s\n' "$*" >> "$CALL_LOG" die() { [ "$1" != TERM ] || kill -TERM $$; exit "$1"; } @@ -168,12 +170,13 @@ def bash_version() -> 'str': [ -z "${CURL_FAIL:-}" ] || { [ "$CURL_FAIL" != TERM ] || kill -TERM $$; exit "$CURL_FAIL"; } ''' -#: What the stand-ins and the former shell run besides each other and bash's builtins. +#: What the stand-ins and the shell forms run besides each other and bash's builtins. TOOLS = ('awk', 'cat', 'cp', 'grep', 'mkdir', 'tail') #: Each stand-in, by name. FAKES = {'python3': FAKE_PYTHON3, 'git': FAKE_GIT, 'mktemp': FAKE_MKTEMP, 'curl': FAKE_CURL} -#: "Read the released Python versions", before :file:`util/release_tiers.py`. +#: "Read the released Python versions" in its inline shell form, the reference +#: :file:`util/release_tiers.py` must match. FORMER_TIERS = textwrap.dedent(r''' set -euo pipefail @@ -212,7 +215,8 @@ def bash_version() -> 'str': echo "conda-builds=${builds}" >> "$GITHUB_OUTPUT" ''').lstrip() -#: "Check PyPI publish completeness", before :file:`util/release_published.py`. +#: "Check PyPI publish completeness" in its inline shell form, the reference +#: :file:`util/release_published.py` must match. FORMER_PYPI = textwrap.dedent(r''' set -euo pipefail @@ -297,8 +301,8 @@ def bash_version() -> 'str': echo "PCAPKIT_CONDA_COMPLETE=$complete" >> "$GITHUB_OUTPUT" ''').lstrip() -#: Each step, its id, the ``run:`` it holds, and the former shell, the -#: ``/tmp`` file it wrote standing for where the cases write it. +#: Each step, its id, the ``run:`` it holds, and its inline shell form, the +#: ``/tmp`` file it writes standing for where the cases write it. STEPS = { 'Read the released Python versions': ('released', 'python3 util/release_tiers.py', FORMER_TIERS), 'Check PyPI publish completeness': ('check_pypi', 'python3 util/release_published.py pypi /tmp/pypi_release.json', @@ -434,7 +438,7 @@ def urls(*names: 'str') -> 'str': ) #: "Check Anaconda publish completeness"'s base, and its platforms. -#: #2060: untagged, as the former shell knew no tag; a tagged version's platforms are +#: #2060: untagged, as the shell form knows no tag; a tagged version's platforms are #: test_release_tags_the_run_sha_1634.TestTheCountsAreOfTheReleasedPlatforms'. ANACONDA_BASE = {'PCAPKIT_VERSION': '1.5.0', 'PCAPKIT_CONDA_BUILDS': 'py311 py312', 'PCAPKIT_TAG_EXISTS': 'false'} PLATFORMS = ('linux-64', 'osx-64', 'osx-arm64', 'win-64') @@ -629,7 +633,7 @@ def unbound(state: 'tuple[object, ...]') -> 'bool': #: other signals, which the script does not copy; the rest is compared. SIGNALLED = frozenset({'the fetch was killed'}) -#: Each way each former shell ends, as a predicate on its state; every case +#: Each way each shell form ends, as a predicate on its state; every case #: meets exactly one. EXITS = { 'Read the released Python versions': { @@ -650,7 +654,7 @@ def unbound(state: 'tuple[object, ...]') -> 'bool': 'complete': lambda s: s[0] == 0 and written(s).endswith('_COMPLETE=true\n'), } -#: Each path through each former shell short of its end; every one is taken. +#: Each path through each shell form short of its end; every one is taken. PATHS = { 'Read the released Python versions': { "this run's tiers": lambda s: s[4] is None, @@ -682,7 +686,7 @@ def unbound(state: 'tuple[object, ...]') -> 'bool': @unittest.skipIf(not BASH, 'needs bash, which ran the former shell') class TestTheFormerShellAgrees(_Stand): - """Each script exits, prints, writes and calls exactly as the shell it replaced, on every case.""" + """Each script exits, prints, writes and calls exactly as its step's inline shell form, on every case.""" def compare(self, step: 'str') -> 'None': _, run, former = STEPS[step] @@ -761,7 +765,7 @@ def test_every_anaconda_branch_is_reached(self) -> None: @unittest.skipIf(not BASH, 'needs bash, which ran the former shell') class TestAPlantedDivergenceIsCaught(_Stand): - """The cases tell each plant from the former shell, so their agreement means something.""" + """The cases tell each plant from the shell form, so their agreement means something.""" def test_each_plant_applies_once(self) -> None: for step, name, old, _ in PLANTS: @@ -787,7 +791,7 @@ def test_each_plant_is_caught(self) -> None: class TestTheStepsRunTheScripts(unittest.TestCase): - """Each step runs its script, after a whole checkout and setup-python, with its ``env:`` and ``id`` unchanged.""" + """Each step runs its script under its own ``id``, after a whole checkout and setup-python.""" def test_each_step_runs_its_script(self) -> None: found = steps(WORKFLOW, 'version_check') @@ -800,7 +804,7 @@ def test_each_step_runs_its_script(self) -> None: self.assertFalse(set(step) - {'name', 'id', 'env', 'run'}) def test_the_scripts_run_after_the_checkout_and_setup_python(self) -> None: - """``python3`` is setup-python's, as each step's was, and the checkout holds :file:`util/` and its history.""" + """``python3`` is setup-python's, and the checkout holds :file:`util/` and its history.""" found = steps(WORKFLOW, 'version_check') uses = [str(step.get('uses', '')) for step in found] checkout = next(index for index, use in enumerate(uses) if use.startswith('actions/checkout@')) diff --git a/tests/project/test_resource_monitor_1740.py b/tests/project/test_resource_monitor_1740.py index d05554e2c1..1721fae1d2 100644 --- a/tests/project/test_resource_monitor_1740.py +++ b/tests/project/test_resource_monitor_1740.py @@ -51,7 +51,7 @@ PROBES = ('free -m', 'df -h / "$GITHUB_WORKSPACE"', 'cat /proc/loadavg', 'ps -eo pid,rss,vsz,etime,args --sort=-rss | head -6') #: The experimental job's caps and its check name, which the monitor leaves alone. -#: #1938 adds the watcher's interpreter and its requests, capped as the rest are. +#: The watcher's interpreter and its requests (#1938) are capped as the rest are. CAPS = {'python': 5, 'install': 10, 'watcher-python': 1, 'watcher': 1, 'tests': 120} CHECK_NAME = 'Python ${{ matrix.python-version }} (experimental)' #: A sample's first line, ``[monitor] sample ``. @@ -146,7 +146,7 @@ def gnu_timeout(env: 'dict[str, str]') -> 'str | None': @unittest.skipUnless(sys.platform.startswith('linux') and all(map(shutil.which, ('bash', 'free', 'tail', 'ps'))), 'the sampler reads /proc and procps, as on the Ubuntu runners') class TestTheSamplesReachTheTestsLog(unittest.TestCase): - """The three scripts, run in order, with ``sleep 30`` made 0.2 s and pytest a stub.""" + """The start, tests and stop scripts, run in order, with ``sleep 30`` made 0.2 s and pytest a stub.""" def setUp(self) -> None: tmp = tempfile.TemporaryDirectory(prefix='pcapkit-1740-') # pylint: disable=consider-using-with @@ -237,8 +237,8 @@ def test_the_stop_step_passes_on_a_leg_the_monitor_never_started(self) -> None: self.assertEqual(stop.stdout, 'The resource monitor did not start on this leg.\n') -#: The containment, as the issue's research sized it (estimates); #1753 -#: adds the exit on a spent heap, which +#: The containment, as the issue's research sized it (estimates); the exit +#: on a spent heap is #1753's, which #: tests/project/test_graalpy_worker_heap_1753.py pins. HEAP, GRAAL_WORKERS, SCOPE_MEMORY = '--vm.Xmx2g --vm.XX:+ExitOnOutOfMemoryError', '3', '12G' #: A ``LATEST``-tier module, as ``List the LATEST tier`` would hand the tests step (#1958): diff --git a/tests/project/test_rfc_anchor_fragments.py b/tests/project/test_rfc_anchor_fragments.py index 64b185f4f1..f6b601a8bf 100644 --- a/tests/project/test_rfc_anchor_fragments.py +++ b/tests/project/test_rfc_anchor_fragments.py @@ -61,7 +61,7 @@ from a citation that dropped its number, the same incompleteness this test exists to catch -- Sphinx happening not to error on it is not the same claim as it being a well-formed citation. Neither shape appears under :mod:`pcapkit`, -so this changes nothing here; rejecting is the more conservative +so rejecting them flags nothing here, and is the more conservative default should either turn up later. One thing the shape check above does not catch: whether the anchor actually @@ -70,11 +70,11 @@ (RFC 959's own rendered HTML carries only ``section-1`` through ``section-8``; verified by reading both ``https://www.rfc-editor.org/rfc/rfc959.html`` and the datatracker mirror). The maintainer ruled on #944 for the enclosing -top-level section, so those citations became ``:rfc:`959#section-4``` -- +top-level section, so those citations cite ``:rfc:`959#section-4``` -- which :data:`ACCEPTED_FRAGMENT` above already accepts as well-formed. More sites carried it than #944 reported: that issue grepped -:mod:`pcapkit` only, and a cross-review of the fix found further rendered +:mod:`pcapkit` only, and #944's cross-review found further rendered roles in :file:`docs/source/contributing/conventions/registry-protocol.rst` and one in :file:`tests/const/test_const_enum_no_mint.py`. Others cited ``959#section-5.3``, equally dead, in @@ -118,7 +118,7 @@ #: excludes the untitled/numberless shapes (``introduction``, bare ``section``) #: that same function also does not error on -- see the module docstring #: above for why those stay rejected. No bare ``N[.N...]`` alternative either -#: -- see the module docstring for why that shape was dropped rather than kept. +#: -- see the module docstring for why that shape is rejected. ACCEPTED_FRAGMENT = re.compile( r'\A(?:section-\d+(?:\.\d+)*|appendix-[A-Z](?:\.\d+)*|page-\d+)\Z') @@ -313,7 +313,7 @@ def test_no_malformed_fragments(self) -> 'None': Catches the *next* ``secion``, not just #942's: any freshly introduced typo of ``section-``/``appendix-``/``page-``, or any bare-number fragment like those #942's sweep found and fixed, shows up as a - finding and fails this test -- there is no allowlist left for one to + finding and fails this test -- there is no allowlist for one to hide behind. Shape only, not anchor existence -- see #944. """ @@ -373,8 +373,8 @@ def test_the_documentation_exemption_holds_on_every_known_mechanism(self) -> 'No ('a live role after a literal whose content ends in a double quote', 'See ``f"``%s`` dead.', True), # The real one: ``'base'`` is an inline literal in - # pcapkit/const/ftp/command.py, and 764 literals across 188 files end in - # one of those five characters, so this shape is one adjacency away. + # pcapkit/const/ftp/command.py, and hundreds of literals across the scanned + # roots end in one of those characters, so this shape is one adjacency away. ("a live role after this repository's own quoted literal", "See ``'base'``%s`` dead.", True), # Pins the suffix half of the (prefix, suffix) pair. Deleting the suffix diff --git a/tests/project/test_rfc_constants_one_home_1739_unit.py b/tests/project/test_rfc_constants_one_home_1739_unit.py index 8bd80bc72b..07b0f76014 100644 --- a/tests/project/test_rfc_constants_one_home_1739_unit.py +++ b/tests/project/test_rfc_constants_one_home_1739_unit.py @@ -5,7 +5,7 @@ RFC (or the file format) fixes, and that more than one module spelled out, is defined once, in the module that owns the protocol -- or that protocol's schema or :mod:`pcapkit.const` module -- and every reader imports it from there. Such a -constant is never configurable, and the old homes do not re-export it. +constant is never configurable, and no other module re-exports it. These checks pin that: @@ -77,8 +77,8 @@ }), ) -#: Names a module other than their home must not define, mapped to the module -#: that once held them (#1739). ``MAGIC_NUMBER`` stays a class attribute of the PCAP +#: Names a module other than their home must not define, each with a module +#: that must not define it (#1739). ``MAGIC_NUMBER`` stays a class attribute of the PCAP #: and PCAP-NG engines -- the extractor dispatches on it -- but is built from #: the home rather than spelled out, which :class:`TestOneDefinition` checks #: through the literals. @@ -670,7 +670,7 @@ def test_attribute_readers(self) -> 'None': self.assertTrue(loads, f'{rel} never reads {module}.{name}') def test_pypcapfile_binds_none(self) -> 'None': - # The names #1739 moved out of there: reading them through + # The homed names it reads (#1739): reading them through # ``pcapkit.toolkit.pypcapfile`` would be a re-export, which # ruling 1 on #1726 rules out, so the module reaches them as # module attributes. diff --git a/tests/project/test_secret_environments_1968.py b/tests/project/test_secret_environments_1968.py index a90e6fedc3..582ef543b8 100644 --- a/tests/project/test_secret_environments_1968.py +++ b/tests/project/test_secret_environments_1968.py @@ -5,7 +5,8 @@ PAT and ``PYPCAPKIT_ACTIONS_DEPLOY_KEY`` in ``conda-tag``, rather than at repository level. GitHub hands an environment's secrets only to a job that names it, so a job reading one without the matching ``environment:`` gets an -empty string -- and the scheduled jobs in Conda Update and Vendor Update did. +empty string -- the shape #1968 found in the scheduled jobs of Conda Update and +Vendor Update. The scan covers every workflow, so a new job reading one of these secrets is held to the same rule. YAML drops comments, so a secret named only in a comment diff --git a/tests/project/test_sentinels_doc_page_934_unit.py b/tests/project/test_sentinels_doc_page_934_unit.py index 0732b2f331..9be3aad378 100644 --- a/tests/project/test_sentinels_doc_page_934_unit.py +++ b/tests/project/test_sentinels_doc_page_934_unit.py @@ -68,7 +68,7 @@ def _toctree_entries(text: 'str') -> 'list[str]': class SentinelsAPIPageTests(unittest.TestCase): - """The page GitHub issue #934 added, and its registration in the toctree.""" + """The API page GitHub issue #934 asks for, and its registration in the toctree.""" def test_page_exists(self) -> 'None': """The API page itself is present on disk.""" @@ -101,7 +101,7 @@ def test_page_is_registered_in_the_corekit_toctree(self) -> 'None': inventory does not care whether a page is reachable from a toctree -- but it produces a distinct "document isn't included in any toctree" warning, which is a different defect from the one GitHub issue #934 - reports. Pinning both closes the two ways this fix can go wrong. + reports. Pinning both closes both ways the page can fail. """ entries = _toctree_entries(INDEX.read_text(encoding='utf-8')) diff --git a/tests/project/test_setuptools_floor_2062.py b/tests/project/test_setuptools_floor_2062.py index 89f19d1ef8..860ed5858a 100644 --- a/tests/project/test_setuptools_floor_2062.py +++ b/tests/project/test_setuptools_floor_2062.py @@ -11,7 +11,7 @@ * every setuptools pin in :file:`conda/pypcapkit/meta.yaml` matches that floor. These read the declarations, not a built wheel: they catch the floor being -lowered again, not a later setuptools release dropping the behaviour. +lowered, not a later setuptools release dropping the behaviour. """ diff --git a/tests/project/test_stdlib_feature_probes_1709_unit.py b/tests/project/test_stdlib_feature_probes_1709_unit.py index d0f32a8684..d4f15220eb 100644 --- a/tests/project/test_stdlib_feature_probes_1709_unit.py +++ b/tests/project/test_stdlib_feature_probes_1709_unit.py @@ -118,7 +118,7 @@ def test_every_exemption_still_matches(self) -> None: self.assertIn(call, [found for (_, found) in probes(source)]) def test_the_issue_1709_shapes_are_matched(self) -> None: - """The scan sees each shape issue #1709 removed, so a pass is not vacuous.""" + """The scan sees each shape issue #1709 is about, so a pass is not vacuous.""" source = ( 'import hashlib\n' 'import inspect\n' diff --git a/tests/project/test_superseded_runs_1938.py b/tests/project/test_superseded_runs_1938.py index e0a74eb0a7..869389cdea 100644 --- a/tests/project/test_superseded_runs_1938.py +++ b/tests/project/test_superseded_runs_1938.py @@ -2,11 +2,11 @@ """#1938: Experimental Tests skips or stops a superseded run instead of cancelling it. GitHub counts a ``cancelled`` check against its commit in the status rollup, so -#1903's ``cancel-in-progress`` marked every superseded commit red, and a merged -pull request's last run, which nothing supersedes, kept its runner to the end. -The owner chose both remedies together: a gate job that skips the rest of a run -whose commit is no longer current, and a watcher that stops the tests once it -stops being current, each ending green. +``cancel-in-progress`` (#1903) marks every superseded commit red, and it never +reaches a merged pull request's last run, which nothing supersedes and which +keeps its runner to the end. The owner chose both remedies together: a gate job +that skips the rest of a run whose commit is no longer current, and a watcher +that stops the tests once it stops being current, each ending green. * :class:`TestNothingIsCancelled` and :class:`TestTheGate` read experimental.yml. * :class:`TestCheck` drives util/superseded.py's rule through a fake transport. diff --git a/tests/project/test_superseded_summary_copy_2078.py b/tests/project/test_superseded_summary_copy_2078.py index a37dae7ba0..8bbd3471cf 100644 --- a/tests/project/test_superseded_summary_copy_2078.py +++ b/tests/project/test_superseded_summary_copy_2078.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """#2078: a stopped experimental leg's summary line reaches the uploaded job summary. -Experimental Tests run 39898485708 uploaded no ``job-summary-*`` artifact: its -legs were stopped as superseded, and the one line their job summaries held was -util/superseded.py's, which went to ``$GITHUB_STEP_SUMMARY`` alone, so -``job-summary.md`` was never written and ``Upload the job summary`` found nothing. -The watcher takes the copy's path as ``--summary-copy``, which ``Run unit +Without the copy, a leg stopped as superseded uploads no ``job-summary-*`` +artifact, as Experimental Tests run 39898485708 measured: the one line its job +summary holds is util/superseded.py's, which goes to ``$GITHUB_STEP_SUMMARY`` +alone, so ``job-summary.md`` is never written and ``Upload the job summary`` +finds nothing. The watcher takes the copy's path as ``--summary-copy``, which ``Run unit tests`` passes it, and the tests it runs never see. * :class:`TestTheSteps` runs ``Run unit tests`` and ``Name the job summary`` as diff --git a/tests/project/test_sweeps_on_latest_only_1732.py b/tests/project/test_sweeps_on_latest_only_1732.py index e9301f985b..cf9eb05f53 100644 --- a/tests/project/test_sweeps_on_latest_only_1732.py +++ b/tests/project/test_sweeps_on_latest_only_1732.py @@ -68,7 +68,7 @@ REPORTERS = ('ci-reports.yml',) #: Each job's ``timeout-minutes``, and its capped steps', by ``id``: ``test``'s are #: main's, ``experimental``'s are sized for GraalPy (88-113 minutes, estimate). -#: #1938 adds the watcher's interpreter and its requests, capped too. +#: The watcher's interpreter and its requests are capped too (#1938). CAPS = {JOB: (45, {}), EXPERIMENTAL: (140, {'python': 5, 'install': 10, 'watcher-python': 1, 'watcher': 1, 'tests': 120})} #: The jobs whose result reaches a gate: ``required-checks`` and what it needs, and #: ``unittest-ordering``, which runs on main's pushes and gates publishing. @@ -477,8 +477,8 @@ def classify(spec: 'dict[str, Any]') -> 'str': return next(entry['run'] for entry in spec['jobs']['changes']['steps'] if entry.get('id') == 'classify') self.assertEqual(classify(self.own), classify(self.unit)) spec = self.own['jobs']['experimental'] - # #1938 adds `current`, the gate every job needs, and `!cancelled()`, so the two results stand in - # for the implicit `success()`. + # `current` is the gate every job needs (#1938), and `!cancelled()` drops the implicit `success()`, + # so the two results stand in for it. self.assertEqual(spec['needs'], ['current', 'changes', 'python-versions']) self.assertEqual(spec['if'], "${{ !cancelled() && needs.changes.result == 'success' && " diff --git a/tests/project/test_unit_tests_queue.py b/tests/project/test_unit_tests_queue.py index 3f3ae27c25..c53f860473 100644 --- a/tests/project/test_unit_tests_queue.py +++ b/tests/project/test_unit_tests_queue.py @@ -26,8 +26,8 @@ * Every test outside :file:`tests/project` that reads ``docs/`` or Markdown is run by ``project-tests``, so a docs-only PR cannot break it unseen. -The workflow is sliced as text, not parsed with :mod:`yaml`, which is in no -extra of :file:`pyproject.toml`; the same split +The workflow is sliced as text, not parsed with :mod:`yaml`, which comes only +with the ``test`` extra of :file:`pyproject.toml`; the same split :file:`tests/project/test_workflow_apt_timeouts.py` uses. """ @@ -357,7 +357,7 @@ def test_unittest_ordering_queues_behind_test_and_survives_its_failure(self) -> " && github.event_name != 'pull_request' }}\n", head) def test_the_ktlo_cell_runs_on_every_event(self) -> None: - """#1633: the KTLO cell runs on every event; #1538's main-only cut was for a prerelease.""" + """#1633: the KTLO cell runs on every event.""" ktlo = step(job('engine-tests'), 'Run the KTLO cells (non-blocking)') self.assertNotRegex(ktlo, r'(?m)^ if:') @@ -730,7 +730,7 @@ class TestDocsOnlyPathRunsEveryDocsReader(unittest.TestCase): """A docs-only PR must still run every test that reads what it changed.""" def listed(self) -> 'set[str]': - """The modules project-tests' second run step names.""" + """The modules project-tests' ``Run the other tests that read docs/ or Markdown`` step names.""" script = step(job('project-tests'), 'Run the other tests that read docs/ or Markdown') command = re.search(r'(?m)^ run: python util/cap_file_size.py python -m unittest -v (.+)$', script) self.assertIsNotNone(command) @@ -748,7 +748,7 @@ def test_every_listed_module_exists(self) -> None: self.assertTrue((ROOT / (name.replace('.', '/') + '.py')).is_file()) def test_the_scan_is_not_blind(self) -> None: - """The one known reader is found, so a passing run is not vacuous.""" + """A known reader is found, so a passing run is not vacuous.""" self.assertIn('tests.corekit.test_sentinel_exports_unit', docs_readers()) diff --git a/tests/project/test_unit_tests_shellcheck_1967.py b/tests/project/test_unit_tests_shellcheck_1967.py index 39fd508f6b..ece0ffc4db 100644 --- a/tests/project/test_unit_tests_shellcheck_1967.py +++ b/tests/project/test_unit_tests_shellcheck_1967.py @@ -2,8 +2,8 @@ """#1967: every ``run:`` block of unit-tests.yml passes shellcheck. Lint's ``actionlint`` step runs shellcheck over every workflow's ``run:`` blocks, -and a finding fails Lint. Step 5 of #1967 fixed unit-tests.yml's findings at the -root, and this test keeps it clean without waiting for a Lint run: it checks each +and a finding fails Lint. This test keeps unit-tests.yml free of findings (#1967, +step 5) without waiting for a Lint run: it checks each ``run:`` block of a ``bash`` or ``sh`` step as actionlint does: each ``${{ ... }}`` expression replaced by underscores of the same length, the shell's ``set -e`` line above it, and the checks actionlint disables left out. @@ -77,7 +77,7 @@ def test_every_run_block_is_clean(self) -> None: self.assertEqual(findings(shell, script), []) def test_a_finding_is_reported(self) -> None: - """The check itself sees the finding #1967 fixed, an unquoted selection.""" + """The check itself sees an unquoted selection, the finding it guards against (#1967).""" self.assertTrue(findings('bash', 'selection=$(echo a b)\npytest $selection\n')) diff --git a/tests/project/test_unittest_ordering_shards.py b/tests/project/test_unittest_ordering_shards.py index 938d8ae0b7..57d76e880f 100644 --- a/tests/project/test_unittest_ordering_shards.py +++ b/tests/project/test_unittest_ordering_shards.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """Tests that the ``unittest-ordering`` matrix shards ``protocols`` and ``project`` without losing a module. -#1029: the ``protocols`` cell of ``unittest-ordering`` measured 30.4 minutes -against the job's ``timeout-minutes: 45`` while every sibling leg stays under -about 6.2 minutes, so the cell was split into sub-legs. Splitting has one way +#1029: unsharded, the ``protocols`` cell of ``unittest-ordering`` measured 30.4 +minutes against the job's ``timeout-minutes: 45`` while every sibling leg stayed +under about 6.2 minutes, so the cell is split into sub-legs. Splitting has one way to go quietly wrong, which is what this module pins: a module that falls between two shards is never run by that job again, and nothing fails to say so -- the job stays green while covering less. The assertions are therefore over the @@ -21,7 +21,7 @@ ``--exclude`` or boundary, i.e. runs that whole directory; * every ``tests/`` subdirectory holding a test module is a leg, except the :data:`NOT_LEGS` names, each left out for the reason the workflow comment - gives (#2115: ``corekit`` was missing, and nothing said so). + gives (#2115: a directory such as ``corekit`` left out, with nothing to say so). The workflow is read with a small hand-rolled scan rather than :mod:`yaml`, which comes only with the ``test`` extra of :file:`pyproject.toml`; the same split diff --git a/tests/project/test_utilities_import_boundary_1735_unit.py b/tests/project/test_utilities_import_boundary_1735_unit.py index b2c2638df0..803f059817 100644 --- a/tests/project/test_utilities_import_boundary_1735_unit.py +++ b/tests/project/test_utilities_import_boundary_1735_unit.py @@ -5,9 +5,9 @@ :mod:`pcapkit.corekit` a module belongs in: ``utilities`` holds cross-cutting services, and a service imports nothing from :mod:`pcapkit` outside ``utilities`` -- not at top level, not inside a function, and not under -``TYPE_CHECKING``. ``pcapkit.utilities.decorators`` broke that, reaching -:mod:`pcapkit.protocols` both under ``TYPE_CHECKING`` and lazily inside -``beholder``, and #1735 moved it to :mod:`pcapkit.corekit.decorators`. +``TYPE_CHECKING``. So the decorators, which reach :mod:`pcapkit.protocols` both +under ``TYPE_CHECKING`` and lazily inside ``beholder``, belong in +:mod:`pcapkit.corekit.decorators` (#1735), not in ``pcapkit.utilities``. The check reads each module with :mod:`ast` rather than importing it, so a lazy import is seen whether or not anything ever calls the function that holds it. diff --git a/tests/project/test_vendor_comment_only_no_bump_1674.py b/tests/project/test_vendor_comment_only_no_bump_1674.py index 7cc3267d2b..ed4d3b1a54 100644 --- a/tests/project/test_vendor_comment_only_no_bump_1674.py +++ b/tests/project/test_vendor_comment_only_no_bump_1674.py @@ -2,8 +2,8 @@ """#1674: a regeneration that changes only comments or docstrings is committed without a version bump. -Vendor Update bumped on any changed file, so 1.5.0b9, b10 and b11 were cut for -comment-only regenerations. The owner chose to commit such a regeneration +Bumping on any changed file cuts a beta for a comment-only regeneration, as +1.5.0b9, b10 and b11 were cut. The owner chose to commit such a regeneration without the bump, so that documentation still reaches ``main`` and only a code change cuts a beta. diff --git a/tests/project/test_vendor_commits_new_modules_1685.py b/tests/project/test_vendor_commits_new_modules_1685.py index 3e410de7a6..d5b33158af 100644 --- a/tests/project/test_vendor_commits_new_modules_1685.py +++ b/tests/project/test_vendor_commits_new_modules_1685.py @@ -1,19 +1,20 @@ # -*- coding: utf-8 -*- """#1685: Vendor Update's bump commit carries the modules a crawl adds. -``Commit changes`` in :file:`.github/workflows/cron-vendor.yml` ran ``git commit --am``, which stages modified and deleted tracked files and never an untracked -one. ``Verify Changed files`` reports untracked files too (the ``??`` entries of -``git status -u --porcelain``), so a crawl that generated a module for a new -registry bumped the version and left the module out of the commit. Every later -crawl found it untracked again, and bumped again. +Guards against ``Commit changes`` in :file:`.github/workflows/cron-vendor.yml` +staging with ``git commit -am`` alone, which stages modified and deleted tracked +files and never an untracked one. ``Verify Changed files`` reports untracked +files too (the ``??`` entries of ``git status -u --porcelain``), so without the +fix a crawl that generates a module for a new registry bumps the version and +leaves the module out of the commit, and every later crawl finds it untracked +again, and bumps again. :class:`TestTheBumpCommitStep` runs that step's script, and ``Verify Changed files``', in a throwaway repository the way the runner runs them, with ``${{ }}`` substituted first. The second test classifies a new module with :file:`util/vendor_code_changed.py` and walks the -steps, which shows ``Commit documentation changes``, still ``-a`` alone, never -sees one. The harness is #1674's, from +steps, which shows ``Commit documentation changes``, which stages with ``-a`` +alone, never sees one. The harness is #1674's, from :mod:`tests.project.test_vendor_comment_only_no_bump_1674`. """ @@ -80,10 +81,10 @@ def setUp(self) -> None: self.base = self.git('rev-parse', 'HEAD') def v17(self) -> 'list[str]': - """What ``tj-actions/verify-changed-files@v17``, which ``Verify Changed - files`` replaced (#1968), reported: its ``entrypoint.sh`` with no ``files`` - pattern, so ``git diff --diff-filter=ACMUXTR``, the ``??`` lines of ``git - status -u --porcelain`` and ``git ls-files --deleted``, de-duplicated. + """What ``tj-actions/verify-changed-files@v17``, whose report ``Verify + Changed files`` reproduces (#1968), reports: its ``entrypoint.sh`` with no + ``files`` pattern, so ``git diff --diff-filter=ACMUXTR``, the ``??`` lines of + ``git status -u --porcelain`` and ``git ls-files --deleted``, de-duplicated. """ tracked = self.git('diff', '--diff-filter=ACMUXTR', '--name-only').splitlines() diff --git a/tests/project/test_vendor_test_gate_1853.py b/tests/project/test_vendor_test_gate_1853.py index 4bde037383..70831a59c7 100644 --- a/tests/project/test_vendor_test_gate_1853.py +++ b/tests/project/test_vendor_test_gate_1853.py @@ -1,9 +1,9 @@ # -*- coding: utf-8 -*- """#1853: Vendor Update tests the regenerated tree before it commits. -:file:`.github/workflows/cron-vendor.yml` committed whatever ``pcapkit-vendor`` -regenerated with no test step in between, and 1.5.0b13 turned ``main`` red. The -owner chose to test the regenerated tree in the job itself, under #872's rule: a +With no test step between the crawl and the commit, +:file:`.github/workflows/cron-vendor.yml` commits whatever ``pcapkit-vendor`` +regenerates, as it did for 1.5.0b13, which turned ``main`` red. The owner chose to test the regenerated tree in the job itself, under #872's rule: a failed sub-vendor's changes are discarded, the rest are kept, and the job as a whole does not fail. diff --git a/tests/project/test_whole_suite_on_latest_1958.py b/tests/project/test_whole_suite_on_latest_1958.py index fe89416219..bd18bdd155 100644 --- a/tests/project/test_whole_suite_on_latest_1958.py +++ b/tests/project/test_whole_suite_on_latest_1958.py @@ -46,7 +46,7 @@ #: ``latest`` cannot install (PCAP_CT needs a system libpcap; the PyPCAP and #: PyPCAPFile markers stop below 3.12). ``engine-tests`` and ``pypcap-parity`` run them. STILL_DARK = frozenset({'HAS_MYPY', 'HAS_PYLINT', 'HAS_PCAP_CT', 'HAS_PYPCAP', 'HAS_PYPCAPFILE'}) -#: A test the audit found running nowhere, or in ``gate`` alone, per flag. +#: Per flag, a test that runs nowhere, or in ``gate`` alone, without the ``latest`` install. DARK_TESTS = { 'HAS_RUNTIME': 'tests/foundation/engines/test_runtime_engines.py', 'HAS_SCAPY': 'tests/interface/test_core.py', diff --git a/tests/project/test_workflow_apt_cache.py b/tests/project/test_workflow_apt_cache.py index 88db30ea2b..edfe54ef9b 100644 --- a/tests/project/test_workflow_apt_cache.py +++ b/tests/project/test_workflow_apt_cache.py @@ -2,9 +2,9 @@ """Tests for the apt ``.deb`` cache and fallback mirror in :file:`unit-tests.yml` (#1432). #1432: ``azure.archive.ubuntu.com`` served ~75 kB/s, so ``timeout 180`` killed -both tshark install attempts, three times in one day. The fix wraps the install -step (pinned by :class:`tests.project.test_unit_tests_queue.TestAptRetry`) with -a plan step that computes a cache key, ``actions/cache`` +both tshark install attempts, three times in one day. So the install step +(pinned by :class:`tests.project.test_unit_tests_queue.TestAptRetry`) is wrapped +with a plan step that computes a cache key, ``actions/cache`` restore/seed/collect/save steps, and a retry that drops azure from the runner's ``mirror+file:`` list. @@ -19,8 +19,8 @@ install step installs, for every engine in the matrix, since both read it; * the key is ``apt-debs----`` of the sorted ``Inst`` lines, and ``restore-keys`` is that same-image, same-set prefix only; -* only one job per key saves, so ``pypcap-parity``'s two cells do not race to - reserve the same entry. +* only one job per key saves, so the parity cells of ``pypcap-parity`` and + ``pypcap-parity-ktlo`` do not race to reserve the same entry. """ diff --git a/tests/project/test_workflow_apt_timeouts.py b/tests/project/test_workflow_apt_timeouts.py index caf8ee8754..5335ba7624 100644 --- a/tests/project/test_workflow_apt_timeouts.py +++ b/tests/project/test_workflow_apt_timeouts.py @@ -9,10 +9,10 @@ budget. It happened twice inside forty minutes. What makes that worth a test rather than only a fix is that the same hazard -existed in a *second* step nobody was looking at. ``pypcap-parity`` installs the -union of the packages ``engine-tests`` installs, from the same mirrors, and had -no bound either -- it simply had not been the leg that stalled yet. Fixing only -the step that happened to bite is the recurrence this module exists to prevent: +sits in every step that runs apt, not only in the one that stalled: +``pypcap-parity`` installs the union of the packages ``engine-tests`` installs, +from the same mirrors. Bounding only the step that happened to bite is the +recurrence this module exists to prevent: nothing about an unbounded ``apt-get`` warns, and the failure is invisible until a mirror misbehaves on whichever step was left out. @@ -30,7 +30,7 @@ Two things this deliberately does **not** test: -* **That the number is well chosen.** Whether 15 minutes is the right bound +* **That the number is well chosen.** Whether a step's bound is the right one depends on how long the step legitimately takes, which is a property of Ubuntu's mirrors on the day and is not observable from the file. The reasoning and the measurements behind the current value live in the workflow's own @@ -52,7 +52,7 @@ does. Comments are stripped before the ``apt-get`` marker is looked for. Without that, -prose *about* apt -- including the long note #974 added above the very step this +prose *about* apt -- including the long #974 note above the very step this module guards -- would register as an invocation, so a step could be flagged for mentioning apt and, worse, a commented-out invocation could be credited as a live one. :func:`tests._dependency_gates._engine_matrix_variants` strips diff --git a/tests/project/test_workflow_hardening_1968.py b/tests/project/test_workflow_hardening_1968.py index be4079e658..2a612297b8 100644 --- a/tests/project/test_workflow_hardening_1968.py +++ b/tests/project/test_workflow_hardening_1968.py @@ -4,10 +4,11 @@ * :class:`TestNoThirdPartyCodeAfterTheDeployKey`: ``webfactory/ssh-agent`` exports the deploy key's agent socket to every later step of its job, so no later step may run an action from outside ``actions/``, ``github/`` or this - repository. ``tj-actions/verify-changed-files`` did, in Vendor Update and Conda - Update. + repository, as ``tj-actions/verify-changed-files`` would in Vendor Update and + Conda Update. * :class:`TestTheChangedFilesStep` runs the ``Verify Changed files`` script that - replaced it, in a throwaway repository, on paths a crawl could write. + stands in for that action, in a throwaway repository, on paths a crawl could + write. * :class:`TestEveryWorkflowDeclaresPermissions`: a workflow without top-level ``permissions:`` hands the repository default to any job without its own. GitHub Pages builds a pull request's tree, so that build holds ``contents: @@ -45,8 +46,8 @@ #: Owners whose actions may run with the deploy key loaded: GitHub's own, and #: ``./`` for an action in this repository. FIRST_PARTY = ('actions/', 'github/', './') -#: The jobs that load the deploy key and then commit, which the step replaced -#: by #1968 ran in. +#: The jobs that load the deploy key and then commit, where ``Verify Changed +#: files`` runs (#1968). CHANGED_FILES_JOBS = (('cron-vendor.yml', 'vendor-update'), ('cron-conda.yml', 'conda-update')) #: The step's id, which every reader of its outputs names. STEP_ID = 'verify-changed-files' diff --git a/tests/project/test_workflow_run_verdict.py b/tests/project/test_workflow_run_verdict.py index 70033b8733..f5dc6011ce 100644 --- a/tests/project/test_workflow_run_verdict.py +++ b/tests/project/test_workflow_run_verdict.py @@ -663,7 +663,7 @@ def test_the_verdict_checkout_reaches_past_a_bot_commit(self) -> None: def test_a_stale_run_cannot_share_a_concurrency_group_with_a_fresh_one(self) -> None: """GitHub cancels a pending run when a newer one joins its group, so a run that will skip must not join the group of one that will not. Every other - event keeps the group it had. + event's group is its ref. """ for workflow in ACTING_JOBS: diff --git a/tests/project/test_workflow_scripts_1967.py b/tests/project/test_workflow_scripts_1967.py index 9b1bbe5201..1721c74c19 100644 --- a/tests/project/test_workflow_scripts_1967.py +++ b/tests/project/test_workflow_scripts_1967.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""#1967: the ``util/`` scripts that replace shell the workflows repeated, run as the steps run them. +"""#1967: the ``util/`` scripts that hold shell the workflows would otherwise repeat, run as the steps run them. * :file:`util/leg_summary.py`, every ``KTLO summary`` and the ``Experimental summary``: :class:`TestLegSummary`. The steps themselves are pinned in @@ -91,7 +91,7 @@ def test_nothing_failed_writes_nothing(self) -> None: self.assertEqual(self.page.read_text(encoding='utf-8'), 'earlier\n') def test_bad_input_fails_the_step(self) -> None: - """As ``jq`` failing under ``bash -e`` did; the step's ``continue-on-error`` contains it.""" + """As a failing command does under ``bash -e``; the step's ``continue-on-error`` contains it.""" for tier, steps in (('ktlo', 'not json'), ('nightly', steps_json(install='failure'))): with self.subTest(tier=tier, steps=steps): result = self.summary(tier, steps) @@ -176,7 +176,7 @@ def test_the_summary_stage_writes_the_table_and_the_numbers(self) -> None: 'packages': [['pcapkit (top level)', 10, 0, 0, 0, 100.0], ['pcapkit/protocols', 10, 4, 4, 1, 64.29]]}) def test_a_failing_coverage_command_ends_the_step_with_its_status(self) -> None: - """As ``set -e`` did: no summary after a failed ``coverage`` command.""" + """As under ``set -e``: no summary after a failed ``coverage`` command.""" (self.tmp / 'coverage-processes.log').write_text( " 100.ab: New process: cmd: ['/x', 'run', '--rcfile=.github/coverage.toml', '-m', 'pytest']\n" ' 100.ab: New process parent pid: 1\n 101.ab: New process parent pid: 100\n', encoding='utf-8') diff --git a/tests/project/test_workflows_at_a_glance_1732.py b/tests/project/test_workflows_at_a_glance_1732.py index be875c74ba..0455cc2a34 100644 --- a/tests/project/test_workflows_at_a_glance_1732.py +++ b/tests/project/test_workflows_at_a_glance_1732.py @@ -2,9 +2,9 @@ """The workflows page's "At a Glance" table and graph, against the files. :file:`docs/source/contributing/workflows.rst` tabulates the triggers of each -workflow under :file:`.github/workflows/` and draws them as a graph. #1732 added -:file:`experimental.yml` and left both without it, so each is checked here against -the ``on:`` block of every file: one table row per file with its name and +workflow under :file:`.github/workflows/` and draws them as a graph. A file can +land with neither a row nor a node, as :file:`experimental.yml` did (#1732), so +each is checked here against the ``on:`` block of every file: one table row per file with its name and triggers, and one graph node per file with an edge for each ``push``, ``pull_request`` and ``workflow_run`` trigger. The page states no count of the files (#1103's ruling, #1811); the row and node per file are what keep it whole. diff --git a/tests/protocols/_edge_roundtrip.py b/tests/protocols/_edge_roundtrip.py index 5c9ffd120c..8fb01c396f 100644 --- a/tests/protocols/_edge_roundtrip.py +++ b/tests/protocols/_edge_roundtrip.py @@ -5,7 +5,7 @@ everywhere. The ``test_*_edge_roundtrip_unit`` modules beside this one hold hand-built edge-case packets -- minimum and maximum lengths, reserved bits set, zero-length values, padding variants, unassigned codes -- and this module runs -them through three checks: +them through these checks: * a **parse case** must parse, its ``data`` must be the input, and ``from_data(info).data`` must be the input too; @@ -208,7 +208,7 @@ def cuts(cases: 'tuple[Case, ...]') -> 'tuple[Case, ...]': class EdgeRoundTripBase(unittest.TestCase): """The assertions, shared by every family module. - A subclass sets the four tables; this class is not collected itself + A subclass sets the tables; this class is not collected itself because it has no tables (:attr:`CASES` is empty). """ diff --git a/tests/protocols/application/test_ftp_unit.py b/tests/protocols/application/test_ftp_unit.py index feb52f0eaa..c72f042c3c 100644 --- a/tests/protocols/application/test_ftp_unit.py +++ b/tests/protocols/application/test_ftp_unit.py @@ -68,13 +68,11 @@ def test_ftp_read_parses_request_response_and_rejects_invalid(self) -> None: def test_ftp_type_values_are_pinned(self) -> None: """GitHub issue #884: pin every member's exact value and the member set. - :class:`~pcapkit.protocols.application.ftp.Type` was converted from - spelled-out literals to ``auto()`` -- which on a - :class:`~pcapkit.utilities.compat.StrEnum` returns ``name.lower()`` - and so is value-preserving here -- but a future change to - ``_generate_next_value_`` could silently alter these wire-visible - strings without this test noticing unless every member and every - value is asserted explicitly. + :class:`~pcapkit.protocols.application.ftp.Type` takes its values from + ``auto()``, which on a :class:`~pcapkit.utilities.compat.StrEnum` + returns ``name.lower()``, so a change to ``_generate_next_value_`` + could silently alter these wire-visible strings without this test + noticing unless every member and every value is asserted explicitly. """ from pcapkit.protocols.application.ftp import Type diff --git a/tests/protocols/application/test_http2_roundtrip_unit.py b/tests/protocols/application/test_http2_roundtrip_unit.py index 7cb0e1b64b..696789a7bf 100644 --- a/tests/protocols/application/test_http2_roundtrip_unit.py +++ b/tests/protocols/application/test_http2_roundtrip_unit.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """HTTP/2 frames rebuild byte for byte through ``from_data``. -The application round-trip audit (#1202) found seven ways a parsed HTTP/2 -frame lost octets on rebuild, and one way ``make`` refused its own input: +The application round-trip audit (#1202) found these ways a parsed HTTP/2 frame +lost octets on rebuild, and a way ``make`` refused its own input: * #1242 -- the 24-octet connection preface was skipped and never rebuilt. * #1243 -- a frame's payload ran to the end of the buffer instead of to its diff --git a/tests/protocols/application/test_http_unit.py b/tests/protocols/application/test_http_unit.py index 1bd3709b83..9a04fad33b 100644 --- a/tests/protocols/application/test_http_unit.py +++ b/tests/protocols/application/test_http_unit.py @@ -459,7 +459,7 @@ def test_guess_version_reaches_http2_on_the_connection_preface(self) -> None: The preface prepended to the frame in one buffer is asserted here on the *explicit* path only: ``read(version=2)`` skips the preface (#1242) rather than reading its ASCII octets as a frame header, which would make - ``b'PRI'`` a declared length of 5,265,993 that the ``schema.length > + ``b'PRI'`` a declared length of 5,263,945 that the ``schema.length > length`` check (#799) refuses. The guess path's handling of the preface is pinned by ``test_guess_version_identifies_the_http2_connection_preface`` below. @@ -507,7 +507,7 @@ def test_guess_version_identifies_the_http2_connection_preface(self) -> None: (:rfc:`9113#section-3.4`). Without the prefix compare the dispatcher has no notion of it: the HTTP/1 arm declines it and the HTTP/2 arm reads the preface's own ASCII as a frame header, ``b'PRI'`` becoming a declared - length of 5,265,993, which the consistency check of #799/#802 refuses -- + length of 5,263,945, which the consistency check of #799/#802 refuses -- so a real HTTP/2 connection opening, bare preface or preface-plus-SETTINGS, would raise ``unknown HTTP version``. @@ -639,7 +639,7 @@ def test_guess_version_does_not_classify_text_as_http2(self) -> None: Guards against ``b'foo bar baz\\r\\nX: y\\r\\n\\r\\n'`` answering ``version='2'`` -- the headline wrong answer of #800 -- as it does when the HTTP/2 arm accepts any buffer of nine octets or more and ``b'foo'`` - reads as a declared length of 6,712,175 that nothing checks. + reads as a declared length of 6,713,199 that nothing checks. Two defences refuse it. #802's ``schema.length > length`` check refuses the inconsistent length, and independently of that check (#800) text @@ -1470,11 +1470,10 @@ def test_guess_version_does_not_leak_a_bare_struct_error_for_an_inner_field_shor ``_guess_version``'s last arm suppresses :exc:`struct.error` alongside ``ProtocolError`` regardless: #805 does not touch every ``__length__``-keyed field in every one of those modules, so the - suppression stays as defence in depth. Narrowing it was tried and broke - the direct ``HTTP()`` guess path for the sub-nine-octet outer-header class. - This test drives real wire bytes through the actual guess path -- no - ``mock.patch`` -- specifically because a mocked fault cannot see a - regression in the *un-mocked* route the fault is meant to stand in for. + suppression stays as defence in depth. This test drives real wire bytes + through the actual guess path -- no ``mock.patch`` -- specifically + because a mocked fault cannot see a regression in the *un-mocked* route + the fault is meant to stand in for. """ import io @@ -2697,13 +2696,11 @@ def make(self, **kwargs: object) -> object: def test_httpv1_type_values_are_pinned(self) -> None: """GitHub issue #884: pin every member's exact value and the member set. - :class:`~pcapkit.protocols.application.httpv1.Type` was converted - from spelled-out literals to ``auto()`` -- which on a - :class:`~pcapkit.utilities.compat.StrEnum` returns ``name.lower()`` - and so is value-preserving here -- but a future change to - ``_generate_next_value_`` could silently alter these wire-visible - strings without this test noticing unless every member and every - value is asserted explicitly. + :class:`~pcapkit.protocols.application.httpv1.Type` takes its values + from ``auto()``, which on a :class:`~pcapkit.utilities.compat.StrEnum` + returns ``name.lower()``, so a change to ``_generate_next_value_`` + could silently alter these wire-visible strings without this test + noticing unless every member and every value is asserted explicitly. """ from pcapkit.protocols.application.httpv1 import Type diff --git a/tests/protocols/application/test_httpv2_payload_length_unit.py b/tests/protocols/application/test_httpv2_payload_length_unit.py index f3448bf9f8..693cf7331a 100644 --- a/tests/protocols/application/test_httpv2_payload_length_unit.py +++ b/tests/protocols/application/test_httpv2_payload_length_unit.py @@ -1,7 +1,8 @@ # -*- coding: utf-8 -*- """HTTP/2 payload lengths, asserted against the octets that went onto the wire. -GitHub issue #668. Guards against three payload length callbacks in +GitHub issue #668. Guards against the payload length callbacks of the frame +types that take a ``PADDED`` flag in :mod:`pcapkit.protocols.schema.application.httpv2` putting the conditional expression in the wrong place, under which an **unpadded** ``DATA``, ``HEADERS`` or ``PUSH_PROMISE`` frame parses with its whole payload silently @@ -79,11 +80,11 @@ Why the zero-length payload cases are here ------------------------------------------ -Each of the three frames has a "frame of only padding" case, where ``pad_len`` +Each padded frame type has a "frame of only padding" case, where ``pad_len`` accounts for the whole payload area and the payload is legitimately empty. Those are not padding of the test matrix: with only the ``DATA`` one present -- every other fixture carrying a non-empty payload -- the module would be satisfied by a -wrong fix returning ``max(computed, 1)`` for the two ``fragment`` fields, which +wrong fix returning ``max(computed, 1)`` for the ``fragment`` fields, which leaks a padding octet into the fragment and swallows a ``SchemaWarning: packet length < 0: -1``. ``0`` is a legitimate answer from these callbacks and has to be asserted as one, at the wire and at the callback @@ -96,8 +97,8 @@ :attr:`~pcapkit.protocols.schema.application.httpv2.ContinuationFrame.fragment`, :attr:`~pcapkit.protocols.schema.application.httpv2.UnassignedFrame.data` and :attr:`~pcapkit.protocols.schema.application.httpv2.GoawayFrame.debug` use the -plain ``length=lambda pkt: pkt['__length__']`` with no conditional, and all -three carry their payload correctly with or without the defect. +plain ``length=lambda pkt: pkt['__length__']`` with no conditional, and each +carries its payload correctly with or without the defect. :class:`HTTPv2PlainLengthFormControlUnitTests` asserts them, because they are what shows the field machinery is not at fault -- it is the conditional's grouping alone. @@ -109,7 +110,7 @@ type, but through ``make`` -> parse -> ``make``, and ``make`` writes the length field from ``HTTP._make_http_length``. Both sides therefore agree on an empty payload and the octets match. Its generator also passes no ``data`` or -``fragment`` argument for these three frames (measured: ``kwargs={}`` for +``fragment`` argument for these frames (measured: ``kwargs={}`` for ``httpv2-frame/DATA``, ``/HEADERS`` and ``/PUSH_PROMISE``), so the payload it round-trips is ``b''`` and there is nothing for the callback to lose. :meth:`tests.protocols.application.test_http_unit.HTTPUnitTests.test_httpv2_frame_readers_cover_successful_frames` @@ -372,7 +373,7 @@ def test_a_padded_headers_frame_of_only_padding_reads_an_empty_fragment(self) -> The zero-length boundary, and it is load-bearing rather than decorative. Without this case -- and its ``PUSH_PROMISE`` and ``PRIORITY`` counterparts below -- the module is satisfied by a wrong fix - returning ``max(computed, 1)`` for these two fields, since every other + returning ``max(computed, 1)`` for the ``fragment`` fields, since every other fixture here has a non-empty fragment and so never asks the callback for ``0``. Measured under that mutation: this frame's ``fragment`` came back ``b'\\xde'`` -- one padding octet leaked in @@ -475,11 +476,11 @@ def test_a_padded_push_promise_frame_of_only_padding_is_empty(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HTTPv2PlainLengthFormControlUnitTests(FramePayloadMixin, unittest.TestCase): - """The three frames whose payload length has no conditional at all. + """The frames whose payload length has no conditional at all. ``ContinuationFrame.fragment``, ``UnassignedFrame.data`` and ``GoawayFrame.debug`` are written ``length=lambda pkt: pkt['__length__']``, - and all three carry their payload correctly whether or not the #668 grouping + and each carries its payload correctly whether or not the #668 grouping defect is present. That is what localises the defect to the conditional's grouping rather than to :class:`~pcapkit.corekit.fields.strings.BytesField`, ``__length__``'s bookkeeping, or the frame dispatch -- all of which these @@ -532,7 +533,7 @@ def test_unassigned_frame_carries_its_data(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class HTTPv2PayloadLengthCallbackUnitTests(unittest.TestCase): - """The three callbacks, called directly, on the arm that was broken. + """The callbacks, called directly, on the arm that was broken. The wire tests above are the ones that matter, but they can only show the *consequence*. These call each length callback with a synthetic ``packet`` @@ -548,7 +549,7 @@ class HTTPv2PayloadLengthCallbackUnitTests(unittest.TestCase): PAD_LEN = 4 def fields(self) -> 'list[tuple[str, Any, str]]': - """The three fields under test, as ``(label, schema, attribute)``. + """The fields under test, as ``(label, schema, attribute)``. Returns: One entry per mis-parenthesised site. @@ -592,7 +593,7 @@ def packet(self, *, padded: 'bool') -> 'dict[str, Any]': def test_the_unpadded_arm_returns_the_remaining_length(self) -> None: """With ``PADDED`` clear the callback returns ``__length__``, not ``0``. - This is #668 stated as a number: with the defect all three return ``0`` + This is #668 stated as a number: with the defect each returns ``0`` here, which is why an unpadded frame's payload is dropped. """ diff --git a/tests/protocols/application/test_layer_placement_unit.py b/tests/protocols/application/test_layer_placement_unit.py index 1781c495a9..dc99f62147 100644 --- a/tests/protocols/application/test_layer_placement_unit.py +++ b/tests/protocols/application/test_layer_placement_unit.py @@ -9,19 +9,21 @@ Link Layer chapter at §2.3.2. The rule and its citations are written down at :doc:`/contributing/conventions/protocol-layer-placement`. -Three separate things are pinned here, because each can break independently: +These are pinned separately, because each can break independently: #. the ``__layer__`` values and the base classes, including the **order** of RARP's two bases -- ``ARP``'s chain reaches ``Link``, which owns ``__layer__``, so ``class RARP(ARP, Application)`` would still report ``'Link'``; -#. that the dispatch keys did **not** move with the modules, only the - ``ModuleDescriptor`` paths they point at; -#. that parsing still works, with **no per-class override**. ``Application`` accepts +#. that the dispatch keys stay in the registries of the layer below -- + ``TransType.OSPFIGP`` in ``Internet.__proto__``, RARP's EtherType in + ``Link.__proto__`` -- and only the ``ModuleDescriptor`` paths they hold point + into the application subpackage; +#. that parsing works, with **no per-class override**. ``Application`` accepts the ``-1`` sentinel (the undissected remainder), so ``OSPF`` and ``RARP`` inherit ``__post_init__``, ``_decode_next_layer`` and ``_import_next_layer`` unchanged. Every case builds its own octets in memory and reads no capture under -``examples/captures/``, which are generated rather than committed. +``examples/captures/``, most of which are generated rather than committed. """ from __future__ import annotations diff --git a/tests/protocols/application/test_ospf_auth_dict_roundtrip_unit.py b/tests/protocols/application/test_ospf_auth_dict_roundtrip_unit.py index 2b88f58ff3..54dd27d0b6 100644 --- a/tests/protocols/application/test_ospf_auth_dict_roundtrip_unit.py +++ b/tests/protocols/application/test_ospf_auth_dict_roundtrip_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""OSPF rebuilds byte for byte from the dict form of its info, :issue:`1464`. +"""OSPF rebuilds byte for byte from the dict form of its info, #1464. Under cryptographic authentication (AuType 2) the ``auth`` field is a :class:`~pcapkit.protocols.data.application.ospf.CryptographicAuthentication`, diff --git a/tests/protocols/application/test_ospf_cryptographic_authentication_unit.py b/tests/protocols/application/test_ospf_cryptographic_authentication_unit.py index 779c2e96b0..46f023e2b5 100644 --- a/tests/protocols/application/test_ospf_cryptographic_authentication_unit.py +++ b/tests/protocols/application/test_ospf_cryptographic_authentication_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Unit tests for the OSPF cryptographic-authentication names, :issue:`1090`. +"""Unit tests for the OSPF cryptographic-authentication names, #1090. ``OSPF_CryptographicAuthentication`` is exported under that spelling, with no alias for the misspelled ``OSPF_CrytographicAuthentication``, and both OSPF names diff --git a/tests/protocols/application/test_ospf_unit.py b/tests/protocols/application/test_ospf_unit.py index 25e1b3a6aa..766541fd96 100644 --- a/tests/protocols/application/test_ospf_unit.py +++ b/tests/protocols/application/test_ospf_unit.py @@ -153,8 +153,8 @@ def test_ospf_id_numbers_rejects_a_bool(self) -> None: Latent rather than live: nothing in this module calls ``_make_id_numbers`` -- ``OSPF.make`` builds ``router_id``/``area_id`` from its own arguments rather than through this helper -- so only a unit test (this one, - and the one above) reaches it. It is guarded alongside the three live sites - anyway, so that it does not resurface the moment a future caller reaches it. + and the one above) reaches it. It is guarded anyway, alongside the live sites #540 + covers, so that it does not resurface the moment a future caller reaches it. Measured without the guard: .. code-block:: text diff --git a/tests/protocols/internet/test_ext_chain_limit_rebuild_1619_unit.py b/tests/protocols/internet/test_ext_chain_limit_rebuild_1619_unit.py index fe1b34f2ca..ba84994c54 100644 --- a/tests/protocols/internet/test_ext_chain_limit_rebuild_1619_unit.py +++ b/tests/protocols/internet/test_ext_chain_limit_rebuild_1619_unit.py @@ -128,9 +128,9 @@ def assert_rebuilds_as_parsed(self, klass: type, data: bytes, warned: int, **kwa return messages def test_rebuilding_a_cut_frame_warns_as_often_as_parsing_it(self) -> None: - # Rebuilt, IPv6 and the Ethernet around it both warned, and over IPv4 - # so did each header rebuilt whose own count got to the chain limit - # before the frame limit: 33 times. + # Without the #1619 fix, a rebuild warns from IPv6 and the Ethernet + # around it both, and over IPv4 from each header rebuilt whose own + # count gets to the chain limit before the frame limit: 33 times. from pcapkit.protocols.link.ethernet import Ethernet for carrier in ETHERTYPE: @@ -158,8 +158,9 @@ def test_a_layer_rebuilt_on_its_own_counts_as_one_parsed_on_its_own(self) -> Non self.assert_rebuilds_as_parsed(HOPOPT, chain(count, 0), warned) def test_where_both_limits_fall_on_one_layer_rebuilding_warns_once(self) -> None: - # The 33rd header in a row is also the 65th layer: rebuilt, every IP - # layer around the chain warned of it once more, 33 times in all. + # The 33rd header in a row is also the 65th layer. Without the #1619 + # fix, a rebuild warns of it once more from every IP layer around the + # chain, 33 times in all. # Ethernet constructed on its own is not counted, so it takes 31 # tunnels, and 30 in a frame. from pcapkit.protocols.link.ethernet import Ethernet @@ -192,10 +193,11 @@ def test_where_the_frame_limit_falls_first_rebuilding_warns_once(self) -> None: self.assertIn('limit of 64 layers', messages[0]) def test_rebuilding_a_cut_frame_goes_no_deeper_than_parsing_it(self) -> None: - # Each header rebuilt over IPv4 counted the chain from itself, so its - # parse went up to 32 headers past the cut: 123 frames deeper than the - # parse, at 1000 headers. It may go a frame or two deeper, from - # ``from_data`` itself, but not a header's worth. + # Without the #1619 fix, each header rebuilt over IPv4 counts the chain + # from itself, so its parse goes up to 32 headers past the cut: 123 + # frames deeper than the parse, at 1000 headers. A rebuild may go a + # frame or two deeper, from ``from_data`` itself, but not a header's + # worth. from pcapkit.protocols.link.ethernet import Ethernet for carrier in ETHERTYPE: diff --git a/tests/protocols/internet/test_hip_locator_set_length_unit.py b/tests/protocols/internet/test_hip_locator_set_length_unit.py index b3ce87580b..7e328700b9 100644 --- a/tests/protocols/internet/test_hip_locator_set_length_unit.py +++ b/tests/protocols/internet/test_hip_locator_set_length_unit.py @@ -221,8 +221,10 @@ def test_padding_comes_from_the_parameters_length_not_the_locators(self) -> None and it is built by hand rather than through the maker precisely so that the two lengths disagree. A parameter ``len`` of 9 over a single locator whose own ``len`` is 4 needs ``rfc_total(9) - 4 - 9`` = 3 padding octets; - the shadowed nested value would give the 4 that ``len = 4`` implies. - Measured on ``f0999858e``: 32 octets, i.e. 4 of padding. The declared + reading the shadowed nested value instead gives + ``parameter_padding_len({'len': 4})`` = 0 under the RFC formula. Measured + on ``f0999858e``, under the contents-aligning rule (#651): 32 octets, + i.e. 4 of padding. The declared ``Length`` deliberately does not match the 24 octets the locator actually occupies, so the record total here is not meaningful -- the padding count is what is under test. diff --git a/tests/protocols/internet/test_hip_r1_counter_width_unit.py b/tests/protocols/internet/test_hip_r1_counter_width_unit.py index 32c4f2f77f..3255bb29cd 100644 --- a/tests/protocols/internet/test_hip_r1_counter_width_unit.py +++ b/tests/protocols/internet/test_hip_r1_counter_width_unit.py @@ -27,9 +27,8 @@ Three things can hide it at once, and the third is the reason this module asserts against the RFC's arithmetic rather than against a round trip. -1. With two copies of each parameter in a frame (``HIP_COPIES = 2`` in - :mod:`examples.generators.options`), two four-octet shortfalls sum to eight, - so the record area stays 8-aligned and ``HIP.make``'s +1. With two copies of each parameter in a frame, two four-octet shortfalls sum + to eight, so the record area stays 8-aligned and ``HIP.make``'s ``total_length // 8 + 4`` comes out exact. 2. A padding rule that aligns the parameter *contents* rather than the record (the #651 defect) appends, at ``Length = 12``, exactly the four surplus octets @@ -45,7 +44,8 @@ -------------------------------------------- Without an entry in ``_hip_overrides()``, the generator builds ``R1_COUNTER`` -with ``counter = 0``. Given the four-octet field, an RFC-only walk over +with ``counter = 0``. Given the four-octet field and two copies of each +parameter in a frame, an RFC-only walk over :file:`options-internet.pcap` reads ``Length = 12``, advances 16 over a 12-octet record, lands four octets inside the second copy, and finds a phantom ``Type = 0, Length = 0`` record in that copy's own zeroed contents -- whose @@ -174,8 +174,8 @@ def test_a_lone_r1_counter_parameter_survives_a_hip_packet(self) -> None: parameter that is not a multiple of eight on its own cannot survive alone -- the floor division drops the remainder. Measured on ``f0999858e`` this raised ``ProtocolError: HIPv2: invalid format`` at - both codes; the two-copy pairing in - :mod:`examples.generators.options` is what concealed that. + both codes; pairing two copies of the parameter in one frame conceals + that. Code 128 is deliberately not exercised here. ``R1CounterParameter`` is registered for 128 as well as 129 (#690), so 128 reaches this same diff --git a/tests/protocols/internet/test_hip_unit.py b/tests/protocols/internet/test_hip_unit.py index 8e95ce08b7..f6e0d6b1a9 100644 --- a/tests/protocols/internet/test_hip_unit.py +++ b/tests/protocols/internet/test_hip_unit.py @@ -2519,8 +2519,8 @@ def test_hip_solution_parameter_length_is_two_whole_octet_fields(self) -> None: On the widths chosen -------------------- Only the widths that are **not** multiples of 8 can tell the right - formula from the wrong ones, which is why five of the eight cases below - are 1, 9, 12, 17 and 25 bits. At 12 bits, for instance, the four + formula from the wrong ones, which is why the cases below include 1, 9, + 12, 17 and 25 bits. At 12 bits, for instance, the four plausible field-pair widths disagree: ``2 * ceil(12 / 8) == 4`` (correct), ``ceil(12 / 4) == 3`` (the defect), ``2 * floor(12 / 8) == 2``, and ``ceil(12 / 8) == 2`` (one field's worth rather than two). At 1 bit @@ -3183,7 +3183,7 @@ def pre_651_total(length: int) -> int: # locators shadow while they pack. The identity of that one callback is # asserted directly in # :meth:`test_hip_padding_sites_are_the_known_callbacks_and_nothing_else`, - # so a third expression cannot appear unnoticed. + # so another expression cannot appear unnoticed. # # The sweep above covers all eight residues, which is enough for any # formula periodic in ``Length % 8`` -- but not for one that is not. @@ -3220,7 +3220,7 @@ def pre_651_total(length: int) -> int: hip_schema.parameter_padding_len({'len': -1}) def test_hip_padding_sites_are_the_known_callbacks_and_nothing_else(self) -> None: - """#651/#664/#679: two padding callbacks cover every parameter, and no third exists. + """#651/#664/#679: every HIP parameter pads through a known callback, and no other. Every HIP padding site goes through :func:`~pcapkit.protocols.schema.internet.hip.parameter_padding_len` @@ -3244,9 +3244,9 @@ def test_hip_padding_sites_are_the_known_callbacks_and_nothing_else(self) -> Non Three failure modes make the shape worth guarding, none of which anything else catches: - * **A third expression appearing.** A later change that gives some + * **Another expression appearing.** A later change that gives some parameter an inline lambda -- to make a pinned literal pass, say -- - would be indistinguishable from the two legitimate callbacks by + would be indistinguishable from the legitimate callbacks by inspection. ``bespoke`` below must stay empty. * **The ``LOCATOR_SET`` callback regressing to a raw ``pkt['len']`` read.** That silently re-introduces diff --git a/tests/protocols/internet/test_ipv4_header_truncated_payload_runtime.py b/tests/protocols/internet/test_ipv4_header_truncated_payload_runtime.py index 2f67615ea1..c781e12bf2 100644 --- a/tests/protocols/internet/test_ipv4_header_truncated_payload_runtime.py +++ b/tests/protocols/internet/test_ipv4_header_truncated_payload_runtime.py @@ -8,7 +8,7 @@ rebuilt those 8 octets as if they had been on the wire. Such a payload is a :class:`~pcapkit.protocols.misc.null.NoPayload`. ``test.pcapng`` frame 3, cut inside the IPv4 header itself (18 of 20 octets), is kept as raw octets -(:issue:`1170`). +(#1170). The capture case reads a generated sample, so this module belongs to the fixture-dependent tier. The in-memory cases need no capture. diff --git a/tests/protocols/internet/test_ipv4_unit.py b/tests/protocols/internet/test_ipv4_unit.py index 6a8186d0fb..a57e1557d0 100644 --- a/tests/protocols/internet/test_ipv4_unit.py +++ b/tests/protocols/internet/test_ipv4_unit.py @@ -156,7 +156,7 @@ def test_ipv4_from_data_rebuilds_a_parsed_datagram(self) -> None: # the same, with a 4-octet Router Alert option ahead of the payload ('with options', '4600002000000005000600007f0000010000000094040001aaaaaaaaaaaaaaaa', [OptionNumber.RTRALT], Raw), - # options-ipv4.pcap frame 8: options, and no payload at all + # the RTRALT frame of options-ipv4.pcap: options, and no payload at all ('with options, no payload', '460000180000000000060000c0000201c633640194040001', [OptionNumber.RTRALT], NoPayload), ] @@ -503,8 +503,8 @@ def test_ipv4_make_opt_sec_sets_the_field_termination_indicator(self) -> None: authority octet a *field termination indicator*: ``1`` means another octet follows, ``0`` means this is the last. ``_read_opt_sec`` enforces it, warning ``field termination indicator not set`` when the final octet - has it set. #537 made the writer and the reader agree with each other on - the opposite convention; #1321 made both agree with the RFC. + has it set. The writer and the reader agree with each other (#537) and + with the RFC (#1321). The round trip is asserted through a real datagram and with warnings promoted to errors, because a disagreement's only symptom is a warning. @@ -674,25 +674,25 @@ def test_ipv4_make_opt_sec_rejects_a_termination_bit_as_an_authority(self) -> No def test_ipv4_sid_option_is_four_octets_wide_on_the_wire(self) -> None: """RFC 791's four-octet Stream ID option survives the round trip. C.f. #534. - ``SIDOption.sid`` was a - :class:`~pcapkit.corekit.fields.numbers.UInt32Field` where :rfc:`791` - section 3.1 gives the Stream ID two octets inside a four-octet option -- - which is what ``_make_opt_sid`` itself had always written into - ``length``. Only the schema field disagreed, so it over-read a - well-formed option by two octets on the way in, warning - ``packet length < 0: -2``, and over-wrote it by two on the way out: - ``880400000037`` for an option that is ``88040037``. - - Six not being a multiple of four, the option area then reached the - 32-bit padding branch and picked up a ``NOP`` and an ``EOOL``, so the - rebuilt datagram came back four octets longer than the one it was read + :rfc:`791` section 3.1 gives the Stream ID two octets inside a + four-octet option, which is the ``length`` ``_make_opt_sid`` writes. + This guards against ``SIDOption.sid`` being a + :class:`~pcapkit.corekit.fields.numbers.UInt32Field`, under which only + the schema field disagrees: it over-reads a well-formed option by two + octets on the way in, warning ``packet length < 0: -2``, and over-writes + it by two on the way out: ``880400000037`` for an option that is + ``88040037``. + + Six not being a multiple of four, the option area then reaches the + 32-bit padding branch and picks up a ``NOP`` and an ``EOOL``, so the + rebuilt datagram comes back four octets longer than the one it was read from, with ``ihl`` and total length grown to match -- 28 octets and ``ihl=7`` against the 24 and ``ihl=6`` on the wire. This starts from wire octets rather than from a constructed option because that is the only place the width shows: constructing and reconstructing both go through the same ``_make_opt_sid``, so the two - halves agreed with each other while disagreeing with :rfc:`791`. That + halves agree with each other while disagreeing with :rfc:`791`. That symmetry is why the defect could not be recorded as an ``EXPECTED_FAILURES`` entry, and it is why the assertion here is byte-for-byte identity against the input rather than a comparison of two @@ -1373,7 +1373,7 @@ def test_ipv4_option_constructors_cover_common_and_error_branches(self) -> None: # Which is why the alignment branch gets a case of its own rather than # relying on SID to reach it. The padding terminator is an EOOL - # *option*, not the bare wire code -- that is what #506 fixed. A SEC + # *option*, not the bare wire code (#506). A SEC # option carrying a two-octet bitmap packs to five, so it is one: three # octets of padding, two NOPs and the EOOL. padded, padded_length = proto._make_ipv4_options([ diff --git a/tests/protocols/internet/test_ipv6_ext_hint_unit.py b/tests/protocols/internet/test_ipv6_ext_hint_unit.py index 9642660f4e..fb461b13be 100644 --- a/tests/protocols/internet/test_ipv6_ext_hint_unit.py +++ b/tests/protocols/internet/test_ipv6_ext_hint_unit.py @@ -3,7 +3,7 @@ GitHub issue #1178. :meth:`~pcapkit.protocols.protocol.ProtocolBase._parse_next_layer` keeps a next layer as :class:`~pcapkit.protocols.misc.raw.Raw` when fewer octets -were captured than its ``__length_hint__`` (:issue:`1170`). +were captured than its ``__length_hint__`` (#1170). :class:`~pcapkit.protocols.internet.ipv6_ext.IPv6_Ext` hinted 2, though :rfc:`6564#section-4` and :rfc:`8200#section-4` make every extension header at least 8 octets, so a 2--7 octet tail of a longer declared payload was parsed as diff --git a/tests/protocols/internet/test_ipv6_ext_unit.py b/tests/protocols/internet/test_ipv6_ext_unit.py index d1af7db59a..94757b6eb6 100644 --- a/tests/protocols/internet/test_ipv6_ext_unit.py +++ b/tests/protocols/internet/test_ipv6_ext_unit.py @@ -416,7 +416,7 @@ class IPv6ExtSharedBaseContractTests(unittest.TestCase): #: has no ``length`` field at all), and ``__index__`` raises. REQUIRED_OVERRIDES = ('name', 'alias', 'protocol', 'length', '__index__') - #: The family as it stands, so a member silently *leaving* the base is + #: The expected family, so a member silently *leaving* the base is #: caught as well as one joining without the overrides. Named rather than #: counted: a count alone cannot tell a swap from a no-op. KNOWN_MEMBERS = frozenset({ @@ -617,8 +617,8 @@ def test_no_subclass_answers_with_the_fallback_identity(self) -> None: self.assertNotEqual(self._constant(klass, 'alias'), base_alias) def test_ah_and_esp_double_inherit_and_linearise(self) -> None: - """Both are IPsec members *and* IPv6 extension headers -- IANA marks - each ``Y`` in the *IPv6 Extension Header* column -- so each carries two + """Both are IPsec members *and* IPv6 extension headers -- IANA's *IPv6 + Extension Header Types* registry lists each -- so each carries two identically-parameterised generic bases. The MRO is pinned because it is what makes the ``super()`` delegation in their ``protocol`` guards land on :class:`~pcapkit.protocols.protocol.ProtocolBase` rather than @@ -768,8 +768,10 @@ def test_length_hint_is_the_eight_octet_rfc_6564_minimum(self) -> None: def test_next_is_shared_by_every_subclass(self) -> None: """Every subclass inherits the base's ``next`` property (#917) -- without it, reading one raises :exc:`AttributeError` -- which is sound - because :rfc:`8200#section-4.1` puts a Next Header octet first in every - extension header and every subclass records it under that name. + because every subclass's data model records its Next Header under that + name: first in the header for the :rfc:`6564#section-4` format, and from + the encrypted trailer for ESP (:rfc:`4303`), :data:`None` when it cannot + be decrypted. """ from pcapkit.const.reg.transtype import TransType from pcapkit.protocols.internet.hopopt import HOPOPT diff --git a/tests/protocols/internet/test_ipv6_extension_unit.py b/tests/protocols/internet/test_ipv6_extension_unit.py index d084a668c8..3800128e77 100644 --- a/tests/protocols/internet/test_ipv6_extension_unit.py +++ b/tests/protocols/internet/test_ipv6_extension_unit.py @@ -1909,7 +1909,7 @@ def test_ipv6_opts_padding_options_parse_from_the_wire(self) -> None: def _assert_smf_dpd_options_parse_from_the_wire(self, protocol_cls: type) -> None: """A hash-based ``SMF_DPD`` option must consume exactly what it declares. - Per :rfc:`6621#section-8.1` the option is an ``Option Type`` octet, an + Per :rfc:`6621#section-6.1.1` the option is an ``Option Type`` octet, an ``Opt Data Len`` octet, and ``Opt Data Len`` further octets whose first bit is the DPD mode -- set, here, for hash-based DPD, so that the whole of the hash assist value is the ``Opt Data Len`` octets. Two things have to @@ -1985,7 +1985,7 @@ def _assert_identification_based_dpd_options_parse_from_the_wire(self, protocol_ Identification-based DPD reaches the same mis-sized read by a different route: its length comes from ``Opt Data Len`` too, but the octets it covers are a TaggerID whose own width comes from the ``TidLen`` nibble - [:rfc:`6621#section-8.1`], so a wrong ``Opt Data Len`` mis-sizes the + [:rfc:`6621#section-6.1.1`], so a wrong ``Opt Data Len`` mis-sizes the identifier rather than the whole option. Without the fix, both cases below hang exactly as the hash-based ones do. @@ -2382,7 +2382,7 @@ def _assert_ilnp_nonce_option_is_sized_by_the_ceiling(self, protocol_cls: type) ``0xFFFFFF`` -- bit length 24, the value the option-round-trip generator happens to use -- is one of the few values floor division gets right, which is why the round-trip suite never caught this. Every - case below bar the two marked controls therefore has a bit length that + case below bar the marked controls therefore has a bit length that is deliberately **not** a multiple of eight, and several are below 256 (c.f. #601). diff --git a/tests/protocols/internet/test_ipv6_reassembly_runtime.py b/tests/protocols/internet/test_ipv6_reassembly_runtime.py index f2796ad948..d5a70511f3 100644 --- a/tests/protocols/internet/test_ipv6_reassembly_runtime.py +++ b/tests/protocols/internet/test_ipv6_reassembly_runtime.py @@ -123,7 +123,7 @@ def test_fragment_offsets_are_octets_so_the_payload_reassembles_intact(self) -> # the payload bytes, not just their count self.assertEqual(self._payload(datagram), body) self.assertEqual(len(self._payload(datagram)), 1536) - # the truncation the unscaled offset produced: 128 units + 512 octets + # the truncation an unscaled offset produces: 128 units + 512 octets self.assertNotEqual(len(self._payload(datagram)), 640) def test_reassembly_keys_on_identification_not_flow_label(self) -> None: diff --git a/tests/protocols/internet/test_length_hint_minimum_header_unit.py b/tests/protocols/internet/test_length_hint_minimum_header_unit.py index abf1dc9529..0858982412 100644 --- a/tests/protocols/internet/test_length_hint_minimum_header_unit.py +++ b/tests/protocols/internet/test_length_hint_minimum_header_unit.py @@ -3,7 +3,7 @@ GitHub issue #1173. :meth:`~pcapkit.protocols.protocol.ProtocolBase._parse_next_layer` keeps a next layer as :class:`~pcapkit.protocols.misc.raw.Raw` when fewer octets -were captured than its ``__length_hint__`` (:issue:`1170`). ``IPv6_Route`` hinted +were captured than its ``__length_hint__`` (#1170). ``IPv6_Route`` hinted 4 octets and ``MH`` 6, though each header is at least 8, so a stream of 4--7 or 6--7 octets was parsed and rebuilt as a full 8-octet header. ``HOPOPT`` and ``IPv6_Opts`` hinted 2 for the same 8-octet minimum. An application layer's diff --git a/tests/protocols/internet/test_message_spelling_2171_unit.py b/tests/protocols/internet/test_message_spelling_2171_unit.py index 35aba5ea06..5a23a61591 100644 --- a/tests/protocols/internet/test_message_spelling_2171_unit.py +++ b/tests/protocols/internet/test_message_spelling_2171_unit.py @@ -1,5 +1,5 @@ # -*- coding: utf-8 -*- -"""Two user-facing messages are spelt correctly. +"""The IPv4 timestamp warning and the MH prefix error are spelt correctly. GitHub issue #2171: :meth:`TSOption.post_process ` warned of an diff --git a/tests/protocols/internet/test_mh_flags_reserved_unit.py b/tests/protocols/internet/test_mh_flags_reserved_unit.py index 228a682171..ef564f36b5 100644 --- a/tests/protocols/internet/test_mh_flags_reserved_unit.py +++ b/tests/protocols/internet/test_mh_flags_reserved_unit.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """MH flag words keep their reserved bits on rebuild. -GitHub issue #1418: 24 MH ``flags`` bit fields named only some of their bits, +GitHub issue #1418: MH ``flags`` bit fields named only some of their bits, and :class:`~pcapkit.corekit.fields.strings.BitField` packs an unnamed bit as zero, so a message, option or sub-structure whose reserved bits were set came back with them cleared. Each site names a ``reserved`` sub-field, read into diff --git a/tests/protocols/internet/test_mh_unit.py b/tests/protocols/internet/test_mh_unit.py index d6ea73ced2..36b1601c61 100644 --- a/tests/protocols/internet/test_mh_unit.py +++ b/tests/protocols/internet/test_mh_unit.py @@ -99,13 +99,10 @@ def test_unregistered_mh_codes_do_not_mutate_the_class_registries(self) -> None: siblings warn about an overwrite that never happened. ``__extension__`` is reached through the construction path rather than a - parse: CGA extensions arrive inside a CGA Parameters option, whose schema - sizes its extension area from ``pkt['length']`` -- a key the nested packet - context does not carry -- so - :meth:`~pcapkit.protocols.internet.mh.MH._read_cga_extensions` is - unreachable from bytes. The sibling read site in - :meth:`~pcapkit.protocols.internet.mh.MH._make_cga_extensions` had the - identical defect. + parse: :meth:`~pcapkit.protocols.internet.mh.MH._make_cga_extensions` + reads that registry as + :meth:`~pcapkit.protocols.internet.mh.MH._read_cga_extensions` does, and + had the identical defect. """ from pcapkit.const.mh.cga_extension import CGAExtension @@ -1646,7 +1643,8 @@ def test_mh_get_default_works_uniformly_through_the_inherited_method(self) -> No as :class:`LMAAddressCode` and :class:`LocalizedRoutingStatus` -- the module's pure re-parents -- do, so ``default`` works the same way on every class here, because there is only one implementation to call. - This test fails with the ``TypeError`` above at 382375811. + Measured at 382375811, the one-argument overrides raise the + ``TypeError`` above. """ from pcapkit.protocols.internet.mh import (FastBindingAcknowledgmentStatus, IPv6AddressPrefixCode, LMAAddressCode, @@ -2208,7 +2206,7 @@ def test_mh_geo_location_degrees_are_signed(self) -> None: self.assertEqual(again.pack(), schema.pack()) def test_mh_multiprefix_extension_length_matches_its_payload(self) -> None: - """The Multi-Prefix CGA extension declared a length it did not emit. + """The Multi-Prefix CGA extension declares the length it emits. ``_make_ext_multiprefix`` computed ``1 + len(prefixes) * 16`` for an extension whose data is a 4-octet flag word followed by one **8**-octet @@ -2605,7 +2603,7 @@ def test_mh_pmipv6_options_round_trip_byte_for_byte(self) -> None: Option.DLIF_Link_Layer_Address: {'lla': b'\x00\x11\x22\x33\x44\x55'}, } - # the whole registry bar the one option that cannot be parsed at all + # the whole registry bar CGA Parameters, left out as the docstring says self.assertEqual(sorted([*cases, Option.CGA_Parameters]), sorted(Option)) for code, args in cases.items(): @@ -3106,8 +3104,8 @@ def test_mh_fid_suboption_takes_an_unassigned_suboption_from_its_data_model(self """The ``option is not None`` path of the unassigned sub-option branch. The same maker that carries #508's target care-of conversion ends in a - catch-all for sub-option types the library does not model, and that - catch-all's data-model path was unexercised. + catch-all for sub-option types the library does not model, and this + exercises that catch-all's data-model path. """ from pcapkit.const.mh.flow_id_suboption import FlowIDSuboption from pcapkit.protocols.data.internet.mh import \ @@ -3513,7 +3511,8 @@ def test_mh_nested_suboptions_build_from_raw_kwargs(self) -> None: proto = object.__new__(MH) payload = b'\xde\xad\xbe\xef' - # the registered vendor-specific attribute: the case that was broken + # the registered vendor-specific attribute, whose ``data`` field is the + # case the docstring describes attr = proto._make_qos_attribute( # type: ignore[arg-type] QoSAttribute.QoS_Vendor_Specific_Attribute, vendor=32473, subtype=3, data=payload) diff --git a/tests/protocols/internet/test_smf_dpd_mode_rebuild_unit.py b/tests/protocols/internet/test_smf_dpd_mode_rebuild_unit.py index ccf560d140..12e5470a32 100644 --- a/tests/protocols/internet/test_smf_dpd_mode_rebuild_unit.py +++ b/tests/protocols/internet/test_smf_dpd_mode_rebuild_unit.py @@ -19,7 +19,7 @@ from tests._support import reimport_once_per_class #: Extension headers carrying one SMF_DPD option (type 0x08), next header 59, -#: padded to 8 octets with Pad1 or PadN. +#: padded to a multiple of 8 octets with Pad1 or PadN where needed. HEADERS = { 'H-DPD': bytes.fromhex('3b00080481020304'), 'I-DPD NULL': bytes.fromhex('3b00080300010200'), diff --git a/tests/protocols/link/test_l2tp_version_unit.py b/tests/protocols/link/test_l2tp_version_unit.py index 04dd3694d2..f8779224a9 100644 --- a/tests/protocols/link/test_l2tp_version_unit.py +++ b/tests/protocols/link/test_l2tp_version_unit.py @@ -201,8 +201,9 @@ def test_l2tpv3_over_ip_degrades_to_raw_rather_than_a_fabricated_header(self) -> Measured without the version guard, that combination reported ``version=4``, ``tunnelid=0x5678`` and ``sessionid=0xff03``: a - confident header assembled out of the top half of a Session ID and the - first two octets of a PPP frame. Degrading to + confident header assembled out of a Session ID and the first two octets + of a PPP frame, its top half read as the flags word and its lower half + as the Tunnel ID. Degrading to :class:`~pcapkit.protocols.misc.raw.Raw` is the honest answer, and is what the library does for any payload it has no dissector for. @@ -239,10 +240,11 @@ def test_l2tpv3_over_udp_1701_does_not_parse_as_l2tpv2(self) -> None: This is the case that guards a real dissection rather than a hypothetical registration: port 1701 is bound, so without the version - guard a v3 datagram arriving on it is dissected as L2TPv2 and reports - ``tunnelid=0x1234``/``sessionid=0x5678`` read out of v3's Control - Connection ID. :rfc:`3931` §3.2.1 requires ``Ver`` to - be 3, so the nibble is exactly the signal that this is not v2. + guard a v3 data message arriving on it is dissected as L2TPv2 and + reports ``tunnelid=0x1234``/``sessionid=0x5678`` read out of v3's + Reserved field and the top half of its Session ID. :rfc:`3931` + §4.1.2.1 requires ``Ver`` to be 3, so the nibble is exactly the signal + that this is not v2. """ def udp(src: 'int', dst: 'int', payload: bytes) -> bytes: diff --git a/tests/protocols/link/test_link_unit.py b/tests/protocols/link/test_link_unit.py index 5038e2c6e0..7b86e6137e 100644 --- a/tests/protocols/link/test_link_unit.py +++ b/tests/protocols/link/test_link_unit.py @@ -280,7 +280,7 @@ def test_arp_proto_resolve_rejects_a_bool(self) -> None: self.assertIn('must not be a bool', str(context.exception)) self.assertIn('int(True)', str(context.exception)) - # the fix must not disturb a real address on either family + # the guard leaves a real address on either family packing as given self.assertEqual( proto._make_proto_resolve('198.51.100.9', EtherType.Internet_Protocol_version_4), b'\xc6\x33\x64\x09') @@ -366,7 +366,8 @@ def test_l2tp_index_is_unsupported_and_make_data_preserves_flags(self) -> None: # Neither the abstract base nor v2 has a numeral registry index: v2 is # reached by UDP port 1701, and a port is not an ``__index__`` value - # anywhere in this package. L2TPv3 will be the first with one (115). + # anywhere in this package. An L2TPv3 class would be the first with one + # (IP protocol 115). for klass in (L2TP, L2TPv2): with self.subTest(protocol=klass.__name__): with self.assertRaises(UnsupportedCall): diff --git a/tests/protocols/misc/pcap/test_frame_length_runtime.py b/tests/protocols/misc/pcap/test_frame_length_runtime.py index 83ffcf92e0..737f9686ff 100644 --- a/tests/protocols/misc/pcap/test_frame_length_runtime.py +++ b/tests/protocols/misc/pcap/test_frame_length_runtime.py @@ -34,7 +34,7 @@ Why fixtures can miss it: **the two lengths are equal unless the snapshot length cut the frame short**, so a fixture without a truncated frame compares a value against itself and cannot tell the two assignments apart. This module therefore -leans on the two truncated fixtures (#614), and asserts that they really are +leans on the truncated fixtures (#614), and asserts that they really are truncated before asserting anything about them -- a fixture regenerated into something untruncated fails :meth:`PcapFrameLengthRuntimeTests.test_the_fixture_really_holds_a_truncated_frame` @@ -67,7 +67,7 @@ HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) #: The classic-PCAP fixtures that carry a truncated frame, as ``(name, struct -#: byte-order prefix)``. All three come from :file:`examples/generators/endian.py` +#: byte-order prefix)``. They all come from :file:`examples/generators/endian.py` #: and hold the same three records, the third of them captured short. PCAP_FIXTURES = ( ('big_endian.pcap', '>'), @@ -281,7 +281,7 @@ def test_cap_len_counts_the_octets_the_frame_actually_carries(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PcapngFrameLengthRuntimeTests(unittest.TestCase): - """#618, on the PCAP-NG reader -- the one that was already right. + """#618, on the PCAP-NG reader -- the one whose assignment matches Wireshark's. This is the control. It passes without the fix too, and that is what it is for: it pins the convention the PCAP reader follows, so a change that diff --git a/tests/protocols/misc/test_misc_short_read_1470_runtime.py b/tests/protocols/misc/test_misc_short_read_1470_runtime.py index a15bd88eaa..79f3109a1b 100644 --- a/tests/protocols/misc/test_misc_short_read_1470_runtime.py +++ b/tests/protocols/misc/test_misc_short_read_1470_runtime.py @@ -1,13 +1,14 @@ # -*- coding: utf-8 -*- """A PCAP global header or PCAP-NG block cut short rebuilds as captured. -GitHub issue #1470. The two classes under :mod:`pcapkit.protocols.misc` that -read a file's own framing do not inherit the layer base classes, so the short -read #1458 records did not reach them. ``Header(raw[:4])`` rebuilt from its -``info`` as all 24 octets, and a PCAP-NG block the capture ended inside rebuilt -at the length it declares, or raised -- :exc:`ProtocolError` on a rebuilt Block -Total Length that was not a multiple of four, and a bare :exc:`ValueError` on a -Decryption Secrets Block cut inside a TLS key log line. +GitHub issue #1470. The PCAP ``Header`` and the ``PCAPNG`` block classes under +:mod:`pcapkit.protocols.misc` read a file's own framing and do not inherit the +layer base classes, so the short read #1458 records did not reach them. +``Header(raw[:4])`` rebuilt from its ``info`` as all 24 octets, and a PCAP-NG +block the capture ended inside rebuilt at the length it declares, or raised -- +:exc:`ProtocolError` on a rebuilt Block Total Length that was not a multiple of +four, and a bare :exc:`ValueError` on a Decryption Secrets Block cut inside a +TLS key log line. Each header and each block type found in the sample captures is cut at every offset. A PCAP header must be refused with an in-library exception, or rebuild diff --git a/tests/protocols/misc/test_pcapng_roundtrip_unit.py b/tests/protocols/misc/test_pcapng_roundtrip_unit.py index 6c9588918b..06956f6a49 100644 --- a/tests/protocols/misc/test_pcapng_roundtrip_unit.py +++ b/tests/protocols/misc/test_pcapng_roundtrip_unit.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """PCAP-NG blocks build from keywords, and rebuild from their data model. -Five defects, each of which stopped a block from surviving +Defects, each of which stopped a block from surviving :meth:`~pcapkit.protocols.protocol.ProtocolBase.from_data`: * #1267 -- :meth:`PCAPNG.__post_init__ ` diff --git a/tests/protocols/misc/test_pcapng_unit.py b/tests/protocols/misc/test_pcapng_unit.py index 79e0791385..729cc01616 100644 --- a/tests/protocols/misc/test_pcapng_unit.py +++ b/tests/protocols/misc/test_pcapng_unit.py @@ -253,9 +253,9 @@ def test_pcapng_option_registry_preserves_duplicate_numeric_codes(self) -> None: self.assertEqual(option_map.get(_option_key(OptionType.get(2, namespace='opt')), 'unknown'), 'unknown') def test_unregistered_pcapng_codes_do_not_mutate_the_class_registries(self) -> None: - """Parsing must not write to any of PCAP-NG's four dispatch registries. + """Parsing must not write to any of PCAP-NG's dispatch registries. - #425's defect, four times over. ``__block__``, ``__option__``, + #425's defect, once per registry. ``__block__``, ``__option__``, ``__record__`` and ``__secrets__`` are each a :class:`collections.defaultdict` on a class attribute shared by every :class:`~pcapkit.protocols.misc.pcapng.PCAPNG` instance in the process, so @@ -2906,7 +2906,7 @@ def _all_block_schemas(): ] def test_pcapng_every_block_schema_packs(self) -> None: - # Regression for #366: seven of the eleven block schemas could not be + # Regression for #366: most of the block schemas could not be # packed at all, from four independent causes -- a ``BytesField`` # standing in for a ``PaddingField``, an option length callback reading # a ``PaddingField``'s value, ``Schema.pre_pack`` never being called, @@ -3414,10 +3414,11 @@ def test_read_timestamp_is_utc_whatever_the_host_timezone_is(self) -> None: #361: ``_read_timestamp`` added ``tzone.utcoffset(None)`` to the epoch it returned, and ``_get_timezone`` fell back to the *reading host's* zone - whenever the capture named no ``if_tzone`` -- which - draft-ietf-opsawg-pcapng-02 §4.2 says should be the normal case, since - the option "SHOULD NOT be used". So the same file parsed to a different - absolute time on every host, by that host's UTC offset. + whenever the capture named no ``if_tzone`` -- which the Interface + Description Block section of draft-ietf-opsawg-pcapng-02 says should be + the normal case, since the option "SHOULD NOT be used". So the same + file parsed to a different absolute time on every host, by that host's + UTC offset. The defect is invisible on a UTC machine, which is why it survived, so this drives several zones explicitly and asserts they were really @@ -3816,9 +3817,7 @@ def test_packing_an_option_is_not_shortened_by_an_unknown_length(self) -> None: :class:`~pcapkit.corekit.fields.strings.StringField` repair a negative width in ``pre_process``, resetting it to ``len(value)``, so the pack path cannot see the guard at all. It is kept as a no-regression check - rather than as a discriminator; - :meth:`test_a_negative_remainder_is_left_alone_rather_than_clamped_to` is - the input that actually exercises the guard. + rather than as a discriminator. """ from pcapkit.protocols.schema.misc.pcapng import UnknownOption @@ -3956,7 +3955,7 @@ class PCAPNGTruncatedFileTests(unittest.TestCase): """ #: The result of :meth:`_sweep`, computed by the first test that asks for it - #: and shared by the other three: one extraction per octet of the sample is + #: and shared by the rest: one extraction per octet of the sample is #: this class's whole cost (GitHub issue #1062). _swept = None # type: tuple[dict[int, int], dict[int, BaseException]] | None @@ -4609,7 +4608,7 @@ def test_a_binary_field_ending_the_first_of_two_entries_is_not_warned(self) -> N def test_a_trailing_separator_with_no_padding_still_starts_an_entry(self) -> None: """A trailing separator landing exactly on the last octet is not dropped. - The two tests above see their trailing separator followed by the + The tests above see their trailing separator followed by the block's own NUL padding, which the walk also treats as ending an entry -- so the extra empty entry that padding produces stands in for the one the separator itself should produce, and can hide a gap. @@ -4685,7 +4684,7 @@ def test_a_journal_field_that_is_not_utf8_is_reported_and_kept_as_captured(self) :exc:`UnicodeDecodeError` is a :exc:`ValueError`, so it is neither in :mod:`pcapkit.utilities.exceptions` nor an :exc:`EOFError`. The octet is reported, and since the replaced text would not rebuild it, the entry is - kept as captured. Asserted for all three sites that decode -- a field + kept as captured. Asserted for each site that decodes -- a field name, a key and a value. """ diff --git a/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py b/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py index 64595c18b4..e3fcd614cb 100644 --- a/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py +++ b/tests/protocols/schema/test_schema_metaclass_abc_cache_unit.py @@ -43,8 +43,10 @@ class keyword collision described in that module. Bypassing it also skipped that same ``dict`` then reads the *same* cache entry straight back, without ever running :class:`Schema`'s real subclass check. - The corruption runs **both ways**, which is why two tests below ask the two - questions in opposite orders rather than one test asking them in the + The corruption runs **both ways**, which is why + :meth:`test_a_plain_dict_is_not_a_schema_after_mapping_is_asked_first` and + :meth:`test_asking_schema_first_does_not_break_mapping_for_a_plain_dict` ask + the two questions in opposite orders rather than one test asking them in the "obvious" order. Asking ``Schema`` *first* computes a fresh (correct) ``False`` for ``dict`` -- but caches it in the shared object's *negative* cache, and since that object is ``Mapping``'s own, a plain ``dict`` then @@ -168,7 +170,7 @@ def _make_shared_hierarchy() -> 'tuple[type, type]': falls through the MRO to :class:`collections.abc.Mapping`'s -- exactly the shape #439's bypass in ``SchemaMeta.__new__`` produced on Python < 3.11, confirmed below rather than assumed. Building this on plain - ``abc.ABCMeta`` rather than ``SchemaMeta`` keeps the two tests below + ``abc.ABCMeta`` rather than ``SchemaMeta`` keeps the probing tests below about ``abc.ABCMeta`` in general, not about this library's fix. """ diff --git a/tests/protocols/test_construction_keyword_check_unit.py b/tests/protocols/test_construction_keyword_check_unit.py index 46dd086928..d18ecdd363 100644 --- a/tests/protocols/test_construction_keyword_check_unit.py +++ b/tests/protocols/test_construction_keyword_check_unit.py @@ -15,7 +15,7 @@ declared seq = 1 built info.seq = 0 warnings captured: [] -Without the fix, twenty-five generated fixture frames carry sequence number +Without the fix, every frame of ``options-tcp.pcap`` carries sequence number ``0`` while the generator says ``1``, one octet per frame, and nothing but reading the mapping against the signature reveals it. Issues #541 and #556 are the same family of silence. @@ -42,11 +42,11 @@ :class:`ForwardedKeywordTests` is where it is pinned; rejecting on ``make``'s signature alone would reject correct code. -Two things are left alone on purpose, and each has a test saying so: +These are left alone on purpose, and each has a test saying so: :meth:`ScopeTests.test_the_parse_path_is_unaffected` Parsing. There the keywords are not field values but whatever the engines and - the four ``_import_next_layer`` implementations forward -- ``alias``, + the ``_import_next_layer`` implementations forward -- ``alias``, ``packet``, and the parse limits -- and a protocol cannot know which of its ancestors' keywords its parent chose to pass on. Nothing was ever lost that way either: a dropped parse keyword changes how a packet is read, not what diff --git a/tests/protocols/test_declared_length_roundtrip_runtime.py b/tests/protocols/test_declared_length_roundtrip_runtime.py index 208e97273c..c4b38308e9 100644 --- a/tests/protocols/test_declared_length_roundtrip_runtime.py +++ b/tests/protocols/test_declared_length_roundtrip_runtime.py @@ -104,7 +104,7 @@ def test_header_truncated_frame_keeps_its_total_length(self) -> None: """``test.pcapng`` frame 3 is cut inside the IPv4 header itself. Only 18 of the 20 header octets were recorded, so they are kept as raw - octets rather than parsed into a header (:issue:`1170`). The rebuild is + octets rather than parsed into a header (#1170). The rebuild is then byte for byte, and keeps the declared 300 rather than the 18 that were kept. diff --git a/tests/protocols/test_dispatch_default_resolution_unit.py b/tests/protocols/test_dispatch_default_resolution_unit.py index 8ac8f320bf..86bf3413e6 100644 --- a/tests/protocols/test_dispatch_default_resolution_unit.py +++ b/tests/protocols/test_dispatch_default_resolution_unit.py @@ -12,7 +12,7 @@ descriptor is not written back either (GitHub issue #1559), so an extraction of :file:`many_interfaces.pcapng` performs 296 resolutions, 48 of them fallbacks. -GitHub pull request #563 proposes paying that cost with a class-level cache of +GitHub pull request #563 proposed paying that cost with a class-level cache of resolved classes and no invalidation, under which an :func:`importlib.reload` leaves it serving the pre-reload class forever. These tests pin the shape taken instead: the repeats are cheap because diff --git a/tests/protocols/test_dispatch_registry_unit.py b/tests/protocols/test_dispatch_registry_unit.py index 22f4e0a870..fdb084c50e 100644 --- a/tests/protocols/test_dispatch_registry_unit.py +++ b/tests/protocols/test_dispatch_registry_unit.py @@ -167,7 +167,7 @@ def test_every_case_has_a_pinned_target(self) -> None: :data:`~examples.generators.dispatch.PINNED_TARGETS` records the expected class by hand, independently of whatever the corresponding - ``__proto__`` table currently holds -- see that table's own docstring + ``__proto__`` table holds -- see that table's own docstring for why: checking dispatch against a value read out of the very table under test cannot tell a correct entry from a mis-pointed one. A case with no pinned entry has :attr:`Case.target` set to :data:`None`, @@ -197,7 +197,7 @@ def test_registry_currently_matches_pinned_target(self) -> None: :meth:`test_cases_cover_every_table_named_in_the_issue` looks at, nor anything a registry-derived dispatch check would, and both pass. This test does not: it compares - :attr:`Case.registered` -- what the table says right now -- against + :attr:`Case.registered` -- what the table says at run time -- against :attr:`Case.target` -- what it is pinned to say -- and is independent of whether the target dissector can actually parse anything, which is :meth:`test_dispatch_reaches_target_or_is_a_recorded_degrade`'s job. diff --git a/tests/protocols/test_frame_layer_limit_1610_unit.py b/tests/protocols/test_frame_layer_limit_1610_unit.py index 55b8bf82c7..e74e2be48d 100644 --- a/tests/protocols/test_frame_layer_limit_1610_unit.py +++ b/tests/protocols/test_frame_layer_limit_1610_unit.py @@ -230,7 +230,7 @@ def test_a_frame_of_64_layers_is_dissected_to_the_tcp(self) -> None: with self.subTest(shape=shape): parsed, messages = self.frame(ethernet(build(64))) self.assertIn('TCP', parsed) - # the 64 layers, and the TCP payload, left raw as before + # the 64 layers, then the TCP payload, left raw self.assertEqual(len(parsed.protochain), 65) # type: ignore[attr-defined] self.assertEqual(messages, []) diff --git a/tests/protocols/test_next_layer_hit_not_memoised_1559_unit.py b/tests/protocols/test_next_layer_hit_not_memoised_1559_unit.py index c1518d04be..882f2f8546 100644 --- a/tests/protocols/test_next_layer_hit_not_memoised_1559_unit.py +++ b/tests/protocols/test_next_layer_hit_not_memoised_1559_unit.py @@ -4,7 +4,7 @@ This guards against :meth:`ProtocolBase._lookup_next_layer ` writing the class a registered :class:`~pcapkit.corekit.module.ModuleDescriptor` resolved to back -over that descriptor. That has three consequences, one test each: +over that descriptor. That has these consequences, one test each: * after :func:`importlib.reload` of the protocol's module, dispatch keeps reaching the pre-reload class, so a parsed layer fails :func:`isinstance` diff --git a/tests/protocols/test_option_generator_tcp_base_unit.py b/tests/protocols/test_option_generator_tcp_base_unit.py index 85fdc60a08..5be79adf18 100644 --- a/tests/protocols/test_option_generator_tcp_base_unit.py +++ b/tests/protocols/test_option_generator_tcp_base_unit.py @@ -26,9 +26,9 @@ ` check a construction keyword against the protocol's signatures and raise :exc:`~pcapkit.utilities.exceptions.UnsupportedCall` for one that no signature -declares. ``TCP.make`` still ends in ``**kwargs`` and still reads nothing out of -it -- the check is upstream of it -- so the account above remains the account of -how the defect was possible, and +declares. ``TCP.make`` ends in ``**kwargs`` and reads nothing out of it -- the +check is upstream of it -- so the account above is the account of how the +defect was possible, and :meth:`TCPBaseKeywordTests.test_an_undeclared_keyword_is_refused_rather_than_absorbed` is where the refusal is pinned. @@ -238,7 +238,7 @@ def test_tcp_base_is_the_expected_header(self) -> None: The two are maintained side by side on purpose. This is the assertion that fails when somebody edits one of them, which is the reminder that - twenty-five captured frames change with it. + every frame of ``options-tcp.pcap`` changes with it. """ options = _load_generator() @@ -335,8 +335,8 @@ def _build(self, **overrides: 'Any') -> 'Any': def test_the_header_fields_are_the_ones_expected(self) -> None: """Every value the mapping states reaches the field it names. - ``seq`` is the one that moved: the mapping read as ``1`` and the segment - reported ``0``. The rest are asserted alongside it because a fixture + ``seq`` is the field #602 lost: a mapping reading ``1`` built a segment + reporting ``0``. The rest are asserted alongside it because a fixture header nobody has checked is a fixture header that could differ anywhere. diff --git a/tests/protocols/test_option_roundtrip_unit.py b/tests/protocols/test_option_roundtrip_unit.py index 89c935e193..74280ecd11 100644 --- a/tests/protocols/test_option_roundtrip_unit.py +++ b/tests/protocols/test_option_roundtrip_unit.py @@ -277,10 +277,10 @@ def test_every_registered_code_resolves_without_growing_the_registry(self) -> No A registry entry naming a handler that does not exist is a live bug that nothing else catches: dispatch falls through to the fallback and the - option is quietly parsed as unassigned. Two of the ``MH`` registries' - own docstrings name prefixes that were never implemented - (``_read_option_*`` and ``_read_extension_*``, where the code uses - ``_read_opt_*`` and ``_read_ext_*``), so the hazard is not theoretical. + option is quietly parsed as unassigned. ``MH``'s registry docstrings + named prefixes the code never implemented (``_read_option_*`` and + ``_read_extension_*``, for ``_read_opt_*`` and ``_read_ext_*``; #1035), + so the hazard is not theoretical. And every one of these registries is a :class:`collections.defaultdict` on a class attribute, so a lookup that misses *inserts*, permanently, for diff --git a/tests/protocols/test_package_reexports_1807_unit.py b/tests/protocols/test_package_reexports_1807_unit.py index e97cdc848a..15e4cf9a18 100644 --- a/tests/protocols/test_package_reexports_1807_unit.py +++ b/tests/protocols/test_package_reexports_1807_unit.py @@ -32,7 +32,7 @@ import types import unittest -#: The two trees this module covers; each root is itself a package. +#: The trees this module covers; each root is itself a package. ROOTS = ('pcapkit.protocols.data', 'pcapkit.protocols.schema') #: Names a submodule lists in :attr:`__all__` that its package deliberately does diff --git a/tests/protocols/test_pcapng_regression.py b/tests/protocols/test_pcapng_regression.py index 67857841c0..c9c3971474 100644 --- a/tests/protocols/test_pcapng_regression.py +++ b/tests/protocols/test_pcapng_regression.py @@ -12,7 +12,7 @@ RUNTIME_DEPS = ('tbtrim', 'aenum', 'chardet', 'dictdumper') HAS_RUNTIME = all(importlib.util.find_spec(name) is not None for name in RUNTIME_DEPS) -#: Block types that carry captured packet octets, i.e. the three whose schema +#: Block types that carry captured packet octets, i.e. those whose schema #: declares a ``__payload__``. Spelled as the wire values so that the test does #: not have to import the library to know what it is looking for. BLOCK_SHB = 0x0A0D0D0A @@ -113,8 +113,8 @@ def _synthetic_pcapng(endian: str = '<') -> bytes: """A PCAP-NG file holding one block of every payload-carrying type. No committed fixture has a Simple Packet Block or an (obsolete) Packet Block -- - :file:`examples/captures/dhcp.pcapng` is all Enhanced Packet Blocks -- so the - other two are built here rather than generated, which also keeps the expected + :file:`examples/captures/dhcp.pcapng` is all Enhanced Packet Blocks -- so those + are built here rather than generated, which also keeps the expected octets in the same file as the assertion. Both packet blocks that can carry options are given some, which no fixture @@ -256,10 +256,10 @@ def test_dhcp_pcapng_first_block_octets_are_the_expected_ethernet_frame(self) -> def test_every_payload_carrying_block_type_carries_its_octets(self) -> None: """EPB, SPB and the obsolete Packet Block each keep their own payload. - All three declare ``__payload__ = 'packet_data'`` and all three put it at a + Each declares ``__payload__ = 'packet_data'`` and each puts it at a different offset, so a fix that only reached the Enhanced Packet Block -- the only type any committed fixture has -- would pass the test above and - still corrupt the other two. + still corrupt the others. Run over both byte orders. The offset of the captured data is the same in either, but the fields it is summed from are not, so a byte order the walk diff --git a/tests/protocols/test_protocol_code_registration_unit.py b/tests/protocols/test_protocol_code_registration_unit.py index 27a5ff4db2..3e2151009f 100644 --- a/tests/protocols/test_protocol_code_registration_unit.py +++ b/tests/protocols/test_protocol_code_registration_unit.py @@ -10,8 +10,8 @@ gave :class:`~pcapkit.foundation.engines.engine.Engine`, :class:`~pcapkit.foundation.reassembly.reassembly.Reassembly`, :class:`~pcapkit.foundation.traceflow.traceflow.TraceFlow` and -:class:`~pcapkit.dumpkit.common.Dumper` (#557 renamed ``Engine``'s from -``name=``): omitting ``code`` leaves the +:class:`~pcapkit.dumpkit.common.Dumper` (``Engine``'s is ``engine=`` rather +than ``name=``, #557): omitting ``code`` leaves the subclass unregistered, which is exactly what every built-in protocol class does, since the built-in dispatch tables are populated by literal assignment in each layer module rather than by ``__init_subclass__``. diff --git a/tests/protocols/test_short_read_roundtrip_unit.py b/tests/protocols/test_short_read_roundtrip_unit.py index 161b716a13..d82f8726d3 100644 --- a/tests/protocols/test_short_read_roundtrip_unit.py +++ b/tests/protocols/test_short_read_roundtrip_unit.py @@ -154,7 +154,7 @@ def test_issue_1458_repro(self) -> None: eth = Ethernet(raw, 13) self.assertEqual(Ethernet.from_data(eth.info).data, raw) self.assertEqual(Ethernet.from_data(eth.info.to_dict()).data, raw) - # the value decodes as before, the missing octet read as zero + # the value still decodes, the missing octet read as zero self.assertEqual(eth.info.type, 0x0800) def test_issue_1451_repro(self) -> None: diff --git a/tests/protocols/test_trial_parse_warnings_1580_unit.py b/tests/protocols/test_trial_parse_warnings_1580_unit.py index d85775fcc3..3a60693ef8 100644 --- a/tests/protocols/test_trial_parse_warnings_1580_unit.py +++ b/tests/protocols/test_trial_parse_warnings_1580_unit.py @@ -284,7 +284,7 @@ def __length_hint__(self) -> 'int': self.assertEqual(self._schema(caught), ['packet length < 0: -2'] * 2) def test_error_filter_chains_match_holding_nothing(self) -> None: - """Under ``error``, every cut of four frames parses to the chain it has holding nothing. + """Under ``error``, every cut of :data:`FRAMES` parses to the chain it has holding nothing. A warning made an error is raised where the trial that kept it ends, inside the guard it was raised in when nothing was held, so the same diff --git a/tests/protocols/transport/test_mptcp_roundtrip_unit.py b/tests/protocols/transport/test_mptcp_roundtrip_unit.py index d84dca8914..54a568822a 100644 --- a/tests/protocols/transport/test_mptcp_roundtrip_unit.py +++ b/tests/protocols/transport/test_mptcp_roundtrip_unit.py @@ -39,8 +39,8 @@ SYN, ACK = 0x02, 0x10 -#: ``name -> (TCP flags, option octets)``; the octets are padded with leading -#: NOPs to a multiple of 4. +#: ``name -> (TCP flags, option octets)``; the octets are padded with NOPs to +#: a multiple of 4. OPTIONS = { # 1212 'join-syn-reserved': (SYN, '1e0c1e01 00000002 00000003'), diff --git a/tests/protocols/transport/test_sctp_stray_octets_unit.py b/tests/protocols/transport/test_sctp_stray_octets_unit.py index 498d45ba59..85df2ce553 100644 --- a/tests/protocols/transport/test_sctp_stray_octets_unit.py +++ b/tests/protocols/transport/test_sctp_stray_octets_unit.py @@ -29,7 +29,7 @@ COMMON = struct.pack('!HHII', 1, 2, 0, 0) #: COOKIE ACK chunk. COOKIE_ACK = struct.pack('!BBH', 11, 0, 4) -#: DATA chunk carrying ``b'hi'`` under PPID 0, padded to eight octets. +#: DATA chunk carrying ``b'hi'`` under PPID 0: Length 18, padded to 20 octets. DATA = struct.pack('!BBHIHHI', 0, 3, 18, 1, 0, 0, 0) + b'hi' + bytes(2) #: INIT chunk declaring 32 octets, 28 present. INIT_CUT = bytes.fromhex('01000020 00000001 00010000 00010001 00000001 000c0010 00050000') diff --git a/tests/protocols/transport/test_sctp_unit.py b/tests/protocols/transport/test_sctp_unit.py index 5ec87f4d3a..942d6dee10 100644 --- a/tests/protocols/transport/test_sctp_unit.py +++ b/tests/protocols/transport/test_sctp_unit.py @@ -1114,9 +1114,8 @@ def test_unregistered_sub_registry_codes_do_not_mutate_the_class(self) -> None: :meth:`~pcapkit.protocols.transport.sctp.SCTP.register_chunk` and its siblings report an overwrite of something nobody registered. - The chunk type is unassigned by IANA, and both the parameter type and the - error cause code are drawn from the ``0xFFF0``-and-up range :rfc:`9260` - reserves for IETF-Defined extensions, so nothing is expected to register + The chunk type, the parameter type and the error cause code (both + ``0xFFF0``) are all unassigned by IANA, so nothing is expected to register them. """ diff --git a/tests/protocols/transport/test_tcp_http_dispatch_unit.py b/tests/protocols/transport/test_tcp_http_dispatch_unit.py index df887f372f..1e30c49e30 100644 --- a/tests/protocols/transport/test_tcp_http_dispatch_unit.py +++ b/tests/protocols/transport/test_tcp_http_dispatch_unit.py @@ -6,7 +6,8 @@ and 8080, which makes a segment on either port HTTP/1 by assertion of the port number: an HTTP/2 payload there would be refused by the HTTP/1 parser and reported as :class:`~pcapkit.protocols.misc.raw.Raw`. Both versions share those ports on the -wire -- :rfc:`9113#section-3.1` keeps the cleartext form on ``http`` -- so the +wire -- :rfc:`9113#section-3` gives HTTP/2 the ``http`` scheme and HTTP/1.1's +default port numbers -- so the port cannot decide the version and the payload has to, which is what :class:`pcapkit.protocols.application.http.HTTP` does. @@ -53,8 +54,8 @@ def setUp(self) -> None: def test_tcp_binds_the_http_proxy_on_both_http_ports(self) -> None: """Ports 80 and 8080 resolve to ``application.http``, not ``application.httpv1``. - The descriptor is checked rather than the imported class because all three - HTTP classes are named ``HTTP`` (c.f. #682's own subject, the registry key + The descriptor is checked rather than the imported class because every + HTTP class is named ``HTTP`` (c.f. #682's own subject, the registry key collision), so the module name is the only thing that tells them apart. """ diff --git a/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py b/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py index 94f2221892..6c2ac68251 100644 --- a/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_addaddr_unit.py @@ -28,21 +28,21 @@ The same missing fields break *unpacking*, not only packing, and for the identical reason: with no real ``kind``/``length`` fields on the base class, ``MPTCPAddAddress``'s own leading -field (``test``, the subtype/IP-version octet) starts reading one octet too early -- the +field (``test``, the subtype/flags octet) starts reading two octets too early -- the ``kind`` octet itself (``0x1e``) -- rather than the third octet of the option. That is an off-by-two in field alignment, not a wire-format change: the octets a correct sender puts on the wire are right, only the reading of them is shifted. The hand-built, spec-correct wire octets :data:`ADD_ADDR_SPEC_OCTETS` and :data:`MP_PRIO_SPEC_OCTETS` below pin it (neither is built through any ``_make_mptcp_*`` maker, so the pack and parse directions cannot cancel -each other's bug out). Misaligned, ``0x1e`` decodes as subtype ``1``/version ``14``, an IP -version nothing accepts, so -:class:`TCPMPTCPUnpackUnitTests.test_add_addr_spec_octets_parse_correctly` would raise -``FieldError: TCP: [OptNo 30] 3 invalid IP version``; MP_PRIO has no such guard, so -:class:`TCPMPTCPUnpackUnitTests.test_mp_prio_spec_octets_parse_correctly` would instead run -the misaligned bytes as far as ``MPTCPPriority.addr_id``'s own ``pkt['length']``-gated -:class:`~pcapkit.corekit.fields.misc.ConditionalField` and raise ``KeyError: 'length'`` -- -the same exception #541 was filed against, reached from the opposite direction. Both parse -every field exactly as the octets were built. +each other's bug out). Misaligned, ``0x1e`` decodes as subtype ``1``, and both cases then +reach a ``pkt['length']`` read with no ``length`` in the packet mapping: +:meth:`TCPMPTCPUnpackUnitTests.test_add_addr_spec_octets_parse_correctly` in +:func:`~pcapkit.protocols.schema.transport.tcp.mptcp_add_address_selector`, which takes the +address family from the option length (#1217), and +:meth:`TCPMPTCPUnpackUnitTests.test_mp_prio_spec_octets_parse_correctly` in +``MPTCPPriority.addr_id``'s own :class:`~pcapkit.corekit.fields.misc.ConditionalField`. +Either raises ``KeyError: 'length'`` -- the same exception #541 was filed against, reached +from the opposite direction. Both parse every field exactly as the octets were built. """ from __future__ import annotations @@ -132,8 +132,8 @@ class TCPMPTCPAddAddressUnitTests(unittest.TestCase): def test_ipv4_without_port(self) -> None: """``kind=0x1e length=8 test=0x34 addr_id=1 address=1.2.3.4``, no ``port`` octets. - Length is ``4 + 4 + 0 == 8``, one of the two values (with 22) the ``port`` - field's own condition never selects, so no port octets are packed. + Length is ``4 + 4 + 0 == 8``, a length the ``port`` field's own condition + does not select, so no port octets are packed. """ packed = make_addaddr(addr_id=1, addr='1.2.3.4') @@ -145,8 +145,8 @@ def test_ipv4_without_port(self) -> None: def test_ipv4_with_port(self) -> None: """``length=10`` (``4 + 4 + 2``) and the port octets ``01bb`` (443) are appended. - 10 is one of the two values the predicate ``pkt['length'] in (10, 22)`` selects - for, so this is the IPv4 case where ``port`` actually packs. + 10 is one of the lengths the predicate ``pkt['length'] in (10, 18, 22, 30)`` + selects for, so this is the IPv4 case where ``port`` actually packs. """ packed = make_addaddr(addr_id=1, addr='1.2.3.4', port=443) @@ -165,7 +165,7 @@ def test_ipv6_without_port(self) -> None: self.assertEqual(packed[1], 20) def test_ipv6_with_port(self) -> None: - """``length=22`` (``4 + 16 + 2``), the second value the predicate selects for.""" + """``length=22`` (``4 + 16 + 2``), the IPv6 length without an HMAC the predicate selects.""" packed = make_addaddr(addr_id=2, addr='::1', port=443) self.assertEqual( @@ -215,7 +215,7 @@ class TCPMPTCPUnpackUnitTests(unittest.TestCase): """Spec-correct MPTCP option octets parse into a :class:`TCP`, field by field. Every case above builds an option and packs it; none of them asks the library to - *parse* one, so the off-by-two that breaks packing -- ``MPTCPAddAddress`` (and its + *parse* one, so the off-by-two the same missing fields cause -- ``MPTCPAddAddress`` (and its siblings) reading the ``kind`` octet as their own first field, when ``MPTCP`` supplies no real ``kind``/``length`` fields for anything ahead of it to consume -- needs pinning on the read side too. These two do not construct anything @@ -228,12 +228,11 @@ class TCPMPTCPUnpackUnitTests(unittest.TestCase): def test_add_addr_spec_octets_parse_correctly(self) -> None: """RFC 8684 section 3.4.1's IPv4 ``ADD_ADDR``, addr_id=1, 192.0.2.1. - Guards against ``FieldError: TCP: [OptNo 30] 3 invalid IP version``, raised when - ``MPTCPAddAddress.test`` reads the ``kind`` octet ``0x1e`` (``0001 1110``) as its - own subtype/version octet, decoding ``version=14``, which a selector keyed on the - version would reject outright since it is neither 4 nor 6. - :func:`~pcapkit.protocols.schema.transport.tcp.mptcp_add_address_selector` takes - the address family from the option length instead (#1217). + Guards against ``KeyError: 'length'``, raised when ``MPTCPAddAddress.test`` reads + the ``kind`` octet ``0x1e`` (``0001 1110``) as its own subtype/flags octet and + :func:`~pcapkit.protocols.schema.transport.tcp.mptcp_add_address_selector`, which + takes the address family from the option length (#1217), finds no ``length`` + read ahead of it. """ from pcapkit.const.tcp.option import Option as Enum_Option @@ -253,8 +252,7 @@ def test_add_addr_spec_octets_parse_correctly(self) -> None: def test_mp_prio_spec_octets_parse_correctly(self) -> None: """RFC 8684 section 3.3.8's ``MP_PRIO`` with the optional Address ID, backup set. - Guards against ``KeyError: 'length'``: ``MP_PRIO`` carries no IP-version guard - the way ``ADD_ADDR`` does, so the same off-by-two misalignment would run on + Guards against ``KeyError: 'length'``: the same off-by-two misalignment runs on through to ``MPTCPPriority.addr_id``'s own ``ConditionalField(UInt8Field(), lambda pkt: pkt['length'] == 4)`` -- the same exception #541 was filed against, reached by parsing rather than by packing. diff --git a/tests/protocols/transport/test_tcp_mptcp_capable_length_unit.py b/tests/protocols/transport/test_tcp_mptcp_capable_length_unit.py index 00e29f63d3..6e162f20f9 100644 --- a/tests/protocols/transport/test_tcp_mptcp_capable_length_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_capable_length_unit.py @@ -3,16 +3,16 @@ GitHub issue #567, with the remaining lengths from #1042. -Three sites that must agree ---------------------------- +The sites that must agree +------------------------- :rfc:`8684` section 3.1 gives ``MP_CAPABLE`` as **12** octets without the receiver's key and -**20** octets with it. Three sites encode that split -- the maker -``TCP._make_mptcp_capable``, the predicate on ``MPTCPCapable.rkey``, and the reader -``TCP._read_mptcp_capable`` -- and the tests here guard each against its own wrong constant: +**20** octets with it. The maker ``TCP._make_mptcp_capable``, the predicate on +``MPTCPCapable.rkey``, and the reader ``TCP._read_mptcp_capable`` all encode that split, and +the tests here guard each against its own wrong constant: * a maker writing ``length=20 if rkey is None else 32`` -- both branches wrong, and the no-key - branch writing the value RFC 8684 assigns to the *other* case. Measured with all three sites + branch writing the value RFC 8684 assigns to the *other* case. Measured with every site wrong, ``rkey=None`` packed a *20*-octet option carrying no key octets, and an explicit ``rkey`` packed a *32*-octet option whose last 12 octets are entirely unaccounted for by the RFC 8684 figure; diff --git a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py index cfc7f37a1c..1c86b42207 100644 --- a/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_error_message_unit.py @@ -11,13 +11,13 @@ it is the string a user sees when an MP_JOIN or DSS option is rejected, and an empty field reads as a value that failed to interpolate. -The correct form is not a matter of taste: every other message in the same file -spells the prefix ``f'{self.alias}: [OptNo {schema.kind}] …'``, so a doubled one -is an outlier against a house form in its own module. +The correct form is not a matter of taste: every other option message in the same +file spells the prefix ``f'{self.alias}: [OptNo …] …'`` with a single separator, so +a doubled one is an outlier against a house form in its own module. Every message here is asserted **in full** rather than by substring. A substring -assertion on ``'invalid flags combination'`` matches both forms, so the two -places that also exercise these messages cannot catch the doubling -- +assertion on ``'invalid flags combination'`` matches both forms, so the other +places that exercise these messages cannot catch the doubling -- * ``TCPMPTCPJoinFlagOrderingUnitTests.test_neither_syn_nor_ack_raises_protocol_error`` in ``test_tcp_mptcp_join_flag_ordering_unit.py``, which asserts diff --git a/tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py b/tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py index 4744b28ae2..9adeacbb08 100644 --- a/tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_join_flag_ordering_unit.py @@ -122,8 +122,8 @@ #: Header fields shared by every constructed segment here, spelled with the parameter #: names :meth:`TCP.make ` actually declares. #: -#: Deliberately *not* a copy of :data:`examples.generators.options.TCP_BASE`, which the -#: sibling modules in this directory reuse -- and, unlike them, this dict omits +#: Deliberately *not* a copy of :data:`examples.generators.options.TCP_BASE`, which +#: sibling modules in this directory mirror -- and, unlike theirs, this dict omits #: ``syn``/``ack`` altogether, since every case here passes those two explicitly to #: select a layout, where a shared constant could not. The names matter: ``ack`` is the #: *acknowledgement flag*, not ``ack_no``, and ``make`` raises ``UnsupportedCall`` for any @@ -631,7 +631,7 @@ class TCPMPTCPJoinReadPathUnitTests(unittest.TestCase): """The control: the parse path dispatches each layout to its own reader. #587 is a defect on the make path only, and its ordering lives inside ``make``. These - are regression guards rather than new coverage -- they fail only if reordering + guard the read path rather than #587 itself -- they fail only if reordering ``make`` disturbs something it is not supposed to touch. """ diff --git a/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py b/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py index a1d317ff90..52771910bd 100644 --- a/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_length_arithmetic_unit.py @@ -4,11 +4,11 @@ GitHub issue #576: the check of the sibling ``_make_mptcp_*`` helpers' length arithmetic that #567 suggests, outside the scope of #579 (``MPTCP.subtype`` and MP_CAPABLE). -An MPTCP option carries its own octet count in its ``Length`` field, and three separate -places have to agree on it: the ``_make_mptcp_*`` maker that *writes* that octet, the schema +An MPTCP option carries its own octet count in its ``Length`` field, and each of these +places has to agree on it: the ``_make_mptcp_*`` maker that *writes* that octet, the schema in :mod:`pcapkit.protocols.schema.transport.tcp` whose fields actually *pack* the octets, and the ``_read_mptcp_*`` reader whose guard *accepts* it coming back. These tests guard against -any of the three disagreeing with :rfc:`8684` at each site below; at MP_FASTCLOSE all three can +any of them disagreeing with :rfc:`8684` at each site below; at MP_FASTCLOSE all of them can disagree with each other. What each site is, and where its number comes from diff --git a/tests/protocols/transport/test_tcp_mptcp_subtype_unit.py b/tests/protocols/transport/test_tcp_mptcp_subtype_unit.py index caf73e80fe..a99d48226b 100644 --- a/tests/protocols/transport/test_tcp_mptcp_subtype_unit.py +++ b/tests/protocols/transport/test_tcp_mptcp_subtype_unit.py @@ -192,8 +192,9 @@ def test_mp_prio_subtype_round_trips(self) -> None: def test_dss_subtype_round_trips(self) -> None: """``DSS`` built through ``TCP()`` reports its own subtype. - ``ack`` alone (no ``dsn``) keeps clear of the unrelated length-arithmetic defect - of #576, which is not what this test is about. + ``ack`` alone (no ``dsn``) keeps the case off the DSS length arithmetic of #576, + which :mod:`tests.protocols.transport.test_tcp_mptcp_length_arithmetic_unit` + covers and which is not what this test is about. """ from pcapkit.const.tcp.mp_tcp_option import MPTCPOption as Enum_MPTCPOption diff --git a/tests/protocols/transport/test_tcp_udp_unit.py b/tests/protocols/transport/test_tcp_udp_unit.py index 03aa83ce78..404ea962f4 100644 --- a/tests/protocols/transport/test_tcp_udp_unit.py +++ b/tests/protocols/transport/test_tcp_udp_unit.py @@ -1262,13 +1262,13 @@ def test_construction_accepts_bare_integer_ports(self) -> None: """``TCP(srcport=80, dstport=443)`` is the documented construction path. :meth:`TCP.make ` advertises - ``Enum_AppType | int``, but the schema field converted an :obj:`int` only - on its way out to :obj:`bytes`, so the schema attribute kept the - :obj:`int` -- and :meth:`TCP.read + ``Enum_AppType | int``. Guards against the schema field converting an + :obj:`int` only on its way out to :obj:`bytes`, which leaves the schema + attribute an :obj:`int` -- and :meth:`TCP.read `, which reads ``srcport.port`` - to pick the next layer, raised ``AttributeError: 'int' object has no - attribute 'port'``. UDP failed identically; SCTP did not, only because it - keys its next layer on the DATA chunk's PPID and never reads ``.port``. + to pick the next layer, then raises ``AttributeError: 'int' object has no + attribute 'port'``. UDP is exposed identically; SCTP is not, only because + it keys its next layer on the DATA chunk's PPID and never reads ``.port``. """ from pcapkit.const.reg.apptype import AppType, TransportProtocol @@ -1301,8 +1301,8 @@ def test_a_fast_open_cookie_request_parses_as_empty_bytes(self) -> None: :rfc:`7413` sends a cookie request as option ``22 02``: length 2 and no cookie. The data model annotates ``cookie`` as :class:`bytes`, and a segment built with ``cookie=b''`` reads back ``b''``, so a parsed one - reading :obj:`None` broke both the annotation and the parse/construct - agreement. Both round trips are checked byte for byte. + reading :obj:`None` would break both the annotation and the + parse/construct agreement. Both round trips are checked byte for byte. """ import struct @@ -1541,8 +1541,8 @@ def test_unregistered_mptcp_subtype_does_not_mutate_the_class_registry(self) -> packet context does not carry -- so it raises before :meth:`~pcapkit.protocols.transport.tcp.TCP._read_mode_mp` ever looks the subtype up. Construction reaches the sibling read site in - :meth:`~pcapkit.protocols.transport.tcp.TCP._make_mode_mp`, which had the - identical defect. + :meth:`~pcapkit.protocols.transport.tcp.TCP._make_mode_mp`, which is open to + the identical defect. """ from pcapkit.const.tcp.mp_tcp_option import MPTCPOption diff --git a/tests/protocols/transport/test_transport_unit.py b/tests/protocols/transport/test_transport_unit.py index 588a43b2ba..8556180b43 100644 --- a/tests/protocols/transport/test_transport_unit.py +++ b/tests/protocols/transport/test_transport_unit.py @@ -15,7 +15,7 @@ @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class TransportUnitTests(unittest.TestCase): def setUp(self) -> None: - # ``isolate_modules`` rather than ``purge_modules``: two tests below bind + # ``isolate_modules`` rather than ``purge_modules``: tests below bind # stand-ins over ``pcapkit.protocols.misc.raw`` and ``...misc.null``, which # are on the import path of most of the package. See #660. isolate_modules(self) diff --git a/tests/test_base_class_contract.py b/tests/test_base_class_contract.py index 7f1729ccbf..f73216b93b 100644 --- a/tests/test_base_class_contract.py +++ b/tests/test_base_class_contract.py @@ -19,14 +19,14 @@ Two different things are checked here, and the split between them is deliberate because only one of them is a behaviour claim. -:class:`BaseClassAliasTests` is the part with teeth. Until #514 part (c), -import statements across the library read ``from ... import ProtocolBase as -Protocol`` -- binding the *base* to the *public* name -- so a module went on to -say ``class Application(Protocol)`` while in fact inheriting ``ProtocolBase``. -Nothing was wrong at runtime; the classes were correct. What was wrong is that -the source said the opposite of what it did, and that is not a cosmetic -complaint: the aliasing was a *convention* rather than a mechanism, so a sweep -over it could miss a site and nothing would fail. It missed one twice -- +:class:`BaseClassAliasTests` is the part with teeth. It guards against an +import such as ``from ... import ProtocolBase as Protocol`` -- binding the +*base* to the *public* name -- after which a module says +``class Application(Protocol)`` while in fact inheriting ``ProtocolBase``. +Nothing is wrong at runtime; the classes are correct. What is wrong is that +the source says the opposite of what it does, and that is not a cosmetic +complaint: aliasing kept in check by *convention* rather than by a mechanism +lets a sweep miss a site with nothing failing. A sweep missed one twice -- issue #506 (a runtime import in ``schema/schema.py`` unreachable for three years) and issue #513 (the ``extraction.py`` gates rejecting the library's own built-ins for three years). These tests replace the convention with @@ -62,7 +62,7 @@ ``tests.protocols.application.test_http_unit`` run before this module, one process, plain :mod:`unittest`, desyncs :class:`RegistrationGateTests.test_user_style_subclass_registers_when_it_opts_in` - on all four suites. That test's own ``setUp`` calls + on every suite it covers. That test's own ``setUp`` calls :func:`tests._support.purge_modules` on ``pcapkit`` and re-imports it from source, which mints a *second generation* of every ``pcapkit`` class -- deliberately asymmetric, see that function's own docstring, and ordinarily @@ -200,11 +200,11 @@ class BaseClassAliasTests(unittest.TestCase): def test_no_library_module_imports_a_base_under_its_public_name(self) -> None: """Source-level sweep: the alias must not appear. - This is the source-level half, and it is the half that sees the ~53 - aliases that live inside ``if TYPE_CHECKING:``. Those have no runtime - effect at all -- they exist so an annotation can say ``Protocol`` -- so + This is the source-level half, and it is the half that sees an alias + inside ``if TYPE_CHECKING:``. Such an alias has no runtime effect at + all -- it exists so an annotation can say ``Protocol`` -- so :meth:`test_library_modules_do_not_bind_the_public_name_at_runtime` - cannot see them and this one has to. + cannot see it and this one has to. """ offenders = {} diff --git a/tests/test_docstring_contract.py b/tests/test_docstring_contract.py index a5c1b0c50f..9e7eea997a 100644 --- a/tests/test_docstring_contract.py +++ b/tests/test_docstring_contract.py @@ -70,15 +70,10 @@ the ``**kwargs`` exclusion exists to prevent. That exclusion has a known cost, recorded so it is not mistaken for coverage. -:meth:`Reassembly.__init_subclass__ -` and -:meth:`TraceFlow.__init_subclass__ -` both -document ``name`` where the parameter is ``protocol``, and both have -``**kwargs``, so both are invisible here even though their bodies use -``protocol`` and never read ``kwargs['name']``. Catching those needs the intent -behind a keyword, not just its absence from the signature -- ``src_ip`` and -``dst_ip`` on :meth:`IPv6_Route.__post_init__ +A function with ``**kwargs`` that documents ``name`` where its parameter is +``protocol``, and whose body never reads ``kwargs['name']``, is invisible here. +Catching that needs the intent behind a keyword, not just its absence from the +signature -- ``src_ip`` and ``dst_ip`` on :meth:`IPv6_Route.__post_init__ ` look identical and are very likely legitimate keywords forwarded to ``super()``. Guessing between the two would fail correct docstrings, so this module does not @@ -352,8 +347,8 @@ def _raised_names(node: 'ast.AST') -> 'set[str]': ``ProtocolError`` and raises none itself, but its ``_read_locator`` helper does and it calls that helper once per locator. Attributing the raise outward is therefore correct there, and the clause is not a phantom. It can - mislead for a nested ``def`` that is *returned* rather than called, and four - of the six are: the wrappers inside + mislead for a nested ``def`` that is *returned* rather than called, and + several are: the wrappers inside :func:`~pcapkit.corekit.decorators.prepare` and :mod:`pcapkit.protocols.schema.misc.pcapng`'s ``bounded_option``, and the methods of the classes :func:`~pcapkit.dumpkit.common.make_dumper` and the @@ -378,10 +373,12 @@ def _calls_anything_callable(node: 'ast.AST') -> 'bool': """Whether the body calls a method or free function that could raise. Data-model construction is excluded: this project's ``_read_*`` methods - build :class:`~pcapkit.corekit.infoclass.Info` subclasses whose names begin - ``Data_`` or ``Schema_``, and neither those nor :meth:`Info.__update__` can - raise a protocol error -- ``ProtocolError`` appears nowhere in - :file:`pcapkit/corekit/` outside one comment. Anything else counts as + build :class:`~pcapkit.corekit.infoclass.Info` subclasses named ``Data_*`` + and :class:`~pcapkit.protocols.schema.schema.Schema` subclasses named + ``Schema_*``, and neither those nor :meth:`Info.__update__` can raise a + protocol error -- ``ProtocolError`` appears in neither + :mod:`pcapkit.corekit.infoclass` nor :mod:`pcapkit.protocols.schema.schema`. + Anything else counts as possibly-raising, which is what keeps :func:`_reachable` conservative. """ @@ -594,7 +591,7 @@ def test_commented_out_raise_leaves_no_clause_behind(self) -> 'None': :meth:`DocstringRaisesTests.test_documented_exceptions_are_reachable`, and ``_read_http_none`` is precisely the case that proves it. Of the six phantom clauses #519 counted, that reachability check finds only five: - ``_read_http_none``'s body opens ``if any(header.flags):``, and ``any`` + ``_read_http_none``'s body opens ``if any(header.flags.values()):``, and ``any`` is not in :func:`_calls_anything_callable`'s inert-call list, so the function is judged possibly-raising and is never reported. Widening that list until it caught this one would be the wrong fix -- every name added diff --git a/tests/test_final_enforcement.py b/tests/test_final_enforcement.py index 42bd8646c5..13a3ff5c3e 100644 --- a/tests/test_final_enforcement.py +++ b/tests/test_final_enforcement.py @@ -60,7 +60,8 @@ :meth:`DecoratorCombinationTests` The four rows above, both families, with ``@info_final @final`` and ``@final @info_final`` asserted to agree rather than merely each asserted to work -- - order-independence is the property that broke, so it is the property stated. + order-independence is the property a ``__final__``-keyed re-entry check + breaks, so it is the property stated. :meth:`AncestryWalkTests` The check walks the whole MRO rather than ``__bases__``, so a *grandchild* @@ -68,11 +69,11 @@ leave a one-line way round the rule. :meth:`VersionPortabilityTests` - ``typing.final`` only records ``__final__`` from Python 3.11 on, and 3.10 is - in the CI matrix. A guard reading the dunder is therefore a silent no-op on - 3.10 unless ``final`` comes from :mod:`pcapkit.utilities.compat`, whose - shim asks ``typing_extensions`` for it below 3.11. This is the test that - would have caught that, and the one that fails first if the import is ever + ``typing.final`` only records ``__final__`` from Python 3.11 on, and the CI + matrix runs older versions. A guard reading the dunder is therefore a silent + no-op below 3.11 unless ``final`` comes from :mod:`pcapkit.utilities.compat`, + whose shim asks ``typing_extensions`` for it below 3.11. This is the test + that catches that, and the one that fails first if the import is ever pointed back at :mod:`typing`. """ @@ -531,11 +532,12 @@ def test_finalising_records_the_dunder_in_the_class_namespace(self) -> None: """Without this the guard is a silent no-op below Python 3.11. :func:`typing.final` gained the ``__final__`` side effect in 3.11 - (gh-90500); before that it returns the class untouched. 3.10 is in the - test matrix, so a guard reading the dunder needs ``final`` to come from - :mod:`pcapkit.utilities.compat`, which asks ``typing_extensions`` for it - below 3.11. On 3.11 and up this assertion holds either way -- it is 3.10 - where it is load-bearing, and it is cheap enough to run everywhere. + (gh-90500); before that it returns the class untouched. The test matrix + runs older versions, so a guard reading the dunder needs ``final`` to + come from :mod:`pcapkit.utilities.compat`, which asks + ``typing_extensions`` for it below 3.11. On 3.11 and up this assertion + holds either way -- it is below 3.11 where it is load-bearing, and it is + cheap enough to run everywhere. """ from pcapkit.corekit.infoclass import Info, info_final @@ -556,8 +558,9 @@ def test_both_modules_take_final_from_the_compat_shim(self) -> None: """Pinned by identity, because the failure is silent on one version only. Pointing either import back at :mod:`typing` would leave every test - above passing on 3.11+ and every one of them failing on 3.10, i.e. a - single matrix leg going red for a reason nothing in the diff explains. + above passing on 3.11+ and every one of them failing below 3.11, i.e. + only the older matrix legs going red for a reason nothing in the diff + explains. Asserting the identity says which import is the load-bearing one. """ @@ -614,9 +617,9 @@ class DecoratorCombinationTests(unittest.TestCase): """One test per row of the table in this module's docstring, both families. Every case exercises the **generated constructor** rather than inspecting - markers, because that is where the defect showed: a class can carry both + markers, because that is where the defect shows: a class can carry both markers, report itself finalised, and still be unusable. Checking - ``__final__`` alone would have passed on the broken code. + ``__final__`` alone passes on such a class. """ @@ -675,10 +678,10 @@ def test_info_final_and_final_agree_in_either_order(self) -> None: Decorators apply bottom-up, so the first spelling runs ``final`` first and reaches :func:`info_final` with ``__final__`` already set. Keying the - re-entry check on that marker made this spelling warn and skip the - generation while the other spelling did the work -- so the two are + re-entry check on that marker makes this spelling warn and skip the + generation while the other spelling does the work -- so the two are asserted to *agree*, not merely each asserted to pass. That is the - property that broke, and comparing them is the only way to state it. + property at stake, and comparing them is the only way to state it. """ from pcapkit.corekit.infoclass import Info, info_final @@ -703,13 +706,13 @@ class TopDown(Info): self.assert_silently_finalised(cls, lambda c: list(c(1).to_dict().items(multi=True)), [('x', 1)]) def test_schema_final_and_final_agree_in_either_order(self) -> None: - """The schema-side half, where the old failure was *silent*. + """The schema-side half, where the failure is *silent*. Fielded rather than field-less, for the reason given in the baseline test above -- a field-less schema cannot distinguish "finalised and usable" - from "silently unfinalised". With a real field, the old defect's shape is - exactly what a field-less class could not show: an unfinalised schema has - an empty :attr:`__excluded__`, so rather than raising it returned its own + from "silently unfinalised". With a real field, the defect's shape is + exactly what a field-less class cannot show: an unfinalised schema has + an empty :attr:`__excluded__`, so rather than raising it returns its own bookkeeping -- ``{'__map__': {}, '__map_reverse__': {}, '__buffer__': {}, '__updated__': True}`` -- from :meth:`to_dict`, with ``x`` nowhere in it. @@ -812,7 +815,7 @@ class Mismarked(Info): self.assertEqual(Mismarked.__finalised__, FinalisedState.NONE) def test_schema_refusal_repeats_rather_than_firing_once(self) -> None: - """The schema-side half of the test above, which had no counterpart.""" + """The schema-side half of the test above.""" from pcapkit.corekit.infoclass import FinalisedState from pcapkit.protocols.schema.schema import Schema from pcapkit.utilities.compat import final @@ -829,7 +832,7 @@ class Mismarked(Schema): self.assertEqual(Mismarked.__finalised__, FinalisedState.NONE) def test_a_prior_bare_info_construction_does_not_defeat_the_guard(self) -> None: - """Order independence is the property #778's cross-review found broken. + """Order independence, against a bare ``Info()`` built first (#778's cross-review). :meth:`Info.__new__` auto-finalises an undecorated class to :attr:`~FinalisedState.BASE` on its first construction -- including @@ -838,11 +841,11 @@ def test_a_prior_bare_info_construction_does_not_defeat_the_guard(self) -> None: ``BASE`` by that bare construction, every subclass declared afterwards would read ``BASE`` off ``Info`` and never see its own ``FinalisedState.NONE`` -- silently defeating the bare-``@final`` guard - nested inside that branch for the rest of the process. This is exactly - what made :meth:`test_the_refusal_repeats_rather_than_firing_once` above + nested inside that branch for the rest of the process. That would make + :meth:`test_the_refusal_repeats_rather_than_firing_once` above order-dependent on whatever else in the suite had already bare-constructed - ``Info()`` -- nothing in this file did, at time of writing, so it passed - by luck rather than by guarantee. This test pins the guarantee directly + ``Info()``, passing by luck rather than by guarantee. This test pins the + guarantee directly by constructing ``Info()`` bare first, in this same process, immediately before declaring the mismarked class. @@ -895,17 +898,17 @@ class MismarkedAfterBareSchema(Schema): self.assertIn('never finalised', message) def test_repeated_bare_info_construction_does_not_grow_excluded_without_bound(self) -> None: - """The cost of the ``cls is not Info`` fix, pinned rather than assumed. + """The cost of the ``cls is not Info`` guard, pinned rather than assumed. ``Info`` never reaches :attr:`~FinalisedState.BASE`, so a bare ``Info()`` would re-enter :func:`info_final` on *every* call rather than once, and ``cls.__excluded__.extend(cls.__builtin__)`` would append the - same ~60 names again each time -- correctness untouched (``__excluded__`` + same names again each time -- correctness untouched (``__excluded__`` is only ever read for membership), but the list growing without bound, with the cost resurfacing at every later subclass declaration, since :class:`InfoMeta`'s own dedup pass is then against a base list that keeps - growing. Fixed by short-circuiting the re-entry itself for ``Info``, via - its own ``__base_ready__`` marker -- + growing. The re-entry itself is short-circuited for ``Info``, via its own + ``__base_ready__`` marker -- :meth:`test_repeated_bare_info_construction_does_not_re_enter_the_base_setup` below pins that the short-circuit is what closes it, not merely that the length happens to stay put. @@ -938,13 +941,12 @@ def test_repeated_bare_schema_construction_does_not_grow_excluded_without_bound( self.assertEqual(len(Schema.__excluded__), len(set(Schema.__excluded__))) def test_repeated_bare_info_construction_does_not_re_enter_the_base_setup(self) -> None: - """The assertion that would have caught both this round and the last. + """Bare construction skips the base setup, rather than redoing it to the same result. - A dedup'd ``extend`` also keeps ``__excluded__``'s *length* bounded, but - it does so by redoing the ``dir()``-over-the-MRO scan and rebuilding - ``cls.__builtin__`` on every single bare call -- the previous round's - fix, and the reason bare construction got 41x-49x slower rather than - cheaper. ``cls.__builtin__ = set(temp)`` is a *reassignment*, so its + A dedup'd ``extend`` would also keep ``__excluded__``'s *length* bounded, + but only by redoing the ``dir()``-over-the-MRO scan and rebuilding + ``cls.__builtin__`` on every single bare call, which makes bare + construction 41x-49x slower rather than cheaper. ``cls.__builtin__ = set(temp)`` is a *reassignment*, so its identity changes every time the base setup actually reruns, even when the resulting set looks the same; a short-circuit that truly skips the work leaves that same object in place. Identity is what tells "skipped" @@ -954,9 +956,8 @@ def test_repeated_bare_info_construction_does_not_re_enter_the_base_setup(self) A held reference, not a bare ``id()`` snapshot: comparing ``id()`` values across separate calls is unreliable the moment the first object is garbage-collected, since CPython is then free to hand the very next - ``set()`` of the same size the same address -- which is exactly what let - this assertion pass by coincidence against the previous round's fix on - one of the two classes. Keeping ``marker`` alive as the object itself, + ``set()`` of the same size the same address, so an ``id()`` comparison + can pass by coincidence on a class that did redo the work. Keeping ``marker`` alive as the object itself, not its address, is what makes the ``is`` check below trustworthy. """ diff --git a/tests/test_support_helpers.py b/tests/test_support_helpers.py index ea3fa20b2e..ed83aaf3ec 100644 --- a/tests/test_support_helpers.py +++ b/tests/test_support_helpers.py @@ -1,28 +1,29 @@ # -*- coding: utf-8 -*- """Tests for the helpers in :mod:`tests._support`. -Two of them are pinned here, both for the same reason: they are *machinery* the -rest of the suite leans on, so a fault in either reports itself as a failure in -whichever test happened to be running rather than as a fault in the helper. -:func:`~tests._support.time_limit` is covered by :class:`TimeLimitTests` at the -end; the rest of the module is :func:`~tests._support.close_extractor`. - -That helper is teardown machinery: nearly every runtime and integration test -hands it an extractor from ``addCleanup`` or a ``finally`` block. Teardown code -is exactly the code whose bugs stay invisible -- a leak leaks silently, and a -teardown that raises reports itself as a failure in whichever test happened to -be running rather than as a fault in the helper. So the contract is pinned here -rather than left to be inferred from the call sites. - -Three things are worth pinning, and they are the three ways this could rot: +They are pinned here for one reason: they are *machinery* the rest of the +suite leans on, so a fault in one reports itself as a failure in whichever test +happened to be running rather than as a fault in the helper. The module opens +with :func:`~tests._support.close_extractor`; :func:`~tests._support.time_limit` +is covered by :class:`TimeLimitTests`, and :func:`~tests._support.stand_in_reason` +by :class:`StandInReasonTests` at the end. + +:func:`~tests._support.close_extractor` is teardown machinery: nearly every +runtime and integration test hands it an extractor from ``addCleanup`` or a +``finally`` block. Teardown code is exactly the code whose bugs stay invisible +-- a leak leaks silently, and a teardown that raises reports itself as a failure +in whichever test happened to be running rather than as a fault in the helper. +So the contract is pinned here rather than left to be inferred from the call +sites, as the ways it could rot: * both resources are released, not just the input file - (:class:`ClosesBothTests`) -- the original helper closed ``_ifile`` alone and - leaked the engine's :class:`pcap.pcap` handle on every abandoned extractor; + (:class:`ClosesBothTests`) -- a helper closing ``_ifile`` alone leaks the + engine's :class:`pcap.pcap` handle on every abandoned extractor; * one broken resource cannot strand the other (:class:`IndependenceTests`); * nothing it is handed in teardown makes it raise - (:class:`ToleranceTests`). + (:class:`ToleranceTests`), while a :exc:`KeyboardInterrupt` still gets + through (:class:`PropagationTests`). The module-isolation helpers are pinned here too, for the same reason and with more cause: :class:`SnapshotRestoreTests` and @@ -341,9 +342,9 @@ def test_the_deadline_fires_on_a_body_that_overruns(self) -> None: def test_an_enclosing_alarm_is_restored(self) -> None: """An outer deadline keeps counting down across the ``with``. - Before this was fixed the helper cancelled the pending alarm on the way out - and never re-armed it, so an outer ``signal.alarm(30)`` read back as ``0`` - afterwards: the enclosing deadline was gone, silently. + Without the fix, the helper cancels the pending alarm on the way out and + never re-arms it, so an outer ``signal.alarm(30)`` reads back as ``0`` + afterwards: the enclosing deadline is gone, silently. """ own_handler = signal.getsignal(signal.SIGALRM) @@ -434,10 +435,11 @@ def test_a_malformed_scale_is_refused_before_anything_is_armed(self) -> None: def test_an_oversized_scale_is_refused_and_leaks_no_handler(self) -> None: """A scale past a day is refused, not handed to ``signal.alarm``. - ``1e9`` once reached :func:`signal.alarm` as an int past its C ``int``, - which raised :exc:`OverflowError` after the helper's handler was installed - and before anything would restore it, so the handler leaked into every test - that ran afterwards. ``1e308`` overflows the float product itself. + Without the refusal, ``1e9`` reaches :func:`signal.alarm` as an int past + its C ``int``, which raises :exc:`OverflowError` after the helper's handler + is installed and before anything would restore it, so the handler leaks + into every test that runs afterwards. ``1e308`` overflows the float + product itself. """ own_handler = signal.getsignal(signal.SIGALRM) @@ -545,9 +547,10 @@ def test_a_raise_an_except_oserror_drops_is_raised_again(self) -> None: def test_a_stall_before_the_try_still_leaves_a_raise_to_drop(self) -> None: """#1902: a runner stalled past the deadline before the body's ``try`` changes nothing. - Armed as the ``with`` was entered, the deadline came due in the stall, - before the ``try`` meant to drop its first raise. That raise went straight - through and none was dropped. + Armed as the ``with`` is entered, the deadline would come due in the + stall, before the ``try`` meant to drop its first raise, and that raise + would go straight through with none dropped. :meth:`held_deadline` arms + it from :meth:`spin` instead, inside the ``try``. """ own_handler = signal.getsignal(signal.SIGALRM) @@ -628,7 +631,7 @@ def test_a_repeat_still_pending_as_the_block_is_left_is_discarded(self) -> None: GraalPy delivers a signal asynchronously, so one that fired just before the cancel can still be pending once the previous handler is back -- under - pytest that is ``SIG_DFL``, and it ended the worker (#2084). Blocking the + pytest that is ``SIG_DFL``, and it ends the worker (#2084). Blocking the signal stands in for that delay here: it is sent from inside the cancel and stays pending until the ``with`` has been left, and this class's handler must not then receive it. @@ -860,11 +863,13 @@ class LoadedModulesDoNotOutliveTheirTestTests(unittest.TestCase): """ - #: Borrowed test cases, as ``module`` / ``class`` pairs. The first three reach - #: the loaders through :func:`~tests._support.bootstrap_core_modules`, the - #: last two through :func:`~tests._support.load_module` on its own. Both - #: routes installed stubs, so both are pinned -- fixing only the former would - #: have left :mod:`tests.utilities.test_compat` leaking. + #: Borrowed test cases, as ``module`` / ``class`` pairs. The ``corekit`` + #: multidict and io cases and the exceptions/warnings case reach the loaders + #: through :func:`~tests._support.bootstrap_core_modules`; the + #: ``test_module`` and ``test_compat`` cases call + #: :func:`~tests._support.load_module` on its own. Both routes install stubs, + #: so both are pinned -- a fix to the former alone would leave + #: :mod:`tests.utilities.test_compat` leaking. BORROWED = ( ('tests.corekit.test_multidict', 'MultiDictTests'), ('tests.corekit.test_io', 'SeekableReaderTests'), @@ -964,10 +969,10 @@ def test_the_first_snapshot_is_the_one_kept(self) -> None: """Repeated calls register one restore, to the table before any load. :func:`~tests._support.bootstrap_core_modules` announces itself and then - makes seven :func:`~tests._support.load_module` calls, each of which + makes a run of :func:`~tests._support.load_module` calls, each of which announces itself too. Re-snapshotting on the later calls would capture a table that already held the earlier loads, and restoring to *that* would - leave them bound -- which is the original bug with more steps. + leave them bound -- which is #674 with more steps. """ probe = _bare_case() @@ -1024,8 +1029,8 @@ def test_an_idle_test_case_in_a_local_named_self_is_not_used(self) -> None: :class:`~unittest.TestCase`, it would win the walk on the name alone -- and a cleanup registered on a test that has already finished is never run, so the stubs outlive the test exactly as in #674 but with no error to show - for it. Measured before :func:`~tests._support._is_running_test` was - consulted: names left bound, silently. + for it. Measured without the :func:`~tests._support._is_running_test` + check: names left bound, silently. Contrived, and deliberately so: no call site in the suite looks like this, which is the point. The heuristic has to be sound for the call sites diff --git a/tests/test_tier_guard.py b/tests/test_tier_guard.py index 6fd19d8d47..78136c6456 100644 --- a/tests/test_tier_guard.py +++ b/tests/test_tier_guard.py @@ -7,7 +7,7 @@ quietly stopped working is worse than no guard, because everybody has stopped looking. -Four things are worth pinning, and they are the four ways this could rot: +These are worth pinning, as the ways this could rot: * the tier rule here still matches the one CI runs (:class:`TierClassificationTests`, :class:`WorkflowAgreementTests`); @@ -20,7 +20,7 @@ * the suite as it stands is clean (:class:`SuiteIsCleanTests`), and a checkout without git degrades instead of failing (:class:`DegradationTests`). -A fifth invariant lives next door and is checked here for the same reason, from +Another invariant lives next door and is checked here for the same reason, from :class:`DependencyRequirementTests` onwards: the tier rule decides *which* job collects a test, and :mod:`tests._dependency_gates` decides whether that job installs what the test is gated on. A ``skipUnless(HAS_CRYPTO, ...)`` reached @@ -211,7 +211,7 @@ def setUpClass(cls) -> None: def test_ignore_flags_match_the_fixture_tier_constants(self) -> None: """Every ignored path and glob is accounted for by a constant. - The point of failure this catches: somebody adds a fourth + The point of failure this catches: somebody adds another fixture-dependent naming convention to the workflow and the guard goes on classifying those modules as unit-tier, flagging their perfectly legal capture reads. No other glob is left: the round-trip sweeps @@ -1614,12 +1614,11 @@ def test_all_is_narrowed_to_core_addons_by_910(self) -> None: self.assertFalse(_dependency_gates.provided_by(declared['all'], removed)) def test_dev_carries_what_all_used_to_for_the_toolchain(self) -> None: - """#910's new extra: what pylint/mypy/Sphinx need to see, not what an end user wants. + """#910's ``dev`` extra: what pylint/mypy/Sphinx need to see, not what an end user wants. - Installed alongside ``all`` (``.[all,dev]``) by ``lint.yml``, - ``deploy-pages.yml`` and ``cron-conda.yml``'s ``conda-update`` job, so - that narrowing ``all`` above did not also grow ``lint.yml``'s own count - of 8 ``import-error`` messages. Deliberately does *not* repeat + Installed alongside ``all`` (``.[all,dev]``) by ``lint.yml`` and + ``deploy-pages.yml``, so that the narrowed ``all`` above does not also + grow ``lint.yml``'s own ``import-error`` messages. Deliberately does *not* repeat ``pypcap``/``pcap-ct``: those two stay out of every extra, and adding them here would install a C extension or a pre-release just to resolve an import CI has already decided to carry as an advisory finding. @@ -1869,7 +1868,7 @@ def test_a_flag_gated_where_it_is_not_defined_resolves_through_the_import(self) ) def test_one_flag_name_may_mean_two_things_and_is_resolved_per_module(self) -> None: - """``HAS_RUNTIME`` is the core dependencies, except in one module. + """``HAS_RUNTIME`` is the core dependencies in most modules, not in every one. :file:`tests/foundation/engines/test_runtime_engines.py` reuses the name for those plus ``dpkt``, ``scapy`` and ``pyshark``. Resolved by @@ -2217,8 +2216,8 @@ def test_the_vendor_extra_closes_every_job_reaching_the_vendor_gates(self) -> No #738 ruled these gates onto a non-blocking leg, and ``engine-tests``, narrowed to explicit engine paths (#849), runs them nowhere. The owner's ruling E on #1958 runs the whole suite on latest CPython, so ``test``'s - ``latest`` leg and ``gate`` install ``vendor``, and the exclusion that - recorded the gap is gone. + ``latest`` leg and ``gate`` install ``vendor``, and no exclusion records + a gap. """ reaching = {job.name for job in _dependency_gates.pytest_jobs() @@ -2250,7 +2249,7 @@ def test_the_mypy_gate_is_visible_and_dark_on_every_job_that_reaches_it(self) -> pytest install line. So the outcome pinned here is deliberately not "no gap". It is a gap on - exactly the three jobs that collect the module, explained rather than + exactly the jobs that collect the module, explained rather than closed -- and, which is the part #745 cares about, countable at all rather than hidden in a function body. @@ -2286,13 +2285,13 @@ def test_the_mypy_gate_is_visible_and_dark_on_every_job_that_reaches_it(self) -> def test_the_isort_gate_is_visible_and_closed_by_the_test_extra(self) -> None: """#766: the same invisible shape as ``mypy``, resolved the opposite way. - :file:`tests/project/test_isort_clean.py` gated on isort with a - ``try: import isort`` inside ``setUpClass``, which - :func:`~tests._dependency_gates._gates_of` cannot see -- it walks a - definition's ``decorator_list`` and never a function body -- and isort - was in no :file:`pyproject.toml` extra, so the module reported + Guards against the shape #766 found in + :file:`tests/project/test_isort_clean.py`: a ``try: import isort`` inside + ``setUpClass`` is invisible to :func:`~tests._dependency_gates._gates_of` + -- it walks a definition's ``decorator_list`` and never a function body -- + and with isort in no :file:`pyproject.toml` extra, the module reports ``OK (skipped=1)`` on every leg and the only guard on ``make isort``'s - four recipe lines ran nowhere. + recipe lines runs nowhere. Where #779 gave ``mypy`` a visible gate and then declined the install line, this closes the gap instead, so there is deliberately no @@ -2301,7 +2300,7 @@ def test_the_isort_gate_is_visible_and_closed_by_the_test_extra(self) -> None: than stylistic: that exclusion's own argument is that lint.yml already runs mypy over the whole package, so a pytest leg would be a second copy -- and lint.yml installs no isort at all, by its own header's - account, so for isort there was no first copy to defer to. + account, so for isort there is no first copy to defer to. The reachability assertion below is load-bearing and is what this test has that its ``mypy`` sibling does not need. That one asserts gaps are @@ -2404,10 +2403,11 @@ def test_every_gated_flag_is_classified(self) -> None: fails right here, with this message naming it *and the gate's own file and line*. An *excluded* one -- the modules :func:`~tests._dependency_gates.flag_exclusions` reads off a negated - probe -- had neither: nothing looped over them at all, so the same gap - surfaced only out of :func:`~tests._dependency_gates.module_providers`, - wherever :func:`~tests._dependency_gates.ambiguous_satisfactions` - happened to call it. Looping over both here is what gives an excluded + probe -- would have neither without this loop: nothing else iterates over + them, so the same gap would surface only out of + :func:`~tests._dependency_gates.module_providers`, wherever + :func:`~tests._dependency_gates.ambiguous_satisfactions` happened to call + it. Looping over both here is what gives an excluded module the same deliberate contract a required one already has, instead of an accident of whichever caller reaches it first. @@ -2476,7 +2476,7 @@ def test_no_provider_mapping_or_exclusion_is_vestigial(self) -> None: callers still need the entry -- it is just invisible to this particular test; :meth:`~tests.test_tier_guard.DependencyGateScanTests.test_a_negated_probe_is_not_a_requirement`, - the two ``#762`` swap tests below, and + the ``#762`` swap tests below, and :meth:`~tests.test_tier_guard.DependencyGateFalsifiabilityTests\ .test_removing_the_negation_or_the_exact_path_reopens_762`'s own mutation all pin ``pcap._pcap`` directly and would catch it. @@ -2559,7 +2559,7 @@ def setUp(self) -> None: self.skipTest(f'git cannot answer here: {reason}') def test_removing_crypto_from_the_test_job_is_caught(self) -> None: - """#745's worked example, verbatim: "deleting ``crypto`` ... would go green".""" + """#745's worked example: dropping ``crypto`` from the ``test`` job must not stay green.""" # #1958: from both of `test`'s lines, the one every leg runs and the one # `latest` adds, since the second alone runs the gate. The first is pinned # to its extras by tests/project/test_whole_suite_on_latest_1958.py. @@ -2598,16 +2598,14 @@ def test_dropping_isort_from_the_test_extra_is_caught(self) -> None: says a gap is *absent*, and an analysis that had quietly stopped resolving ``HAS_ISORT`` at all would satisfy every one of them. So this takes the requirement back out on a copy and requires the gaps to - reappear -- which is also the state #766 predicted a visible gate would - produce before the extra carried isort ("would correctly turn the - dependency-gate guard red until isort is on a pytest install line"), and - the reason the gate and the install line had to land in one change. + reappear -- which is also the state #766 predicts for a visible gate + while no pytest install line carries isort, and the reason the gate and + the install line go together. This mutates :file:`pyproject.toml` rather than the workflow because there is no isort to remove from an install line: every pytest job already installs ``test``, so the extra's own contents are where the - dependency enters. That is why :file:`unit-tests.yml` needed no edit for - #766 at all. + dependency enters. """ doctored = doctored_pyproject(self, '\n "isort",\n', '\n') @@ -2648,11 +2646,10 @@ def test_removing_dpkt_is_caught_on_every_job_that_installs_it(self) -> None: engine-tests is not: it chooses its extra per matrix.engine cell (#849) rather than from the shared comma-joined extras list this test doctors, so the literal ``'DPKT,'``/``',DPKT'`` substitution below does not touch - it -- and, separately, engine-tests does not reach - test_runtime_engines.py regardless (see the HAS_RUNTIME exclusion), so - there is no DPKT-via-engine-tests gap for this doctoring to reopen. Nor - has it a DPKT cell (#1538): that cell ran only test_dpkt_unit.py, which - ``test`` runs too. + it -- and, separately, no engine-tests ``TEST_PATHS`` list names + test_runtime_engines.py, so there is no DPKT-via-engine-tests gap for + this doctoring to reopen. Nor has it a DPKT cell (#1538): such a cell + would run only test_dpkt_unit.py, which ``test`` runs too. """ text = _dependency_gates.WORKFLOW.read_text(encoding='utf-8') @@ -2669,7 +2666,8 @@ def test_removing_cli_only_darkens_the_jobs_that_reach_the_emoji_gates(self) -> """Precision, not just detection. Every ``HAS_EMOJI`` gate is in :file:`tests/integration/test_cli_subprocess.py`, - which the ``test`` and ``engine-tests`` jobs both ignore wholesale. So + which the ``test`` job ignores wholesale and no ``engine-tests`` cell + names. So dropping ``cli`` must be reported against ``integration``, ``gate`` and ``pypcap-parity`` -- #751's job that mirrors ``integration``'s fixture-tier selection and so reaches the same gate -- and *not* @@ -2730,13 +2728,14 @@ def test_swapping_engine_tests_to_pypcap_is_an_ambiguous_satisfaction(self) -> N ``engine-tests``'s ``PCAP_CT`` cell (#849: a ``case "$ENGINE" in ...`` block chooses the extra, not a shared comma-joined install line) - swapped to install ``PyPCAP`` instead still reaches the 1 + swapped to install ``PyPCAP`` instead still reaches the ``HAS_PCAP_CT`` gate -- its ``TEST_PATHS`` mapping is keyed on the engine name ``'PCAP_CT'``, untouched by this doctoring -- and still reads as satisfied to :func:`~tests._dependency_gates.dependency_gate_gaps`, for the mirrored reason: ``extras_providing`` truncates ``pcap._pcap`` to - ``pcap`` before its lookup, per this module's own docstring. + ``pcap`` before its lookup, per :mod:`tests._dependency_gates`'s own + docstring. """ doctored = doctored_workflow( @@ -2890,7 +2889,7 @@ def test_a_non_distribution_flag_is_not_reported_even_when_doctored_ambiguous(se self.assertNotIn(('pypcap-parity', 'HAS_PYPCAP'), findings) def test_deleting_an_engine_from_the_matrix_list_is_caught(self) -> None: - """#849's second cross-review: ``_engine_matrix_variants`` reads ``engine:``. + """#849: ``_engine_matrix_variants`` checks its case arms against ``engine:``. Guards against the function building its :class:`Job` set purely from the ``case "$ENGINE" in ...`` arms without checking them against the ``engine:`` @@ -2917,8 +2916,8 @@ def test_deleting_an_engine_from_the_matrix_list_is_caught(self) -> None: def test_a_second_baseline_install_line_is_caught(self) -> None: """The ``base_installs`` branch of ``_engine_matrix_variants``, exercised. - Each of that function's ``raise AssertionError`` branches needs a test, as this - guard's own docstring says ("the only honest way to show a guard works is to + Each of that function's ``raise AssertionError`` branches needs a test, as + :func:`doctored_workflow`'s docstring says ("the only honest way to show a guard works is to break the thing it guards and watch it fire"). Doctoring in a second literal ``pip install -e '.[...]'`` baseline line makes the count the function checks for (exactly one) come out to two. @@ -2946,7 +2945,7 @@ def test_deleting_every_test_paths_case_arm_is_caught(self) -> None: """ text = _dependency_gates.WORKFLOW.read_text(encoding='utf-8') - # Two `case "$ENGINE" in ... esac` blocks exist in this job -- the install step's + # More than one `case "$ENGINE" in ... esac` block is in this job -- the install step's # (`EXTRA=` arms) and the pytest step's (`TEST_PATHS=` arms) -- so finding the # first match is not enough; pick the one that actually carries `TEST_PATHS=`. matches = [m for m in re.finditer(r'(?m)^( +)case "\$ENGINE" in\n(?:.*\n)*?\1esac\n', text) @@ -2985,12 +2984,12 @@ def test_a_test_paths_arm_with_no_matching_extra_arm_is_caught(self) -> None: self.assertIn('Scapy', message) def test_a_commented_out_test_paths_arm_does_not_silently_win(self) -> None: - """The relocated blocker the second cross-review flagged, not just commented on. + """A commented-out ``TEST_PATHS`` arm never overrides the live one (#849). ``dict()`` over ``re.findall()`` is last-wins, so a *commented-out* ``ENGINE) TEST_PATHS="..."`` arm placed after the live one would silently - override it -- the exact comment-vs-code confusion #849's first cross-review - found in this module's ``pip install -e`` scan, relocated into this function's + override it -- the same comment-vs-code confusion #849's cross-review + found in :mod:`tests._dependency_gates`'s ``pip install -e`` scan, in the case-arm scan instead. Stripping ``#``-prefixed lines before every regex in :func:`~tests._dependency_gates._engine_matrix_variants` is what keeps a commented-out arm from being read as a live one. @@ -3026,7 +3025,7 @@ def test_describe_ambiguous_satisfaction_names_residual_ambiguity_too(self) -> N Both real findings above are the "resolved to something illegitimate" shape. The other shape -- resolved to more than one distribution even after narrowing to the legitimate set -- has no real workflow state - that produces it (the legitimate set for both watched flags is + that produces it (the legitimate set for each watched flag is always a singleton), so it needs its own construction to reach. """ @@ -3133,11 +3132,11 @@ def test_a_workflow_with_no_jobs_block_fails_loudly(self) -> None: def test_a_flag_no_extra_could_satisfy_is_not_reported_as_a_gap(self) -> None: """:data:`~tests._dependency_gates.NON_DISTRIBUTION_FLAGS`, exercised. - No flag in the suite is *gated* on something pip cannot install -- + No real non-distribution flag leaves a gap to suppress -- ``HAS_PROC_FD`` asks whether :file:`/proc/self/fd` exists and is consulted - inline rather than through a decorator -- so this stands a real gated flag - in for one, to pin that naming a flag there suppresses its gap instead of - merely documenting it. + inline rather than through a decorator, and ``HAS_WIRESHARK`` requires + nothing -- so this stands a real gapped flag in for one, to pin that + naming a flag there suppresses its gap instead of merely documenting it. """ if not _dependency_gates.WORKFLOW.is_file(): @@ -3189,12 +3188,12 @@ def test_an_unmapped_module_names_itself_and_the_table_to_add_it_to(self) -> Non self.assertIn(table, message) def test_a_mapped_module_still_resolves_through_the_same_truncation(self) -> None: - """The control for the test above: #779 changed the diagnostic, not the answer. + """The control for the test above: #779 is about the diagnostic, not the answer. :func:`~tests._dependency_gates._top_level_providers` truncates to the top-level package unconditionally -- so ``pcap._pcap`` gets plain - ``pcap``'s two-wide entry there, exactly as the bare subscript it - replaced did -- while + ``pcap``'s two-wide entry there, exactly as a bare subscript would -- + while :func:`~tests._dependency_gates.module_providers` keeps its own exact-key preference on top of that, which is what makes ``pcap._pcap`` resolve to ``pcap-ct`` alone. Those two answers differing for the same diff --git a/tests/test_tier_guard_xdist.py b/tests/test_tier_guard_xdist.py index 0e30182ee5..9b46bbee1e 100644 --- a/tests/test_tier_guard_xdist.py +++ b/tests/test_tier_guard_xdist.py @@ -312,9 +312,9 @@ class ProbeWorkerCrashTests(unittest.TestCase): """A worker dying mid-probe fails that one test, not the session (#1756). xdist replaces the dead worker, and the replacement collects ``tests/`` - afresh while the probe's cleanup never ran. Before ``collect_ignore`` in - :mod:`tests.conftest`, that collection audited the stale probe and the - controller ended the session with ``xdist.dsession.Interrupted``, exit 2. + afresh while the probe's cleanup never ran. Without ``collect_ignore`` in + :mod:`tests.conftest`, that collection audits the stale probe and the + controller ends the session with ``xdist.dsession.Interrupted``, exit 2. """ @@ -343,7 +343,7 @@ def setUp(self) -> None: handle.write(_CRASH_PLUGIN_SOURCE) def test_a_worker_crash_mid_probe_does_not_interrupt_the_session(self) -> None: - """Fails on main: exit 2 and the probe's diagnostic as the interrupt reason.""" + """Without ``collect_ignore``: exit 2 and the probe's diagnostic as the interrupt reason.""" protocols = self.probe_path.parent environ = _child_environ() environ['PYTHONPATH'] = os.pathsep.join([self.plugin_dir, environ['PYTHONPATH']]) @@ -490,7 +490,7 @@ def _take(self, polls: 'bool', exclusive: 'bool') -> None: self.assertEqual(self.held, 0, f'not every lock taken was released: {self.calls!r}') def test_graalpys_path_polls_and_never_blocks(self) -> None: - """Fails on the code before db36aed75, which asked for the lock blocking:: + """Fails where GraalPy's path asks for the lock blocking:: AssertionError: Lists differ: [1] != [] @@ -547,7 +547,7 @@ def setUp(self) -> None: self.addCleanup(patcher.stop) def test_a_reader_arriving_behind_a_waiting_writer_waits_for_it(self) -> None: - """Fails on db36aed75 and before, which have no gate: ``['R'] != []``.""" + """Fails without the gate: ``['R'] != []``.""" # The blocking path only where it is the real one: on GraalPy it holds # the GIL, and these threads would deadlock exactly as #1977 describes. for polls in sorted({getattr(_conftest, 'TIER_GUARD_LOCK_POLLS', False), True}): @@ -724,10 +724,10 @@ class ProbeLockNestedRunTests(unittest.TestCase): :mod:`tests.project.test_module_isolation` runs a nested :program:`pytest` inside its shared hold. A writer arriving meanwhile holds the gate while it - waits on that hold, so a nested shared request waited behind it, and the - hold waited on the nested run: on GraalPy the run's ``subprocess.run`` - timeout, 600 s, was what broke it, with the writer and every other reader - stalled as long. + waits on that hold, so without the fix a nested shared request waits behind + it, and the hold waits on the nested run: on GraalPy only the run's + ``subprocess.run`` timeout, 600 s, breaks it, with the writer and every + other reader stalled as long. """ diff --git a/tests/test_windows_posix_gates_2102_unit.py b/tests/test_windows_posix_gates_2102_unit.py index ba0bbccaee..a4daaccb76 100644 --- a/tests/test_windows_posix_gates_2102_unit.py +++ b/tests/test_windows_posix_gates_2102_unit.py @@ -1,8 +1,8 @@ # -*- coding: utf-8 -*- """The POSIX-only stdlib the tests use is gated off on Windows (#2102). -The first Windows run failed on :mod:`resource`, :func:`signal.alarm` and -``chmod`` mode bits. Windows is not available here, so each case below loads or +Without the gates, a Windows run fails on :mod:`resource`, :func:`signal.alarm` +and ``chmod`` mode bits. Windows is not available here, so each case below loads or runs the affected module with the platform faked -- ``os.name`` set to ``'nt'``, :func:`signal.alarm` and ``SIGALRM`` removed, ``chmod`` keeping only the read-only bit -- and checks that it collects and skips, rather than errors. diff --git a/tests/toolkit/test_dpkt_reassembly_rawbytes_unit.py b/tests/toolkit/test_dpkt_reassembly_rawbytes_unit.py index de6c1b1171..7e1035f746 100644 --- a/tests/toolkit/test_dpkt_reassembly_rawbytes_unit.py +++ b/tests/toolkit/test_dpkt_reassembly_rawbytes_unit.py @@ -222,9 +222,9 @@ def test_tcp_header_is_found_behind_every_extension_header(self) -> None: """One TCP segment per extension header DPKT can parse. The Authentication header and the odd-length Routing header are the two - whose ``len()`` disagrees with the wire; before the offset was taken from - ``length`` the first returned an empty header and the second returned the - Routing header's own trailing octets. + whose ``len()`` disagrees with the wire; with an offset built from + ``len()`` rather than ``length``, the first gives an empty header and the + second the Routing header's own trailing octets. """ from pcapkit.const.reg.linktype import LinkType @@ -239,7 +239,7 @@ def test_tcp_header_is_found_behind_every_extension_header(self) -> None: self.assertEqual(bytes(data.payload), TCP_SEGMENT[20:]) def test_ipv6_fragment_behind_an_authentication_header(self) -> None: - """``ipv6_hdr_len`` counted a fixed list of types and missed this one.""" + """``ipv6_hdr_len`` counts an AH before the Fragment header, which a fixed list of types misses.""" from pcapkit.toolkit import dpkt as toolkit frame = self._read_back((AH_FRAG_FRAME,))[0] @@ -286,10 +286,9 @@ def test_options_transport_inputs_are_the_captured_octets(self) -> None: Each record is read by hand, and each input is the IHL's octets, then the rest up to the Total Length. The number of datagrams is counted here - rather than pinned: the capture is generated by ``make samples``, and that - was not reproducible when this test was written -- the same commit produced - a 43-frame and a 44-frame file in different worktrees -- so any absolute - count is flaky whichever value it picks. + rather than pinned: the capture is generated by ``make samples``, and one + commit has produced a 43-frame and a 44-frame file from it in different + worktrees, so any absolute count is flaky whichever value it picks. """ from pcapkit.toolkit import dpkt as toolkit diff --git a/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py b/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py index 6f01fda56a..5f69dab4af 100644 --- a/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py +++ b/tests/toolkit/test_dpkt_timestamp_copy_1647_unit.py @@ -3,13 +3,13 @@ :func:`copy.copy` and :func:`copy.deepcopy` look up ``__copy__`` and ``__deepcopy__`` before ``__reduce_ex__``, and :class:`decimal.Decimal` defines -both, so :class:`~pcapkit.toolkit.dpkt.DecimalTimestamp`'s own -:meth:`~object.__reduce__` was never asked. The C :mod:`_decimal` returns +both, so unless :class:`~pcapkit.toolkit.dpkt.DecimalTimestamp` defines them +too, its own :meth:`~object.__reduce__` is never asked. The C :mod:`_decimal` returns ``self``. The pure-Python :mod:`_pydecimal` returns ``self.__class__(str(self))``, which resets ``resolution`` and ``orig_len`` to the constructor's defaults. CPython 3.15 no longer falls back to its bundled libmpdec, so a build on a host without the library has no :mod:`_decimal` -- the ``actions/setup-python`` 3.15.0 build -reports it missing -- and CI's 3.15 leg alone lost them. +reports it missing -- so without the fix CI's 3.15 leg alone loses them. The in-process tests run under whichever implementation this interpreter has; :class:`PurePythonDecimalTests` reruns them under :mod:`_pydecimal` in a fresh diff --git a/tests/toolkit/test_dpkt_unit.py b/tests/toolkit/test_dpkt_unit.py index 63deb45bfd..5db1498507 100644 --- a/tests/toolkit/test_dpkt_unit.py +++ b/tests/toolkit/test_dpkt_unit.py @@ -43,8 +43,8 @@ class FakeFragment: Next Header field, ``frag_off`` is the fragment offset in 8-octet units and ``m_flag`` is the More Fragments flag. In particular there is **no** ``nh`` attribute, because the real :class:`dpkt.ip6.IP6FragmentHeader` has none -- - a fake that invented one is exactly what let the defect behind the - ``AttributeError`` reach a release unnoticed. + a fake that invented one would let code reading ``nh`` pass here and raise + ``AttributeError`` on every real fragment. """ @@ -256,7 +256,8 @@ def test_every_adapter_hands_on_a_float_timestamp(self) -> None: def test_a_bare_ip_packet_is_its_own_network_layer(self) -> None: # On a raw-IP link type dpkt reads a frame as a bare IP or IP6, which - # names no ``ip`` or ``ip6`` layer, so every adapter returned None (#1548) + # names no ``ip`` or ``ip6`` layer, so without the fix every adapter + # returns None (#1548) import dpkt from pcapkit.const.reg.linktype import LinkType @@ -900,8 +901,8 @@ def _assemble(segments: 'list[tuple[int, bytes]]') -> bytes: class DPKTEngineReassemblyTests(unittest.TestCase): """The ``dpkt`` engine reassembles the captured octets, octet for octet. - This is the check that would have caught the Data Offset defect: the split - was wrong in a way that left the datagram *lengths* untouched, because the + This is the check that catches a Data Offset defect: a split in the wrong + place leaves the datagram *lengths* untouched, because the reassembly buffer is indexed by sequence number, so only a byte-level comparison against the capture's own octets shows it. @@ -941,9 +942,9 @@ class DPKTTimestampTests(unittest.TestCase): DPKT's reader yields ``(timestamp, bytes)`` and only the octets become a packet, so a frame does not know when it was captured unless the engine says - so. It did not, and + so. Without the fix it does not, and :func:`~pcapkit.interface.misc.follow_tcp_stream` -- which reads frames back - after the extraction loop has finished -- had nothing to pass its reassembler + after the extraction loop has finished -- has nothing to pass its reassembler but a bound zero. """ @@ -987,7 +988,7 @@ def test_following_a_stream_reads_every_stream(self) -> None: """And the real timestamp changes nothing, which is worth pinning. TCP reassembly has no timeout by default, so the value never reaches a - decision -- the fix removes a workaround rather than altering a result. + decision, and no result depends on it. ``in.pcap`` holds two TCP conversations, of two frames and one, each segment alone, so none has a payload to reassemble. diff --git a/tests/toolkit/test_engine_pcap_trace_runtime.py b/tests/toolkit/test_engine_pcap_trace_runtime.py index 6d660f40c1..a53b8b4241 100644 --- a/tests/toolkit/test_engine_pcap_trace_runtime.py +++ b/tests/toolkit/test_engine_pcap_trace_runtime.py @@ -275,7 +275,8 @@ def test_a_nanosecond_pcap_keeps_its_resolution(self) -> None: @unittest.skipUnless(HAS_DPKT, 'dpkt not installed') def test_a_raw_ip_link_type_is_traced(self) -> None: # With the Ethernet header stripped, dpkt reads each frame as a bare IP or - # IP6, which names no ``ip`` or ``ip6`` layer, so it traced nothing (#1548). + # IP6, which names no ``ip`` or ``ip6`` layer, so without the fix it traces + # nothing (#1548). # The capture keeps every frame, its IPv6 ones under LINKTYPE_IPV4 # included, and those are not traced. for capture, linktype, flows in (('tcp.pcap', 228, RAW_TCP_FLOWS), ('http6.cap', 229, RAW_HTTP6_FLOWS)): diff --git a/tests/toolkit/test_pcap_ct_unit.py b/tests/toolkit/test_pcap_ct_unit.py index 62e429da21..4945120130 100644 --- a/tests/toolkit/test_pcap_ct_unit.py +++ b/tests/toolkit/test_pcap_ct_unit.py @@ -1,7 +1,7 @@ """Unit tests for :mod:`pcapkit.toolkit.pcap_ct`. -`pcap-ct`_ hands back raw bytes, so this toolkit has only the two auxiliary -functions; the reassembly and flow tracing adapters exist purely to refuse +`pcap-ct`_ hands back raw bytes, so this toolkit's only working functions are +its auxiliary ones; the reassembly and flow tracing adapters exist purely to refuse loudly, and that refusal is asserted here so it cannot regress into a silent :data:`None`. diff --git a/tests/toolkit/test_pcap_tcp_segment_ipv6_version_1666_unit.py b/tests/toolkit/test_pcap_tcp_segment_ipv6_version_1666_unit.py index 30752e56ab..452bac6588 100644 --- a/tests/toolkit/test_pcap_tcp_segment_ipv6_version_1666_unit.py +++ b/tests/toolkit/test_pcap_tcp_segment_ipv6_version_1666_unit.py @@ -2,11 +2,12 @@ """The default engine reads past an IPv6 header whose Version is 4, and hands it nothing. C.f. #1666, #1680. :func:`pcapkit.toolkit.pcap.tcp_segment` reads a TCP header the parser rejected -from its fixed 20 octets, unless the datagram is a later fragment (#1518). It -took the IPv4 branch of that fragment check by the header's Version field, and -IPv6 then read any Version, so an IPv6 layer with Version 4 took it too and read -an ``offset`` IPv6 does not have: the ``AttributeError`` ended the extraction, -PCAP and PCAP-NG alike (#1666). The branch goes by the layer's class. And +from its fixed 20 octets, unless the datagram is a later fragment (#1518). +Without the fix, it takes the IPv4 branch of that fragment check by the header's +Version field, so an IPv6 layer with Version 4, which an IPv6 parser reading any +Version yields, takes it too and reads an ``offset`` IPv6 does not have: the +``AttributeError`` ends the extraction, PCAP and PCAP-NG alike (#1666). The +branch goes by the layer's class. And IPv6 rejects a Version other than 6 (#1680), so such a header is kept raw: its frame hands over no input, and every frame after it is still read. diff --git a/tests/toolkit/test_pcap_tcp_segment_public_1570_unit.py b/tests/toolkit/test_pcap_tcp_segment_public_1570_unit.py index 81d330b823..ce9f023469 100644 --- a/tests/toolkit/test_pcap_tcp_segment_public_1570_unit.py +++ b/tests/toolkit/test_pcap_tcp_segment_public_1570_unit.py @@ -2,11 +2,10 @@ """``tcp_segment`` and ``TCPSegment`` are public in :mod:`pcapkit.toolkit.pcap`. C.f. #1570. The helper the TCP adapters of :mod:`pcapkit.toolkit.pcap` and -:mod:`pcapkit.toolkit.pcapng` share was the private ``pcap._tcp_segment``, which -``pcapng`` imported across modules. Under the rulings on #1519 a shared helper -becomes public in the module both callers already import, as ``test_start_line`` -did in #1525, so the helper and its result type are public and stay in ``pcap``, -which ``pcapng`` already imports. The adapters' end-to-end behaviour is covered by +:mod:`pcapkit.toolkit.pcapng` share, and its result type, are public in +``pcap``, which ``pcapng`` imports: under the rulings on #1519 a helper shared +across modules is public in the module its callers already import, as +``test_start_line`` is (#1525). The adapters' end-to-end behaviour is covered by ``tests/toolkit/test_pcap_tcp_segment_unit.py``; these tests pin the public surface and that both toolkits read the segment through it. diff --git a/tests/toolkit/test_pcap_unit.py b/tests/toolkit/test_pcap_unit.py index ffeec828b8..8675c9c959 100644 --- a/tests/toolkit/test_pcap_unit.py +++ b/tests/toolkit/test_pcap_unit.py @@ -333,8 +333,8 @@ def test_tcp_reassembly_descriptor_drives_the_reassembler(self) -> None: """The descriptor the toolkit builds reassembles to the octets sent. Guards the seam rather than either side of it: the toolkit and the - reassembler each looked self-consistent while disagreeing about what - ``first`` and ``last`` meant. + reassembler can each look self-consistent while disagreeing about what + ``first`` and ``last`` mean. """ from pcapkit.foundation.reassembly.tcp import TCP diff --git a/tests/toolkit/test_pypcap_unit.py b/tests/toolkit/test_pypcap_unit.py index 040d7c24bc..30d0ac00a9 100644 --- a/tests/toolkit/test_pypcap_unit.py +++ b/tests/toolkit/test_pypcap_unit.py @@ -1,7 +1,7 @@ """Unit tests for :mod:`pcapkit.toolkit.pypcap`. -`pypcap`_ hands back raw bytes, so this toolkit has only the two auxiliary -functions; the reassembly and flow tracing adapters exist purely to refuse +`pypcap`_ hands back raw bytes, so this toolkit's only working functions are +its auxiliary ones; the reassembly and flow tracing adapters exist purely to refuse loudly, and that refusal is asserted here so it cannot regress into a silent :data:`None`. diff --git a/tests/toolkit/test_pypcapfile_tcp_last_unit.py b/tests/toolkit/test_pypcapfile_tcp_last_unit.py index 21f36ac398..58112273fe 100644 --- a/tests/toolkit/test_pypcapfile_tcp_last_unit.py +++ b/tests/toolkit/test_pypcapfile_tcp_last_unit.py @@ -1,5 +1,5 @@ """Regression tests for the TCP sequence range of -:func:`pcapkit.toolkit.pypcapfile.tcp_reassembly` (GH#1100). +:func:`pcapkit.toolkit.pypcapfile.tcp_reassembly` (#1100). :attr:`TCP_Packet.last ` is the *inclusive* sequence number of the segment's last payload octet, as every diff --git a/tests/toolkit/test_pypcapfile_unit.py b/tests/toolkit/test_pypcapfile_unit.py index 9565aaf411..bc37011a27 100644 --- a/tests/toolkit/test_pypcapfile_unit.py +++ b/tests/toolkit/test_pypcapfile_unit.py @@ -101,7 +101,7 @@ def __init__(self, payload, **fields) -> None: self.p = 6 self.sum = 0xBEEF # Dotted-decimal ASCII bytes, PyPCAPFile 0.12.0's actual ``IP.src``/``.dst`` - # form (see GH#743) -- not an ``int``, so this stand-in exercises the same + # form (see #743) -- not an ``int``, so this stand-in exercises the same # parsing path the real decoder does instead of hiding behind a shortcut. self.src = b'10.1.1.2' self.dst = b'10.1.1.3' @@ -242,7 +242,7 @@ def test_packet2dict_and_chain_tolerate_an_undecoded_frame(self) -> None: data_link=LinkType.ETHERNET), 'ETHERNET:Raw') def test_packet2dict_reports_an_undecoded_frames_octets(self) -> None: - # GH#1667: the engine hands a frame left undecoded over hex-encoded + # #1667: the engine hands a frame left undecoded over hex-encoded from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import packet2dict @@ -254,7 +254,7 @@ def test_packet2dict_reports_an_undecoded_frames_octets(self) -> None: self.assertEqual(info[link.name], {'raw_len': 47, 'raw': frame}) ########################################################################## - # Address and hex-text parsing helpers (see GH#743). + # Address and hex-text parsing helpers (see #743). ########################################################################## def test_parse_ipv4_address_decodes_dotted_decimal_ascii_bytes(self) -> None: @@ -284,10 +284,9 @@ def test_parse_ipv4_address_wraps_invalid_input(self) -> None: ('not an address', b'not-an-ip'), ('not ASCII', b'\xff\xff\xff\xff\xff'), ('none', None), - # ``int`` is deliberately rejected, not merely unparsed: an earlier - # revision accepted it only to keep ``FakeIP``'s int-typed stand-in - # passing, which is exactly the fixture that hid GH#743 in the - # first place. See ``_parse_ipv4_address``'s docstring. + # ``int`` is deliberately rejected, not merely unparsed: accepting it + # would only keep an int-typed stand-in passing, the kind of fixture + # that hides #743. See ``_parse_ipv4_address``'s docstring. ('int', int(ipaddress.IPv4Address('10.1.1.2'))), ): with self.subTest(case=label): @@ -419,7 +418,7 @@ def test_tcp_adapters_decline_non_tcp_and_truncated_segments(self) -> None: self.assertIsNone(tcp_traceflow(packet, data_link=LinkType.ETHERNET, count=1)) def test_a_later_fragment_carries_no_tcp_header(self) -> None: - # GH#1576: from its offset on, a fragment's data is no TCP header + # #1576: from its offset on, a fragment's data is no TCP header from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import _transport, tcp_reassembly, tcp_traceflow @@ -431,7 +430,7 @@ def test_a_later_fragment_carries_no_tcp_header(self) -> None: self.assertEqual(_transport(FakeIP(segment, flags=0b001)), segment) # a first fragment def test_the_tcp_segment_ends_at_the_total_length(self) -> None: - # GH#1577: the Ethernet padding after the datagram is no part of the segment + # #1577: the Ethernet padding after the datagram is no part of the segment from pcapkit.toolkit.pypcapfile import _transport segment = make_tcp(b'x') @@ -446,7 +445,7 @@ def test_the_tcp_segment_ends_at_the_total_length(self) -> None: self.assertEqual(_transport(FakeIP(padded, **fields)), expected) def test_a_data_offset_below_five_words_is_no_tcp_header(self) -> None: - # GH#1590: the header is not read as 20 octets + # #1590: the header is not read as 20 octets from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import _transport, tcp_reassembly, tcp_traceflow @@ -460,7 +459,7 @@ def test_a_data_offset_below_five_words_is_no_tcp_header(self) -> None: self.assertEqual(_transport(FakeIP(segment)), segment) def test_an_ihl_past_the_captured_octets_is_no_ipv4_layer(self) -> None: - # GH#1591: pypcapfile slices the options at the IHL, however few octets + # #1591: pypcapfile slices the options at the IHL, however few octets # there are -- here up to 20 short of a declared 60 from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import _network, ipv4_reassembly, tcp_reassembly, tcp_traceflow @@ -478,7 +477,7 @@ def test_an_ihl_past_the_captured_octets_is_no_ipv4_layer(self) -> None: self.assertIs(_network(make_packet(FakeEthernet(whole))), whole) def test_tcp_over_ipv6_warns_once_per_capture(self) -> None: - # GH#1513 + # #1513 from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import tcp_reassembly, tcp_traceflow from pcapkit.utilities.warnings import AttributeWarning @@ -504,7 +503,7 @@ def test_tcp_over_ipv6_warns_once_per_capture(self) -> None: for count in (3, 4)]) def test_either_tcp_adapter_alone_warns_of_tcp_over_ipv6(self) -> None: - # GH#1513: a capture whose TCP over IPv6 reaches only one of the two + # #1513: a capture whose TCP over IPv6 reaches only one of the two from pcapkit.const.reg.linktype import LinkType from pcapkit.toolkit.pypcapfile import tcp_reassembly, tcp_traceflow from pcapkit.utilities.warnings import AttributeWarning @@ -521,7 +520,7 @@ def test_either_tcp_adapter_alone_warns_of_tcp_over_ipv6(self) -> None: [(AttributeWarning, 'Frame 1: TCP over IPv6 is left out of')]) def test_tcp_over_ipv6_is_read_off_a_whole_ipv6_header_only(self) -> None: - # GH#1513: the Next Header of an IPv6 frame, whose 40-octet header was captured + # #1513: the Next Header of an IPv6 frame, whose 40-octet header was captured from pcapkit.toolkit.pypcapfile import tcp_reassembly whole = binascii.unhexlify(ipv6_frame(6).payload)[:40] @@ -562,7 +561,7 @@ def test_capture_of_keys_a_ctypes_header_by_its_address(self) -> None: self.assertEqual(_capture_of(packet), (id(value), value)) def test_tcp_over_ipv6_warns_once_per_capture_of_ctypes_records(self) -> None: - # GH#1513: pypcapfile's record reaches its savefile header through a + # #1513: pypcapfile's record reaches its savefile header through a # ``ctypes`` pointer field, which builds a new pointer on every read from pcapkit.toolkit.pypcapfile import tcp_reassembly @@ -688,18 +687,18 @@ def packet(raw: bytes): return make_packet(FakeEthernet(IP(raw))) segment = make_tcp(b'x', flags=0b00011000) - # GH#1577: a datagram padded to the Ethernet minimum + # #1577: a datagram padded to the Ethernet minimum padded = packet(make_ipv4(segment) + b'\x00' * 5) data = tcp_reassembly(padded, count=1) self.assertEqual((data.header, bytes(data.payload), data.len), (segment[:20], b'x', 1)) self.assertEqual(bytes(tcp_traceflow(padded, data_link=LinkType.ETHERNET, count=1).payload), b'x') - # GH#1576, GH#1590: a later fragment, and a Data Offset of 4 + # #1576, #1590: a later fragment, and a Data Offset of 4 for label, raw in (('later fragment', make_ipv4(b'A' * 40, offset=185)), ('data offset 4', make_ipv4(with_data_offset(segment, 4)))): with self.subTest(case=label): self.assertIsNone(tcp_reassembly(packet(raw), count=2)) self.assertIsNone(tcp_traceflow(packet(raw), data_link=LinkType.ETHERNET, count=2)) - # GH#1591: an IHL of 15 in a 40-octet packet + # #1591: an IHL of 15 in a 40-octet packet cut = bytes([0x4F]) + make_ipv4(bytes(20), protocol=17, flags=0b001)[1:] self.assertIsNone(ipv4_reassembly(packet(cut), count=3)) @@ -730,9 +729,9 @@ def test_layer2dict_keeps_opt_and_nested_payload_length_consistent(self) -> None from pcapkit.toolkit.pypcapfile import _layer2dict - # Regression for the inconsistency a partial fix left behind: ``opt`` - # correctly un-hexed while the nested ``'Raw'`` payload stayed - # hex-encoded (and twice its true length) in the very same dict. + # Guards against a partial un-hexing: ``opt`` un-hexed while the nested + # ``'Raw'`` payload stays hex-encoded (and twice its true length) in the + # very same dict. payload = b'x' * 32 raw = make_ipv4(payload, options=b'\x94\x04\x00\x00') info = _layer2dict(IP(raw)) diff --git a/tests/toolkit/test_pyshark_unit.py b/tests/toolkit/test_pyshark_unit.py index 4d193bc55f..cf6ac6cfe3 100644 --- a/tests/toolkit/test_pyshark_unit.py +++ b/tests/toolkit/test_pyshark_unit.py @@ -73,8 +73,8 @@ def __init__(self, *, ipv6: bool = False, tcp: bool = True, # ``LayerFieldsContainer``, which subclasses ``str`` -- so # ``frame.encap_type`` arrives as text, never as an ``int``, and the # fake has to match that or it would not exercise the ``int()`` the - # toolkit needs. Left absent by default so the tests below that - # predate it keep exercising the filter-name fallback: a frame layer + # toolkit needs. Left absent by default so the tests below that do + # not set it exercise the filter-name fallback: a frame layer # without the field is exactly what selects that path. frame_fields['encap_type'] = encap_type self.frame_info = FakeLayer('frame', **frame_fields) @@ -159,7 +159,7 @@ def test_packet2dict_nests_frame_and_layers(self) -> None: def test_tcp_traceflow_negative_paths(self) -> None: """The ``ip=False``/``tcp=False`` short-circuits return ``None`` before ``tcp_traceflow`` ever resolves a link type, so they are - unaffected by #775 tier 1 dropping the ``LinkType.NULL`` default + unaffected by the lookup having no ``LinkType.NULL`` default (#775 tier 1) (see ``test_tcp_traceflow_raises_for_an_ip_rooted_packet`` and the ``FILTER_NAME_TO_LINKTYPE``-resolving tests below for that). @@ -199,7 +199,7 @@ def test_tcp_traceflow_resolves_token_ring_via_filter_name_mapping(self) -> None """Wireshark's Token Ring dissector -- the ``DLT_IEEE802`` root layer -- files its PDML node under the filter name ``tr``, per ``packet-tr.c``'s ``proto_register_protocol("Token-Ring", - "Token-Ring", "tr")``, and measured since on this host: ``editcap -T + "Token-Ring", "tr")``, and measured on this host: ``editcap -T tr`` writes DLT 6 and ``tshark`` roots the result at ``tr``. That name is also not a LinkType member on its own, so it needs the same ``FILTER_NAME_TO_LINKTYPE`` entry as ``eth`` to resolve to @@ -219,8 +219,8 @@ def test_tcp_traceflow_resolves_token_ring_via_filter_name_mapping(self) -> None def test_tcp_traceflow_raises_for_an_ip_rooted_packet(self) -> None: """``'ip'`` is absent from ``FILTER_NAME_TO_LINKTYPE``, so the fallback path raises. The name is reported as PyShark gave it - rather than upper-cased (#843), there being no upper-case - ``Enum_LinkType.get()`` attempt left to upper-case it for. + rather than upper-cased (#843), the fallback making no upper-cased + ``Enum_LinkType.get()`` attempt to upper-case it for. The double feeds ``'ip'`` as ``layers[0]`` to exercise that path. A real raw-IP capture would root at ``raw`` instead -- measured, and recorded in @@ -267,7 +267,7 @@ def test_tcp_traceflow_raises_for_a_genuinely_unknown_name(self) -> None: def test_tcp_traceflow_disambiguates_the_shared_filter_names(self) -> None: """#843: one Wireshark display-filter name serves several DLTs, so - keying on ``packet.layers[0].layer_name`` answered a *valid but + keying on ``packet.layers[0].layer_name`` alone answers a *valid but wrong* DLT with no error. ``frame.encap_type`` separates them, and ``ENCAP_TYPE_TO_LINKTYPE`` turns it into the right member. @@ -389,13 +389,13 @@ def test_an_ambiguous_filter_name_raises_rather_than_upper_casing_onto_a_member( def test_filter_name_fallback_resolves_the_measured_names(self) -> None: """Per the maintainer's ruling on #842, the fallback table covers - **every** filter name the sweep measured as unambiguous, not only the - two it started with. The owner's reason on #842 for covering every + **every** filter name the sweep measured as unambiguous. The owner's + reason on #842 for covering every filter name that differs from the one pcapkit uses was that nobody should have to come back later to update the table; he also wanted the values generated if possible, or at least a comment pointing at the source of truth. None of the names below spells a LinkType member, so - each one raised before the expansion. + each one raises without its table entry. Each pair was measured the same way as the rest of the table: the root PDML ```` of an ``editcap -F pcap -T `` @@ -494,7 +494,7 @@ class LayerField(str): @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PySharkFlagTests(unittest.TestCase): """#1514: tshark 4.x reports a boolean field as ``'True'``/``'False'``, which - ``bool(int(...))`` refused with :exc:`ValueError` on the first traced TCP frame. + ``bool(int(...))`` refuses with :exc:`ValueError` on the first traced TCP frame. """ @@ -546,7 +546,7 @@ def test_tcp_traceflow_raises_for_an_unparsable_flag(self) -> None: @unittest.skipUnless(HAS_RUNTIME, 'runtime dependencies not installed') class PySharkTraceFieldTests(unittest.TestCase): """The ``timestamp``, ``seq`` and ``ack`` ``tcp_traceflow`` hands the tracer - must equal the default engine's. #1514's ``ValueError`` hid that they did not. + must equal the default engine's, which #1514's ``ValueError`` hides. """ @@ -557,7 +557,7 @@ def test_timestamp_is_the_default_engines_float(self) -> None: """tshark prints ``frame.time_epoch`` to the nanosecond. The default engine computes ``ts_sec + Decimal(ts_usec) / 10**6`` (or ``10**9``) and hands the tracer ``float()`` of it - (``pcapkit/protocols/misc/pcap/frame.py``), so the float must be + (:meth:`Frame.read `), so the float must be identical, not just close. """ @@ -720,9 +720,10 @@ def test_encap_type_and_time_epoch_come_from_the_frame_layer(self) -> None: self.assertEqual(data.index, 6) def test_a_commented_tcp_packet_resolves_its_link_type(self) -> None: - """Reading ``encap_type`` from ``frame_info`` found none on the - ``pkt_comment`` layer, fell back to ``layers[0]``, which is ``frame``, - and raised ``MissingKeyError: 'frame'``. + """Reading ``encap_type`` from ``frame_info`` finds none on the + ``pkt_comment`` layer, falls back to ``layers[0]``, which is ``frame``, + and raises ``MissingKeyError: 'frame'``, so it is read from the + ``frame`` layer. """ from pcapkit.const.reg.linktype import LinkType @@ -734,8 +735,8 @@ def test_a_commented_tcp_packet_resolves_its_link_type(self) -> None: self.assertEqual(data.timestamp, 70.5) def test_packet2dict_puts_the_frame_layer_on_top(self) -> None: - """``frame_info`` put the comment's fields at the top level and nested - the frame fields under ``FRAME``. The frame layer's fields belong on + """Without the fix, ``frame_info`` puts the comment's fields at the top + level and nests the frame fields under ``FRAME``. The frame layer's fields belong on top, as for any other packet, and the comment keeps its own key. """ @@ -783,10 +784,10 @@ def test_a_packet_without_a_frame_layer_raises(self) -> None: class PySharkFixedHeaderTests(unittest.TestCase): """#1611: a TCP layer without a whole fixed header is not traced. - Reading a field tshark had not reported raised a bare :exc:`AttributeError` - and aborted the whole extraction. A header cut at octets 14 to 19 has every - field ``tcp_traceflow`` reads, so it was traced where the default engine - skips it. + Without the fix, reading a field tshark did not report raises a bare + :exc:`AttributeError` and aborts the whole extraction, and a header cut at + octets 14 to 19, which has every field ``tcp_traceflow`` reads, is traced + where the default engine skips it. """ @@ -823,8 +824,8 @@ class PySharkAddressTests(unittest.TestCase): """#1671: a TCP layer whose IP layer lacks an address is not traced. tshark reports no ``ip.dst`` behind an LSRR or SSRR option whose route data - is no whole number of addresses. Reading it raised a bare - :exc:`AttributeError` and aborted the whole extraction. + is no whole number of addresses. Without the fix, reading it raises a bare + :exc:`AttributeError` and aborts the whole extraction. """ diff --git a/tests/toolkit/test_rfc_constants_readers_1739_unit.py b/tests/toolkit/test_rfc_constants_readers_1739_unit.py index 9063890761..6528a45a5d 100644 --- a/tests/toolkit/test_rfc_constants_readers_1739_unit.py +++ b/tests/toolkit/test_rfc_constants_readers_1739_unit.py @@ -1,7 +1,7 @@ # -*- coding: utf-8 -*- """Toolkit readers of the RFC-fixed constants behave at the constants' edges. C.f. #1739. -The constants moved to their homes in :mod:`pcapkit.protocols`; these pin the +The constants live in :mod:`pcapkit.protocols`; these pin the behaviour of the toolkit code that reads them, at exactly the value each one gives: diff --git a/tests/toolkit/test_scapy_linktype_unit.py b/tests/toolkit/test_scapy_linktype_unit.py index aae0383c13..41174c9644 100644 --- a/tests/toolkit/test_scapy_linktype_unit.py +++ b/tests/toolkit/test_scapy_linktype_unit.py @@ -2,10 +2,10 @@ """:func:`pcapkit.toolkit.scapy.tcp_traceflow` takes the link type the engine attached (#1517). A packet's first layer does not name its link type: a raw IPv4 packet's is -``IP``, which is no :class:`~pcapkit.const.reg.linktype.LinkType` name, so the -name lookup raised :exc:`~pcapkit.utilities.exceptions.MissingKeyError`, and a -raw-IP packet whose first layer is ``IPv6`` resolved to ``LINKTYPE_IPV6`` whatever -its interface said. :class:`~pcapkit.foundation.engines.scapy.Scapy` attaches +``IP``, which is no :class:`~pcapkit.const.reg.linktype.LinkType` name, so a +name lookup raises :exc:`~pcapkit.utilities.exceptions.MissingKeyError`, and +resolves a raw-IP packet whose first layer is ``IPv6`` to ``LINKTYPE_IPV6`` +whatever its interface says. :class:`~pcapkit.foundation.engines.scapy.Scapy` attaches the interface's link type with :func:`~pcapkit.toolkit.scapy.attach_linktype`, and only a packet built by hand, which carries none, is looked up by name. diff --git a/tests/toolkit/test_scapy_unit.py b/tests/toolkit/test_scapy_unit.py index 0f8d4e867e..4b97da2f26 100644 --- a/tests/toolkit/test_scapy_unit.py +++ b/tests/toolkit/test_scapy_unit.py @@ -128,10 +128,10 @@ def _make_ether_raw(self): return Ether(bytes(Ether(**self._ether_kwargs()) / Raw(b'raw'))) def _make_ip_rooted_tcp_packet(self): - # No Ether layer, so ``packet.name`` (what #775's tier 1 fix in - # ``tcp_traceflow`` feeds ``Enum_LinkType.get()``) is ``'IP'`` -- not a - # LinkType member name, unlike the Ether-rooted packets above whose - # name is ``'Ethernet'``. + # No Ether layer, so ``packet.name`` (what ``tcp_traceflow`` feeds + # ``Enum_LinkType.get()`` for a packet built by hand, #775 tier 1) is + # ``'IP'`` -- not a LinkType member name, unlike the Ether-rooted packets + # above whose name is ``'Ethernet'``. from scapy.layers.inet import IP, TCP from scapy.packet import Raw @@ -380,9 +380,9 @@ class ScapyTrailerTests(unittest.TestCase): Scapy files the octets past an IP layer's Total Length or Payload Length as a ``Padding`` layer, and ``bytes()`` of every layer above it ends with them, so - an IPv6 fragment's data and a TCP segment's payload both carried the frame's - trailer. Every frame here is written to a capture and read back through the - engine. + without the fix an IPv6 fragment's data and a TCP segment's payload both + carry the frame's trailer. Every frame here is written to a capture and read + back through the engine. """ @@ -471,7 +471,8 @@ def test_reassembled_datagrams_are_the_datagram_sent(self) -> None: datagram, = datagrams # only ``Completion.COMPLETE`` is truthy self.assertTrue(datagram.completed) - # IPv6 came back 84 octets long, ending in the trailer + # without the fix IPv6 comes back 84 octets long, ending in the + # trailer self.assertEqual(datagram.payload, DATAGRAM) def test_tcp_payload_stops_at_the_datagram_end(self) -> None: @@ -523,7 +524,8 @@ def test_a_data_offset_below_the_fixed_header_gives_no_segment(self) -> None: from scapy.packet import Raw # a header shorter than 20 octets is malformed (:rfc:`9293#section-3.1`); - # the payload here had started with the last of the header's octets + # without the fix the payload here starts with the last of the header's + # octets self._assert_tcp_records([ bytes(self._ether() / IP(src='192.0.2.1', dst='198.51.100.1') / TCP(sport=1, dport=2, flags='PA', dataofs=dataofs) / Raw(b'abcdef')) + TRAILER @@ -537,7 +539,7 @@ def test_a_total_length_below_the_header_gives_no_segment(self) -> None: frame = bytes(self._ether() / IP(src='192.0.2.1', dst='198.51.100.1') / TCP(sport=1, dport=2, flags='PA') / Raw(b'abcdef')) # 10 octets is less than the IPv4 header, and 30 holds only half the TCP - # header; the former had come back as an empty record + # header; without the fix the former comes back as an empty record self._assert_tcp_records([frame[:16] + struct.pack('!H', length) + frame[18:] for length in (10, 30)], [None, None]) @@ -694,8 +696,8 @@ def test_a_packet_built_by_hand_is_read_as_the_octets_it_builds(self) -> None: def test_ipv6_reassembly_takes_only_its_own_fragment_header(self) -> None: # The Fragment header of a packet the IPv6 layer carries, here the inner - # packet of 6in6, had been handed on with the outer packet's addresses, - # and both IPv6 headers as the part before the Fragment header + # packet of 6in6, is handed on without the fix with the outer packet's + # addresses, and both IPv6 headers as the part before the Fragment header from scapy.layers.inet import IP from scapy.layers.inet6 import IPv6, IPv6ExtHdrFragment from scapy.layers.l2 import Ether diff --git a/tests/toolkit/test_tcp_carrier_walk_1606_1617_unit.py b/tests/toolkit/test_tcp_carrier_walk_1606_1617_unit.py index 09decb0382..ce591a8dbe 100644 --- a/tests/toolkit/test_tcp_carrier_walk_1606_1617_unit.py +++ b/tests/toolkit/test_tcp_carrier_walk_1606_1617_unit.py @@ -9,8 +9,8 @@ A later IPv4 fragment carries no upper-layer header (:rfc:`791#section-3.2`), so the default engine reads no TCP in one, but `Scapy`_ binds IPv6 to IPv4 protocol -41, and AH to protocol 51, whatever the fragment offset, and its engine handed on -the TCP it then dissected (#1617). It stops at such a fragment. +41, and AH to protocol 51, whatever the fragment offset, and without the fix its +engine hands on the TCP it dissects there (#1617). It stops at such a fragment. `dpkt`_ decodes protocol 0 as IPv4, so a Hop-by-Hop header there can read as an IPv4 packet dpkt cannot decode, or whose payload is no packet; neither ends the @@ -241,7 +241,7 @@ def test_the_default_engine_reads_tcp_behind_an_extension_header_in_ipv4(self) - self.assert_reads('default', EXTENSION_HEADERS) def test_no_tcp_is_read_in_a_later_ipv4_fragment(self) -> None: - # #1617: Scapy dissected TCP behind a later fragment + # #1617: Scapy dissects TCP behind a later fragment for engine in ('default', 'scapy'): with self.subTest(engine=engine): if engine != 'default' and not has(engine): diff --git a/tests/toolkit/test_tunnelled_tcp_flow_key_1581_unit.py b/tests/toolkit/test_tunnelled_tcp_flow_key_1581_unit.py index ea614a978b..76ca8fe00e 100644 --- a/tests/toolkit/test_tunnelled_tcp_flow_key_1581_unit.py +++ b/tests/toolkit/test_tunnelled_tcp_flow_key_1581_unit.py @@ -1,11 +1,11 @@ # -*- coding: utf-8 -*- """TCP in an IP-in-IP tunnel is keyed by the innermost IP header. C.f. #1581. -The TCP reassembly and flow tracing adapters keyed a segment by the frame's -*first* IP layer, which in a tunnel is the tunnel's own: every connection through -one tunnel that shares a port pair merged into one flow, under the tunnel's -endpoints. The default engine, scapy and pyshark did so (scapy found the inner -IPv4 layer of 4in6 only). Each keys it by the IP layer nearest above the TCP +Without the fix, the TCP reassembly and flow tracing adapters of the default +engine, scapy and pyshark key a segment by the frame's *first* IP layer, which in +a tunnel is the tunnel's own: every connection through one tunnel that shares a +port pair merges into one flow, under the tunnel's endpoints (scapy finds the +inner IPv4 layer of 4in6 only). Each keys it by the IP layer nearest above the TCP segment, as Wireshark's TCP conversations do. Every capture is built here, so the module is in the unit tier. pyshark runs diff --git a/tests/utilities/test_compat.py b/tests/utilities/test_compat.py index e151d54746..d2d7e2f427 100644 --- a/tests/utilities/test_compat.py +++ b/tests/utilities/test_compat.py @@ -27,7 +27,7 @@ 'TransportProtocol' object has no attribute '__set_name__'``, so a later ``import pcapkit`` failed outright. -Hence the two guards here, neither of which is about the shims themselves: +Hence the guards here, none of which is about the shims themselves: #. :data:`WARM_BEFORE_FAKING` is imported *before* the fake goes in, so the memoised value is the true one, and @@ -122,23 +122,23 @@ #: running the test until it stopped complaining, and that distinction is the #: point of having it. Every module imported at ``(3, 8)``: :mod:`builtins`, #: :mod:`collections.abc`, :mod:`functools`, :mod:`pathlib` and :mod:`typing`, -#: which no version gate guards (#2051), and, behind the gates that -#: remain, :mod:`urllib.parse`, :mod:`aenum`, :mod:`typing_extensions`, +#: which no version gate guards (#2051), and, behind its version gates, +#: :mod:`urllib.parse`, :mod:`aenum`, :mod:`typing_extensions`, #: :mod:`contextlib`, :mod:`decimal`, :mod:`enum` and :mod:`_thread`. -#: :mod:`threading` stays on the list because :meth:`CompatTests.load_compat_as` +#: :mod:`threading` is on the list because :meth:`CompatTests.load_compat_as` #: reads :data:`threading.excepthook` for PyPy's stand-in ``_thread._excepthook``. #: -#: Four of those never actually surface, and *why* is the point. Three -- -#: :mod:`collections.abc`, :mod:`contextlib` and :mod:`pathlib` -- are imported at +#: Some of those never actually surface, and *why* is the point. +#: :mod:`collections.abc`, :mod:`contextlib` and :mod:`pathlib` are imported at #: module scope by :mod:`tests._support`, and :mod:`enum` by this module itself. It -#: is not the runtime that keeps them warm: under ``python -S`` none of the four is +#: is not the runtime that keeps them warm: under ``python -S`` none of them is #: in :data:`sys.modules` at all. So "some other module imports it for us" is #: precisely the unenforced ordering this file exists to stop relying on, which is -#: why the four are on the list despite costing a :data:`sys.modules` lookup each -#: and nothing else. Measured: with only the five that do surface warmed and +#: why they are on the list despite costing a :data:`sys.modules` lookup each +#: and nothing else. Measured: with only the ones that do surface warmed and #: :mod:`pathlib` forced cold, the window leaks ``['pathlib', 'pathlib._os']``; #: with the full list, then nine names, it is empty even with five of them forced -#: cold. :mod:`urllib.parse`, added since for issue #1693, is warm before this +#: cold. :mod:`urllib.parse`, on the list for issue #1693, is warm before this #: module runs on 3.8 to 3.10, where :mod:`pathlib` imports it, and cold on 3.14 #: (both under ``python -S``). WARM_BEFORE_FAKING = ('_thread', 'aenum', 'builtins', 'collections.abc', 'contextlib', diff --git a/tests/utilities/test_config_unit.py b/tests/utilities/test_config_unit.py index 521e5d3a97..f66a59f766 100644 --- a/tests/utilities/test_config_unit.py +++ b/tests/utilities/test_config_unit.py @@ -1,15 +1,16 @@ # -*- coding: utf-8 -*- """Every tunable limit has one home, :mod:`pcapkit.utilities.config`. -GitHub issue #1726 guards against the limits being spread across packages, with -the readers in ``ipv6.py`` and ``toolkit/pypcapfile.py`` copying two of them with -``from ... import``, so that patching the defining module leaves those readers on -the old value. Each is a ``PCAPKIT_*`` environment variable read once by -:mod:`pcapkit.utilities.config`, every reader looks it up there at call time, -and the old import paths are gone. A bad value warns and falls back to the -default; ``-1`` lifts a limit, and that or any value above the default warns of -the risk. The :mod:`pcapkit.vendor` crawlers' variables are not held here: -``tests/vendor/test_vendor_variables_1726_unit.py`` covers them. +Guards against the limits being spread across packages, with readers such as +:mod:`pcapkit.protocols.internet.ipv6` and :mod:`pcapkit.toolkit.pypcapfile` +copying them with ``from ... import``, so that patching the defining module leaves +those readers on a stale value (#1726). Each is a ``PCAPKIT_*`` environment +variable read once by :mod:`pcapkit.utilities.config`, every reader looks it up +there at call time, and no other module re-exports it (#1715). A bad value +warns and falls back to the default; ``-1`` lifts a limit, and that or any +value above the default warns of the risk. The :mod:`pcapkit.vendor` crawlers' +variables are not held here: ``tests/vendor/test_vendor_variables_1726_unit.py`` +covers them. """ @@ -596,8 +597,8 @@ class ImporterStacklevelTests(unittest.TestCase): CPython names :mod:`importlib`'s bootstrap frames ```` and ````, GraalPy ```` and ````, and each one's :func:`warnings.warn` - leaves its own out. Counting GraalPy's ran the level past the outermost frame, - so the import-time warnings above named ``:0`` there. + leaves its own out. Counting GraalPy's would run the level past the outermost + frame, so the import-time warnings above would name ``:0`` there. """ diff --git a/tests/utilities/test_exceptions_excepthook.py b/tests/utilities/test_exceptions_excepthook.py index fd07b7d41d..d9f6b68b55 100644 --- a/tests/utilities/test_exceptions_excepthook.py +++ b/tests/utilities/test_exceptions_excepthook.py @@ -274,21 +274,20 @@ def previous_hook(etype, value, tb): 'its chain, so it is no reference for the shape (#1958)') class ChainedExceptionTests(unittest.TestCase): """A chained exception must come out in the *shape* ``tracebacklimit = 0`` - produced, not just "one line". - - ``traceback.print_exception(etype, value, None)`` -- the hook's first - draft -- discards only the *top* exception's own traceback by handing in - ``tb=None``; it never touches ``value.__cause__`` or ``value.__context__``, - each of which carries its *own*, real ``__traceback__`` that - :func:`traceback.print_exception` walks and prints in full regardless, - since no ``limit`` was given for it to apply there either. The old - ``sys.tracebacklimit = 0`` applied to the whole chain, because + produces, not just "one line". + + ``traceback.print_exception(etype, value, None)`` discards only the *top* + exception's own traceback by handing in ``tb=None``; it never touches + ``value.__cause__`` or ``value.__context__``, each of which carries its + *own*, real ``__traceback__`` that :func:`traceback.print_exception` walks + and prints in full regardless, since no ``limit`` is given for it to apply + there either. ``sys.tracebacklimit = 0`` applies to the whole chain, because :func:`traceback.StackSummary.extract` -- which every link's formatting goes through -- consults the global itself, independently, at every link. ``limit=0`` against the *real* traceback (not ``None``) is what reaches - every link the same way, and is what the two tests below compare against: + every link the same way, and is what the chain tests below compare against: not a one-line assertion, but the exact text ``sys.tracebacklimit = 0`` - would have produced for the same exception, chain included. + produces for the same exception, chain included. """ @@ -535,7 +534,7 @@ def test_a_fresh_instance_installs_over_a_stale_instance_hook(self) -> None: class ThreadingExcepthookTests(unittest.TestCase): """The thread analogue of :class:`LoudErrorPrintsOneLineTests` and friends. - :data:`threading.excepthook` needed its own hook for a reason distinct from + :data:`threading.excepthook` needs its own hook for a reason distinct from "symmetry with :data:`sys.excepthook`": the interpreter's *default* :data:`threading.excepthook` itself consults :data:`sys.tracebacklimit` when printing, so without the global and without a hook on this side too, a @@ -751,8 +750,8 @@ def exploding_delegate(args): def test_a_non_base_error_with_no_previous_hook_goes_to_the_default_hook(self) -> None: """Nothing captured yet: the default hook's own output, on every Python. - GitHub issue #1691: this delegate was ``threading.__excepthook__``, - which Python 3.8 and 3.9 lack, so the call raised instead of printing. + GitHub issue #1691: a delegate of ``threading.__excepthook__``, which + Python 3.8 and 3.9 lack, raises there instead of printing. """ code = ''' diff --git a/tests/utilities/test_logging.py b/tests/utilities/test_logging.py index 7883c1ee6b..9dd021b1f4 100644 --- a/tests/utilities/test_logging.py +++ b/tests/utilities/test_logging.py @@ -63,7 +63,7 @@ def test_boolean_environment_flags_are_parsed(self) -> None: self.assertTrue(logging_module.SPHINX_TYPE_CHECKING) def test_invalid_boolean_environment_values_fall_back_to_false_and_warn(self) -> None: - # GitHub issue #1726 guards against the value being taken as off in silence. + # Guards against the value being taken as off in silence (#1726). # logging loads before warnings, so it records the value and warnings # reports it once it has loaded, naming the variable and the value. os.environ['PCAPKIT_DEVMODE'] = 'maybe' @@ -511,9 +511,9 @@ def test_verbose_extraction_prints_frames_to_stdout(self) -> None: The frame chains are a feature of the tool and of the CLI's ``-v``, so they go to stdout via :func:`print` and are readable without the - consumer configuring a logging handler. Removing the import-time stderr - handler must not turn that into silence, and moving it onto the logger - would have changed both the stream and the visibility. + consumer configuring a logging handler. The logger having no import-time + stderr handler must not turn that into silence, and moving the output + onto the logger would change both the stream and the visibility. """ import contextlib diff --git a/tests/utilities/test_stacklevel.py b/tests/utilities/test_stacklevel.py index f53547f2f1..ca3d39cd41 100644 --- a/tests/utilities/test_stacklevel.py +++ b/tests/utilities/test_stacklevel.py @@ -20,7 +20,7 @@ *empty*, the ``for``/``else`` branch runs, and every subsequent warning in the process gets ``-1``. -Three groups of tests here, because they fail for different reasons: +The tests here fall into groups, because they fail for different reasons: * :class:`StacklevelArithmeticTests` -- the derivation and both clamps, over synthetic frames, including the shapes no real stack under :program:`pytest` @@ -268,10 +268,10 @@ def test_level_names_the_innermost_frame_outside_pcapkit(self) -> None: def test_level_is_never_below_one(self) -> None: """``0`` and ``-1`` are not levels: exhaustively, over every shape. - ``-1`` was reachable -- ``index`` is ``0`` when the outermost frame is - already inside :mod:`pcapkit`, e.g. running the package as a script -- and - :meth:`logging.Logger.findCaller` reads a level below ``1`` as "do not walk - out at all", so it blamed :mod:`logging` itself. + Without the clamp ``-1`` is reachable -- ``index`` is ``0`` when the + outermost frame is already inside :mod:`pcapkit`, e.g. running the package + as a script -- and :meth:`logging.Logger.findCaller` reads a level below + ``1`` as "do not walk out at all", so it blames :mod:`logging` itself. """ for outer in range(0, 5): @@ -348,7 +348,7 @@ def warning_site(self) -> 'pywarnings.WarningMessage': return emitted[-1] def test_warning_blames_the_same_line_from_every_depth(self) -> None: - """One logical call, six outer depths, one attribution.""" + """One logical call, every outer depth, one attribution.""" seen = {} # type: dict[int, tuple[str, int]] for depth in DEPTHS: record = through(depth, self.warning_site) diff --git a/tests/utilities/test_utilities_datetime_module_unit.py b/tests/utilities/test_utilities_datetime_module_unit.py index 1f5e044e40..244d7be05e 100644 --- a/tests/utilities/test_utilities_datetime_module_unit.py +++ b/tests/utilities/test_utilities_datetime_module_unit.py @@ -2,9 +2,7 @@ """#2134: the timestamp conversions live in :mod:`pcapkit.utilities.datetime`. The owner ruled on #2136 that the module reuses the stdlib name, as -:mod:`pcapkit.utilities.logging` and :mod:`pcapkit.utilities.warnings` do. The -old ``pcapkit.utilities.timestamp`` path was added by #2111 and is in no -release, so it is deleted rather than re-exported (#1715). +:mod:`pcapkit.utilities.logging` and :mod:`pcapkit.utilities.warnings` do. """ diff --git a/tests/vendor/test_ftp_return_code_unit.py b/tests/vendor/test_ftp_return_code_unit.py index c35b8e5e38..7a291a027c 100644 --- a/tests/vendor/test_ftp_return_code_unit.py +++ b/tests/vendor/test_ftp_return_code_unit.py @@ -57,8 +57,9 @@ #: Every distribution importing :mod:`pcapkit.vendor` needs -- see #: :mod:`tests.vendor.test_user_agent_unit` for why ``requests`` alone is not #: enough. ``html5lib`` ships in the ``vendor`` extra (:file:`pyproject.toml`), -#: not ``test`` -- so these skip on the ``test`` and ``gate`` jobs, which do not -#: install it. ``engine-tests`` does not run them either: each of its cells +#: not ``test`` -- so these skip on every leg of the ``test`` job but ``latest``, +#: the one that installs ``vendor`` (#1958). ``engine-tests`` does not run them +#: either: each of its cells #: runs only that engine's own test files (#849). VENDOR_DEPS = ('requests', 'bs4', 'html5lib') diff --git a/tests/vendor/test_ipx_packet_unit.py b/tests/vendor/test_ipx_packet_unit.py index a9f1c19766..3d07606528 100644 --- a/tests/vendor/test_ipx_packet_unit.py +++ b/tests/vendor/test_ipx_packet_unit.py @@ -57,8 +57,9 @@ #: Whether the crawlers are importable at all. ``html5lib`` ships in the #: ``vendor`` extra (:file:`pyproject.toml`), not ``test`` -- so these tests skip -#: on the ``test`` and ``gate`` jobs of :file:`.github/workflows/unit-tests.yml`, -#: which never install ``vendor``. ``engine-tests`` does not run them either: +#: on every leg of the ``test`` job of :file:`.github/workflows/unit-tests.yml` +#: but ``latest``, the one that installs ``vendor`` (#1958), as ``gate`` does. +#: ``engine-tests`` does not run them either: #: each of its cells runs only that engine's own test files (#849). #: Guarded the same way #: :file:`tests/protocols/test_dispatch_registry_unit.py` guards its own optional diff --git a/tests/vendor/test_request_prompt_unit.py b/tests/vendor/test_request_prompt_unit.py index a4709390da..986fe56990 100644 --- a/tests/vendor/test_request_prompt_unit.py +++ b/tests/vendor/test_request_prompt_unit.py @@ -109,7 +109,8 @@ #: Whether the crawlers are importable at all. ``html5lib`` ships in the #: ``vendor`` extra (:file:`pyproject.toml`), not ``test`` -- so these cases skip -#: on the ``test`` and ``gate`` jobs of :file:`.github/workflows/unit-tests.yml`. +#: on every leg of the ``test`` job of :file:`.github/workflows/unit-tests.yml` +#: but ``latest``, the one that installs ``vendor`` (#1958), as ``gate`` does. #: ``engine-tests`` does not run them either: each of its cells runs only that #: engine's own test files (#849). #: Guarded the way :file:`tests/protocols/test_dispatch_registry_unit.py` guards diff --git a/tests/vendor/test_user_agent_unit.py b/tests/vendor/test_user_agent_unit.py index d461456746..d94e3d301c 100644 --- a/tests/vendor/test_user_agent_unit.py +++ b/tests/vendor/test_user_agent_unit.py @@ -72,8 +72,9 @@ #: Whether the crawlers are importable at all. ``html5lib`` ships in the #: ``vendor`` extra (:file:`pyproject.toml`), not ``test`` -- so these tests skip -#: on the ``test`` and ``gate`` jobs of :file:`.github/workflows/unit-tests.yml`, -#: which never install ``vendor``. ``engine-tests`` does not run them either: +#: on every leg of the ``test`` job of :file:`.github/workflows/unit-tests.yml` +#: but ``latest``, the one that installs ``vendor`` (#1958), as ``gate`` does. +#: ``engine-tests`` does not run them either: #: each of its cells runs only that engine's own test files (#849). #: Guarded the same way #: :file:`tests/protocols/test_dispatch_registry_unit.py` guards its own optional diff --git a/tests/vendor/test_vendor_dest_path_unit.py b/tests/vendor/test_vendor_dest_path_unit.py index d0f0504fb4..c2e1675828 100644 --- a/tests/vendor/test_vendor_dest_path_unit.py +++ b/tests/vendor/test_vendor_dest_path_unit.py @@ -19,7 +19,7 @@ with open(os.path.join(ROOT, '..', 'const', STEM, FILE), 'w') as file: That is correct only for a crawler exactly one level under ``vendor/``, e.g. -:mod:`pcapkit.vendor.reg.apptype` writing :file:`pcapkit/const/reg/apptype.py`. +:mod:`pcapkit.vendor.reg.ethertype` writing :file:`pcapkit/const/reg/ethertype.py`. Traced by hand for a module one level deeper -- :file:`pcapkit/vendor/reg/apptype/tcp.py`, the shape #732 needs -- ``temp`` is :file:`.../vendor/reg/apptype`, so ``ROOT`` becomes :file:`.../vendor/reg` and @@ -94,7 +94,8 @@ anything under :file:`tests/vendor/` at all, since ``integration`` and ``pypcap-parity`` never reach it and ``engine-tests`` runs only each engine's own test files (#849) -- unlike :file:`tests/vendor/test_user_agent_unit.py`, -whose own gate also lists ``html5lib`` and so skips on both. +whose own gate also lists ``html5lib`` and so skips on every ``test`` leg but +``latest``, the one that installs ``vendor`` (#1958). """ from __future__ import annotations @@ -137,8 +138,8 @@ #: Whether the crawlers are importable at all. Both packages ship in the #: ``test`` extra (:file:`pyproject.toml`), so this is ``True`` under a plain -#: ``pip install -e '.[test]'`` and both classes below run in CI as things -#: stand -- kept as belt-and-braces for an environment that lacks them, the +#: ``pip install -e '.[test]'`` and both classes below run in CI -- kept as +#: belt-and-braces for an environment that lacks them, the #: same way :file:`tests/vendor/test_crawler_reachability_unit.py` guards it. HAS_VENDOR_DEPS = all(importlib.util.find_spec(name) is not None for name in VENDOR_DEPS) diff --git a/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py b/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py index 533284e0dd..2edc3c7337 100644 --- a/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py +++ b/tests/vendor/test_vendor_ipv6_extension_header_925_unit.py @@ -187,7 +187,7 @@ def _normalize(context: 'str') -> 'str': @unittest.skipUnless(HAS_VENDOR_DEPS, f'vendor extra not installed ({", ".join(VENDOR_DEPS)})') class ExtensionHeaderVendorTests(unittest.TestCase): - """The crawler fix: correct registry, 3-column parser, names preserved.""" + """The crawler: authoritative registry, 3-column parser, names preserved.""" if TYPE_CHECKING: vendor_module: 'Any' @@ -271,7 +271,7 @@ def test_the_divergent_comments_differ_from_the_historical_registry(self) -> Non # Documents the divergence rather than hiding it: these five members' # *comments* differ from the committed const file because the two # registries carry different Reference/Description text for the same - # header -- not because the new parser mishandles them. Names and + # header -- not because the parser mishandles them. Names and # values still match EXPECTED_MEMBERS exactly (see the sibling test). vendor, lines = self._vendor() enum, _ = vendor.process(lines) diff --git a/tests/vendor/test_vendor_snapshot_restore_unit.py b/tests/vendor/test_vendor_snapshot_restore_unit.py index 2257a70e41..efefc12635 100644 --- a/tests/vendor/test_vendor_snapshot_restore_unit.py +++ b/tests/vendor/test_vendor_snapshot_restore_unit.py @@ -61,7 +61,7 @@ class directly would exercise :meth:`Vendor.__init__` in isolation, which is :func:`~pcapkit.vendor.__main__._snapshot_and_restore` is what restores it afterwards. It asserts on the *reported* error (:meth:`~unittest.TestCase.assertWarnsRegex` against the crawler's own -``RuntimeError`` text) rather than merely on ``run()`` returning falsy, +``RuntimeError`` text) as well as on ``run()`` returning falsy, deliberately: a mutant that deletes the ``raise`` from ``_snapshot_and_restore``'s ``except BaseException:`` clause does not merely misreport the failure, it makes it disappear. Because @@ -69,9 +69,11 @@ class directly would exercise :meth:`Vendor.__init__` in isolation, which is exception its generator catches and does not re-raise, :meth:`Vendor.__init__`'s ``RuntimeError`` never reaches ``run()``'s own ``except Exception as error:`` at all under that mutant -- no warning, no -stderr line, and ``run()`` returns :data:`True`. A bare -``assertFalse(run(...))`` cannot catch a return value that flips to *true*; -only asserting that the crawler's own error was actually reported can. It +stderr line, and ``run()`` returns :data:`True`, which the ``assertFalse`` +on its result catches. The error-text assertion is what catches the variant +that also collapses the ``else:`` clause into an unindented +``os.remove(backup)``: ``run()`` stays falsy there, but reports that call's +:exc:`FileNotFoundError` instead of the crawler's own error. It also asserts the destination's mode (``0o640`` in, ``0o640`` out) alongside its content, which is the measurement that justifies a single mode assertion: restore preserves mode as a side effect of :func:`shutil.copy2` @@ -285,11 +287,12 @@ def _raise_on_render(*args: object, **kwargs: object) -> None: # assertWarnsRegex against the crawler's OWN error text, not merely # assertFalse(run(...)): a mutant that deletes the `raise` from - # _snapshot_and_restore's `except BaseException:` still returns a - # falsy run() -- the unguarded os.remove(backup) that then runs - # raises FileNotFoundError instead (the backup was just renamed away - # by os.replace), and run() reports THAT, not this test's injected - # RuntimeError. A bare falsy check cannot tell "failed for the right + # _snapshot_and_restore's `except BaseException:` and collapses its + # `else:` into an unindented os.remove(backup) still returns a falsy + # run() -- that os.remove raises FileNotFoundError (the backup was + # just renamed away by os.replace), and run() reports THAT, not this + # test's injected RuntimeError. Deleting the `raise` alone makes + # run() return True, which the assertFalse below catches. A bare falsy check cannot tell "failed for the right # reason" from "failed for the wrong one"; this can. with mock.patch('builtins.print', side_effect=_raise_on_render): with self.assertWarnsRegex(self.VendorRuntimeWarning, 'simulated failure'): diff --git a/tests/vendor/test_vendor_variables_1726_unit.py b/tests/vendor/test_vendor_variables_1726_unit.py index bf3cc7455c..52eeb711ee 100644 --- a/tests/vendor/test_vendor_variables_1726_unit.py +++ b/tests/vendor/test_vendor_variables_1726_unit.py @@ -1,10 +1,9 @@ # -*- coding: utf-8 -*- """The :mod:`pcapkit.vendor` crawlers read their own variables. -GitHub issue #1726: #1743 moved ``pcapkit.vendor.default.MAX_RETRY`` and -``CI_MODE``, and the proxy lookups, into :mod:`pcapkit.utilities.config`; the -issue then settled that the crawlers keep their variables in -:mod:`pcapkit.vendor`. So :mod:`pcapkit.vendor.default` reads +GitHub issue #1726 settled that the crawlers keep their variables -- +``pcapkit.vendor.default.MAX_RETRY`` and ``CI_MODE``, and the proxy lookups -- +in :mod:`pcapkit.vendor` rather than in :mod:`pcapkit.utilities.config`. So :mod:`pcapkit.vendor.default` reads :envvar:`PCAPKIT_VENDOR_RETRY` and :envvar:`PCAPKIT_CI_MODE` itself, through the public :func:`~pcapkit.utilities.config.read_limit` and :func:`~pcapkit.utilities.config.read_flag`, so a bad value still warns with a @@ -194,8 +193,7 @@ def test_max_retry_in_the_proxy_loop(self) -> None: for count in range(1, retry + 1)]) def test_proxies_are_read_on_every_call(self) -> None: - # Set after the import: the variables are read on every call, as - # get_proxies always read them. + # Set after the import: the variables are read on every call. with mock.patch.dict(os.environ, {'PCAPKIT_HTTP_PROXY': 'http://late'}): os.environ.pop('PCAPKIT_HTTPS_PROXY', None) self.assertEqual(self.default.get_proxies(), {'http': 'http://late'})