test: test more of schedTest

This commit is contained in:
dualfroz committed 2026-09-11 14:58:32 +02:00
1 parent faaf04fc8c
commit c31ac87199
1 file changed
+158
+158
View File
@@ -784,3 +784,161 @@ KTEST(threadStopAcceptsSleepingBlockedAndRunningStates)
uint32 id = threadCreate("stopFromEveryState", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
/* Fabricated states, the same way threadReapClosesOpenWritePipeFdSoReaderSeesEof
below pokes threadDead directly: this thread is never actually
scheduled in this pre-schedStart() harness, so its `state` field
stands in for "genuinely Sleeping/Blocked/Running" -- exactly the
states a real SIGSTOP target could be caught in. */
threadAt(id)->state = threadSleeping;
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
KT_TRUE(t, threadContinue(id));
threadAt(id)->state = threadBlocked;
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
KT_TRUE(t, threadContinue(id));
/* Running -- the self-stop case (sysSigSend targeting the caller's own
pid, syscall.c): threadStop does not special-case "am I the one
calling this" the way threadKill implicitly does (Running is simply
absent from threadKill's accepted set); a thread can stop itself. */
threadAt(id)->state = threadRunning;
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
KT_TRUE(t, threadContinue(id));
KT_TRUE(t, threadDiscard(id));
}
KTEST(threadStopOnDeadOrUnusedSlotFails)
{
uint32 id = threadCreate("stopDead", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
threadAt(id)->state = threadDead;
KT_FALSE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadDead);
KT_TRUE(t, threadReap(id));
/* threadUnused again after reap: still rejected. */
KT_FALSE(t, threadStop(id));
}
KTEST(threadContinueRejectsOutOfRangeAndNonStoppedThreads)
{
KT_FALSE(t, threadContinue(threadCapacity()));
KT_FALSE(t, threadContinue(threadCapacity() + 1000));
uint32 id = threadCreate("notStopped", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
/* Runnable, never stopped: SIGCONT against a process that was never
stopped is a harmless no-op at this level (see signal.c's
sigRaise, which still delivers the signal itself regardless). */
KT_FALSE(t, threadContinue(id));
KT_EQ(t, threadAt(id)->state, threadRunnable);
KT_TRUE(t, threadDiscard(id));
}
KTEST(threadStopExcludesTargetFromUnrelatedWaitChannelWake)
{
int channel;
uint32 id = threadCreate("stoppedWhileWaiting", noopThreadEntry);
KT_NE(t, id, threadInvalidId);
/* Fabricate exactly what schedBlockCurrent would have left behind had
this thread been the one calling it (see
threadDiscardClosesOpenReadPipeFdSoWriterSeesBrokenPipe below for
the same fabricate-then-assert convention). */
threadAt(id)->state = threadBlocked;
threadAt(id)->waitChannel = &channel;
KT_TRUE(t, threadStop(id));
KT_EQ(t, threadAt(id)->state, threadStopped);
/* The defining difference from threadBlocked: schedWakeChannel must
NOT match a Stopped thread even though its waitChannel field still
names this exact channel -- only threadContinue (an explicit
SIGCONT) may move it back to threadRunnable, never "the resource it
was originally waiting on became available". */
schedWakeChannel(&channel);
KT_EQ(t, threadAt(id)->state, threadStopped);
KT_TRUE(t, threadContinue(id));
KT_EQ(t, threadAt(id)->state, threadRunnable);
KT_TRUE(t, threadDiscard(id));
}
/*
* threadStop/threadContinue's own new SIGCHLD-on-stop/SIGCHLD-on-continue
* (sched.c, mirroring threadExit/threadKill's parentId/sigChld block
* above): real job control notifies the parent asynchronously the instant
* a child stops or resumes, not only when it eventually dies. The live
* ring-3 proof that this reaches a genuinely blocked-mid-syscall target
* and a real installed handler is user/stopchldtest.c spawning
* user/stopchldtarget.c (needle "stopchldtest: job-control-sigchld-ok" in
* tools/ci-test.sh); what is deterministic here is the same plumbing shape
* threadKillRaisesSigChldAgainstParentId/
* threadKillEarlyWakesAParentGenuinelyBlockedWaitingForSigChld above
* already establish for the death case.
*/
KTEST(threadStopRaisesSigChldAgainstParentIdOnTheActualStopTransition)
{
uint32 parent = threadCreate("stopChldParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("stopChldVictim", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull);
KT_TRUE(t, threadStop(child));
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld));
KT_TRUE(t, threadContinue(child));
KT_TRUE(t, threadDiscard(child));
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadStopDoesNotReRaiseSigChldAgainstAnAlreadyStoppedTarget)
{
uint32 parent = threadCreate("stopChldParent2", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("stopChldVictim2", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;
KT_TRUE(t, threadStop(child));
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld));
/* Simulate the parent's own handler already having consumed that one
notification (sigConsumePending, signal.c, clears the bit on
delivery) -- exactly the "next natural ring-3 return" a real
sigchldtest.c-style handler would have already run through before a
second, redundant sigStop against the same still-Stopped target
could possibly arrive. */
threadAt(parent)->sigPending &= ~(1ull << sigChld);
KT_TRUE(t, threadStop(child));
KT_EQ(t, threadAt(child)->state, threadStopped);
KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull);
KT_TRUE(t, threadContinue(child));
KT_TRUE(t, threadDiscard(child));
KT_TRUE(t, threadDiscard(parent));
}
KTEST(threadStopEarlyWakesAParentGenuinelyBlockedWaitingForSigChldOnStop)
{
int channel;
uint32 parent = threadCreate("stopChldBlockedParent", noopThreadEntry);
KT_NE(t, parent, threadInvalidId);
uint32 child = threadCreate("stopChldVictim3", noopThreadEntry);
KT_NE(t, child, threadInvalidId);
threadAt(child)->parentId = parent;