From d4cdde3ea00cd284f00db6f982607f19ddec141d Mon Sep 17 00:00:00 2001 From: DualFroz Date: Fri, 11 Sep 2026 12:58:20 +0200 Subject: [PATCH] test: extend coverage of noopThreadEntry --- src/schedTest.c | 151 ++++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 151 insertions(+) create mode 100644 src/schedTest.c diff --git a/src/schedTest.c b/src/schedTest.c new file mode 100644 index 0000000..458b9c6 --- /dev/null +++ b/src/schedTest.c @@ -0,0 +1,151 @@ +#include "ktest.h" +#include "sched.h" +#include "ports.h" +#include "elf.h" +#include "paging.h" +#include "pmm.h" +#include "pipe.h" +#include "signal.h" +#include "pit.h" +#include "klib.h" + +/* + * ktestRunAll() runs before schedStart() (see ktest.h), on a single thread + * with no live scheduler - there is no second execution context to actually + * context-switch into, and no interrupts to wake a genuinely halted CPU. + * The first block of tests below therefore exercises schedBlockCurrent/ + * schedWakeChannel as the pure state-machine they are (mark-blocked / + * match-and-wake), using the always-present current thread (idle, id 0 at + * this point in boot) rather than creating new threads. + * + * The second block tests threadCreate/threadReap/threadJoin's bookkeeping + * (slot allocation, exhaustion, reuse, bounds-checking) directly: a thread + * created here is never actually scheduled to run (the scheduler is not yet + * live), so as long as every thread a test creates is reaped again before + * the test returns, the real thread table is left exactly as it found it - + * no throwaway slot is ever left behind to eat into kmain.c's later budget. + * The one path that genuinely cannot be tested here is threadJoin() ever + * actually parking (schedBlockCurrent + halt()) on a thread that is still + * alive: that requires a second thread for the scheduler to switch to, + * which does not exist before schedStart(). That path is instead proven + * live by the join demo in kmain.c (needle "join-reap-ok" in ci-test.sh). + */ + +KTEST(schedBlockCurrentMarksBlockedWithChannel) +{ + ThreadState saved = currentThread()->state; + int channel; + + schedBlockCurrent(&channel); + KT_EQ(t, currentThread()->state, threadBlocked); + KT_EQP(t, currentThread()->waitChannel, &channel); + + schedWakeChannel(&channel); + currentThread()->state = saved; +} + +KTEST(schedWakeChannelIgnoresMismatchedChannel) +{ + ThreadState saved = currentThread()->state; + int channelA; + int channelB; + + schedBlockCurrent(&channelA); + KT_EQ(t, currentThread()->state, threadBlocked); + + schedWakeChannel(&channelB); + KT_EQ(t, currentThread()->state, threadBlocked); + KT_EQP(t, currentThread()->waitChannel, &channelA); + + schedWakeChannel(&channelA); + KT_EQ(t, currentThread()->state, threadRunnable); + + currentThread()->state = saved; +} + +KTEST(schedWakeChannelClearsWaitChannelOnWake) +{ + ThreadState saved = currentThread()->state; + int channel; + + schedBlockCurrent(&channel); + schedWakeChannel(&channel); + KT_EQ(t, currentThread()->state, threadRunnable); + KT_NULL(t, currentThread()->waitChannel); + + currentThread()->state = saved; +} + +KTEST(schedPollChannelIsStableAndUsableLikeAnyOtherChannel) +{ + ThreadState saved = currentThread()->state; + + /* Same address every call -- every not-yet-satisfied sysPoll (poll.h) + across every thread blocks on this ONE shared channel, so callers + must never see it move. */ + KT_EQP(t, schedPollChannel(), schedPollChannel()); + KT_NOTNULL(t, schedPollChannel()); + + /* And it works exactly like any other channel schedBlockCurrent/ + schedWakeChannel already accept -- the schedule()-driven pollDeadline + expiry scan (sched.c) that forces a wake even with nobody calling + schedWakeChannel(schedPollChannel()) needs a live scheduler + (schedulerActive) to run at all, so it is proven live instead, by + user/polltest.c's real timeout phase (tools/ci-test.sh's + "polltest: poll-timeout-ok" needle) -- the same split schedTest.c's + own file comment already documents for sleepUntil/alarmAt. */ + schedBlockCurrent(schedPollChannel()); + KT_EQ(t, currentThread()->state, threadBlocked); + KT_EQP(t, currentThread()->waitChannel, schedPollChannel()); + + schedWakeChannel(schedPollChannel()); + KT_EQ(t, currentThread()->state, threadRunnable); + + currentThread()->state = saved; +} + +KTEST(schedWakeChannelWithNoWaiterIsHarmlessNoop) +{ + ThreadState saved = currentThread()->state; + int channel; + + /* Nobody is blocked on this address: waking it must not touch any + thread's state, including the caller's own. */ + schedWakeChannel(&channel); + KT_EQ(t, currentThread()->state, saved); + + currentThread()->state = saved; +} + +KTEST(schedBlockCurrentIsIdempotentUnderRepeatedCalls) +{ + ThreadState saved = currentThread()->state; + int channelA; + int channelB; + + schedBlockCurrent(&channelA); + schedBlockCurrent(&channelB); + KT_EQ(t, currentThread()->state, threadBlocked); + KT_EQP(t, currentThread()->waitChannel, &channelB); + + /* Waking the stale first channel must not affect a thread that has since + re-blocked on a different one. */ + schedWakeChannel(&channelA); + KT_EQ(t, currentThread()->state, threadBlocked); + + schedWakeChannel(&channelB); + KT_EQ(t, currentThread()->state, threadRunnable); + + currentThread()->state = saved; +} + +/* Never actually invoked (the scheduler is not live yet) -- just a valid + function pointer for threadCreate's entry parameter. If it were ever + scheduled for real it would be a harmless, correctly-behaved kernel + thread body (park forever), same convention as workerA/workerB. */ +static void noopThreadEntry(void) +{ + for (;;) + halt(); +} +