diff --git a/ChangeLog b/ChangeLog index 766cdb7e..bd7c15dc 100644 --- a/ChangeLog +++ b/ChangeLog @@ -1,4 +1,9 @@ Version 3.0.0 - 2026-10-10 + Register each native executable once in Automake test evidence inventories. + Run Windows allocation-failure injection against the installed static + library and require ordinary readiness cases in both linkage lanes. + Keep PSK cancellation lookup held until owner release and avoid artificial + per-fragment scheduling delays in the TLS test transport. Avoid Windows DLL shutdown hangs by retaining the default timer service during process termination while preserving ordinary timer cleanup. Report the saved Winsock error code when host WSAPoll fails. diff --git a/scripts/lib/package_integration.py b/scripts/lib/package_integration.py index f2462e87..71ca28df 100644 --- a/scripts/lib/package_integration.py +++ b/scripts/lib/package_integration.py @@ -38,6 +38,11 @@ def run_suites(inventory, host, mode, linkage, directory, prefix, source, build, receipt = dict(name=name, tls=mode, linkage=linkage, execution=definition.get('execution', 'installed-library')) try: flags = cflags + ['-DHTTPSERVER_COMPILATION', '-I' + str(source / 'src'), '-I' + str(source / 'test')] + if host == 'Windows' and linkage == 'shared' and definition.get('windows_static_cases'): + # An executable's operator new cannot interpose PE DLL imports. + # The installed static lane retains every fault-injection case. + flags += ['-DLHS_TEST_DLL_LINKAGE'] + receipt['windows_static_cases'] = definition['windows_static_cases'] for macro, value in (('TLS_TEST_DIR', source / 'test'), ('PARITY_TRANSCRIPT_DIR', source / 'test/parity/transcripts'), ('PARITY_DATA_ROOT', source / 'test'), ('HTTP2_CORPUS_DIR', source / 'test/conformance/http2')): flags += ['-D' + macro + '="' + value.as_posix() + '"'] @@ -61,9 +66,7 @@ def run_suites(inventory, host, mode, linkage, directory, prefix, source, build, started = time.monotonic() output = command([str(executable)], source / 'test', env, timeout=180, allow_failure=True) receipt.update(matrix.suite_result(output, command.records[-1]['exit'])) - missing = set(definition.get('required_cases', [])) - set(receipt['case_names']) - if missing: - raise ValueError('required suite cases absent: ' + ', '.join(sorted(missing))) + matrix.check_suite_cases(definition, host, linkage, receipt['case_names']) receipt.update(duration=time.monotonic() - started, log=command.records[-1]['log']) if name in ('tls_io_loopback', 'native_http1_tls_parity'): matrix.check_tls_witnesses(output, inventory['platforms'][host]) diff --git a/scripts/lib/package_matrix.py b/scripts/lib/package_matrix.py index 3a86b850..3cfae3e3 100644 --- a/scripts/lib/package_matrix.py +++ b/scripts/lib/package_matrix.py @@ -150,6 +150,18 @@ def suites(inventory, platform, tls): return {name: inventory['suites'][name] for name in names} +def check_suite_cases(definition, host, linkage, observed): + required = set(definition.get('required_cases', [])) + windows_static = set(definition.get('windows_static_cases', [])) + if not windows_static <= required: + raise ValueError('Windows static cases must also be required suite cases') + if host == 'Windows' and linkage == 'shared': + required -= windows_static + missing = required - set(observed) + if missing: + raise ValueError('required suite cases absent: ' + ', '.join(sorted(missing))) + + def suite_result(output, status): output = output.replace('\r\n', '\n') tests = re.findall(r'^(\d+) tests executed$', output, re.M) diff --git a/scripts/lib/release_platform.py b/scripts/lib/release_platform.py index 6b44e1c1..766841d9 100644 --- a/scripts/lib/release_platform.py +++ b/scripts/lib/release_platform.py @@ -153,7 +153,7 @@ def package_origin(root, origin, read_json, read_text, expected, platform, read_ receipt = suites[name] observed = matrix.suite_result(read_text(receipt['log']), 0) require(all(receipt.get(k) == v for k, v in observed.items()), 'stale platform suite summary') - require(set(definition.get('required_cases', [])) <= set(observed['case_names']), 'pruned platform suite') + matrix.check_suite_cases(definition, platform, linkage, observed['case_names']) return result diff --git a/scripts/test-v3-installed-consumer.py b/scripts/test-v3-installed-consumer.py index 977611e8..2f4aceb7 100644 --- a/scripts/test-v3-installed-consumer.py +++ b/scripts/test-v3-installed-consumer.py @@ -1580,5 +1580,32 @@ def test_nonzero_shutdown_is_failure(self): self.assertEqual(8, json.loads(receipt.read_text())['exit']) +class SuiteCaseCoverageTest(unittest.TestCase): + def setUp(self): + self.definition = dict(required_cases=['ordinary', 'allocation_failure'], + windows_static_cases=['allocation_failure']) + + def test_windows_dll_requires_all_ordinary_cases(self): + matrix.check_suite_cases(self.definition, 'Windows', 'shared', ['ordinary']) + with self.assertRaisesRegex(ValueError, 'ordinary'): + matrix.check_suite_cases(self.definition, 'Windows', 'shared', []) + + def test_static_proof_requires_all_allocation_failure_cases(self): + for host in ('Windows', 'Linux', 'Darwin', 'FreeBSD'): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, 'allocation_failure'): + matrix.check_suite_cases(self.definition, host, 'static', ['ordinary']) + matrix.check_suite_cases(self.definition, host, 'static', ['ordinary', 'allocation_failure']) + + def test_other_shared_platforms_retain_allocation_failure_coverage(self): + for host in ('Linux', 'Darwin', 'FreeBSD'): + with self.subTest(host=host), self.assertRaisesRegex(ValueError, 'allocation_failure'): + matrix.check_suite_cases(self.definition, host, 'shared', ['ordinary']) + + def test_windows_static_cases_must_also_be_required(self): + self.definition['required_cases'] = ['ordinary'] + with self.assertRaisesRegex(ValueError, 'static.*required'): + matrix.check_suite_cases(self.definition, 'Windows', 'static', ['ordinary']) + + if __name__ == '__main__': unittest.main() diff --git a/specs/tasks/FEATURE-ci-v3-runtime-green.md b/specs/tasks/FEATURE-ci-v3-runtime-green.md index 3117565e..32665928 100644 --- a/specs/tasks/FEATURE-ci-v3-runtime-green.md +++ b/specs/tasks/FEATURE-ci-v3-runtime-green.md @@ -1,6 +1,6 @@ # FEATURE-ci-v3-runtime-green -Status: Stages 1 and 3 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. +Status: Stages 1, 3, 4, 5 and 6 implemented with focused proof; complete CI and remaining supported runtime repairs are pending. Groundwork validation is explicitly prohibited until FULL CI IS GREEN. Base: `v3` at `8f3083977f49706e66cefbb3ee8b6f6685469719`. Working branch: `task/FEATURE-ci-v3-runtime-green`. User requested integration into `v3` before full CI completion; retain incomplete verification explicitly. Plan: primary checkout `.groundwork-plans/FEATURE-ci-v3-runtime-green-plan.md`. @@ -70,3 +70,61 @@ Integrate the focused repairs and ChangeLog update now. Full CI remains incomple Groundwork validation has not run and stays deferred until full CI is green. This integration does not mark the overall CI repair complete or reopen closed diagnostic batches. Draft PR #382 carries the original working-branch runs. + +## Stage 4: PSK test fixture scheduling (2026-10-27) + +- [x] Read the completed, previously authorized diagnostic receipts; keep their budgets closed. +- [x] Preserve the native Darwin RED: 14 tests/313 checks, three failures; held TLS1.3 cancellation callback already returned, and certificate-default TLS1.3 handshake timed out. +- [x] Release the held cancellation lookup explicitly after assertions and close; bound the callback wait to five seconds for failure cleanup. +- [x] Drain ready fragmented transport work and yield while waiting for provider work, preserving the original fragments and every deadline/assertion. +- [x] Reproduce the real certificate fixture timeout under coarse scheduling, then pass the same regression and all 14 PSK tests/313 checks. +- [x] Pass changed-file cpplint and diff checks. +- [ ] Native current-source CI, including Darwin selected archive acceptance. +- [ ] Resolve the remaining Windows external-loop failure from supported evidence. +- [ ] Close the FreeBSD selected-archive timeout gate with actual evidence. + +Evidence and commands: primary checkout `.debug/v3-ci-native-repairs-PJFEkn/journal.md`. +Current c1 installed matrix passed Linux, Darwin, FreeBSD; Windows failed. Native/fuzz, CodeQL and Verify Build passed. Source-equivalent selected archive remains failed; no current-source green claim. + +The diagnostic Windows four-test program failed one of 52 checks in 0.741 seconds with WSAPoll error10038 before stop was requested. Two bounded new socket-only probes on local Windows11 returned POLLNVAL successfully; they do not establish the Server2022 failure cause. No poll error suppression or guessed production fix was applied. Previous capture budgets remain closed. + +Fixture root cause: the cancellation rescue unconditionally released after250ms, before the owner could assert cancellation under slower scheduling. The transport also slept after every41/73-byte fragment, charging scheduler delays to real handshake deadlines. A minimal real TLS1.3 certificate fixture with1ms sleeps delayed to15ms failed at2089ms/outcome8(timeout); draining progress first passed at625ms. An intermediate full coarse-scheduling check still failed two held/capacity assertions because idle sleeps could expire the400ms pre-lookup deadline. Yielding while idle preserved that deadline and passed all313 checks in1.029seconds, with zero skips. The100ms owner-responsiveness assertion, cancellation/timeout/capacity/late-result assertions, bounded drains and all protocol settings remain unchanged. Only test source changed; production TLS is unchanged. + +Setup evidence is retained honestly: system OpenSSL was rejected before selecting the existing3.5.9 provider; the first suite launch used build-root cwd instead of test cwd and failed fixture path lookup. Correcting cwd, as required by the generated relative TLS_TEST_DIR, produced the meaningful behavioral check. Neither setup failure justifies a source workaround. No broad baseline or Groundwork validation was run. + +## Stage 5: Windows allocator fault-injection fixture linkage + +- [x] Identify the current PR383 failed program from one bounded selected artifact assessment (3,891,726 range bytes; no full ZIP or original execution). +- [x] Prove that executable operator-new replacement does not affect Windows DLL allocation, and that static linkage delivers the intended fault. +- [x] Keep all 22 ordinary readiness cases against the installed Windows DLL; require all 27 cases, including the five unchanged allocation-failure cases, against the installed static archive. +- [x] Make the required case names explicit in the inventory and reject every missing static fault case. Other platforms retain their original fault coverage. +- [x] Pass native Windows focused DLL22/438 and static27/486 gates, Linux28/497, 28 package coverage/matrix checks, changed-file lint and diff checks. +- [ ] Full exact-source CI with the corrected installed linkage profiles. + +Current PR383 package job122630892885 passed native62 in4.812276s and external4 in0.572681s, then failed external_readiness_adapter27/486 with13 failures, all inside five allocation-failure cases. The assertion expecting bad_alloc was false and regular work completed. The PSK change cannot affect these allocation boundaries; this repair is within the accepted overall CI goal. Member CRC/SHA are verified; the full212MB ZIP digest is metadata-only. Assessment is closed. + +A tiny native PE fixture reproduces the cause: executable allocation throws under its replacement operator new, DLL-owned allocation does not (exit1/0.362s). Linking the same owner object into the executable makes both throw (exit0/0.309s). Microsoft documents per-module DLL import binding: https://learn.microsoft.com/en-us/cpp/build/linking-an-executable-to-a-dll?view=msvc-170 . The chosen correction scopes allocation injection to linkage where it can actually reach the owner. Artificially throwing in the caller would not exercise the backend; a new production allocator hook or loader patch is unnecessary. + +Only Windows shared compilation selects LHS_TEST_DLL_LINKAGE; this omits allocator replacement and the five fault registrations from that executable. All five original assertion bodies remain unchanged and mandatory in the installed static lane. The inventory names all27 ordinary/static cases, and the receipt validator requires the22 DLL cases or all27 static cases. No test skip or failed/empty receipt is accepted. Every original C++ assertion line is preserved. The other platforms keep all five fault cases in both linkage lanes. + +Fresh current-source focused Windows backend DLL (16 owning translation units) passed22/438 in0.899s; matching static executable passed27/486 in1.237s, both zero skips and retired. Linux passes28/497 including its descriptor-zero case. Removing any one of the five fault names from the static receipt is rejected. Python coverage/matrix tests pass28 cases. Native setup initially missed net_address.cpp at link; adding that existing owning translation unit corrected the test build without a production workaround. Context is qualified local Windows11/cached LLVM23.1.4/GCC15.2 headers and retained runtime DLLs, not Server2022/MSYS2 CI. Evidence: primary `.debug/v3-ci-native-repairs-PJFEkn/allocator-linkage/`. + +## Approved Windows capture closure + +User approved exact430fa17e publication and ONE1/60 Windows original external-loop capture. Run40635371672 completed once:4tests/52checks/zero skips, exit0 in0.4701966s, retirement confirmed, stderr empty. Budget is CLOSED; no retry or remaining-time authority. This establishes nonreproduction in that context, not a fix or independence of the earlier WSAPoll10038 failure. The diagnostic branch remains NEVER MERGE. Only a proven repair may change production behavior. + +Stage5 receipt-import closure: source review found the selected-archive importer independently required all cases in each linkage. A new real receipt-bundle regression was RED with `valid DLL/static split rejected: pruned platform suite`. Reusing the same coverage validator fixes this producer/consumer mismatch while still rejecting missing static fault cases. All14 platform-bundle tests and changed Python syntax/diff checks pass. This is part of the same proven setup repair; native binaries are unchanged, so their focused evidence is carried without repetition. Current-source CI remains pending. + +## Stage 6: unique native Automake inventory + +- [x] Preserve Darwin archive failure `duplicate/empty native suite inventory` after its native commands completed. +- [x] Reproduce the exact producer inventory command against a real configured Makefile: TLSyes271 entries/266 unique, five duplicated executables. +- [x] Remove only four redundant TESTS additions; keep each executable in check_PROGRAMS, which already feeds TESTS recursively. +- [x] Regenerate actual Automake/configured Makefiles and verify unique inventories in both TLS modes: yes266/266, no235/235. All five affected executables remain registered exactly once. +- [ ] Full current-source archive evidence with this registration correction. + +The duplicated names are http_cookie, consumer_v3_cookie, consumer_v3_cookie_wire, protocol_limits and protocol_compression. TESTS already equals $(check_PROGRAMS); the extra TESTS += lines counted them twice. The strict receipt duplicate rejection is unchanged. No executable, assertion, script test, protocol setting or failure deadline was removed. This four-line configuration repair is proved by the actual producer command, not a source-text mirror test or a broad baseline rerun. + +Evidence: primary `.debug/v3-ci-native-repairs-PJFEkn/native-inventory/`. Autoreconf succeeded with existing AC_OUTPUT/.PHONY warnings. Initial config.status invocation used the source directory instead of its configured build directory and failed before any native execution; rechecking/regenerating in the correct build directory fixed setup. No unrelated source or generated tracked file changed. + +Current9c92 Windows package job122646921257 fails earlier at the public shared external consumer: exit6, captured stderr WSAPoll error10038. The selected820-byte process receipt was verified within a closed2,581,790-byte range assessment; no full artifact or original program ran. It contains no failed-call slot/state values. The earlier approved external4 capture remains CLOSED/PASS4/52/nonreproduction, and does not authorize running the distinct public consumer. No guessed polling or shutdown suppression was applied. A fresh consumer-specific bounded capture proposal is needed if further native diagnosis is required. diff --git a/test/Makefile.am b/test/Makefile.am index c8df9354..d0daa708 100644 --- a/test/Makefile.am +++ b/test/Makefile.am @@ -1588,7 +1588,6 @@ consumer_v3_peer_policy_runtime_LDADD = $(top_builddir)/src/libhttpserver_v3core consumer_v3_peer_policy_runtime_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) check_PROGRAMS += http_cookie consumer_v3_cookie -TESTS += http_cookie consumer_v3_cookie http_cookie_SOURCES = unit/http_cookie_test.cpp http_cookie_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) http_cookie_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) @@ -1598,7 +1597,6 @@ consumer_v3_cookie_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) consumer_v3_cookie_LDFLAGS = $(V3_TLS_LDFLAGS) $(AM_LDFLAGS) check_PROGRAMS += consumer_v3_cookie_wire -TESTS += consumer_v3_cookie_wire consumer_v3_cookie_wire_SOURCES = integ/consumer_v3_cookie_wire.cpp consumer_v3_cookie_wire_CPPFLAGS = -I$(top_srcdir)/src -I$(top_srcdir)/test -I$(top_srcdir)/examples consumer_v3_cookie_wire_LDADD = $(top_builddir)/src/libhttpserver.la $(V3_TLS_LIBS) @@ -1646,11 +1644,9 @@ compiled_operations_CPPFLAGS = -I$(top_srcdir)/src compiled_operations_LDADD = $(top_builddir)/src/libhttpserver.la check_PROGRAMS += protocol_limits -TESTS += protocol_limits protocol_limits_SOURCES = unit/protocol_limits_test.cpp check_PROGRAMS += protocol_compression -TESTS += protocol_compression protocol_compression_SOURCES = unit/protocol_compression_test.cpp TESTS += integ/release_ci_test.py integ/release_platform_bundle_test.py diff --git a/test/installed/v3-package-matrix.json b/test/installed/v3-package-matrix.json index 210a368c..3811a44c 100644 --- a/test/installed/v3-package-matrix.json +++ b/test/installed/v3-package-matrix.json @@ -73,6 +73,42 @@ "external_readiness_adapter": { "sources": [ "unit/external_readiness_adapter_test.cpp" + ], + "required_cases": [ + "host_drives_read_write_and_snapshot_is_owned", + "spurious_unknown_and_wrong_generation_do_not_consume", + "omitted_then_republished_registration_rejects_old_callbacks", + "reopen_same_id_and_descriptor_rejects_every_old_flag_combination", + "detached_read_cannot_rearm_into_reopened_id", + "detached_accept_cannot_rearm_into_reopened_id", + "exclusive_retirement_expires_real_timer_without_host_dispatch", + "timers_use_host_clock_order_and_reject_decreasing_samples", + "timer_cancel_expiry_race_is_exactly_once", + "wake_is_doorbell_and_post_snapshot_work_survives_acknowledgement", + "overlapping_dispatch_rejects_without_consuming_valid_read", + "host_accept_and_close_deliver_once", + "wake_published_during_dispatch_survives_and_recursive_overlap_reject", + "release_during_completion_defers_close_and_cannot_step_replacement", + "hard_wake_failure_stops_and_unwinds_pending_work", + "decreasing_clock_rejects_a_valid_socket_event_before_io", + "exhausted_identity_never_wraps_and_closes_unregistered_socket", + "buffered_read_precedes_combined_close_error_and_duplicate_callback", + "complete_snapshots_prune_retired_connection_records", + "allocation_failure_read_batch_remains_owned_until_close", + "allocation_failure_write_batch_remains_owned_until_close", + "allocation_failure_accept_batch_remains_owned_until_close", + "allocation_failure_second_direction_keeps_first_direction_owned", + "allocation_failure_due_timers_remain_owned_until_close", + "exhausted_generation_fails_snapshot_and_shutdown_completes_once", + "successful_accept_after_listener_retirement_closes_fresh_handle", + "successful_accept_after_backend_close_closes_fresh_handle" + ], + "windows_static_cases": [ + "allocation_failure_read_batch_remains_owned_until_close", + "allocation_failure_write_batch_remains_owned_until_close", + "allocation_failure_accept_batch_remains_owned_until_close", + "allocation_failure_second_direction_keeps_first_direction_owned", + "allocation_failure_due_timers_remain_owned_until_close" ] }, "readiness_contract": { diff --git a/test/integ/release_platform_bundle_test.py b/test/integ/release_platform_bundle_test.py index c646844c..2e8a6600 100755 --- a/test/integ/release_platform_bundle_test.py +++ b/test/integ/release_platform_bundle_test.py @@ -132,7 +132,7 @@ def test_valid_archive_import_rejects_duplicate_members_and_namespace_collisions stream.add(root/'Darwin/catalog.json',arcname='Darwin/catalog.json') with self.assertRaises(ValueError): module.unpack(duplicate,root/'duplicate') - def platform_origin_fixture(self): + def platform_origin_fixture(self, platform='Darwin'): module = self.module() import package_matrix as matrix import release_artifact as identity @@ -147,7 +147,7 @@ def write(path, value): path.write_text(json.dumps(value)) header = root / 'provider.h'; header.write_bytes(b'selected OpenSSL\r\n') selected = dict(archive_sha256='a'*64, snapshot_sha256='b'*64, extracted_sha256=hashlib.sha256(json.dumps({},sort_keys=True).encode()).hexdigest(), tls='both', sanitizer='none') - write(package/'summary.json', dict(status='passed', host='Darwin', lanes={'no':{},'yes':{}}, + write(package/'summary.json', dict(status='passed', host=platform, lanes={'no':{},'yes':{}}, revision='d'*40, provider_header_sha256=identity.sha256(header), release_artifact={'archive':{'sha256':'a'*64}, 'source':{'snapshot_sha256':'b'*64,'head':'d'*40}})) for mode in ('no','yes'): @@ -160,7 +160,7 @@ def write(path, value): public[linkage]=dict(dependencies=graph, compile={'dependencies':[]}, cells=[{'configurations':inventory['cases']},{'configurations':inventory['cases']}]) suites={} - for name, definition in matrix.suites(inventory, 'Darwin', mode).items(): + for name, definition in matrix.suites(inventory, platform, mode).items(): cases=definition.get('required_cases',[]) or ['fixture_case'] log=lane/('integration-'+linkage)/(name+'.log') log.parent.mkdir(parents=True, exist_ok=True) @@ -171,28 +171,56 @@ def write(path, value): write(lane/'audit.json',dict(violations=[],tls_mode=mode,public=public)) origin=dict(package=str(package), source_root=str(root/'source'), source_commit='d'*40, provider_header=str(header), native={}) # This test checks installed matrix origin independently; native origin is mandatory in complete bundles. - counts=module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), lambda p: Path(p).read_text(), selected, 'Darwin') + counts=module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), lambda p: Path(p).read_text(), selected, platform) self.assertGreater(counts['tests'],0) wrong_origin=dict(origin,source_commit='e'*40) with self.assertRaises(ValueError): - module.package_origin(ROOT,wrong_origin,lambda p:json.loads(Path(p).read_text()),lambda p:Path(p).read_text(),selected,'Darwin') + module.package_origin(ROOT,wrong_origin,lambda p:json.loads(Path(p).read_text()),lambda p:Path(p).read_text(),selected,platform) for mode in ('no','yes'): build=root/('native-'+mode); (build/'test').mkdir(parents=True) names=['protocol_fuzz_replay','hpack_corpus','http2_fuzz_replay','quic_fuzz_replay', - 'quic_recovery_fuzz_replay','http3_fuzz_replay','qpack_fuzz_replay','io_kqueue_backend'] + 'quic_recovery_fuzz_replay','http3_fuzz_replay','qpack_fuzz_replay','io_' + inventory['platforms'][platform] + '_backend'] if mode=='yes': names.append('quic_tls_callbacks_fuzz_replay') for name in names: (build/'test'/(name+'.trs')).write_text(':test-result: PASS\n') (build/'test/test-suite.log').write_text('# TOTAL: %d\n# PASS: %d\n# SKIP: 0\n# XFAIL: 0\n# FAIL: 0\n# XPASS: 0\n# ERROR: 0\n'%(len(names),len(names))) origin['native'][mode]=dict(build=str(build), tests=names) counts=checks.counts(counts['tests']+len(names),counts['checks']+len(names)) artifacts=[dict(path=str(header),sha256=identity.sha256(header),bytes=header.stat().st_size)] - row=dict(id='Darwin',identity=selected,archive_sha256='a'*64,origin=origin,status='passed',artifacts=artifacts, - receipt=dict(status='passed',gate='Darwin',identity=selected,counts=counts,artifacts=artifacts)) + row=dict(id=platform,identity=selected,archive_sha256='a'*64,origin=origin,status='passed',artifacts=artifacts, + receipt=dict(status='passed',gate=platform,identity=selected,counts=counts,artifacts=artifacts)) retained=root/'retained' - catalog=module.retain(retained,'Darwin',row,[p for p in root.rglob('*') if p.is_file()]) + catalog=module.retain(retained,platform,row,[p for p in root.rglob('*') if p.is_file()]) module.validate_origin(ROOT,row,catalog) return module, root, package, header, selected, origin, row, catalog + def test_windows_origin_retains_static_fault_proof_and_accepts_dll_profile(self): + import package_matrix as matrix + module, _, package, _, selected, origin, _, _ = self.platform_origin_fixture('Windows') + definition = matrix.load()['suites']['external_readiness_adapter'] + + def rewrite(linkage, cases): + directory = package / 'tls-no' / ('integration-' + linkage) + suites_path = directory / 'suites.json' + suites = json.loads(suites_path.read_text()) + receipt = suites['external_readiness_adapter'] + log = Path(receipt['log']) + log.write_text(''.join('Running test (%d): %s\n' % (i, case) for i, case in enumerate(cases)) + + '%d tests executed\n%d checks\n-> 0 failures\n-> 0 skipped\n' % (len(cases), len(cases))) + receipt.update(matrix.suite_result(log.read_text(), 0)) + suites_path.write_text(json.dumps(suites)) + + ordinary = [name for name in definition['required_cases'] if name not in definition['windows_static_cases']] + rewrite('shared', ordinary) + try: + module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), + lambda p: Path(p).read_text(), selected, 'Windows') + except ValueError as error: + self.fail('valid DLL/static split rejected: ' + str(error)) + rewrite('static', ordinary) + with self.assertRaisesRegex(ValueError, 'required suite cases absent'): + module.package_origin(ROOT, origin, lambda p: json.loads(Path(p).read_text()), + lambda p: Path(p).read_text(), selected, 'Windows') + def test_platform_origin_rejects_pruned_mandatory_native_suite(self): import release_checks as checks module, root, _, _, _, _, row, catalog = self.platform_origin_fixture() diff --git a/test/unit/external_readiness_adapter_test.cpp b/test/unit/external_readiness_adapter_test.cpp index e10a216e..70ee2601 100644 --- a/test/unit/external_readiness_adapter_test.cpp +++ b/test/unit/external_readiness_adapter_test.cpp @@ -39,6 +39,7 @@ #include "./io_backend_contract.hpp" #include "./littletest.hpp" +#ifndef LHS_TEST_DLL_LINKAGE // Fault injection is confined to this test executable and the dispatching // thread. Restore allocation before asserting or shutting down the backend. namespace { @@ -79,6 +80,7 @@ void* operator new(std::size_t size) { void operator delete(void* memory) noexcept { std::free(memory); } void operator delete(void* memory, std::size_t) noexcept { std::free(memory); } #endif +#endif // LHS_TEST_DLL_LINKAGE namespace srv = httpserver::server; namespace { @@ -203,6 +205,7 @@ void detached_reopen(littletest::test_runner* __lt_tr__, const char* __lt_name__ LT_CHECK_EQ(fx.backend.pending_count(), std::size_t{0}); } +#ifndef LHS_TEST_DLL_LINKAGE void failed_direction_collection(littletest::test_runner* __lt_tr__, const char* __lt_name__, hd::io_op_kind kind) { fixture fx; @@ -253,6 +256,8 @@ void failed_direction_collection(littletest::test_runner* __lt_tr__, LT_CHECK_EQ(second.delivered.load(), 1); } +#endif // LHS_TEST_DLL_LINKAGE + void accepted_after_teardown(littletest::test_runner* __lt_tr__, const char* __lt_name__, bool shutdown) { fixture fx; @@ -855,6 +860,9 @@ LT_BEGIN_AUTO_TEST(external_adapter_suite, complete_snapshots_prune_retired_conn } LT_END_AUTO_TEST(complete_snapshots_prune_retired_connection_records) +#ifndef LHS_TEST_DLL_LINKAGE +// PE DLL allocations bind their own allocator imports. These cases run against +// the installed static library, where this executable's override reaches them. LT_BEGIN_AUTO_TEST(external_adapter_suite, allocation_failure_read_batch_remains_owned_until_close) failed_direction_collection(__lt_tr__, __lt_name__, hd::io_op_kind::read); LT_END_AUTO_TEST(allocation_failure_read_batch_remains_owned_until_close) @@ -923,6 +931,7 @@ LT_BEGIN_AUTO_TEST(external_adapter_suite, allocation_failure_due_timers_remain_ LT_CHECK_EQ(first.delivered.load(), 1); LT_CHECK_EQ(second.delivered.load(), 1); LT_END_AUTO_TEST(allocation_failure_due_timers_remain_owned_until_close) +#endif // LHS_TEST_DLL_LINKAGE LT_BEGIN_AUTO_TEST(external_adapter_suite, exhausted_generation_fails_snapshot_and_shutdown_completes_once) fixture fx; diff --git a/test/unit/tls_psk_fixture.hpp b/test/unit/tls_psk_fixture.hpp index c8ca74d6..3c478476 100644 --- a/test/unit/tls_psk_fixture.hpp +++ b/test/unit/tls_psk_fixture.hpp @@ -135,17 +135,20 @@ struct connection : mtls_test::adapter_connection { if (host) SSL_set_tlsext_host_name(peer.get(), host); handshake = hd::tls_handshake_operation(owner, 1, std::chrono::steady_clock::now() + timeout); } - void tick() { - ex.run_pending(); - for (const auto& op : raw.pending_ops()) transfer(op); + bool tick() { + bool progressed = ex.run_pending() != 0; + for (const auto& op : raw.pending_ops()) progressed = transfer(op) || progressed; advance_peer(); + return progressed || ex.pending() != 0; } template bool until(Predicate predicate, std::chrono::milliseconds timeout = std::chrono::seconds(3)) { const auto deadline = std::chrono::steady_clock::now() + timeout; while (std::chrono::steady_clock::now() < deadline) { - tick(); + const bool progressed = tick(); if (predicate()) return true; - std::this_thread::sleep_for(std::chrono::milliseconds(1)); + // Fragmentation is deliberate; adding a scheduling delay to every + // ready fragment can consume the real handshake deadline. + if (!progressed) std::this_thread::yield(); } return false; } diff --git a/test/unit/tls_psk_test.cpp b/test/unit/tls_psk_test.cpp index 72a642fb..e59e24d3 100644 --- a/test/unit/tls_psk_test.cpp +++ b/test/unit/tls_psk_test.cpp @@ -119,23 +119,23 @@ LT_BEGIN_AUTO_TEST(psk_suite, blocking_lookup_leaves_owner_responsive_and_cancel auto released = release.get_future().share(); auto config = psk_test::credentials(runtime, [&](auto, const auto&) { entered = true; - released.wait(); + // The owner releases lookup only after checking cancellation. + // A broken blocking implementation must still unwind the fixture. + released.wait_for(5s); returned = true; return hd::psk_lookup_result{hd::psk_lookup_status::accepted, hd::secure_bytes(psk_test::bytes("secret"))}; }); hd::tls_credentials_registry registry; LT_ASSERT(registry.replace(config).ok()); psk_test::connection peer(registry.acquire()->select_default(), version, "identity", psk_test::bytes("secret")); - // Bounded rescue ensures RED cannot leave a fixture hanging in lookup. - auto rescue = std::async(std::launch::async, [&] { std::this_thread::sleep_for(250ms); release.set_value(); }); peer.handshake.submit(*peer.tls); std::chrono::steady_clock::duration longest{}; const auto until = std::chrono::steady_clock::now() + 1s; while (!entered && std::chrono::steady_clock::now() < until) { const auto before = std::chrono::steady_clock::now(); - peer.tick(); + const bool progressed = peer.tick(); longest = std::max(longest, std::chrono::steady_clock::now() - before); - std::this_thread::sleep_for(1ms); + if (!progressed) std::this_thread::yield(); } const auto cancelled = peer.tls->request_cancel(*peer.handshake.state()); peer.ex.run_pending(); @@ -147,7 +147,7 @@ LT_BEGIN_AUTO_TEST(psk_suite, blocking_lookup_leaves_owner_responsive_and_cancel LT_CHECK(peer.handshake.state()->stored_result().code == hh::outcome_code::cancelled); LT_CHECK(!peer.tls->peer_metadata()); peer.close(); - rescue.get(); + release.set_value(); runtime->stop(); LT_CHECK(runtime->drain(std::chrono::steady_clock::now() + 1s)); peer.ex.run_pending();