diff --git a/src/schedTest.c b/src/schedTest.c index d4cd652..4463cdf 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -942,3 +942,156 @@ KTEST(threadStopEarlyWakesAParentGenuinelyBlockedWaitingForSigChldOnStop) KT_NE(t, child, threadInvalidId); threadAt(child)->parentId = parent; + /* Same real-handler-required setup as + threadKillEarlyWakesAParentGenuinelyBlockedWaitingForSigChld above -- + sigChld's default disposition is Ignore, so sigWouldDeliver would + (correctly) decline to early-wake without one. */ + sigSetHandler(threadAt(parent), sigChld, 0x40005000ull, 0, 0); + + threadAt(parent)->state = threadBlocked; + threadAt(parent)->waitChannel = &channel; + + KT_TRUE(t, threadStop(child)); + KT_EQ(t, threadAt(parent)->state, threadRunnable); + + KT_TRUE(t, threadContinue(child)); + KT_TRUE(t, threadDiscard(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadContinueRaisesSigChldAgainstParentIdOnTheActualResumeTransition) +{ + uint32 parent = threadCreate("contChldParent", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("contChldVictim", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; + + KT_TRUE(t, threadStop(child)); + /* Drain the stop's own notification first (see the "already stopped" + test above for why) so the assertion below is unambiguously about + threadContinue's OWN raise, not a leftover bit from the stop. */ + threadAt(parent)->sigPending &= ~(1ull << sigChld); + + KT_TRUE(t, threadContinue(child)); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), (1ull << sigChld)); + + KT_TRUE(t, threadDiscard(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadContinueOnATargetThatWasNeverStoppedRaisesNothing) +{ + uint32 parent = threadCreate("contChldParent2", noopThreadEntry); + KT_NE(t, parent, threadInvalidId); + uint32 child = threadCreate("contChldVictim2", noopThreadEntry); + KT_NE(t, child, threadInvalidId); + threadAt(child)->parentId = parent; + + KT_EQ(t, threadAt(child)->state, threadRunnable); + KT_FALSE(t, threadContinue(child)); + KT_EQU(t, threadAt(parent)->sigPending & (1ull << sigChld), 0ull); + + KT_TRUE(t, threadDiscard(child)); + KT_TRUE(t, threadDiscard(parent)); +} + +KTEST(threadStopAgainstAnOrphanWithADeadParentIdRaisesNothing) +{ + uint32 deadParent = threadCreate("stopDeadParent", noopThreadEntry); + KT_NE(t, deadParent, threadInvalidId); + KT_TRUE(t, threadKill(deadParent)); + KT_EQ(t, threadAt(deadParent)->state, threadDead); + + uint32 orphan = threadCreate("stopOrphan", noopThreadEntry); + KT_NE(t, orphan, threadInvalidId); + threadAt(orphan)->parentId = deadParent; + + /* Same sigRaise targetId-state guard as + threadKillAgainstAnOrphanWithADeadParentIdRaisesNothing above -- an + orphan whose spawning parent already died has nowhere for its own + stop notification to land either. */ + KT_TRUE(t, threadStop(orphan)); + KT_EQ(t, threadAt(orphan)->state, threadStopped); + KT_EQU(t, threadAt(deadParent)->sigPending & (1ull << sigChld), 0ull); + + KT_TRUE(t, threadContinue(orphan)); + KT_TRUE(t, threadReap(deadParent)); + KT_TRUE(t, threadDiscard(orphan)); +} + +KTEST(threadWakeForSignalRejectsIdleOutOfRangeAndNeverCreatedSlots) +{ + KT_FALSE(t, threadWakeForSignal(0)); + KT_FALSE(t, threadWakeForSignal(threadCapacity())); + KT_FALSE(t, threadWakeForSignal(threadCapacity() + 1000)); + + uint32 neverUsed = threadCount(); + if (neverUsed < threadCapacity()) + KT_FALSE(t, threadWakeForSignal(neverUsed)); +} + +KTEST(threadWakeForSignalMovesSleepingOrBlockedThreadToRunnable) +{ + uint32 id = threadCreate("wakeFromSleep", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + threadAt(id)->state = threadSleeping; + KT_TRUE(t, threadWakeForSignal(id)); + KT_EQ(t, threadAt(id)->state, threadRunnable); + + threadAt(id)->state = threadBlocked; + KT_TRUE(t, threadWakeForSignal(id)); + KT_EQ(t, threadAt(id)->state, threadRunnable); + + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadWakeForSignalIsNoOpOnRunnableRunningStoppedDeadAndUnused) +{ + uint32 id = threadCreate("wakeNoOp", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + /* Already Runnable/Running: nothing to force. */ + KT_EQ(t, threadAt(id)->state, threadRunnable); + KT_FALSE(t, threadWakeForSignal(id)); + KT_EQ(t, threadAt(id)->state, threadRunnable); + + threadAt(id)->state = threadRunning; + KT_FALSE(t, threadWakeForSignal(id)); + KT_EQ(t, threadAt(id)->state, threadRunning); + + /* Stopped is the one deliberately EXCLUDED state -- an ordinary signal + must never undo job control's SIGSTOP; only threadContinue (an + explicit SIGCONT) may ever move a Stopped thread. This is what + signal.c's sigRaise relies on for a signal raised against a Stopped + target: sigWouldDeliver may say yes, but threadWakeForSignal still + refuses. */ + KT_TRUE(t, threadStop(id)); + KT_FALSE(t, threadWakeForSignal(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + KT_TRUE(t, threadContinue(id)); + + threadAt(id)->state = threadDead; + KT_FALSE(t, threadWakeForSignal(id)); + KT_TRUE(t, threadReap(id)); + + /* threadUnused again after reap: still rejected. */ + KT_FALSE(t, threadWakeForSignal(id)); +} + +KTEST(threadKillAcceptsStoppedThreadLikeRealSigkillOnAStoppedProcess) +{ + uint32 id = threadCreate("killStopped", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + KT_TRUE(t, threadStop(id)); + KT_EQ(t, threadAt(id)->state, threadStopped); + + KT_TRUE(t, threadKill(id)); + KT_EQ(t, threadAt(id)->state, threadDead); + KT_EQU(t, threadAt(id)->exitCode, threadKilledExitCode); + + KT_TRUE(t, threadReap(id)); +} +