test: cover schedTest

This commit is contained in:
dualfroz committed 2026-09-11 13:19:11 +02:00
1 parent d4cdde3ea0
commit bf31b363a6
1 file changed
+161
+161
View File
@@ -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));