feat(xce-test-cli-ux): build interruptible-call heap-ownership mechanism
xcetest_run_interruptible() runs a blocking call on a detached worker thread while the caller polls xcetest_interrupted() every 50ms. A single atomic CAS (RUNNING -> DONE_UNCLAIMED | ABANDONED) decides which side frees the heap-allocated call block, so an abandoned call's worker thread never double-frees or leaks once it eventually finishes. Proven under active ASAN with a real detached thread and a real SIGINT-forced abandonment. 1282/1282 controller suite passing (+2). Task 1.4 of .kiro/specs/xce-test-cli-ux/tasks.md. Requirements: 1.1, 1.2, 1.3, 1.4 Co-Authored-By: Claude Sonnet 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_019jaMf9QEwpugH2W9KAuc7f
This commit is contained in:
parent
b4f5a460fc
commit
9a06047da2
6 changed files with 467 additions and 2 deletions
166
src/controller/testsuite/test_interruptcall.c
Normal file
166
src/controller/testsuite/test_interruptcall.c
Normal file
|
|
@ -0,0 +1,166 @@
|
|||
/*
|
||||
* test_interruptcall.c -- proves xcetest_run_interruptible()'s (task 1.4,
|
||||
* Requirements 1.1, 1.2, 1.3, 1.4) heap-block CAS ownership handoff: a
|
||||
* normal/fast call returns its real result and frees cleanly, and a
|
||||
* forced-abandonment call returns -EINTR immediately with no double-free
|
||||
* and no leak -- the abandoned worker thread finishes in the background
|
||||
* and frees the block itself, proven via a side channel rather than
|
||||
* assumed from a fixed sleep.
|
||||
*
|
||||
* xcetest_interrupted() (declared in xcetest_interrupt.h) is normally
|
||||
* defined in test.c, over test.c's own private break_out flag -- but
|
||||
* test.c itself is never linked into controller_unit_test (see
|
||||
* xcetest_interrupt.h's own header comment: test.c pulls in libedit/
|
||||
* cJSON/libxce/Cap'n Proto/OpenSSL and defines its own main(), both of
|
||||
* which make it impractical to link into a small Unity binary). So this
|
||||
* file provides its OWN local fake definition of xcetest_interrupted(),
|
||||
* over its own local flag -- the same "local fake standing in for a
|
||||
* real dependency this binary never links" pattern already used
|
||||
* elsewhere in this directory (e.g. test_commit_barrier_reply.c's/
|
||||
* test_worker_join.c's own fakes for on_scheduler_join()/notify()/
|
||||
* collect_confvars(), per src/controller/meson.build's comment on
|
||||
* sched_push_unit_test_src). interruptcall.c only ever calls
|
||||
* xcetest_interrupted() through its documented pure-read contract, so a
|
||||
* fake reading a test-local flag exercises interruptcall.c's real CAS
|
||||
* state machine exactly as production would.
|
||||
*
|
||||
* The forced-abandonment test triggers a REAL, OS-delivered SIGINT (via
|
||||
* kill(getpid(), SIGINT), sent from a small helper thread once the
|
||||
* worker has had time to start) rather than flipping the fake flag
|
||||
* directly by hand, and installs its handler via xcetest_install_sigint()
|
||||
* (task 1.3) -- the exact installer test.c's own main() uses -- so this
|
||||
* test also doubles as a real, signal-level exercise of the same
|
||||
* interrupt path a live xce_test process would take, not a purely
|
||||
* hand-simulated one. This is choice (ii) from the task brief (a real
|
||||
* SIGINT), preferred over choice (i) (a direct break_out setter) because
|
||||
* no such setter exists on the REAL break_out anyway (xcetest_interrupt.h
|
||||
* deliberately exposes none) -- and a real signal exercises the actual
|
||||
* delivery path end-to-end, the same thing test_xcetest_interrupt.c
|
||||
* already does for xcetest_install_sigint() itself.
|
||||
*/
|
||||
#include <errno.h>
|
||||
#include <pthread.h>
|
||||
#include <signal.h>
|
||||
#include <stdatomic.h>
|
||||
#include <time.h>
|
||||
#include <unistd.h>
|
||||
|
||||
#include <unity/unity.h>
|
||||
|
||||
#include "interruptcall.h"
|
||||
#include "xcetest_interrupt.h"
|
||||
|
||||
/* --- local fake for xcetest_interrupted() (see file header) --- */
|
||||
static volatile sig_atomic_t g_test_break_out = 0;
|
||||
|
||||
int xcetest_interrupted(void) { return g_test_break_out; }
|
||||
|
||||
static void test_sigint_handler(int signo)
|
||||
{
|
||||
(void)signo;
|
||||
g_test_break_out = 1;
|
||||
}
|
||||
|
||||
/* ---- Scenario (a): normal, uninterrupted completion ---- */
|
||||
|
||||
#define NORMAL_FN_RC 7
|
||||
|
||||
static int fast_fn(void* ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
return NORMAL_FN_RC;
|
||||
}
|
||||
|
||||
static void test_normal_completion_returns_real_result(void)
|
||||
{
|
||||
g_test_break_out = 0;
|
||||
|
||||
int rc = xcetest_run_interruptible(fast_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(NORMAL_FN_RC, rc);
|
||||
/* No side channel to poll here: on this path the CALLING thread's own
|
||||
* poll loop reads block->rc and frees the block synchronously, before
|
||||
* xcetest_run_interruptible() ever returns -- so by the time the
|
||||
* TEST_ASSERT_EQUAL_INT above runs, cleanup has unconditionally
|
||||
* already happened. The remaining proof is this whole suite running
|
||||
* clean under ASAN (no leak, no double-free) in the validating
|
||||
* build+test run. */
|
||||
}
|
||||
|
||||
/* ---- Scenario (b): forced abandonment via a real SIGINT ---- */
|
||||
|
||||
#define SLOW_FN_RC 99
|
||||
#define SLOW_FN_SLEEP_MS 900
|
||||
#define SIGNAL_DELAY_MS 150
|
||||
#define WAIT_FOR_FREE_MS 3000
|
||||
#define POLL_STEP_MS 20
|
||||
|
||||
static int slow_fn(void* ctx)
|
||||
{
|
||||
(void)ctx;
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = (long)SLOW_FN_SLEEP_MS * 1000L * 1000L;
|
||||
nanosleep(&ts, NULL);
|
||||
return SLOW_FN_RC;
|
||||
}
|
||||
|
||||
/* Runs on its own helper thread so it can deliver SIGINT to the process
|
||||
* WHILE the main test thread is blocked inside
|
||||
* xcetest_run_interruptible()'s poll loop below. */
|
||||
static void* send_sigint_after_delay(void* arg)
|
||||
{
|
||||
(void)arg;
|
||||
struct timespec ts;
|
||||
ts.tv_sec = 0;
|
||||
ts.tv_nsec = (long)SIGNAL_DELAY_MS * 1000L * 1000L;
|
||||
nanosleep(&ts, NULL);
|
||||
kill(getpid(), SIGINT);
|
||||
return NULL;
|
||||
}
|
||||
|
||||
static void test_forced_abandonment_returns_eintr_and_worker_cleans_up(void)
|
||||
{
|
||||
g_test_break_out = 0;
|
||||
atomic_store(&xcetest_test_abandoned_free_count, 0);
|
||||
xcetest_install_sigint(test_sigint_handler);
|
||||
|
||||
pthread_t signaler;
|
||||
TEST_ASSERT_EQUAL_INT(
|
||||
0, pthread_create(&signaler, NULL, send_sigint_after_delay, NULL));
|
||||
|
||||
/* SLOW_FN_SLEEP_MS (900ms) comfortably exceeds SIGNAL_DELAY_MS (150ms)
|
||||
* plus several 50ms poll intervals, so the interrupt is reliably
|
||||
* noticed well before slow_fn() itself would have returned -- this
|
||||
* call is expected to return -EINTR, not SLOW_FN_RC. */
|
||||
int rc = xcetest_run_interruptible(slow_fn, NULL);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(-EINTR, rc);
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(0, pthread_join(signaler, NULL));
|
||||
|
||||
/* The abandoned worker is still running slow_fn() in the background
|
||||
* at this point. Poll the XCE_TESTING-only side channel (NOT part of
|
||||
* the production API) instead of a single fixed sleep, so this test
|
||||
* proves the abandoned worker's asynchronous cleanup actually
|
||||
* happened -- confirming no leak -- rather than merely hoping enough
|
||||
* wall-clock time passed before the process exits. */
|
||||
int waited_ms = 0;
|
||||
while (atomic_load(&xcetest_test_abandoned_free_count) == 0
|
||||
&& waited_ms < WAIT_FOR_FREE_MS) {
|
||||
struct timespec poll;
|
||||
poll.tv_sec = 0;
|
||||
poll.tv_nsec = (long)POLL_STEP_MS * 1000L * 1000L;
|
||||
nanosleep(&poll, NULL);
|
||||
waited_ms += POLL_STEP_MS;
|
||||
}
|
||||
|
||||
TEST_ASSERT_EQUAL_INT(1, atomic_load(&xcetest_test_abandoned_free_count));
|
||||
}
|
||||
|
||||
void test_interruptcall(void)
|
||||
{
|
||||
UnitySetTestFile(__FILE__);
|
||||
RUN_TEST(test_normal_completion_returns_real_result);
|
||||
RUN_TEST(test_forced_abandonment_returns_eintr_and_worker_cleans_up);
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue