test: cover jobtest and killtest

This commit is contained in:
dualfroz committed 2026-09-12 22:17:33 +02:00
1 parent f6a32938a6
commit 01ad058c1c
2 files changed
+199

No files matched your search

+131
View File
@@ -0,0 +1,131 @@
#include "ulib.h"
/*
* Boot-time driver for job control (signal.h's SIGSTOP/SIGCONT/SIGTSTP,
* sched.h's threadStopped): spawns jobtarget.c, waits for it to actually
* be parked in a real blocking userRead (not guessed via a sleep), stops it
* there with userSigSend(target, sigStop), writes the very byte it is
* blocked waiting for WHILE it is stopped, and only then sends sigCont --
* proving the resume happens through the genuine restart-syscall retry
* path sysRead already uses (syscall.c), not some shortcut that would have
* worked even if threadStop/threadContinue did nothing at all.
*
* Two independent pipes, not one, exactly because this needs traffic in
* BOTH directions kept open across the whole exchange (sigtest.c's own
* single "ready" pipe is closed by both sides right after one byte, which
* is not enough here): jobtest attaches BOTH ends it owns (ready as
* reader, go as writer) before ever spawning jobtarget, for the same
* "attach first" reason sigtest.c documents at length -- whichever side
* would otherwise attach second risks the pipe's slot already being freed
* by pipeReleaseIfUnused if the other side raced through attach+use+close
* first.
*/
int main(void)
{
print("jobtest: start, pid=");
printDec(userGetpid());
print("\n");
long readyHandle = userPipeCreate();
long goHandle = userPipeCreate();
if (readyHandle < 0 || goHandle < 0) {
print("jobtest: FAIL create pipes\n");
return 1;
}
long readyFd = userPipeAttach(readyHandle, 0);
long goFd = userPipeAttach(goHandle, 1);
if (readyFd < 0 || goFd < 0) {
print("jobtest: FAIL attach pipes\n");
return 1;
}
char readyHandleStr[24];
char goHandleStr[24];
decToString(readyHandle, readyHandleStr);
decToString(goHandle, goHandleStr);
char* targetArgv[] = { "/bin/jobtarget", readyHandleStr, goHandleStr, 0 };
long target = userSpawn("/bin/jobtarget", targetArgv);
if (target < 0) {
print("jobtest: FAIL spawn jobtarget\n");
return 1;
}
/* sched.h's Thread.pgid: a RUNTIME userSpawn from a genuinely running,
non-idle parent (this process) must have jobtarget inherit THIS
process's own pgid, matching real fork()'s default before any
setpgid ever runs -- the opposite default from every boot-time
spawnUser call in kmain.c (each becomes its own new group leader,
sched.c's initialPgid), which schedTest.c's own KTEST coverage
cannot exercise at all (ktestRunAll always runs as the permanent
idle thread, the one case that can never be a real spawning parent
-- see that file's header comment). This is the only live ring-3
proof of the inherit-from-parent half of initialPgid. */
if (userGetpgid(target) != userGetpgid(0)) {
print("jobtest: FAIL jobtarget did not inherit parent pgid\n");
return 1;
}
print("jobtest: child-inherits-parent-pgid-ok\n");
char readyByte;
/* Real blocking read: does not return until jobtarget has actually
attached the go pipe as a reader AND written its ready byte -- so by
the time this returns, jobtarget is either about to call, or already
inside, its own blocking read on the go pipe. */
if (userRead((int)readyFd, &readyByte, 1) != 1) {
print("jobtest: FAIL read ready byte\n");
return 1;
}
userClose((int)readyFd);
print("jobtest: jobtarget ready\n");
if (userSigSend(target, sigStop) != 0) {
print("jobtest: FAIL send sigStop to jobtarget\n");
return 1;
}
print("jobtest: sent sigStop to jobtarget\n");
/* Writing the byte jobtarget is blocked waiting for WHILE it is
stopped, rather than after sigCont, is deliberate: it exercises the
realistic job-control shape (a stopped job's input keeps queuing up
behind it) and proves stop+cont do not lose or corrupt that pending
write. It is NOT, on its own, an airtight proof that sigStop above
did anything at all -- pipeWrite (pipe.c) calls schedWakeChannel
unconditionally on every successful write, so a jobtarget that was
merely left threadBlocked (sigStop silently broken/a no-op) would
ALSO be woken by this very write and finish on its own, without
needing sigCont below at all. That rigorous "would this still pass
if stop were a no-op" distinction is exactly what
schedTest.c's threadStopExcludesTargetFromUnrelatedWaitChannelWake
proves instead (fault-injection verified: a genuinely Stopped
thread's waitChannel is never matched by schedWakeChannel) -- what
THIS live demo proves is the piece that KTEST cannot: a real ring-3
program blocked in a real syscall survives being stopped and
resumed through the real syscall retry path, end to end, without
crashing or wedging the system. */
if (userWrite((int)goFd, "g", 1) != 1) {
print("jobtest: FAIL write go byte\n");
return 1;
}
if (userSigSend(target, sigCont) != 0) {
print("jobtest: FAIL send sigCont to jobtarget\n");
return 1;
}
print("jobtest: sent sigCont to jobtarget\n");
userClose((int)goFd);
long status = userWait(target);
print("jobtest: jobtarget exit status=");
printDec(status);
print("\n");
if (status != 55) {
print("jobtest: FAIL expected jobtarget exit status 55\n");
return 1;
}
print("jobtest: job-stop-continue-ok\n");
return 0;
}
+68
View File
@@ -0,0 +1,68 @@
#include "ulib.h"
/*
* Exercises threadKill (sched.c) end to end through sysKill: spawns
* killvictim.c (which blocks forever in a real, syscall-level pipe read
* with no writer ever attaching), waits long enough for it to actually
* reach and park inside that read, then kills it and confirms userWait
* reports exactly the killed exit status (9) -- the only way this kernel
* can ever recover a process stuck in a genuine block, now that
* sysRead/sysWrite can really block (pipe.h/vfs.h, same wave as this).
*/
int main(void)
{
print("killtest: start, pid=");
printDec(userGetpid());
print("\n");
if (userKill(userGetpid()) != -1) {
print("killtest: FAIL self-kill should be rejected\n");
return 1;
}
print("killtest: self-kill rejected\n");
if (userKill(999999) != -1) {
print("killtest: FAIL kill of bogus pid should fail\n");
return 1;
}
print("killtest: kill bad pid rejected\n");
long victim = userSpawn("/bin/killvictim", 0);
if (victim < 0) {
print("killtest: FAIL spawn killvictim\n");
return 1;
}
/* Give the victim time to actually attach and reach its blocking read
-- killing it before it ever calls userRead would still "work" (any
Runnable/Sleeping/Blocked thread is a valid target), but would not
prove the interesting case: tearing down a call already parked
mid-syscall. */
userSleep(100);
if (userKill(victim) != 0) {
print("killtest: FAIL kill victim\n");
return 1;
}
print("killtest: killed victim\n");
long status = userWait(victim);
print("killtest: victim exit status=");
printDec(status);
print("\n");
if (status != 9) {
print("killtest: FAIL expected exit status 9\n");
return 1;
}
/* Already dead and reaped (userWait above reaped it): killing it again
must fail cleanly, not resurrect a slot that has since been reused. */
if (userKill(victim) != -1) {
print("killtest: FAIL kill of already-reaped pid should fail\n");
return 1;
}
print("killtest: kill-ok\n");
return 0;
}