diff --git a/src/schedTest.c b/src/schedTest.c index 68b29a5..09ed3a1 100644 --- a/src/schedTest.c +++ b/src/schedTest.c @@ -1411,3 +1411,162 @@ KTEST(threadSetPriorityChangesWeightOfALiveThread) uint32 id = threadCreate("priorityChange", noopThreadEntry); KT_NE(t, id, threadInvalidId); + KT_TRUE(t, threadSetPriority(id, 4096)); + KT_EQU(t, threadPriority(id), 4096u); + + KT_TRUE(t, threadDiscard(id)); +} + +KTEST(threadSetPriorityRejectsDeadOrUnusedTarget) +{ + uint32 id = threadCreate("prioritySetOnDead", noopThreadEntry); + KT_NE(t, id, threadInvalidId); + + KT_TRUE(t, threadKill(id)); + KT_FALSE(t, threadSetPriority(id, 4096)); + + KT_TRUE(t, threadReap(id)); + KT_FALSE(t, threadSetPriority(id, 4096)); +} + +KTEST(threadKillGroupKillsEveryLiveMemberAndLeavesOtherGroupsAlone) +{ + uint32 a = threadCreate("groupKillA", noopThreadEntry); + uint32 b = threadCreate("groupKillB", noopThreadEntry); + uint32 outsider = threadCreate("groupKillOutsider", noopThreadEntry); + KT_NE(t, a, threadInvalidId); + KT_NE(t, b, threadInvalidId); + KT_NE(t, outsider, threadInvalidId); + + uint32 pgid = threadAt(a)->pgid; + KT_TRUE(t, threadSetPgid(b, pgid)); + KT_NE(t, threadAt(outsider)->pgid, pgid); + + KT_TRUE(t, threadKillGroup(pgid)); + KT_EQ(t, threadAt(a)->state, threadDead); + KT_EQ(t, threadAt(b)->state, threadDead); + /* A completely different group must be untouched -- the exact scoping + property keyboard.c's real Ctrl-C/Ctrl-Z depends on (user/pgroupfg.c/ + user/pgroupbg.c is the live ring-3 proof for sigRaiseGroup's own + version of this same scoping, signal.c). */ + KT_EQ(t, threadAt(outsider)->state, threadRunnable); + + KT_TRUE(t, threadReap(a)); + KT_TRUE(t, threadReap(b)); + KT_TRUE(t, threadDiscard(outsider)); +} + +KTEST(threadKillGroupReturnsFalseWhenNoLiveThreadHasThatPgid) +{ + /* No thread created anywhere in this whole KTEST run ever uses this + exact value as its pgid -- ids never reach anywhere near it (see + threadCapacity/maxThreads), so it can never collide with a real + group by coincidence. */ + KT_FALSE(t, threadKillGroup(0xBEEF0000u)); +} + +KTEST(threadKillGroupSkipsAlreadyDeadMembersButStillReportsTheGroupAsFound) +{ + uint32 a = threadCreate("groupKillDeadA", noopThreadEntry); + uint32 b = threadCreate("groupKillDeadB", noopThreadEntry); + KT_NE(t, a, threadInvalidId); + KT_NE(t, b, threadInvalidId); + + uint32 pgid = threadAt(a)->pgid; + KT_TRUE(t, threadSetPgid(b, pgid)); + + /* b is killed FIRST, independently -- threadKillGroup must still find + and kill a (already-Dead members are simply skipped, exactly like + sigRaiseGroup below skips them via sigRaiseInfo's own Dead/Unused + guard), and must still report true since a real target existed. */ + KT_TRUE(t, threadKill(b)); + KT_TRUE(t, threadKillGroup(pgid)); + KT_EQ(t, threadAt(a)->state, threadDead); + + KT_TRUE(t, threadReap(a)); + KT_TRUE(t, threadReap(b)); +} + +/* currentThreadGetEnv/currentThreadSetEnv (sched.h's new Thread.env) always + act on whoever is "current" -- at KTEST time that is the idle thread + (id 0, same "always-present current thread" reasoning this file's own + header comment already gives for schedBlockCurrent above), so every test + below resets threadAt(0)->envCount to 0 first: idle's own table is pure + scratch space for these tests (initialEnv gives every REAL boot-time + spawn an empty table regardless of whatever idle's own happens to hold, + see initialEnv's own "currentIndex == 0" branch, sched.c), but leaving + it dirty between tests would let an earlier test's entries leak into a + later one's count/index assumptions. */ +KTEST(currentThreadSetEnvThenGetEnvRoundTrips) +{ + threadAt(0)->envCount = 0; + + KT_TRUE(t, currentThreadSetEnv("FOO", "bar")); + KT_STREQ(t, currentThreadGetEnv("FOO"), "bar"); + KT_NULL(t, currentThreadGetEnv("MISSING")); +} + +KTEST(currentThreadSetEnvOverwritesExistingNameInPlace) +{ + threadAt(0)->envCount = 0; + + KT_TRUE(t, currentThreadSetEnv("FOO", "bar")); + KT_TRUE(t, currentThreadSetEnv("FOO", "baz")); + KT_STREQ(t, currentThreadGetEnv("FOO"), "baz"); + /* An overwrite must never consume a second slot. */ + KT_EQ(t, currentThreadEnvCount(), 1); +} + +KTEST(currentThreadSetEnvRejectsEntryTooLongForStorage) +{ + threadAt(0)->envCount = 0; + + /* envEntryMax (sched.h) bounds "NAME=VALUE\0" together -- a value long + enough to blow that budget must be rejected outright, leaving no + partially-written entry behind. */ + char hugeValue[envEntryMax]; + for (int i = 0; i < envEntryMax - 1; i++) + hugeValue[i] = 'x'; + hugeValue[envEntryMax - 1] = 0; + + KT_FALSE(t, currentThreadSetEnv("FOO", hugeValue)); + KT_NULL(t, currentThreadGetEnv("FOO")); + KT_EQ(t, currentThreadEnvCount(), 0); +} + +KTEST(currentThreadSetEnvRejectsNewNameOnceTableIsFull) +{ + threadAt(0)->envCount = 0; + + char name[8]; + for (int i = 0; i < envMaxVars; i++) { + snprintf(name, sizeof(name), "V%d", i); + KT_TRUE(t, currentThreadSetEnv(name, "x")); + } + KT_EQ(t, currentThreadEnvCount(), envMaxVars); + + KT_FALSE(t, currentThreadSetEnv("ONE_TOO_MANY", "x")); + KT_EQ(t, currentThreadEnvCount(), envMaxVars); + + /* Overwriting an EXISTING name must still work even while the table is + completely full -- it takes the already-occupied slot, never a new + one. */ + KT_TRUE(t, currentThreadSetEnv("V0", "changed")); + KT_STREQ(t, currentThreadGetEnv("V0"), "changed"); + KT_EQ(t, currentThreadEnvCount(), envMaxVars); + + threadAt(0)->envCount = 0; +} + +KTEST(currentThreadEnvEntryExposesRawStorageForListing) +{ + threadAt(0)->envCount = 0; + + KT_TRUE(t, currentThreadSetEnv("A", "1")); + KT_TRUE(t, currentThreadSetEnv("B", "2")); + KT_EQ(t, currentThreadEnvCount(), 2); + KT_STREQ(t, currentThreadEnvEntry(0), "A=1"); + KT_STREQ(t, currentThreadEnvEntry(1), "B=2"); + KT_NULL(t, currentThreadEnvEntry(2)); + KT_NULL(t, currentThreadEnvEntry(-1)); +