Files
agentic-coding-workflow/internal/infra/proc/proc_unix_test.go
T
2026-06-09 22:43:29 +08:00

66 lines
1.6 KiB
Go

//go:build !windows
package proc
import (
"fmt"
"os/exec"
"syscall"
"testing"
"time"
)
// TestUnixGroup_KillsChildProcess 验证整树清理:父 shell 派生一个后台 sleep
// 子进程并打印其 PID,TerminateGroup 后该子进程(孙级)也应被 kill(-pgid) 回收,
// 证明终止覆盖整个进程组而非仅组长。
func TestUnixGroup_KillsChildProcess(t *testing.T) {
g := NewGroup()
// `sleep 30 &` 后台子进程;`echo $!` 打印其 PID;`wait` 让 shell 阻塞存活。
cmd := exec.Command("sh", "-c", "sleep 30 & echo $!; wait")
g.Prepare(cmd)
stdout, err := cmd.StdoutPipe()
if err != nil {
t.Fatalf("stdout pipe: %v", err)
}
if err := cmd.Start(); err != nil {
t.Fatalf("start: %v", err)
}
if err := g.Adopt(cmd); err != nil {
t.Fatalf("adopt: %v", err)
}
defer g.Close()
var childPID int
if _, err := fmt.Fscan(stdout, &childPID); err != nil {
t.Fatalf("read child pid: %v", err)
}
if childPID <= 0 {
t.Fatalf("invalid child pid: %d", childPID)
}
done := make(chan struct{})
go func() {
_ = cmd.Wait()
close(done)
}()
g.TerminateGroup(done, 2*time.Second)
select {
case <-done:
case <-time.After(10 * time.Second):
t.Fatal("parent not terminated")
}
// 给内核一点时间回收子进程,然后用 kill(pid, 0) 探测:返回错误(ESRCH)=已死。
deadline := time.Now().Add(5 * time.Second)
for {
if err := syscall.Kill(childPID, 0); err != nil {
return // 子进程已死,符合预期。
}
if time.Now().After(deadline) {
t.Fatalf("child process %d still alive after group terminate", childPID)
}
time.Sleep(100 * time.Millisecond)
}
}