- Add a comprehensive ST knowledge base document: - openclaw/memory/srs-coroutines.md - Add ST-focused developer skill: - openclaw/skills/st-develop/SKILL.md - openclaw/skills/st-develop/scripts/verify.sh - Add KB workflow skills that support ST documentation quality and learning: - openclaw/skills/kb-review/SKILL.md - openclaw/skills/srs-learn/SKILL.md - Update openclaw/skills/srs-support/SKILL.md to use dynamic SRS_ROOT path resolution, improving portability for KB/source loading. --------- Co-authored-by: Claude Opus 4.6 <noreply@anthropic.com> Co-authored-by: chatgpt-codex-connector[bot] <199175422+chatgpt-codex-connector[bot]@users.noreply.github.com>
803 lines
22 KiB
C++
803 lines
22 KiB
C++
/* SPDX-License-Identifier: MIT */
|
|
/* Copyright (c) 2013-2026 The SRS Authors */
|
|
|
|
#include <st_utest.hpp>
|
|
|
|
#include <st.h>
|
|
#include <stdint.h>
|
|
#include <errno.h>
|
|
#include <string.h>
|
|
|
|
#include <sys/socket.h>
|
|
#include <fcntl.h>
|
|
#include <unistd.h>
|
|
|
|
#define ST_UTIME_MILLISECONDS 1000
|
|
#define ST_UTEST_TIMEOUT (100 * ST_UTIME_MILLISECONDS)
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// Unit tests for context switching: verify that _st_md_cxt_save/_st_md_cxt_restore
|
|
// and st_thread_create's save-then-patch-SP trick actually work.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
// Test: a coroutine runs on a different stack than the primordial thread.
|
|
// This proves the SP-patching trick in st_thread_create works.
|
|
static void* coroutine_stack_addr(void* arg)
|
|
{
|
|
int local_var = 42;
|
|
// Write our stack variable address back to the caller.
|
|
*(uintptr_t*)arg = (uintptr_t)&local_var;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, CoroutineRunsOnSeparateStack)
|
|
{
|
|
uintptr_t primordial_stack_addr = 0;
|
|
uintptr_t coroutine_stack = 0;
|
|
|
|
// Capture primordial thread's stack address.
|
|
int local = 0;
|
|
primordial_stack_addr = (uintptr_t)&local;
|
|
|
|
st_thread_t trd = st_thread_create(coroutine_stack_addr, &coroutine_stack, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
st_thread_join(trd, NULL);
|
|
|
|
ASSERT_NE((uintptr_t)0, coroutine_stack);
|
|
|
|
// The coroutine's stack should be far from the primordial stack.
|
|
// Default stack is 128KB, so the difference should be at least that much.
|
|
uintptr_t diff = (primordial_stack_addr > coroutine_stack)
|
|
? (primordial_stack_addr - coroutine_stack)
|
|
: (coroutine_stack - primordial_stack_addr);
|
|
EXPECT_GT(diff, (uintptr_t)4096) << "Coroutine stack should be on a separate heap-allocated stack";
|
|
}
|
|
|
|
// Test: context switches preserve local state across yields.
|
|
// Each coroutine writes to its own local variable, yields, then checks the value.
|
|
// This proves save/restore of registers and stack is correct.
|
|
static void* coroutine_preserve_local(void* arg)
|
|
{
|
|
int id = *(int*)arg;
|
|
int local_state = id * 1000;
|
|
|
|
// Yield to let other coroutines run.
|
|
st_usleep(0);
|
|
|
|
// After resume, local_state should be intact — it lives on our own stack.
|
|
local_state += 1;
|
|
|
|
// Write result back.
|
|
*(int*)arg = local_state;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, LocalStatePreservedAcrossYield)
|
|
{
|
|
int a = 1, b = 2, c = 3;
|
|
st_thread_t t1 = st_thread_create(coroutine_preserve_local, &a, 1, 0);
|
|
st_thread_t t2 = st_thread_create(coroutine_preserve_local, &b, 1, 0);
|
|
st_thread_t t3 = st_thread_create(coroutine_preserve_local, &c, 1, 0);
|
|
ASSERT_TRUE(t1 && t2 && t3);
|
|
|
|
st_thread_join(t1, NULL);
|
|
st_thread_join(t2, NULL);
|
|
st_thread_join(t3, NULL);
|
|
|
|
// Each coroutine computed: id * 1000 + 1
|
|
EXPECT_EQ(1001, a);
|
|
EXPECT_EQ(2001, b);
|
|
EXPECT_EQ(3001, c);
|
|
}
|
|
|
|
// Test: context switching via st_thread_yield works correctly.
|
|
// We create coroutines that increment a shared counter in a deterministic order
|
|
// using yields. This verifies the scheduler + context switch round-trips.
|
|
static int g_counter = 0;
|
|
|
|
static void* coroutine_yield_order(void* arg)
|
|
{
|
|
int my_order = *(int*)arg;
|
|
|
|
// Wait until it's our turn.
|
|
while (g_counter < my_order) {
|
|
st_usleep(0);
|
|
}
|
|
|
|
// It's our turn — increment.
|
|
g_counter++;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, YieldOrderPreserved)
|
|
{
|
|
g_counter = 0;
|
|
|
|
int order0 = 0, order1 = 1, order2 = 2;
|
|
st_thread_t t0 = st_thread_create(coroutine_yield_order, &order0, 1, 0);
|
|
st_thread_t t1 = st_thread_create(coroutine_yield_order, &order1, 1, 0);
|
|
st_thread_t t2 = st_thread_create(coroutine_yield_order, &order2, 1, 0);
|
|
ASSERT_TRUE(t0 && t1 && t2);
|
|
|
|
st_thread_join(t0, NULL);
|
|
st_thread_join(t1, NULL);
|
|
st_thread_join(t2, NULL);
|
|
|
|
EXPECT_EQ(3, g_counter);
|
|
}
|
|
|
|
// Test: return value from coroutine is correctly passed through st_thread_join.
|
|
// This proves the full lifecycle: create (save+patch SP) → schedule (restore) →
|
|
// run → exit (save retval) → join (read retval).
|
|
static void* coroutine_retval(void* arg)
|
|
{
|
|
int input = *(int*)arg;
|
|
st_usleep(0);
|
|
// Return a computed value as void*.
|
|
return (void*)(intptr_t)(input * input);
|
|
}
|
|
|
|
VOID TEST(LearnKB, ReturnValueThroughJoin)
|
|
{
|
|
int input = 7;
|
|
st_thread_t trd = st_thread_create(coroutine_retval, &input, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
void* retval = NULL;
|
|
st_thread_join(trd, &retval);
|
|
|
|
EXPECT_EQ(49, (int)(intptr_t)retval);
|
|
}
|
|
|
|
// Test: start routine must NOT execute inline in st_thread_create.
|
|
// It should run only after scheduler handoff.
|
|
static int g_create_started = 0;
|
|
|
|
static void* coroutine_mark_started(void* /*arg*/)
|
|
{
|
|
g_create_started++;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, StartRoutineNotExecutedInline)
|
|
{
|
|
g_create_started = 0;
|
|
|
|
st_thread_t trd = st_thread_create(coroutine_mark_started, NULL, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
// Creator path: _st_md_cxt_save returns 0, so _st_thread_main is not run inline.
|
|
EXPECT_EQ(0, g_create_started) << "Coroutine must not run inline inside st_thread_create";
|
|
|
|
// After yielding, scheduler can run the created coroutine.
|
|
st_usleep(0);
|
|
EXPECT_EQ(1, g_create_started) << "Coroutine should run after scheduler handoff";
|
|
|
|
st_thread_join(trd, NULL);
|
|
}
|
|
|
|
static void* coroutine_run_once(void* arg)
|
|
{
|
|
int* runs = (int*)arg;
|
|
(*runs)++;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, JoinDrivesFirstRunWhenNoManualYield)
|
|
{
|
|
int runs = 0;
|
|
|
|
st_thread_t trd = st_thread_create(coroutine_run_once, &runs, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
// Still not run yet, because creator hasn't yielded.
|
|
EXPECT_EQ(0, runs);
|
|
|
|
// Join blocks current coroutine and hands control to scheduler.
|
|
st_thread_join(trd, NULL);
|
|
EXPECT_EQ(1, runs);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for st_mutex cooperative lock/wakeup workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct MutexLearnCtx {
|
|
st_mutex_t lock;
|
|
int* order;
|
|
int* index;
|
|
int id;
|
|
};
|
|
|
|
static void* coroutine_mutex_fifo_waiter(void* arg)
|
|
{
|
|
MutexLearnCtx* ctx = (MutexLearnCtx*)arg;
|
|
|
|
int r0 = st_mutex_lock(ctx->lock);
|
|
ST_ASSERT_ERROR(r0 != 0, r0, "waiter failed to lock mutex");
|
|
|
|
ctx->order[*ctx->index] = ctx->id;
|
|
(*ctx->index)++;
|
|
|
|
r0 = st_mutex_unlock(ctx->lock);
|
|
ST_ASSERT_ERROR(r0 != 0, r0, "waiter failed to unlock mutex");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, MutexCooperativeWorkflow)
|
|
{
|
|
st_mutex_t lock = st_mutex_new();
|
|
ASSERT_TRUE(lock != NULL);
|
|
|
|
int r0 = st_mutex_lock(lock);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
// Same-owner re-lock should fail with EDEADLK.
|
|
errno = 0;
|
|
r0 = st_mutex_lock(lock);
|
|
EXPECT_EQ(-1, r0);
|
|
EXPECT_EQ(EDEADLK, errno);
|
|
|
|
int order[2] = {0, 0};
|
|
int index = 0;
|
|
|
|
MutexLearnCtx w1 = {lock, order, &index, 1};
|
|
MutexLearnCtx w2 = {lock, order, &index, 2};
|
|
|
|
st_thread_t t1 = st_thread_create(coroutine_mutex_fifo_waiter, &w1, 1, 0);
|
|
st_thread_t t2 = st_thread_create(coroutine_mutex_fifo_waiter, &w2, 1, 0);
|
|
ASSERT_TRUE(t1 && t2);
|
|
|
|
// Let both waiters run and block on mutex wait_q in FIFO order.
|
|
st_usleep(0);
|
|
|
|
// Owner unlock should hand off to the first waiter without preemption.
|
|
r0 = st_mutex_unlock(lock);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
// Yield to allow waiter1 then waiter2 to run.
|
|
st_usleep(0);
|
|
st_usleep(0);
|
|
|
|
ST_COROUTINE_JOIN(t1, t1_err);
|
|
ST_COROUTINE_JOIN(t2, t2_err);
|
|
ST_EXPECT_SUCCESS(t1_err);
|
|
ST_EXPECT_SUCCESS(t2_err);
|
|
|
|
EXPECT_EQ(2, index);
|
|
EXPECT_EQ(1, order[0]);
|
|
EXPECT_EQ(2, order[1]);
|
|
|
|
r0 = st_mutex_destroy(lock);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for epoll/kqueue driven I/O sleep/wakeup behavior.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct WakeCtx {
|
|
st_netfd_t reader;
|
|
st_netfd_t writer;
|
|
char value;
|
|
};
|
|
|
|
static void* delayed_writer(void* arg)
|
|
{
|
|
WakeCtx* ctx = (WakeCtx*)arg;
|
|
|
|
// Give reader coroutine time to enter st_read() and block in st_poll().
|
|
st_usleep(10 * ST_UTIME_MILLISECONDS);
|
|
|
|
ssize_t n = st_write(ctx->writer, &ctx->value, 1, ST_UTEST_TIMEOUT);
|
|
ST_ASSERT_ERROR(n != 1, (int)n, "Writer failed");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void* waiting_reader(void* arg)
|
|
{
|
|
WakeCtx* ctx = (WakeCtx*)arg;
|
|
|
|
char ch = 0;
|
|
ssize_t n = st_read(ctx->reader, &ch, 1, ST_UTEST_TIMEOUT);
|
|
ST_ASSERT_ERROR(n != 1, (int)n, "Reader failed");
|
|
ST_ASSERT_ERROR(ch != ctx->value, (int)ch, "Unexpected byte");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(EpollWorkflowTest, ReaderSleepsAndWakesOnWriteReady)
|
|
{
|
|
int fds[2] = {-1, -1};
|
|
int r0 = ::socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
|
EXPECT_EQ(0, r0);
|
|
|
|
st_netfd_t reader = st_netfd_open_socket(fds[0]);
|
|
st_netfd_t writer = st_netfd_open_socket(fds[1]);
|
|
ASSERT_TRUE(reader != NULL);
|
|
ASSERT_TRUE(writer != NULL);
|
|
|
|
WakeCtx ctx;
|
|
ctx.reader = reader;
|
|
ctx.writer = writer;
|
|
ctx.value = 'S';
|
|
|
|
st_thread_t rd = st_thread_create(waiting_reader, &ctx, 1, 0);
|
|
st_thread_t wr = st_thread_create(delayed_writer, &ctx, 1, 0);
|
|
|
|
ASSERT_TRUE(rd != NULL);
|
|
ASSERT_TRUE(wr != NULL);
|
|
|
|
ST_COROUTINE_JOIN(rd, rd_err);
|
|
ST_COROUTINE_JOIN(wr, wr_err);
|
|
|
|
ST_EXPECT_SUCCESS(rd_err);
|
|
ST_EXPECT_SUCCESS(wr_err);
|
|
|
|
st_netfd_close(reader);
|
|
st_netfd_close(writer);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for st_cond condition variable workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct CondLearnCtx {
|
|
st_cond_t cond;
|
|
int* wake_order;
|
|
int* wake_index;
|
|
int id;
|
|
st_utime_t timeout;
|
|
};
|
|
|
|
static void* coroutine_cond_wait_and_record(void* arg)
|
|
{
|
|
CondLearnCtx* ctx = (CondLearnCtx*)arg;
|
|
|
|
int r0 = st_cond_timedwait(ctx->cond, ctx->timeout);
|
|
ST_ASSERT_ERROR(r0 != 0, r0, "cond wait expected success");
|
|
|
|
ctx->wake_order[*ctx->wake_index] = ctx->id;
|
|
(*ctx->wake_index)++;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
static void* coroutine_cond_wait_timeout(void* arg)
|
|
{
|
|
CondLearnCtx* ctx = (CondLearnCtx*)arg;
|
|
|
|
errno = 0;
|
|
int r0 = st_cond_timedwait(ctx->cond, ctx->timeout);
|
|
ST_ASSERT_ERROR(r0 != -1, r0, "cond wait should timeout");
|
|
ST_ASSERT_ERROR(errno != ETIME, errno, "cond wait should set ETIME");
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, CondSignalWakeOne)
|
|
{
|
|
st_cond_t cond = st_cond_new();
|
|
ASSERT_TRUE(cond != NULL);
|
|
|
|
int signal_order[2] = {0, 0};
|
|
int signal_index = 0;
|
|
|
|
CondLearnCtx s1 = {cond, signal_order, &signal_index, 1, ST_UTEST_TIMEOUT};
|
|
CondLearnCtx s2 = {cond, signal_order, &signal_index, 2, ST_UTEST_TIMEOUT};
|
|
|
|
st_thread_t ts1 = st_thread_create(coroutine_cond_wait_and_record, &s1, 1, 0);
|
|
st_thread_t ts2 = st_thread_create(coroutine_cond_wait_and_record, &s2, 1, 0);
|
|
ASSERT_TRUE(ts1 && ts2);
|
|
|
|
st_usleep(0); // Let both waiters enter cond wait_q.
|
|
|
|
int r0 = st_cond_signal(cond);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
st_usleep(0); // Allow one waiter to resume.
|
|
EXPECT_EQ(1, signal_index);
|
|
|
|
// Wake remaining waiter to end the case cleanly.
|
|
r0 = st_cond_signal(cond);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
ST_COROUTINE_JOIN(ts1, ts1_err);
|
|
ST_COROUTINE_JOIN(ts2, ts2_err);
|
|
ST_EXPECT_SUCCESS(ts1_err);
|
|
ST_EXPECT_SUCCESS(ts2_err);
|
|
EXPECT_EQ(2, signal_index);
|
|
|
|
r0 = st_cond_destroy(cond);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
VOID TEST(LearnKB, CondBroadcastWakeAll)
|
|
{
|
|
st_cond_t cond = st_cond_new();
|
|
ASSERT_TRUE(cond != NULL);
|
|
|
|
int broadcast_order[2] = {0, 0};
|
|
int broadcast_index = 0;
|
|
|
|
CondLearnCtx b1 = {cond, broadcast_order, &broadcast_index, 1, ST_UTEST_TIMEOUT};
|
|
CondLearnCtx b2 = {cond, broadcast_order, &broadcast_index, 2, ST_UTEST_TIMEOUT};
|
|
|
|
st_thread_t tb1 = st_thread_create(coroutine_cond_wait_and_record, &b1, 1, 0);
|
|
st_thread_t tb2 = st_thread_create(coroutine_cond_wait_and_record, &b2, 1, 0);
|
|
ASSERT_TRUE(tb1 && tb2);
|
|
|
|
st_usleep(0); // Let both waiters enter cond wait_q.
|
|
|
|
int r0 = st_cond_broadcast(cond);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
ST_COROUTINE_JOIN(tb1, tb1_err);
|
|
ST_COROUTINE_JOIN(tb2, tb2_err);
|
|
ST_EXPECT_SUCCESS(tb1_err);
|
|
ST_EXPECT_SUCCESS(tb2_err);
|
|
EXPECT_EQ(2, broadcast_index);
|
|
|
|
r0 = st_cond_destroy(cond);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
VOID TEST(LearnKB, CondTimedwaitTimeout)
|
|
{
|
|
st_cond_t cond = st_cond_new();
|
|
ASSERT_TRUE(cond != NULL);
|
|
|
|
CondLearnCtx t1 = {cond, NULL, NULL, 0, 10 * ST_UTIME_MILLISECONDS};
|
|
st_thread_t tt1 = st_thread_create(coroutine_cond_wait_timeout, &t1, 1, 0);
|
|
ASSERT_TRUE(tt1 != NULL);
|
|
|
|
ST_COROUTINE_JOIN(tt1, tt1_err);
|
|
ST_EXPECT_SUCCESS(tt1_err);
|
|
|
|
int r0 = st_cond_destroy(cond);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for st_thread_exit coroutine termination workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static void* coroutine_explicit_exit_with_retval(void* /*arg*/)
|
|
{
|
|
st_thread_exit((void*)(intptr_t)123);
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, ThreadExitExplicitRetvalThroughJoin)
|
|
{
|
|
st_thread_t trd = st_thread_create(coroutine_explicit_exit_with_retval, NULL, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
void* retval = NULL;
|
|
int r0 = st_thread_join(trd, &retval);
|
|
ASSERT_EQ(0, r0);
|
|
EXPECT_EQ(123, (int)(intptr_t)retval);
|
|
}
|
|
|
|
static void* coroutine_nonjoinable_exit(void* arg)
|
|
{
|
|
int* finished = (int*)arg;
|
|
*finished = 1;
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, ThreadExitNonJoinableCannotJoin)
|
|
{
|
|
int finished = 0;
|
|
|
|
st_thread_t trd = st_thread_create(coroutine_nonjoinable_exit, &finished, 0, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
errno = 0;
|
|
int r0 = st_thread_join(trd, NULL);
|
|
EXPECT_EQ(-1, r0);
|
|
EXPECT_EQ(EINVAL, errno);
|
|
|
|
// Non-joinable thread still runs and exits on its own.
|
|
st_usleep(0);
|
|
EXPECT_EQ(1, finished);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for st_thread_interrupt wakeup workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
struct InterruptSleepCtx {
|
|
volatile int ready;
|
|
int r0;
|
|
int err;
|
|
};
|
|
|
|
static void* coroutine_interruptible_sleep(void* arg)
|
|
{
|
|
InterruptSleepCtx* ctx = (InterruptSleepCtx*)arg;
|
|
|
|
ctx->ready = 1;
|
|
errno = 0;
|
|
ctx->r0 = st_usleep(ST_UTEST_TIMEOUT);
|
|
ctx->err = errno;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, ThreadInterruptWakeupFromUsleep)
|
|
{
|
|
InterruptSleepCtx ctx = {0, 0, 0};
|
|
|
|
st_thread_t trd = st_thread_create(coroutine_interruptible_sleep, &ctx, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
while (!ctx.ready) {
|
|
st_usleep(0);
|
|
}
|
|
st_usleep(0); // Let target enter st_usleep wait state.
|
|
|
|
st_thread_interrupt(trd);
|
|
|
|
ST_COROUTINE_JOIN(trd, trd_err);
|
|
ST_EXPECT_SUCCESS(trd_err);
|
|
EXPECT_EQ(-1, ctx.r0);
|
|
EXPECT_EQ(EINTR, ctx.err);
|
|
}
|
|
|
|
struct InterruptCondCtx {
|
|
st_cond_t cond;
|
|
volatile int ready;
|
|
int r0;
|
|
int err;
|
|
};
|
|
|
|
static void* coroutine_interruptible_condwait(void* arg)
|
|
{
|
|
InterruptCondCtx* ctx = (InterruptCondCtx*)arg;
|
|
|
|
ctx->ready = 1;
|
|
errno = 0;
|
|
ctx->r0 = st_cond_timedwait(ctx->cond, ST_UTIME_NO_TIMEOUT);
|
|
ctx->err = errno;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, ThreadInterruptWakeupFromCondWait)
|
|
{
|
|
st_cond_t cond = st_cond_new();
|
|
ASSERT_TRUE(cond != NULL);
|
|
|
|
InterruptCondCtx ctx = {cond, 0, 0, 0};
|
|
st_thread_t trd = st_thread_create(coroutine_interruptible_condwait, &ctx, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
while (!ctx.ready) {
|
|
st_usleep(0);
|
|
}
|
|
st_usleep(0); // Let target enter cond wait_q.
|
|
|
|
st_thread_interrupt(trd);
|
|
|
|
ST_COROUTINE_JOIN(trd, trd_err);
|
|
ST_EXPECT_SUCCESS(trd_err);
|
|
EXPECT_EQ(-1, ctx.r0);
|
|
EXPECT_EQ(EINTR, ctx.err);
|
|
|
|
int r0 = st_cond_destroy(cond);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
struct InterruptMutexCtx {
|
|
st_mutex_t lock;
|
|
volatile int ready;
|
|
int r0;
|
|
int err;
|
|
};
|
|
|
|
static void* coroutine_interruptible_mutex_lock(void* arg)
|
|
{
|
|
InterruptMutexCtx* ctx = (InterruptMutexCtx*)arg;
|
|
|
|
ctx->ready = 1;
|
|
errno = 0;
|
|
ctx->r0 = st_mutex_lock(ctx->lock);
|
|
ctx->err = errno;
|
|
|
|
return NULL;
|
|
}
|
|
|
|
VOID TEST(LearnKB, ThreadInterruptWakeupFromMutexWait)
|
|
{
|
|
st_mutex_t lock = st_mutex_new();
|
|
ASSERT_TRUE(lock != NULL);
|
|
|
|
int r0 = st_mutex_lock(lock);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
InterruptMutexCtx ctx = {lock, 0, 0, 0};
|
|
st_thread_t trd = st_thread_create(coroutine_interruptible_mutex_lock, &ctx, 1, 0);
|
|
ASSERT_TRUE(trd != NULL);
|
|
|
|
while (!ctx.ready) {
|
|
st_usleep(0);
|
|
}
|
|
st_usleep(0); // Let target block in mutex wait_q.
|
|
|
|
st_thread_interrupt(trd);
|
|
|
|
ST_COROUTINE_JOIN(trd, trd_err);
|
|
ST_EXPECT_SUCCESS(trd_err);
|
|
EXPECT_EQ(-1, ctx.r0);
|
|
EXPECT_EQ(EINTR, ctx.err);
|
|
|
|
r0 = st_mutex_unlock(lock);
|
|
EXPECT_EQ(0, r0);
|
|
r0 = st_mutex_destroy(lock);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for _st_netfd_t abstraction workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
static int g_netfd_destructor_calls = 0;
|
|
|
|
static void netfd_specific_destructor(void* arg)
|
|
{
|
|
g_netfd_destructor_calls++;
|
|
free(arg);
|
|
}
|
|
|
|
VOID TEST(LearnKB, NetfdSpecificAndDestructorOnClose)
|
|
{
|
|
int fds[2] = {-1, -1};
|
|
int r0 = ::socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
st_netfd_t stfd = st_netfd_open_socket(fds[0]);
|
|
ASSERT_TRUE(stfd != NULL);
|
|
|
|
// st_netfd_open_socket takes ownership of fds[0], keep peer as raw fd.
|
|
int peer = fds[1];
|
|
|
|
g_netfd_destructor_calls = 0;
|
|
int* payload = (int*)malloc(sizeof(int));
|
|
ASSERT_TRUE(payload != NULL);
|
|
*payload = 2026;
|
|
|
|
st_netfd_setspecific(stfd, payload, netfd_specific_destructor);
|
|
void* got = st_netfd_getspecific(stfd);
|
|
EXPECT_EQ(payload, got);
|
|
|
|
r0 = st_netfd_close(stfd);
|
|
EXPECT_EQ(0, r0);
|
|
EXPECT_EQ(1, g_netfd_destructor_calls);
|
|
|
|
::close(peer);
|
|
}
|
|
|
|
VOID TEST(LearnKB, NetfdFreeKeepsOsfdOpen)
|
|
{
|
|
int fds[2] = {-1, -1};
|
|
int r0 = ::socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
st_netfd_t stfd = st_netfd_open_socket(fds[0]);
|
|
ASSERT_TRUE(stfd != NULL);
|
|
|
|
int osfd = fds[0];
|
|
int peer = fds[1];
|
|
|
|
g_netfd_destructor_calls = 0;
|
|
int* payload = (int*)malloc(sizeof(int));
|
|
ASSERT_TRUE(payload != NULL);
|
|
*payload = 7;
|
|
|
|
st_netfd_setspecific(stfd, payload, netfd_specific_destructor);
|
|
|
|
// Free wrapper only: should trigger destructor but keep underlying osfd open.
|
|
st_netfd_free(stfd);
|
|
EXPECT_EQ(1, g_netfd_destructor_calls);
|
|
|
|
errno = 0;
|
|
int flags = fcntl(osfd, F_GETFD);
|
|
EXPECT_NE(-1, flags);
|
|
|
|
// Raw fd should still be usable.
|
|
char ch = 'N';
|
|
ssize_t n = ::write(peer, &ch, 1);
|
|
EXPECT_EQ(1, n);
|
|
|
|
char got = 0;
|
|
n = ::read(osfd, &got, 1);
|
|
EXPECT_EQ(1, n);
|
|
EXPECT_EQ('N', got);
|
|
|
|
::close(osfd);
|
|
::close(peer);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for basic ST netfd read/write workflow.
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
VOID TEST(LearnKB, BasicNetfdWriteThenRead)
|
|
{
|
|
int fds[2] = {-1, -1};
|
|
int r0 = ::socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
st_netfd_t reader = st_netfd_open_socket(fds[0]);
|
|
st_netfd_t writer = st_netfd_open_socket(fds[1]);
|
|
ASSERT_TRUE(reader != NULL);
|
|
ASSERT_TRUE(writer != NULL);
|
|
|
|
const char* msg = "hello-st";
|
|
ssize_t wn = st_write(writer, msg, 8, ST_UTEST_TIMEOUT);
|
|
ASSERT_EQ(8, wn);
|
|
|
|
char buf[16] = {0};
|
|
ssize_t rn = st_read(reader, buf, 8, ST_UTEST_TIMEOUT);
|
|
ASSERT_EQ(8, rn);
|
|
EXPECT_STREQ("hello-st", buf);
|
|
|
|
r0 = st_netfd_close(reader);
|
|
EXPECT_EQ(0, r0);
|
|
r0 = st_netfd_close(writer);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
VOID TEST(LearnKB, BasicNetfdReadTimeout)
|
|
{
|
|
int fds[2] = {-1, -1};
|
|
int r0 = ::socketpair(AF_UNIX, SOCK_STREAM, 0, fds);
|
|
ASSERT_EQ(0, r0);
|
|
|
|
st_netfd_t reader = st_netfd_open_socket(fds[0]);
|
|
st_netfd_t writer = st_netfd_open_socket(fds[1]);
|
|
ASSERT_TRUE(reader != NULL);
|
|
ASSERT_TRUE(writer != NULL);
|
|
|
|
char ch = 0;
|
|
errno = 0;
|
|
ssize_t rn = st_read(reader, &ch, 1, 10 * ST_UTIME_MILLISECONDS);
|
|
EXPECT_EQ(-1, rn);
|
|
EXPECT_EQ(ETIME, errno);
|
|
|
|
r0 = st_netfd_close(reader);
|
|
EXPECT_EQ(0, r0);
|
|
r0 = st_netfd_close(writer);
|
|
EXPECT_EQ(0, r0);
|
|
}
|
|
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
// The utest for _st_eventsys_t abstraction behavior (selected backend, immutable after st_init).
|
|
////////////////////////////////////////////////////////////////////////////////////////////////////////////////////////
|
|
|
|
VOID TEST(LearnKB, EventSysSelectedAndLockedAfterInit)
|
|
{
|
|
// st_utest main already called st_set_eventsys(...) then st_init().
|
|
// Here we verify what this abstraction guarantees at runtime.
|
|
int active = st_get_eventsys();
|
|
const char* name = st_get_eventsys_name();
|
|
|
|
EXPECT_TRUE(active == ST_EVENTSYS_SELECT || active == ST_EVENTSYS_ALT);
|
|
EXPECT_TRUE(name != NULL);
|
|
EXPECT_GT((int)strlen(name), 0);
|
|
|
|
// Once selected, eventsys cannot be changed again.
|
|
errno = 0;
|
|
int r0 = st_set_eventsys(ST_EVENTSYS_SELECT);
|
|
EXPECT_EQ(-1, r0);
|
|
EXPECT_EQ(EBUSY, errno);
|
|
|
|
errno = 0;
|
|
r0 = st_set_eventsys(ST_EVENTSYS_ALT);
|
|
EXPECT_EQ(-1, r0);
|
|
EXPECT_EQ(EBUSY, errno);
|
|
}
|