test: cover schedTest
This commit is contained in:
1 parent
d4cdde3ea0
commit
bf31b363a6
1 file changed
+161
+161
@@ -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));
|
||||
|
||||
Reference in new issue
Block a user