Merge pull request #2406 from DeusData/fix/spill-test-disk-determinism

test(spill): make spill test verdicts independent of host free disk
This commit is contained in:
Martin Vogel authored and GitHub committed 2026-09-30 22:21:17 +02:00
commit 0bee4c1b79
4 files changed
+78 -1

No files matched your search

+26 -1
View File
@@ -106,6 +106,31 @@ struct cbm_result_spill {
* target to race against, and a failed write stays handled underneath. */
enum { SPILL_FREE_FLOOR_GB = 10, SPILL_FREE_SHARE_DIV = 2 };
#ifdef CBM_ENABLE_TEST_SEAMS
/* Set by the test's main thread before any worker exists; atomic so a reading
* worker and a restoring test can never race under TSan. */
static _Atomic size_t g_free_bytes_pin;
size_t cbm_result_spill_pin_free_bytes_for_tests(size_t bytes) {
return atomic_exchange_explicit(&g_free_bytes_pin, bytes, memory_order_relaxed);
}
size_t cbm_result_spill_free_floor_bytes_for_tests(void) {
return (size_t)SPILL_FREE_FLOOR_GB * 1024 * 1024 * 1024;
}
#endif
/* Free space under the spill directory. Production always asks the disk. */
static size_t spill_free_bytes(const char *spill_dir) {
#ifdef CBM_ENABLE_TEST_SEAMS
size_t pinned = atomic_load_explicit(&g_free_bytes_pin, memory_order_relaxed);
if (pinned > 0) {
return pinned;
}
#endif
return cbm_fs_free_bytes(spill_dir);
}
/* Remove spill files left by runs that are no longer here. Only files this
* module names (spill-<pid>-<writer>.bin) are touched, and on Windows an open
* file refuses deletion, which is exactly the liveness check we want: a running
@@ -164,7 +189,7 @@ cbm_result_spill_t *cbm_result_spill_open(const char *dir, int writers, int slot
* caller in the memory path it was already in (back-pressure, then its own
* budget decision) — the same outcome as a disk that fills mid-run, minus
* the filled disk. */
size_t free_bytes = cbm_fs_free_bytes(spill_dir);
size_t free_bytes = spill_free_bytes(spill_dir);
int64_t byte_cap = 0;
if (free_bytes > 0) {
size_t floor_bytes = (size_t)SPILL_FREE_FLOOR_GB * 1024 * 1024 * 1024;
+11
View File
@@ -70,4 +70,15 @@ void cbm_result_spill_close(cbm_result_spill_t *sp);
* `new_base`. Exposed for tests. */
void cbm_result_relocate(CBMFileResult *result, const char *old_base, size_t len, char *new_base);
#ifdef CBM_ENABLE_TEST_SEAMS
/* Test builds only: open's free-space reading (and so its low-disk refusal and
* its disk-share cap) answers `bytes` instead of asking the disk; 0 asks the
* disk again. Returns the previous pin. A test's verdict must not depend on how
* full the machine's disk happens to be, so the test runner pins ample space
* for the whole process and the refusal test pins a low value on purpose. */
size_t cbm_result_spill_pin_free_bytes_for_tests(size_t bytes);
/* The production floor: below this much free space, open refuses. */
size_t cbm_result_spill_free_floor_bytes_for_tests(void);
#endif
#endif /* CBM_RESULT_SPILL_H */
+32
View File
@@ -8726,6 +8726,37 @@ TEST(extract_spill_round_trip_keeps_every_field) {
PASS();
}
/* The low-disk guard, pinned through the free-space seam rather than the host
* disk: one byte under the floor refuses (spilling onto a nearly full disk is
* a worse failure than the memory pressure it relieves), the floor itself
* opens. The runner pins ample space everywhere else; restored before asserts. */
TEST(extract_spill_refuses_below_the_free_disk_floor) {
char dir[512];
snprintf(dir, sizeof(dir), "%s/cbm_spill_XXXXXX", cbm_tmpdir());
ASSERT_NOT_NULL(cbm_mkdtemp(dir));
size_t floor_bytes = cbm_result_spill_free_floor_bytes_for_tests();
size_t saved = cbm_result_spill_pin_free_bytes_for_tests(floor_bytes - 1);
cbm_result_spill_t *below = cbm_result_spill_open(dir, 1, 1);
(void)cbm_result_spill_pin_free_bytes_for_tests(floor_bytes);
cbm_result_spill_t *at = cbm_result_spill_open(dir, 1, 1);
(void)cbm_result_spill_pin_free_bytes_for_tests(saved);
bool refused = below == NULL;
bool opened = at != NULL;
cbm_result_spill_close(below);
cbm_result_spill_close(at);
char spill_sub[600];
snprintf(spill_sub, sizeof(spill_sub), "%s/spill", dir);
cbm_rmdir(spill_sub);
cbm_rmdir(dir);
ASSERT_EQ(floor_bytes, (size_t)10 * 1024 * 1024 * 1024);
ASSERT_TRUE(refused);
ASSERT_TRUE(opened);
PASS();
}
/* ── A skipped file gets no LSP walk, per-file or cross-file ── */
/* CBM_TEST_LSP_SKIP_ON names the file: the result carries lsp_skipped, the
@@ -8785,6 +8816,7 @@ SUITE(extraction) {
RUN_TEST(extract_compact_keeps_every_field_and_shrinks_the_arena);
RUN_TEST(extract_compact_is_idempotent_and_survives_empty_results);
RUN_TEST(extract_spill_round_trip_keeps_every_field);
RUN_TEST(extract_spill_refuses_below_the_free_disk_floor);
RUN_TEST(extract_lsp_skipped_file_gets_no_walk);
RUN_TEST(extract_walk_truncated_when_a_node_budget_is_set);
/* Initialize extraction library */
+9
View File
@@ -24,6 +24,7 @@ int tf_skip_count = 0;
#include "mcp/index_supervisor.h" /* cbm_index_set_worker_role */
#include "mcp/mcp.h" /* cbm_mcp_handle_tool — act as a real worker */
#include "ui/http_server.h" /* deleted-self executable probe */
#include "result_spill.h" /* pinned free disk: spill verdicts ignore the host disk */
#include <sqlite3.h>
#include <errno.h>
#include <stdbool.h>
@@ -957,6 +958,14 @@ int main(int argc, char **argv) {
(void)printf("sanitized=%d test_seams=%d\n", sanitized, test_seams);
return 0;
}
/* A test's verdict is a pure function of code, test, platform and seed --
* never of how full this machine's disk is. The spill store refuses to open
* below 10 GB free, so without this pin every spill test failed, and every
* budget-driven pipeline test silently took the no-store path, on a host
* with less free space. Pin ample space for this process and every worker
* it re-execs; the refusal itself is pinned on purpose by
* extraction::extract_spill_refuses_below_the_free_disk_floor. */
(void)cbm_result_spill_pin_free_bytes_for_tests((size_t)64 * 1024 * 1024 * 1024);
/* Skip the multi-hundred-MB executable-image hash that computes the exact
* build fingerprint: it is tens of seconds per spawned worker/daemon under
* ASan on constrained CI runners and the sole cause of the daemon-family