diff --git a/src/schedTest.c b/src/schedTest.c index 458b9c6..84bee3e 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -149,3 +149,164 @@ static void noopThreadEntry(void) halt(); } +KTEST(threadCreateExhaustsThenThreadDiscardFreesSlotsBackForReuse) +{ + /* Sized off threadCapacity() itself, not a hardcoded number that would + silently stop meaning "the whole table" the next time maxThreads + (sched.c) grows -- exactly the staleness this test's own array size + once had (fixed at 64, which only ever happened to equal maxThreads + by coincidence of when it was written; the very next maxThreads bump + after this one otherwise made the while loop below stop at the array + bound before threadCreate ever actually failed, so the "must refuse + to grow past capacity" check two lines down silently stopped testing + anything and leaked one never-discarded thread for the rest of + boot). idle already occupies one slot, so successful creates always + stay strictly below `capacity`, and the loop below is guaranteed to + hit its threadInvalidId break at or before `count == capacity - 1` -- + this array can never actually overflow. */ + uint32 capacity = threadCapacity(); + uint32 created[capacity]; + uint32 count = 0; + + /* Every thread created here stays Runnable/never-run for its entire + life in this test (schedule() never runs before schedStart()), so + threadDiscard -- not threadReap, which requires threadDead -- is the + correct way to tear them back down. */ + while (count < capacity) { + uint32 id = threadCreate("ktestDummy", noopThreadEntry); + if (id == threadInvalidId) + break; + created[count++] = id; + } + + /* The table must have refused to grow past its fixed capacity instead + of silently writing past threads[] -- this is the bounds-check the + original allocator did not have. */ + KT_EQU(t, threadCreate("shouldFail", noopThreadEntry), threadInvalidId); + + for (uint32 i = 0; i < count; i++) + KT_TRUE(t, threadDiscard(created[i])); + + /* Every discarded slot must be usable again: the very next create + should succeed (it would fail immediately if discard had left the + table full). */ + uint32 reused = threadCreate("afterDiscard", noopThreadEntry); + KT_NE(t, reused, threadInvalidId); + KT_TRUE(t, threadDiscard(reused)); +} + +KTEST(threadCreateSkipsAFreshlyFreedLowSlotInFavorOfMovingForward) +{ + /* allocSlot (sched.c) used to always hand out the LOWEST currently-free + slot -- this test used to assert exactly that (KT_EQ(reused, b) + after discarding the middle one of three). That was a real, + reproducible bug: this fala's own gate caught killtest.c's and + nicetest.c's "a pid I just userWait()-reaped must reject a further + kill/nice" checks occasionally failing for real, because some + completely unrelated thread elsewhere in the system called + threadCreate in the tiny window between the reap and the recheck + and got handed that exact just-freed low index straight back. The + fix -- allocSlot's own new round-robin cursor -- means the very + next create after freeing a LOW slot, with plenty of headroom still + available, deliberately does NOT reuse it: it keeps moving forward + and grows the high-water mark instead, exactly like a real OS's own + pid allocator avoids immediately handing back a pid that just + exited. See allocSlot's own doc comment for the full story and + why this is a practical mitigation via headroom, not a mathematical + impossibility. */ + uint32 a = threadCreate("rrA", noopThreadEntry); + uint32 b = threadCreate("rrB", noopThreadEntry); + uint32 c = threadCreate("rrC", noopThreadEntry); + KT_LT(t, a, b); + KT_LT(t, b, c); + + KT_TRUE(t, threadDiscard(b)); + + uint32 afterFree = threadCreate("rrAfterFree", noopThreadEntry); + KT_NE(t, afterFree, b); + KT_GT(t, afterFree, c); + + KT_TRUE(t, threadDiscard(a)); + KT_TRUE(t, threadDiscard(afterFree)); + KT_TRUE(t, threadDiscard(c)); + + /* b itself is still sitting there Unused, unreachable until the + cursor eventually wraps back around to it -- the very next test + below proves it is not leaked forever, just deferred. */ + KT_EQU(t, threadPgid(b), threadInvalidId); +} + +KTEST(threadCreateReusesTheOnlyFreedSlotOnceTheTableIsFull) +{ + /* Same "must reuse freed slots, not just grow the high-water mark + forever" property the old lowest-index-specific test above used to + cover, restated so it holds regardless of WHERE allocSlot's cursor + happens to be: fill the ENTIRE table (sized off threadCapacity() + itself, same staleness-proofing reasoning as + threadCreateExhaustsThenThreadDiscardFreesSlotsBackForReuse above), + free exactly one slot picked from the middle, then prove the very + next create can ONLY land on that one slot -- there is nowhere else + left to land, so the round-robin scan is forced all the way around + to it regardless of its own starting position. */ + uint32 capacity = threadCapacity(); + uint32 created[capacity]; + uint32 count = 0; + + while (count < capacity) { + uint32 id = threadCreate("rrFill", noopThreadEntry); + if (id == threadInvalidId) + break; + created[count++] = id; + } + KT_EQU(t, threadCreate("rrShouldFail", noopThreadEntry), threadInvalidId); + + uint32 freedId = created[count / 2]; + KT_TRUE(t, threadDiscard(freedId)); + + uint32 reused = threadCreate("rrReusesOnlyFree", noopThreadEntry); + KT_EQ(t, reused, freedId); + + for (uint32 i = 0; i < count; i++) { + if (created[i] != freedId) + KT_TRUE(t, threadDiscard(created[i])); + } + KT_TRUE(t, threadDiscard(reused)); +} + +KTEST(threadDiscardRejectsIdleOutOfRangeAndDoubleDiscard) +{ + /* id 0 is the permanent idle thread: everRan is true for it from + schedInit onward, so discard must refuse it. */ + KT_FALSE(t, threadDiscard(0)); + + /* Past the fixed capacity entirely. */ + KT_FALSE(t, threadDiscard(threadCapacity())); + KT_FALSE(t, threadDiscard(threadCapacity() + 1000)); + + uint32 id = threadCreate("discardMe", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + KT_TRUE(t, threadDiscard(id)); + + /* Already threadUnused: a second discard on the same id must fail + rather than double-free the (already-freed) stack. */ + KT_FALSE(t, threadDiscard(id)); +} + +KTEST(threadReapRejectsIdleOutOfRangeAndNeverCreatedSlots) +{ + /* id 0 is the permanent idle thread: it is always Running/Runnable, + never Dead, so reap must refuse it. */ + KT_FALSE(t, threadReap(0)); + + /* Past the fixed capacity entirely. */ + KT_FALSE(t, threadReap(threadCapacity())); + KT_FALSE(t, threadReap(threadCapacity() + 1000)); + + /* A slot nobody has ever created a thread in yet is threadUnused, not + threadDead. A freshly created, never-run thread (Runnable) is not + threadDead either -- reap must refuse it too, distinctly from + threadDiscard above. */ + uint32 neverUsed = threadCount(); + if (neverUsed < threadCapacity()) + KT_FALSE(t, threadReap(neverUsed)); +