Land the QUIC Cut 3 eighth fix batch: release config, whole-duration seam, pollhammer live objects, zero-timeout bound, closerace starvation fix
Self's ruling of 2026-09-22 named six findings against Soul's eighth pass. This lands the code side of all six: - The wait-record seam (CULTMESH_QUIC_DEBUG_RECORD_WAIT) now wraps the whole duration handed to `wait_for`, not the raw `timeout_ms` before it is converted to milliseconds, so arithmetic applied after that conversion can no longer live outside what the seam records. Its "unset" sentinel moves to a separate `debug_wait_recorded` flag so a recorded -1 could never be confused with nothing recorded. - `pollhammer` also runs against a runtime holding a live connection to an unroutable address (RFC 5737 TEST-NET-1), a best-effort stream on it, and a real (never-connected) listener, closing the gap where the wait's predicate was only ever exercised on an empty runtime. - A new `zerotimeout` scenario pins that `timeout_ms <= 0` returns within 50 ms, probing 0, -1 and INT32_MIN. - `closerace`'s settle no longer sleeps a guessed duration: where the development seam exists it waits for `debug_peak_calls` to reach the poller count (reset before any poller thread exists, so nothing can zero a count a poller already raised) before a short fixed margin closes the last gap between being counted and being inside the condition wait. - `kGenerousLateToleranceMs` narrows from 800 ms to 400 ms so the wall-clock scenarios can catch arithmetic applied outside the seam's own argument, which `waitseam`'s equality check structurally cannot see. scripts/mutate-cultmesh.mjs gains a plain `release` configuration on both platforms (CULTMESH_QUIC_DEBUG_ASSERTS=OFF, CMAKE_BUILD_TYPE=Release) running `polltimeout`, `pollbusy`, `pollhammer` and `zerotimeout`, and seven new mutation entries: R1 (the release macro divides the duration by 10), W1-W3 (600 ms added, a 500 ms floor, and a 1000/909 scale, each applied outside the seam's own argument), P1 (the idle predicate also ends on a live connection), and Z1-Z2 (a 500 ms sleep and a 300 ms spin on a non-positive timeout). Two stale comments are corrected: the scenario list above `assertScenarios` now names `pollhammer` among the clock-sensitive entries, and a historical "1300" probe value that no longer matches the test file is removed. The header's section 7 intro now says the seam serves both the quiesce and the wait recording, not quiesce alone. Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com>
This commit is contained in:
parent
a1309c638c
commit
5646ff7351
4 changed files with 463 additions and 98 deletions
|
|
@ -33,6 +33,7 @@
|
|||
#include <algorithm>
|
||||
#include <array>
|
||||
#include <atomic>
|
||||
#include <chrono>
|
||||
#include <condition_variable>
|
||||
#include <cstdint>
|
||||
#include <cstdio>
|
||||
|
|
@ -43,6 +44,7 @@
|
|||
#include <mutex>
|
||||
#include <sstream>
|
||||
#include <string>
|
||||
#include <thread>
|
||||
#include <unordered_map>
|
||||
#include <vector>
|
||||
|
||||
|
|
@ -88,24 +90,27 @@
|
|||
// decrements the count and notifies. That is the ordinary exit every export
|
||||
// makes, and it is the bridge's wait — not the hold — that keeps the runtime
|
||||
// alive underneath it.
|
||||
// - RECORD_WAIT wraps the exact `timeout_ms` handed to the condition wait in
|
||||
// `cultmesh_quic_next_event`, in place, as a pass-through: it stores the
|
||||
// value and returns it unchanged, so a mutation to what is actually waited
|
||||
// on changes what this reports rather than the two ever being able to
|
||||
// diverge. A scenario reads it and asserts equality against the timeout it
|
||||
// asked for, deterministically and with no timing tolerance — the method
|
||||
// Self's ruling of 2026-09-22 asks for in place of inferring the same fact
|
||||
// from how long a call took to return.
|
||||
// - RECORD_WAIT wraps the whole duration handed to the condition wait in
|
||||
// `cultmesh_quic_next_event`, in place, as a pass-through: it stores that
|
||||
// duration's count and returns the duration unchanged, so a mutation to
|
||||
// what is actually waited on changes what this reports rather than the two
|
||||
// ever being able to diverge. It wraps `timeout_ms` after conversion to
|
||||
// `std::chrono::milliseconds`, not before — Self's ruling of 2026-09-22,
|
||||
// after arithmetic applied to the converted duration survived a seam that
|
||||
// only recorded the raw integer underneath it. A scenario reads it and
|
||||
// asserts equality against the timeout it asked for, deterministically and
|
||||
// with no timing tolerance, in place of inferring the same fact from how
|
||||
// long a call took to return.
|
||||
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
|
||||
#define CULTMESH_QUIC_DEBUG_PEAK(count) DebugRaisePeak(count)
|
||||
#define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) DebugRecordCallsAtClose(count)
|
||||
#define CULTMESH_QUIC_DEBUG_HELD(lock) DebugHoldCall(lock)
|
||||
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms) DebugRecordWait(timeout_ms)
|
||||
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) DebugRecordWait(duration)
|
||||
#else
|
||||
#define CULTMESH_QUIC_DEBUG_PEAK(count) ((void)0)
|
||||
#define CULTMESH_QUIC_DEBUG_AT_CLOSE(count) ((void)0)
|
||||
#define CULTMESH_QUIC_DEBUG_HELD(lock) (false)
|
||||
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms) (timeout_ms)
|
||||
#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) (duration)
|
||||
#endif
|
||||
|
||||
namespace {
|
||||
|
|
@ -249,6 +254,13 @@ std::mutex debug_hold_gate;
|
|||
std::condition_variable debug_hold_signal;
|
||||
bool debug_hold_armed = false;
|
||||
std::atomic<int32_t> debug_last_wait_ms{-1};
|
||||
// Separate from the value above on purpose: once the seam records the whole
|
||||
// duration (not just a millisecond count already known to be positive), a
|
||||
// mutation could in principle make a wait get recorded at exactly -1, and an
|
||||
// int32_t alone cannot tell that apart from "nothing recorded yet". This flag
|
||||
// is the "recorded" bit; the getter below is the only thing that still hands
|
||||
// out -1 for the unset case.
|
||||
std::atomic<bool> debug_wait_recorded{false};
|
||||
|
||||
void DebugRaisePeak(int count) {
|
||||
int peak = debug_peak_calls.load(std::memory_order_relaxed);
|
||||
|
|
@ -270,14 +282,18 @@ bool DebugHoldCall(std::unique_lock<std::mutex>& lock) {
|
|||
return true;
|
||||
}
|
||||
|
||||
// A pass-through: it stores `timeout_ms` and returns it unmodified, so wrapping
|
||||
// it around the exact expression handed to the condition wait makes the two
|
||||
// impossible to desync by construction. Whatever that expression evaluates to
|
||||
// — the plain argument today, or a derivation a future change or a mutation
|
||||
// introduces — is what gets recorded, because it is the same evaluation.
|
||||
int32_t DebugRecordWait(int32_t timeout_ms) {
|
||||
debug_last_wait_ms.store(timeout_ms);
|
||||
return timeout_ms;
|
||||
// A pass-through: it stores the duration's count and returns the duration
|
||||
// unmodified, so wrapping it around the whole expression handed to the
|
||||
// condition wait makes the two impossible to desync by construction. Whatever
|
||||
// that expression evaluates to — the plain converted argument today, or a
|
||||
// derivation a future change or a mutation introduces inside this call — is
|
||||
// what gets recorded, because it is the same evaluation. Anything applied
|
||||
// outside this call, after it returns, is what the wall-clock scenarios exist
|
||||
// to catch instead; see their generous-tolerance comment.
|
||||
std::chrono::milliseconds DebugRecordWait(std::chrono::milliseconds duration) {
|
||||
debug_last_wait_ms.store(static_cast<int32_t>(duration.count()));
|
||||
debug_wait_recorded.store(true);
|
||||
return duration;
|
||||
}
|
||||
#endif
|
||||
|
||||
|
|
@ -1199,7 +1215,7 @@ CULTMESH_API int32_t cultmesh_quic_next_event(
|
|||
// sooner, and `woken` is which of the two happened. RECORD_WAIT wraps
|
||||
// this exact argument; see the development seam note near its macros.
|
||||
const bool woken = runtime->signal.wait_for(
|
||||
lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),
|
||||
lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),
|
||||
[runtime] { return !runtime->events.empty() || runtime->closing; });
|
||||
// Development builds only; a no-op unless a scenario has armed the hold.
|
||||
// A held call returns the same 0 an empty queue would have, and makes no
|
||||
|
|
@ -1487,10 +1503,11 @@ CULTMESH_API int32_t cultmesh_quic_debug_calls_at_close(void) {
|
|||
}
|
||||
|
||||
CULTMESH_API int32_t cultmesh_quic_debug_last_wait_ms(void) {
|
||||
return debug_last_wait_ms.load();
|
||||
return debug_wait_recorded.load() ? debug_last_wait_ms.load() : -1;
|
||||
}
|
||||
|
||||
CULTMESH_API void cultmesh_quic_debug_reset_last_wait_ms(void) {
|
||||
debug_wait_recorded.store(false);
|
||||
debug_last_wait_ms.store(-1);
|
||||
}
|
||||
#endif
|
||||
|
|
|
|||
|
|
@ -316,9 +316,10 @@ CULTMESH_QUIC_API int32_t cultmesh_quic_last_status(void* runtime);
|
|||
*
|
||||
* Present only in a library configured with `CULTMESH_QUIC_DEBUG_ASSERTS`, which
|
||||
* a shipped build never is. It exists for the runtime-lifetime scenarios: the
|
||||
* quiesce is a promise about host calls that are inside the library, and a
|
||||
* scenario that can only sleep and hope cannot tell a bridge that counts them
|
||||
* from one that does not.
|
||||
* quiesce is a promise about host calls that are inside the library, and the
|
||||
* wait scenarios need the same kind of promise about the exact duration the
|
||||
* bridge hands its own condition wait. A scenario that can only sleep and hope
|
||||
* cannot tell a bridge that counts and waits correctly from one that does not.
|
||||
*
|
||||
* It is process-wide, not per-runtime, because the numbers outlive the runtime
|
||||
* they describe: `cultmesh_quic_runtime_close` frees the runtime, and the count
|
||||
|
|
@ -341,12 +342,14 @@ CULTMESH_QUIC_API int32_t cultmesh_quic_debug_peak_calls(void);
|
|||
* since the hold was armed. This is the number the wait exists for. */
|
||||
CULTMESH_QUIC_API int32_t cultmesh_quic_debug_calls_at_close(void);
|
||||
|
||||
/* The `timeout_ms` most recently handed to the condition wait inside
|
||||
* `cultmesh_quic_next_event`, recorded at the point the wait is entered rather
|
||||
* than observed from outside. A poll whose queue is not empty, or whose
|
||||
* `timeout_ms` is not greater than zero, never enters that wait and leaves this
|
||||
* unchanged. -1 until the first such wait, or after
|
||||
* `cultmesh_quic_debug_reset_last_wait_ms`.
|
||||
/* The whole duration, in milliseconds, most recently handed to the condition
|
||||
* wait inside `cultmesh_quic_next_event`, recorded at the point the wait is
|
||||
* entered rather than observed from outside. A poll whose queue is not empty,
|
||||
* or whose `timeout_ms` is not greater than zero, never enters that wait and
|
||||
* leaves this unchanged. -1 until the first such wait, or after
|
||||
* `cultmesh_quic_debug_reset_last_wait_ms` — tracked by a separate internal
|
||||
* flag, not by -1 being stored here, so a wait that ever recorded exactly -1
|
||||
* itself could not be confused with nothing having been recorded.
|
||||
*
|
||||
* This exists because a wall-clock probe cannot tell the host's timeout from
|
||||
* any function of it that is the identity at the probe's own value — a floor,
|
||||
|
|
|
|||
|
|
@ -25,6 +25,8 @@
|
|||
// cultmesh_quic_native_tests holdtimeout [iterations]
|
||||
// cultmesh_quic_native_tests waitseam [iterations]
|
||||
// cultmesh_quic_native_tests pollhammer [iterations]
|
||||
// cultmesh_quic_native_tests zerotimeout [iterations]
|
||||
// cultmesh_quic_native_tests payloadfit [iterations]
|
||||
//
|
||||
// `closerace` hunts the race: many pollers, many iterations, and whatever the
|
||||
// schedule gives. `holdclose` takes the guessing out of it — the bridge's
|
||||
|
|
@ -124,6 +126,17 @@ std::string CloseRaceOnce(int pollers) {
|
|||
std::atomic<bool> closing_started{false};
|
||||
std::atomic<int> left_before_the_close{0};
|
||||
|
||||
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
|
||||
// Reset before any poller thread exists, so nothing here can race a
|
||||
// `CallScope` that has already been counted: `cultmesh_quic_next_event`
|
||||
// raises `debug_peak_calls` the moment its `CallScope` is constructed, at
|
||||
// the very top of the call, and a reset performed even microseconds after
|
||||
// that construction would zero a count no further call is coming to raise
|
||||
// again, stranding the settle loop below at "not yet" until its deadline.
|
||||
cultmesh_quic_debug_hold_calls(1);
|
||||
cultmesh_quic_debug_hold_calls(0);
|
||||
#endif
|
||||
|
||||
std::vector<std::thread> threads;
|
||||
threads.reserve(static_cast<size_t>(pollers));
|
||||
for (int index = 0; index < pollers; ++index) {
|
||||
|
|
@ -153,10 +166,36 @@ std::string CloseRaceOnce(int pollers) {
|
|||
}
|
||||
|
||||
while (entering.load() < pollers) std::this_thread::yield();
|
||||
// Settle so the pollers are inside the blocking wait rather than on their way
|
||||
// to it. A poller that has not entered yet is a call beginning during the
|
||||
// close, which is the host's to avoid, not the bridge's to survive.
|
||||
#if defined(CULTMESH_QUIC_DEBUG_ASSERTS)
|
||||
// A fixed settle races CPU starvation: under enough contending threads a
|
||||
// poller that has only incremented `entering` can sit preempted for far
|
||||
// longer than any sleep this scenario could afford, so the close would
|
||||
// begin before it ever reached the blocking wait — not the race this
|
||||
// scenario exists to run, just a fixture that gave up too soon. Where the
|
||||
// development seam is compiled in, this reads the bridge's own count
|
||||
// instead of guessing at a duration: the settle ends only once every
|
||||
// poller is actually counted inside, however long the scheduler took.
|
||||
const auto counted_deadline =
|
||||
std::chrono::steady_clock::now() + std::chrono::seconds(30);
|
||||
while (cultmesh_quic_debug_peak_calls() < pollers &&
|
||||
std::chrono::steady_clock::now() < counted_deadline)
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(1));
|
||||
// `active_calls` — what `peak_calls` reflects — is raised the moment a
|
||||
// poller's `CallScope` is constructed, at the top of
|
||||
// `cultmesh_quic_next_event`, not when it reaches the condition wait a few
|
||||
// statements later. Counted-inside is the expensive part to wait for under
|
||||
// starvation (getting the scheduler to run a thread at all); a short fixed
|
||||
// settle covers the cheap remainder (a lock acquisition and an empty-queue
|
||||
// check) once every poller is already scheduled and running.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(50));
|
||||
#else
|
||||
// No seam in this configuration (the sanitizer build deliberately ships
|
||||
// without one), so the settle falls back to a longer fixed window rather
|
||||
// than the 50 ms that starved under 16 contending burners. It is still a
|
||||
// window, not a guarantee — the honest limit of a config with no way to
|
||||
// ask the bridge what it has actually counted.
|
||||
std::this_thread::sleep_for(std::chrono::milliseconds(500));
|
||||
#endif
|
||||
|
||||
closing_started.store(true);
|
||||
cultmesh_quic_runtime_close(runtime);
|
||||
|
|
@ -194,9 +233,15 @@ constexpr int kEarlyToleranceMs = 20;
|
|||
// — `waitseam` catches those by equality, with no clock involved — so this only
|
||||
// has to catch a wait that is wrong by a lot: a constant of the bridge's own, a
|
||||
// wait that never happens, a hold that parks a call its own timeout already
|
||||
// ended. Each of those misses by seconds, not milliseconds, so one flat, wide
|
||||
// margin serves every probe this file times, on a machine under real load.
|
||||
constexpr int kGenerousLateToleranceMs = 800;
|
||||
// ended, or the W1-W3 family Self's ruling of 2026-09-22 added, which apply
|
||||
// their arithmetic to the duration `wait_for` actually receives *after* the
|
||||
// seam has already recorded it and so cannot be caught by `waitseam`'s
|
||||
// equality check at all. 400 ms sits above the worst overshoot a 16-burner
|
||||
// stress pass measured on a loaded win32 host (180 ms; 27 ms on linux) and
|
||||
// below every one of W1-W3's smallest offsets (485 ms), so one flat margin
|
||||
// still serves every probe this file times without going blind to the family
|
||||
// it was widened for.
|
||||
constexpr int kGenerousLateToleranceMs = 400;
|
||||
|
||||
struct TimedPoll {
|
||||
int32_t result;
|
||||
|
|
@ -917,7 +962,109 @@ int WaitSeam(int iterations) {
|
|||
// loop, and it leaves no listener behind.
|
||||
constexpr int32_t kHammerPollMs = 1000;
|
||||
|
||||
std::string PollHammerOnce(bool with_recorded_error) {
|
||||
// A self-signed ECDSA P-256 PKCS12, generated once with OpenSSL and embedded so
|
||||
// this scenario needs no crypto library of its own and no network fetch:
|
||||
// openssl req -x509 -newkey ec -pkeyopt ec_paramgen_curve:prime256v1 \
|
||||
// -keyout key.pem -out cert.pem -days 3650 -nodes -subj "/CN=cultmesh-quic-native-tests"
|
||||
// openssl pkcs12 -export -out test.p12 -inkey key.pem -in cert.pem -passout pass:
|
||||
// Used only to give pollhammer's live-objects run a listener that can actually
|
||||
// load a credential; nothing ever connects to it, so it never accepts and never
|
||||
// queues an event this scenario would have to tell apart from a bug.
|
||||
constexpr std::array<uint8_t, 1088> kSelfSignedPkcs12{{
|
||||
0x30, 0x82, 0x04, 0x3c, 0x02, 0x01, 0x03, 0x30, 0x82, 0x03, 0xf2, 0x06,
|
||||
0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x82,
|
||||
0x03, 0xe3, 0x04, 0x82, 0x03, 0xdf, 0x30, 0x82, 0x03, 0xdb, 0x30, 0x82,
|
||||
0x02, 0x8a, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07,
|
||||
0x06, 0xa0, 0x82, 0x02, 0x7b, 0x30, 0x82, 0x02, 0x77, 0x02, 0x01, 0x00,
|
||||
0x30, 0x82, 0x02, 0x70, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d,
|
||||
0x01, 0x07, 0x01, 0x30, 0x5f, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7,
|
||||
0x0d, 0x01, 0x05, 0x0d, 0x30, 0x52, 0x30, 0x31, 0x06, 0x09, 0x2a, 0x86,
|
||||
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0c, 0x30, 0x24, 0x04, 0x10, 0x48,
|
||||
0xdf, 0x04, 0x5a, 0x31, 0x34, 0x6d, 0xe0, 0x7c, 0xde, 0x3c, 0xa2, 0xde,
|
||||
0xf2, 0x8d, 0x20, 0x02, 0x02, 0x08, 0x00, 0x30, 0x0c, 0x06, 0x08, 0x2a,
|
||||
0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x09, 0x05, 0x00, 0x30, 0x1d, 0x06,
|
||||
0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01, 0x2a, 0x04, 0x10,
|
||||
0x44, 0x04, 0xd9, 0x9b, 0x99, 0x3a, 0xcb, 0x89, 0xc0, 0xcb, 0x53, 0xe3,
|
||||
0xe3, 0x5b, 0xb4, 0x48, 0x80, 0x82, 0x02, 0x00, 0x92, 0xcc, 0x04, 0xcf,
|
||||
0x7d, 0x58, 0xec, 0x68, 0x1a, 0x1b, 0xd5, 0x3b, 0x17, 0x57, 0x22, 0xd5,
|
||||
0x45, 0x00, 0xff, 0x05, 0x0f, 0x0d, 0x18, 0x8b, 0x72, 0x5b, 0xcc, 0xd9,
|
||||
0x87, 0xf4, 0x99, 0x44, 0x7c, 0xd6, 0xc3, 0x02, 0x2a, 0xaf, 0xd9, 0x10,
|
||||
0x14, 0xd3, 0x9f, 0x1a, 0x2c, 0x7b, 0x4f, 0xe6, 0x0b, 0x1d, 0x7f, 0xb9,
|
||||
0xf4, 0x01, 0x5c, 0x4b, 0xd5, 0xa1, 0x57, 0xb0, 0x3d, 0xe0, 0xbc, 0x70,
|
||||
0x72, 0xe0, 0xe9, 0xb8, 0x9a, 0xde, 0x68, 0xfe, 0x7c, 0xc7, 0xca, 0xcc,
|
||||
0x61, 0x32, 0x88, 0x72, 0xdd, 0x97, 0x28, 0x7b, 0x5b, 0x0d, 0x30, 0xf5,
|
||||
0xa2, 0xd1, 0xe8, 0x89, 0xd4, 0x1d, 0x88, 0x28, 0xd2, 0xc3, 0x02, 0x93,
|
||||
0xa1, 0x27, 0xfb, 0x4a, 0xe7, 0xdc, 0xac, 0xf3, 0x0d, 0xa9, 0xd3, 0x14,
|
||||
0x37, 0x71, 0xb7, 0xe5, 0xaf, 0xdb, 0xb6, 0xda, 0x42, 0xb7, 0x24, 0x7a,
|
||||
0x94, 0xad, 0x73, 0x79, 0xde, 0x21, 0xb9, 0x13, 0x63, 0x98, 0x33, 0x62,
|
||||
0x4a, 0x36, 0xb8, 0x6d, 0x14, 0x8b, 0x84, 0xf0, 0x8e, 0x55, 0xcc, 0x95,
|
||||
0x14, 0x54, 0xcc, 0x68, 0x4b, 0x09, 0x24, 0xb4, 0x79, 0x49, 0x05, 0xc7,
|
||||
0x0e, 0xfe, 0xed, 0x23, 0x8c, 0x87, 0xc6, 0xc3, 0x76, 0x25, 0xd9, 0xd6,
|
||||
0xae, 0x5a, 0xf4, 0x86, 0x74, 0xb8, 0x44, 0x22, 0xe8, 0xfc, 0xa5, 0xc2,
|
||||
0x25, 0xbd, 0x84, 0xfd, 0x92, 0x96, 0xe2, 0x66, 0xa9, 0xb0, 0x1f, 0xd3,
|
||||
0x8d, 0xf7, 0x1c, 0xce, 0xed, 0x09, 0x11, 0x61, 0xec, 0x2d, 0xa4, 0x9d,
|
||||
0xb9, 0xdb, 0xd4, 0x2e, 0xa4, 0xff, 0xe6, 0x09, 0x8e, 0x9a, 0x0b, 0xdf,
|
||||
0x30, 0xf4, 0xea, 0xe6, 0x09, 0x2d, 0x16, 0xec, 0x5d, 0xb6, 0x39, 0xda,
|
||||
0xd4, 0x43, 0x41, 0x95, 0xc8, 0x32, 0xd2, 0xac, 0x17, 0x58, 0x05, 0x7e,
|
||||
0x10, 0x24, 0xae, 0xd6, 0x58, 0x80, 0x0a, 0x29, 0xcb, 0x4b, 0x78, 0x62,
|
||||
0xbe, 0xf6, 0x6c, 0x7d, 0x44, 0x07, 0x7b, 0xfb, 0xd9, 0xb6, 0x74, 0x75,
|
||||
0x9f, 0xe6, 0xb4, 0xc3, 0x54, 0x53, 0x46, 0xdb, 0xf2, 0xa1, 0x47, 0xb8,
|
||||
0xf1, 0xff, 0x01, 0xba, 0xde, 0xf9, 0xca, 0xd6, 0xc0, 0x38, 0x40, 0xd0,
|
||||
0x86, 0x72, 0xc8, 0xcf, 0xdd, 0xd6, 0xf6, 0x36, 0xe0, 0xf3, 0x2e, 0x95,
|
||||
0x23, 0xdb, 0x99, 0x72, 0x1c, 0xa6, 0xa0, 0xf7, 0x17, 0x93, 0x1e, 0x20,
|
||||
0x6c, 0x75, 0x25, 0x0e, 0x1c, 0xab, 0xa4, 0xce, 0x35, 0xd4, 0xf4, 0xc6,
|
||||
0x63, 0x89, 0x0d, 0xa9, 0x91, 0x0b, 0x7e, 0xce, 0x7d, 0x4d, 0x5e, 0xbf,
|
||||
0xfd, 0xa9, 0xbd, 0xb6, 0x1e, 0x2b, 0x08, 0x5d, 0x7a, 0xdd, 0x61, 0xdf,
|
||||
0x24, 0xe0, 0xff, 0x4b, 0xeb, 0xa2, 0xeb, 0xb6, 0x2f, 0x2c, 0xbf, 0x9b,
|
||||
0x5f, 0x6a, 0x1a, 0x33, 0xae, 0x02, 0x6d, 0xbf, 0x8e, 0x6f, 0xeb, 0xda,
|
||||
0x17, 0x88, 0x79, 0xf8, 0xe0, 0x1c, 0x1d, 0x64, 0x7c, 0xc3, 0xc2, 0xc1,
|
||||
0xce, 0x12, 0x67, 0x74, 0xbc, 0xef, 0x9f, 0x2f, 0x35, 0xef, 0x40, 0x24,
|
||||
0xc1, 0x5f, 0x82, 0xc9, 0x00, 0x2c, 0xbf, 0x2b, 0xbf, 0x64, 0x39, 0x63,
|
||||
0x0c, 0xcd, 0x6f, 0xe5, 0x18, 0xfa, 0xeb, 0x02, 0x61, 0xc3, 0x22, 0xb0,
|
||||
0xfd, 0xfb, 0x9d, 0x32, 0xbb, 0x2f, 0xd8, 0x9d, 0x63, 0x8d, 0x9e, 0xb1,
|
||||
0x5a, 0x7d, 0xc3, 0xe5, 0xcc, 0xae, 0xe4, 0x71, 0x0d, 0x7b, 0xa9, 0x80,
|
||||
0xbd, 0xef, 0x86, 0x12, 0x13, 0x42, 0xf8, 0x14, 0xcb, 0xa9, 0xfd, 0x86,
|
||||
0xea, 0xd5, 0x81, 0x54, 0x87, 0xbc, 0x66, 0x24, 0xfd, 0x12, 0xf5, 0x6b,
|
||||
0xf4, 0x45, 0x7e, 0x41, 0x8e, 0x00, 0xf3, 0x63, 0xc7, 0x24, 0xc5, 0xa1,
|
||||
0xfd, 0x70, 0xbc, 0xfa, 0x26, 0xa0, 0x07, 0xc2, 0x97, 0xb1, 0xe0, 0x42,
|
||||
0x5a, 0x2b, 0x65, 0xec, 0xda, 0x45, 0x79, 0xe5, 0x18, 0xf2, 0xce, 0x8e,
|
||||
0xc3, 0xe3, 0x74, 0x6d, 0x30, 0x82, 0x01, 0x49, 0x06, 0x09, 0x2a, 0x86,
|
||||
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x07, 0x01, 0xa0, 0x82, 0x01, 0x3a, 0x04,
|
||||
0x82, 0x01, 0x36, 0x30, 0x82, 0x01, 0x32, 0x30, 0x82, 0x01, 0x2e, 0x06,
|
||||
0x0b, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x0c, 0x0a, 0x01, 0x02,
|
||||
0xa0, 0x81, 0xf7, 0x30, 0x81, 0xf4, 0x30, 0x5f, 0x06, 0x09, 0x2a, 0x86,
|
||||
0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0d, 0x30, 0x52, 0x30, 0x31, 0x06,
|
||||
0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x05, 0x0c, 0x30, 0x24,
|
||||
0x04, 0x10, 0xd8, 0x91, 0xb9, 0x92, 0x3a, 0x1e, 0x6b, 0xa2, 0x57, 0x44,
|
||||
0x16, 0x3a, 0xea, 0x36, 0x8d, 0x60, 0x02, 0x02, 0x08, 0x00, 0x30, 0x0c,
|
||||
0x06, 0x08, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x02, 0x09, 0x05, 0x00,
|
||||
0x30, 0x1d, 0x06, 0x09, 0x60, 0x86, 0x48, 0x01, 0x65, 0x03, 0x04, 0x01,
|
||||
0x2a, 0x04, 0x10, 0x35, 0x48, 0x03, 0x23, 0x6a, 0x3d, 0x5f, 0x96, 0xb7,
|
||||
0xfb, 0xb1, 0xcb, 0xdc, 0x0c, 0x1f, 0x79, 0x04, 0x81, 0x90, 0x4d, 0x36,
|
||||
0x5b, 0x1e, 0xc1, 0xc1, 0xa3, 0xfa, 0x85, 0xe6, 0x58, 0x89, 0x5d, 0xbe,
|
||||
0x17, 0xb4, 0x90, 0x96, 0x2d, 0x3d, 0x32, 0xe2, 0x73, 0x6c, 0x2c, 0xfd,
|
||||
0x7b, 0xe4, 0x9c, 0x82, 0x3c, 0xe4, 0x7a, 0x58, 0x45, 0xdd, 0xcd, 0x00,
|
||||
0xa2, 0x52, 0xf9, 0x98, 0xd4, 0x0f, 0x64, 0x0b, 0x58, 0x27, 0xfe, 0xfc,
|
||||
0xfd, 0x60, 0x3f, 0x7e, 0x6f, 0x73, 0x73, 0xda, 0x16, 0x34, 0x83, 0x38,
|
||||
0x89, 0x3d, 0xbe, 0xde, 0x0a, 0x47, 0x7b, 0xcd, 0xc1, 0x88, 0x72, 0x38,
|
||||
0x95, 0xb5, 0x54, 0xfa, 0x2a, 0x84, 0x2b, 0xcd, 0x98, 0xe3, 0x20, 0xd2,
|
||||
0xac, 0xd8, 0xf5, 0xa8, 0xd2, 0xef, 0x1f, 0x99, 0x88, 0x16, 0x6f, 0xf2,
|
||||
0x08, 0xe9, 0x28, 0xc8, 0x7a, 0x02, 0x57, 0x5c, 0x1c, 0x94, 0x0f, 0x57,
|
||||
0xfc, 0x0e, 0x85, 0x63, 0xc5, 0x44, 0x45, 0x4f, 0xc6, 0x35, 0x52, 0x3d,
|
||||
0xd4, 0xe2, 0x22, 0x6f, 0x72, 0xde, 0x55, 0xad, 0x20, 0x84, 0x91, 0x14,
|
||||
0x7e, 0x5f, 0x98, 0x2a, 0x5e, 0x2b, 0xf4, 0xc5, 0x10, 0x1b, 0x31, 0x25,
|
||||
0x30, 0x23, 0x06, 0x09, 0x2a, 0x86, 0x48, 0x86, 0xf7, 0x0d, 0x01, 0x09,
|
||||
0x15, 0x31, 0x16, 0x04, 0x14, 0xbd, 0x8a, 0x08, 0x3c, 0x66, 0xf7, 0x9d,
|
||||
0xa0, 0x25, 0x8b, 0x33, 0xac, 0xc2, 0x5e, 0xdc, 0x76, 0xd4, 0x33, 0x1a,
|
||||
0xd6, 0x30, 0x41, 0x30, 0x31, 0x30, 0x0d, 0x06, 0x09, 0x60, 0x86, 0x48,
|
||||
0x01, 0x65, 0x03, 0x04, 0x02, 0x01, 0x05, 0x00, 0x04, 0x20, 0x4e, 0x6f,
|
||||
0x75, 0x90, 0x57, 0xb4, 0x39, 0x1e, 0xc3, 0x97, 0x50, 0x5e, 0x4d, 0xf2,
|
||||
0xa8, 0xf9, 0x0e, 0xcd, 0x7f, 0x57, 0x31, 0xae, 0xf2, 0x87, 0x0a, 0x55,
|
||||
0xb0, 0xbc, 0xd6, 0x11, 0x83, 0xc3, 0x04, 0x08, 0x52, 0x79, 0xb1, 0x83,
|
||||
0xe6, 0xe4, 0x6e, 0x4d, 0x02, 0x02, 0x08, 0x00,
|
||||
}};
|
||||
|
||||
std::string PollHammerOnce(bool with_recorded_error, bool with_live_objects) {
|
||||
void* runtime = nullptr;
|
||||
const int32_t opened = cultmesh_quic_runtime_open("cultmesh-quic-native-tests", &runtime);
|
||||
if (opened != 0 || runtime == nullptr)
|
||||
|
|
@ -938,6 +1085,36 @@ std::string PollHammerOnce(bool with_recorded_error) {
|
|||
}
|
||||
}
|
||||
|
||||
// Self's ruling of 2026-09-22, F3: the predicate covered by the hammer set
|
||||
// above is only ever exercised on a runtime with nothing live in it. An
|
||||
// unroutable address (RFC 5737 TEST-NET-1) gets a connection into
|
||||
// `runtime->connections` at once — `cultmesh_quic_connection_open` inserts
|
||||
// it before the handshake starts — and that handshake then never completes
|
||||
// inside this scenario's poll window, so it queues no event of its own. A
|
||||
// best-effort stream rides the same connection. The listener is real (a
|
||||
// credential MsQuic actually loads) but nothing ever connects to it, so it
|
||||
// never accepts and never queues an event either: every object here is
|
||||
// live in the runtime's maps without ever waking the idle poll on its own.
|
||||
uint64_t live_connection_id = 0;
|
||||
uint64_t live_stream_id = 0;
|
||||
uint64_t live_listener_id = 0;
|
||||
if (with_live_objects) {
|
||||
const int32_t connecting =
|
||||
cultmesh_quic_connection_open(runtime, "192.0.2.1", 9, &live_connection_id);
|
||||
if (connecting != 0) {
|
||||
cultmesh_quic_runtime_close(runtime);
|
||||
return "cultmesh_quic_connection_open (live-objects setup) returned " +
|
||||
std::to_string(connecting);
|
||||
}
|
||||
// Best-effort: whether this succeeds is not the point of the
|
||||
// scenario, and P1's predicate names `connections`, not `streams`.
|
||||
cultmesh_quic_stream_open(
|
||||
runtime, live_connection_id, CULTMESH_QUIC_STREAM_RELIABLE, &live_stream_id);
|
||||
uint16_t bound_port = 0;
|
||||
cultmesh_quic_listener_open(runtime, "127.0.0.1", 0, kSelfSignedPkcs12.data(),
|
||||
static_cast<int32_t>(kSelfSignedPkcs12.size()), "", &live_listener_id, &bound_port);
|
||||
}
|
||||
|
||||
std::atomic<bool> hammering{true};
|
||||
std::thread hammer([runtime, &hammering] {
|
||||
constexpr uint64_t kBogusId = 0xffffffffffffffffull;
|
||||
|
|
@ -958,6 +1135,9 @@ std::string PollHammerOnce(bool with_recorded_error) {
|
|||
const TimedPoll poll = PollFor(runtime, kHammerPollMs);
|
||||
hammering.store(false, std::memory_order_relaxed);
|
||||
hammer.join();
|
||||
if (live_listener_id != 0) cultmesh_quic_listener_close(runtime, live_listener_id);
|
||||
if (live_connection_id != 0) cultmesh_quic_connection_shutdown(runtime, live_connection_id, 0);
|
||||
(void)live_stream_id;
|
||||
cultmesh_quic_runtime_close(runtime);
|
||||
|
||||
if (poll.result != 0)
|
||||
|
|
@ -974,18 +1154,68 @@ std::string PollHammerOnce(bool with_recorded_error) {
|
|||
|
||||
int PollHammer(int iterations) {
|
||||
for (int iteration = 0; iteration < iterations; ++iteration) {
|
||||
std::string failure = PollHammerOnce(false);
|
||||
std::string failure = PollHammerOnce(false, false);
|
||||
if (!failure.empty())
|
||||
return Fail("pollhammer iteration " + std::to_string(iteration) + " (clean runtime): " + failure);
|
||||
failure = PollHammerOnce(true);
|
||||
failure = PollHammerOnce(true, false);
|
||||
if (!failure.empty())
|
||||
return Fail("pollhammer iteration " + std::to_string(iteration) +
|
||||
" (after a refused call recorded an error): " + failure);
|
||||
failure = PollHammerOnce(false, true);
|
||||
if (!failure.empty())
|
||||
return Fail("pollhammer iteration " + std::to_string(iteration) +
|
||||
" (with a live connection, stream and listener): " + failure);
|
||||
}
|
||||
std::printf("pollhammer %dx: ok\n", iterations);
|
||||
return 0;
|
||||
}
|
||||
|
||||
// Self's ruling of 2026-09-22, F4: any `timeout_ms <= 0` has to return within a
|
||||
// small, generously-margined bound, and a negative timeout has to be probed
|
||||
// too — the bridge's own guard (`timeout_ms > 0`) already treats zero and
|
||||
// negative alike, so 0, -1 and INT32_MIN all take the same fast path and none
|
||||
// of them may collide with the seam's own "nothing recorded" sentinel.
|
||||
constexpr std::array<int32_t, 3> kZeroTimeoutProbesMs{0, -1, INT32_MIN};
|
||||
|
||||
// Generous next to `kEarlyToleranceMs`/`kGenerousLateToleranceMs` because a
|
||||
// non-positive timeout never enters the condition wait at all, so this is
|
||||
// bounding a runtime lock acquisition and an empty-queue check, not a
|
||||
// scheduler wakeup — the margin only has to be well clear of a mistake that
|
||||
// makes this path sleep or spin, not of ordinary jitter.
|
||||
constexpr int kZeroTimeoutBoundMs = 50;
|
||||
|
||||
std::string ZeroTimeoutOnce(int32_t timeout_ms) {
|
||||
void* runtime = nullptr;
|
||||
const int32_t opened = cultmesh_quic_runtime_open("cultmesh-quic-native-tests", &runtime);
|
||||
if (opened != 0 || runtime == nullptr)
|
||||
return "cultmesh_quic_runtime_open returned " + std::to_string(opened);
|
||||
|
||||
const TimedPoll poll = PollFor(runtime, timeout_ms);
|
||||
cultmesh_quic_runtime_close(runtime);
|
||||
|
||||
if (poll.result != 0)
|
||||
return "a poll asking for " + std::to_string(timeout_ms) + " ms returned " +
|
||||
std::to_string(poll.result) + ", not the 0 an idle runtime owes it";
|
||||
if (poll.elapsed_ms > kZeroTimeoutBoundMs)
|
||||
return "a poll asking for " + std::to_string(timeout_ms) + " ms returned after " +
|
||||
std::to_string(poll.elapsed_ms) + " ms: a non-positive timeout must return within " +
|
||||
std::to_string(kZeroTimeoutBoundMs) + " ms, not stall or spin";
|
||||
return {};
|
||||
}
|
||||
|
||||
int ZeroTimeout(int iterations) {
|
||||
for (int iteration = 0; iteration < iterations; ++iteration) {
|
||||
for (int32_t probe : kZeroTimeoutProbesMs) {
|
||||
const std::string failure = ZeroTimeoutOnce(probe);
|
||||
if (!failure.empty())
|
||||
return Fail("zerotimeout iteration " + std::to_string(iteration) + " probe " +
|
||||
std::to_string(probe) + ": " + failure);
|
||||
}
|
||||
}
|
||||
std::printf("zerotimeout %dx: ok (%zu probes)\n", iterations, kZeroTimeoutProbesMs.size());
|
||||
return 0;
|
||||
}
|
||||
|
||||
int CloseRace(int iterations, int pollers) {
|
||||
for (int iteration = 0; iteration < iterations; ++iteration) {
|
||||
const std::string failure = CloseRaceOnce(pollers);
|
||||
|
|
@ -1001,8 +1231,8 @@ int CloseRace(int iterations, int pollers) {
|
|||
int main(int argc, char** argv) {
|
||||
if (argc < 2) {
|
||||
std::fprintf(stderr,
|
||||
"usage: %s closerace|holdclose|holdtimeout|polltimeout|pollbusy|pollhammer|payloadfit|"
|
||||
"latecall|waitseam [iterations] [pollers]\n",
|
||||
"usage: %s closerace|holdclose|holdtimeout|polltimeout|pollbusy|pollhammer|zerotimeout|"
|
||||
"payloadfit|latecall|waitseam [iterations] [pollers]\n",
|
||||
argv[0]);
|
||||
return 2;
|
||||
}
|
||||
|
|
@ -1017,6 +1247,7 @@ int main(int argc, char** argv) {
|
|||
if (scenario == "polltimeout") return PollTimeout(iterations);
|
||||
if (scenario == "pollbusy") return PollBusy(iterations);
|
||||
if (scenario == "pollhammer") return PollHammer(iterations);
|
||||
if (scenario == "zerotimeout") return ZeroTimeout(iterations);
|
||||
if (scenario == "payloadfit") return PayloadFit(iterations);
|
||||
if (scenario == "holdclose" || scenario == "latecall" || scenario == "holdtimeout" ||
|
||||
scenario == "waitseam") {
|
||||
|
|
|
|||
|
|
@ -468,8 +468,8 @@ const mutations = [
|
|||
// 50 ms settle, so the poller left before the close began and the scenario
|
||||
// complained about its own fixture rather than the bridge.
|
||||
rule: "a positive timeout blocks until an event or the close (loosening: it waits a token 1 ms)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(1)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(1)),",
|
||||
},
|
||||
// And the rule underneath that one: the duration waited on is the host's, not
|
||||
// one of the bridge's own. Self's ruling of 2026-09-22 moved this whole family
|
||||
|
|
@ -485,8 +485,8 @@ const mutations = [
|
|||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (revert: a shorter constant of the bridge's own)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(100)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(100)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
|
|
@ -494,8 +494,8 @@ const mutations = [
|
|||
// The lengthening, which is the half that looks harmless: every poll still
|
||||
// blocks, every poll still returns, and nothing anywhere reports an error.
|
||||
rule: "the wait is on the host's timeout (loosening: a longer constant of the bridge's own)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(2000)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(2000)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
|
|
@ -504,30 +504,30 @@ const mutations = [
|
|||
// and the most ordinary spelling this line will ever be given: cap the wait
|
||||
// so a shutdown gets noticed.
|
||||
rule: "the wait is on the host's timeout (loosening: the bridge caps it at a second)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 1000))),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 1000))),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
// The same clamp at a round ceiling nobody would call short.
|
||||
rule: "the wait is on the host's timeout (loosening: the bridge caps it at a round five seconds)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 5000))),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 5000))),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: a floor, so a polling host cannot spin)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::max)(timeout_ms, 100))),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::max)(timeout_ms, 100))),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: a little grace added to it)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms + 100)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms + 100)),",
|
||||
},
|
||||
// Soul's seventh pass named eight more derivations that were each the
|
||||
// identity at every wall-clock probe the previous six passes could afford:
|
||||
|
|
@ -541,22 +541,22 @@ const mutations = [
|
|||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: a floor at 50, invisible to a late-only check)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms < 50 ? 0 : timeout_ms)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms < 50 ? 0 : timeout_ms)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: rounded down to a 16 ms quantum)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms / 16 * 16)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms / 16 * 16)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: rounded up to a 40 ms quantum)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((timeout_ms + 39) / 40 * 40)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((timeout_ms + 39) / 40 * 40)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
|
|
@ -566,41 +566,41 @@ const mutations = [
|
|||
// a fault that only shows past the twelfth had nowhere to be seen at all.
|
||||
rule: "the wait is on the host's timeout (loosening: it doubles once a runtime has polled more than 12 times)",
|
||||
old: " const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " static std::atomic<int> debug_poll_count{0};\n" +
|
||||
" const int32_t doubled_timeout_ms = (++debug_poll_count > 12) ? timeout_ms * 2 : timeout_ms;\n" +
|
||||
" const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(doubled_timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(doubled_timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: the bridge caps it at 7275 ms)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::min)(timeout_ms, 7275))),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::min)(timeout_ms, 7275))),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: 25 ms of grace added to it)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms + 25)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms + 25)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: a floor of 40, so a polling host cannot spin)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT((std::max)(timeout_ms, 40))),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds((std::max)(timeout_ms, 40))),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait is on the host's timeout (loosening: scaled up by 6%)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms * 106 / 100)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms * 106 / 100)),",
|
||||
},
|
||||
// The seam itself: development-only, folded away to a pass-through in a
|
||||
// release build (see the macro's `#else` in cultmesh_quic_native.cpp), and
|
||||
|
|
@ -612,7 +612,7 @@ const mutations = [
|
|||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "the wait seam records what the bridge actually waits on (revert: it never records)",
|
||||
old: "std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),",
|
||||
old: "CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "std::chrono::milliseconds(timeout_ms),",
|
||||
},
|
||||
{
|
||||
|
|
@ -632,10 +632,10 @@ const mutations = [
|
|||
" // sooner, and `woken` is which of the two happened. RECORD_WAIT wraps\n" +
|
||||
" // this exact argument; see the development seam note near its macros.\n" +
|
||||
" const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " std::unique_lock<std::mutex> lock(runtime->gate);\n" +
|
||||
" CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms);\n" +
|
||||
" CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms));\n" +
|
||||
" if (runtime->events.empty() && timeout_ms > 0) {\n" +
|
||||
" // The host's timeout bounds the wait; an event or the close ends it\n" +
|
||||
" // sooner, and `woken` is which of the two happened. RECORD_WAIT wraps\n" +
|
||||
|
|
@ -659,10 +659,10 @@ const mutations = [
|
|||
// milliseconds of its 1000 ms ask.
|
||||
rule: "an idle poll stays for its timeout while other host threads call (revert: the wait has no predicate)",
|
||||
old: " const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms))) ==\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms))) ==\n" +
|
||||
" std::cv_status::no_timeout;\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -673,10 +673,10 @@ const mutations = [
|
|||
// any wake at all. Only a second host thread can see it.
|
||||
rule: "an idle poll stays for its timeout while other host threads call (loosening: it reads what woke it and does not wait again)",
|
||||
old: " const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)));\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)));\n" +
|
||||
" const bool woken = !runtime->events.empty() || runtime->closing;\n",
|
||||
},
|
||||
{
|
||||
|
|
@ -689,11 +689,11 @@ const mutations = [
|
|||
// of the input, so no single probe value makes it the identity.
|
||||
rule: "an idle poll stays for its timeout while other host threads call (loosening: each wake restarts the whole timeout)",
|
||||
old: " const bool woken = runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms)),\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),\n" +
|
||||
" [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " while (runtime->events.empty() && !runtime->closing &&\n" +
|
||||
" runtime->signal.wait_for(\n" +
|
||||
" lock, std::chrono::milliseconds(CULTMESH_QUIC_DEBUG_RECORD_WAIT(timeout_ms))) ==\n" +
|
||||
" lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms))) ==\n" +
|
||||
" std::cv_status::no_timeout) {\n" +
|
||||
" }\n" +
|
||||
" const bool woken = !runtime->events.empty() || runtime->closing;\n",
|
||||
|
|
@ -759,11 +759,10 @@ const mutations = [
|
|||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
// S5: the same guard at 1500, which `holdtimeout` used to carry at a long
|
||||
// probe of 1300 and could not see. The long probe is 1600 now, above the
|
||||
// stated ceiling, with the hold kept armed past it — a call this guard
|
||||
// parks anyway sits out the rest of the arming instead of leaving on its
|
||||
// own timeout, well past the scenario's generous late tolerance.
|
||||
// S5: the same guard at 1500. `holdtimeout`'s long probe sits above it, at
|
||||
// 1600, with the hold kept armed past it — a call this guard parks anyway
|
||||
// sits out the rest of the arming instead of leaving on its own timeout,
|
||||
// well past the scenario's generous late tolerance.
|
||||
rule: "the hold parks only a call the wait woke (loosening: it also parks any long wait, over 1500)",
|
||||
old: " if (woken && CULTMESH_QUIC_DEBUG_HELD(lock)) return 0;\n",
|
||||
new: " if ((woken || timeout_ms > 1500) && CULTMESH_QUIC_DEBUG_HELD(lock)) return 0;\n",
|
||||
|
|
@ -939,6 +938,93 @@ const mutations = [
|
|||
new: " runtime->signal.wait_for(lock, std::chrono::milliseconds(50),\n" +
|
||||
" [runtime] { return runtime->active_calls == 0; });",
|
||||
},
|
||||
// Self's ruling of 2026-09-22, the eighth fix batch. Six findings, six
|
||||
// entries below (R1, W1-W3, P1, Z1-Z2); F5 (closerace under starvation) and
|
||||
// F6 (stale prose) needed no new mutant.
|
||||
//
|
||||
// F1: nothing ran the release macro's `#else` branch. `polltimeout`,
|
||||
// `pollbusy`, `pollhammer` and `zerotimeout` now run against a plain
|
||||
// release configuration (asserts OFF, CMAKE_BUILD_TYPE=Release) on both
|
||||
// targets, and R1 changes that branch from a pass-through to a tenth of the
|
||||
// real timeout — invisible to every configuration that never built it.
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "R1: the release macro is a pass-through (loosening: it divides the duration by 10)",
|
||||
old: "#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) (duration)",
|
||||
new: "#define CULTMESH_QUIC_DEBUG_RECORD_WAIT(duration) ((duration)/10)",
|
||||
},
|
||||
// F2: the seam now wraps the whole duration handed to `wait_for`, so
|
||||
// anything that reaches the wait through the seam's own argument is caught
|
||||
// by `waitseam`'s equality check. These three apply their arithmetic
|
||||
// outside that argument instead — to the return value of RECORD_WAIT,
|
||||
// after it has already recorded and returned the honest duration — so
|
||||
// `waitseam` cannot see them by construction; only the wall-clock
|
||||
// scenarios can, which is why `kGenerousLateToleranceMs` was narrowed
|
||||
// alongside this entry.
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "W1: 600 ms added to the wait outside the seam's own argument",
|
||||
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)) + " +
|
||||
"std::chrono::milliseconds(600),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "W2: the wait floored at 500 ms outside the seam's own argument",
|
||||
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "lock, (std::max)(CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)), " +
|
||||
"std::chrono::milliseconds(500)),",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "W3: the wait scaled by a 1000/909 ratio outside the seam's own argument",
|
||||
old: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)),",
|
||||
new: "lock, CULTMESH_QUIC_DEBUG_RECORD_WAIT(std::chrono::milliseconds(timeout_ms)) * 1000 / 909,",
|
||||
},
|
||||
// F3: `pollhammer` covered the predicate only on a runtime with nothing
|
||||
// live in it. P1 adds the survivor: a busy host with a connection is
|
||||
// treated the same as a busy host with events queued, so it never sleeps.
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "P1: an idle poll also ends when the runtime holds a live connection",
|
||||
old: " [runtime] { return !runtime->events.empty() || runtime->closing; });\n",
|
||||
new: " [runtime] {\n" +
|
||||
" return !runtime->events.empty() || runtime->closing ||\n" +
|
||||
" !runtime->connections.empty();\n" +
|
||||
" });\n",
|
||||
},
|
||||
// F4: a non-positive timeout was never timed at all — the bridge's own
|
||||
// guard skips the wait outright, so nothing pinned how fast that path has
|
||||
// to return. Both entries insert a stall on exactly that path, ahead of
|
||||
// the guard that already exists.
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "Z1: a non-positive timeout sleeps 500 ms before returning",
|
||||
old: " if (runtime->events.empty() && timeout_ms > 0) {\n",
|
||||
new: " if (runtime->events.empty() && timeout_ms <= 0) {\n" +
|
||||
" std::this_thread::sleep_for(std::chrono::milliseconds(500));\n" +
|
||||
" }\n" +
|
||||
" if (runtime->events.empty() && timeout_ms > 0) {\n",
|
||||
},
|
||||
{
|
||||
target: "native",
|
||||
honestOn: ["linux-x64", "win32-x64"],
|
||||
rule: "Z2: a non-positive timeout spins for 300 ms before returning",
|
||||
old: " if (runtime->events.empty() && timeout_ms > 0) {\n",
|
||||
new: " if (runtime->events.empty() && timeout_ms <= 0) {\n" +
|
||||
" const auto debug_z2_until =\n" +
|
||||
" std::chrono::steady_clock::now() + std::chrono::milliseconds(300);\n" +
|
||||
" while (std::chrono::steady_clock::now() < debug_z2_until) {\n" +
|
||||
" }\n" +
|
||||
" }\n" +
|
||||
" if (runtime->events.empty() && timeout_ms > 0) {\n",
|
||||
},
|
||||
];
|
||||
|
||||
function digest(bytes) {
|
||||
|
|
@ -965,23 +1051,28 @@ function testsPass(workspace) {
|
|||
// the scenario runner beside it and run the scenarios. There is no npm here and
|
||||
// no `dist/`; the artifacts live under artifacts/, which is git-ignored.
|
||||
//
|
||||
// Two configurations, because the two rules are visible to different things. The
|
||||
// assertion build states the quiesce invariant outright, which is what makes the
|
||||
// missing wait fail on the first close instead of on an unlucky one. The
|
||||
// ThreadSanitizer build is the only thing that sees a notify land on a destroyed
|
||||
// condition variable, and it exists on linux-x64 alone.
|
||||
// Two or three configurations, because the rules are visible to different
|
||||
// things. The assertion build states the quiesce invariant outright, which is
|
||||
// what makes the missing wait fail on the first close instead of on an unlucky
|
||||
// one. The ThreadSanitizer build is the only thing that sees a notify land on
|
||||
// a destroyed condition variable, and it exists on linux-x64 alone. The
|
||||
// release build is the thing that actually ships: asserts off, no development
|
||||
// seam, no sanitizer — added by Self's ruling of 2026-09-22 because it is the
|
||||
// one configuration where R1's release-only macro branch is reachable at all.
|
||||
//
|
||||
// `holdclose`, `latecall` and `holdtimeout` run only where the development seam
|
||||
// exists, which is the assertion build: they drive the quiesce and the refusal
|
||||
// through that seam rather than through a sleep, and they are what kill the
|
||||
// rules about counting a host call at all. The ThreadSanitizer build
|
||||
// deliberately keeps the shipped shape — no assertions, no seam — because what
|
||||
// it is there for is the race in a library built like the one that ships.
|
||||
// it is there for is the race in a library built like the one that ships, and
|
||||
// the release build keeps that same shape for the rules that ship-shape alone
|
||||
// can hide.
|
||||
//
|
||||
// `polltimeout`, `pollbusy` and `holdtimeout` measure a wait against the
|
||||
// timeout it asked for, so they are run only where nothing distorts the clock:
|
||||
// a sanitizer's slowdown would make their bands meaningless, which is the honest
|
||||
// limit of those entries.
|
||||
// `polltimeout`, `pollbusy`, `pollhammer` and `holdtimeout` measure a wait
|
||||
// against the timeout it asked for, so they are run only where nothing
|
||||
// distorts the clock: a sanitizer's slowdown would make their bands
|
||||
// meaningless, which is the honest limit of those entries.
|
||||
const assertScenarios = [
|
||||
["holdclose", "3", "64"],
|
||||
["latecall", "5"],
|
||||
|
|
@ -990,15 +1081,34 @@ const assertScenarios = [
|
|||
["polltimeout", "3"],
|
||||
["pollbusy", "3"],
|
||||
["pollhammer", "3"],
|
||||
["zerotimeout", "3"],
|
||||
["payloadfit", "3"],
|
||||
["closerace", "20", "256"],
|
||||
];
|
||||
// Self's ruling of 2026-09-22, F1: the assertion build is the only
|
||||
// configuration that ever ran the timed scenarios, so R1's release-only
|
||||
// `#else` branch (cultmesh_quic_native.cpp's CULTMESH_QUIC_DEBUG_RECORD_WAIT
|
||||
// macro) was unobservable by construction — nothing built without
|
||||
// CULTMESH_QUIC_DEBUG_ASSERTS ever ran `polltimeout`, `pollbusy` or
|
||||
// `pollhammer` against it. This configuration is the thing that ships: a
|
||||
// plain Release build, asserts off, no sanitizer, running the timed
|
||||
// scenarios plus the new zero-timeout check against it.
|
||||
const releaseScenarios = [
|
||||
["polltimeout", "3"],
|
||||
["pollbusy", "3"],
|
||||
["pollhammer", "3"],
|
||||
["zerotimeout", "3"],
|
||||
];
|
||||
const nativeConfigurations = platform === "linux-x64"
|
||||
? [
|
||||
{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios },
|
||||
{ name: "tsan", asserts: "OFF", sanitizer: "thread", scenarios: [["closerace", "20", "256"]] },
|
||||
{ name: "release", asserts: "OFF", sanitizer: null, buildType: "Release", scenarios: releaseScenarios },
|
||||
]
|
||||
: [{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios }];
|
||||
: [
|
||||
{ name: "asserts", asserts: "ON", sanitizer: null, scenarios: assertScenarios },
|
||||
{ name: "release", asserts: "OFF", sanitizer: null, buildType: "Release", scenarios: releaseScenarios },
|
||||
];
|
||||
|
||||
function msquicArguments() {
|
||||
if (platform === "win32-x64") {
|
||||
|
|
@ -1087,10 +1197,14 @@ function nativeScenariosPass() {
|
|||
const flags = configuration.sanitizer
|
||||
? [`-fsanitize=${configuration.sanitizer}`, "-fno-omit-frame-pointer", "-g"].join(" ")
|
||||
: "";
|
||||
// The release configuration builds Release rather than RelWithDebInfo — it
|
||||
// is the thing that actually ships, and R1 targets the macro branch that
|
||||
// only a build without CULTMESH_QUIC_DEBUG_ASSERTS ever compiles.
|
||||
const buildType = configuration.buildType ?? "RelWithDebInfo";
|
||||
// Multi-config generators (MSVC) put the binaries in a per-config directory;
|
||||
// single-config ones (Ninja, Makefiles) do not.
|
||||
const binaries = platform === "win32-x64"
|
||||
? join(build, "bin", "RelWithDebInfo")
|
||||
? join(build, "bin", buildType)
|
||||
: join(build, "bin");
|
||||
try {
|
||||
execFileSync("cmake", [
|
||||
|
|
@ -1099,7 +1213,7 @@ function nativeScenariosPass() {
|
|||
// default is Makefiles, and the container the bridge is built in carries
|
||||
// ninja rather than make, so the default configured nothing at all.
|
||||
...(platform === "linux-x64" ? ["-G", "Ninja"] : []),
|
||||
"-DCMAKE_BUILD_TYPE=RelWithDebInfo",
|
||||
`-DCMAKE_BUILD_TYPE=${buildType}`,
|
||||
`-DCMAKE_CXX_FLAGS=${flags}`,
|
||||
`-DCMAKE_EXE_LINKER_FLAGS=${flags}`,
|
||||
`-DCMAKE_SHARED_LINKER_FLAGS=${flags}`,
|
||||
|
|
@ -1107,7 +1221,7 @@ function nativeScenariosPass() {
|
|||
`-DCULTMESH_QUIC_DEBUG_ASSERTS=${configuration.asserts}`,
|
||||
...msquic.configure,
|
||||
], { cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] });
|
||||
execFileSync("cmake", ["--build", build, "--config", "RelWithDebInfo"],
|
||||
execFileSync("cmake", ["--build", build, "--config", buildType],
|
||||
{ cwd: repoRoot, stdio: ["ignore", "pipe", "pipe"] });
|
||||
} catch (error) {
|
||||
// A mutation that does not compile is killed by the build, which is a
|
||||
|
|
|
|||
Loading…
Reference in a new issue