test: strengthen tests for schedTest

This commit is contained in:
dualfroz committed 2026-09-11 14:45:17 +02:00
1 parent 67d097e51b
commit faaf04fc8c
1 file changed
+155
+155
View File
@@ -629,3 +629,158 @@ KTEST(threadKillAgainstAnOrphanWithADeadParentIdRaisesNothing)
KT_EQ(t, threadAt(orphan)->state, threadDead); KT_EQ(t, threadAt(orphan)->state, threadDead);
KT_EQU(t, threadAt(deadParent)->sigPending & (1ull << sigChld), 0ull); KT_EQU(t, threadAt(deadParent)->sigPending & (1ull << sigChld), 0ull);
KT_TRUE(t, threadReap(deadParent));
KT_TRUE(t, threadReap(orphan));
}
/*
* threadJoinAnyChild (sched.c) -- waitpid(-1)-style "wait for ANY child"
* behind sysWait's waitAnyPid case (syscall.c) / userWaitAny (ulib.c). Live
* proof that it genuinely blocks and is genuinely woken by an arbitrary
* child's death (not just a specifically-named one) is user/waitanytest.c
* (needle "waitanytest: any-child-reap-set-ok" in tools/ci-test.sh); what
* is deterministic here is its own non-blocking scan logic, the same shape
* as the parentId/sigChld block above.
*/
KTEST(threadJoinAnyChildReturnsNoChildrenForAThreadWithNoneAtAll)
{
uint32 parent = threadCreate("anyNoKids", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 outId = 0;
int64 outCode = 0;
KT_EQ(t, threadJoinAnyChild(parent, &outId, &outCode), waitScanNoChildren);
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadJoinAnyChildReturnsNoneDeadWhileAllChildrenAreStillAlive)
{
uint32 parent = threadCreate("anyLiveParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("anyLiveChild", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;
uint32 outId = 0;
int64 outCode = 0;
KT_EQ(t, threadJoinAnyChild(parent, &outId, &outCode), waitScanNoneDead);
/* Not reaped -- still exactly as threadCreate left it. */
KT_EQ(t, threadAt(child)->state, threadRunnable);
KT_TRUE(t, threadDiscard(child));
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadJoinAnyChildPicksTheLowestIndexedDeadChildAndCanBeCalledAgainForTheRest)
{
uint32 parent = threadCreate("anyTwoKidsParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 childLow = threadCreate("anyChildLow", noopThreadEntry);
KT_NE(t, childLow, threadInvalidId);
uint32 childHigh = threadCreate("anyChildHigh", noopThreadEntry);
KT_NE(t, childHigh, threadInvalidId);
/* allocSlot (sched.c) always hands out the lowest currently-free slot
and nothing else has been created in between, so childHigh's own id
is guaranteed higher than childLow's -- the "lowest index wins"
assertion below actually depends on this. */
KT_LT(t, childLow, childHigh);
threadAt(childLow)->parentId = parent;
threadAt(childHigh)->parentId = parent;
/* Kill the HIGHER-indexed one first, on purpose: proves the scan below
picks the lowest matching slot, not whichever died most recently. */
KT_TRUE(t, threadKill(childHigh));
KT_TRUE(t, threadKill(childLow));
uint32 outId = 0xFFFFFFFFu;
int64 outCode = -1;
KT_EQ(t, threadJoinAnyChild(parent, &outId, &outCode), waitScanFoundDead);
KT_EQU(t, outId, childLow);
KT_EQ(t, outCode, threadKilledExitCode);
/* childLow is fully reaped already -- its slot is threadUnused now. */
KT_EQ(t, threadAt(childLow)->state, threadUnused);
/* childHigh is untouched by that call: still Dead, NOT yet reaped. */
KT_EQ(t, threadAt(childHigh)->state, threadDead);
outId = 0xFFFFFFFFu;
outCode = -1;
KT_EQ(t, threadJoinAnyChild(parent, &outId, &outCode), waitScanFoundDead);
KT_EQU(t, outId, childHigh);
KT_EQ(t, outCode, threadKilledExitCode);
KT_EQ(t, threadJoinAnyChild(parent, &outId, &outCode), waitScanNoChildren);
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadKillWakesAParentGenuinelyBlockedInWaitAnyOnAnyChildDeath)
{
uint32 parent = threadCreate("anyBlockedParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("anyBlockedChild", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;
/* Fabricate exactly what sysWait's waitAnyPid path leaves behind via
schedBlockCurrent(currentThread()) when parent itself is the one
calling it (see threadStopExcludesTargetFromUnrelatedWaitChannelWake
below for the same white-box convention) -- channel is the PARENT's
own slot address, not the child's, unlike
threadKillWakesAThreadParkedInJoin's channel above. */
threadAt(parent)->state = threadBlocked;
threadAt(parent)->waitChannel = threadAt(parent);
KT_TRUE(t, threadKill(child));
KT_EQ(t, threadAt(parent)->state, threadRunnable);
KT_TRUE(t, threadReap(child));
KT_TRUE(t, threadDiscard(parent));
}
/*
* threadStop/threadContinue (sched.c) -- job control (signal.h's SIGSTOP/
* SIGCONT/SIGTSTP) on top of the new threadStopped state. The live proof
* that SIGSTOP genuinely stops a thread parked mid-syscall and SIGCONT
* genuinely resumes it through the exact restart-syscall path sysRead
* already uses is user/jobtest.c spawning user/jobtarget.c (needle
* "jobtest: job-stop-continue-ok" in tools/ci-test.sh); what is
* deterministic here is threadStop/threadContinue's own state-machine
* bookkeeping, mirroring threadKillRejects.../threadKillMarks... above.
*/
KTEST(threadStopRejectsIdleOutOfRangeAndNeverCreatedSlots)
{
KT_FALSE(t, threadStop(0));
KT_FALSE(t, threadStop(threadCapacity()));
KT_FALSE(t, threadStop(threadCapacity() + 1000));
uint32 neverUsed = threadCount();
if (neverUsed < threadCapacity())
KT_FALSE(t, threadStop(neverUsed));
}
KTEST(threadStopMovesRunnableThreadToStoppedAndIsIdempotent)
{
uint32 id = threadCreate("stopMe", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
KT_EQ(t, threadAt(id)->state, threadRunnable);
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
/* Idempotent: stopping an already-Stopped thread still succeeds,
matching real kill(pid, SIGSTOP) sent a second time. */
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
KT_TRUE(t, threadContinue(id));
KT_TRUE(t, threadDiscard(id));
}
KTEST(threadStopAcceptsSleepingBlockedAndRunningStates)
{
uint32 id = threadCreate("stopFromEveryState", noopThreadEntry);
KT_NE(t, id, threadInvalidId);