diff --git a/src/schedTest.c b/src/schedTest.c index fdb27ad..12c7e21 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -629,3 +629,158 @@ KTEST(threadKillAgainstAnOrphanWithADeadParentIdRaisesNothing) KT_EQ(t, threadAt(orphan)->state, threadDead); 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); +