Files
runner/internal/pkg/process/killer_unix_test.go
T
silverwind 30e600d8f6 test: speed up (#1386)
Reduce redundant fixture setup, Git subprocesses, and large data copies while preserving test coverage.

Local macOS measurements against main:

| Measurement | Before | After |
| --- | ---: | ---: |
| Git package time, median of 3 alternating runs | 4.09 s | 3.48 s (-15%) |
| Allocations across 18 action-bundle cases | 489 MiB | 251 MiB (-49%) |
| Traced Git starts per checkout table run | 234 | 175 |

Reviewed-on: https://gitea.com/gitea/runner/pulls/1386
Reviewed-by: bircni <[email protected]>
2026-11-05 07:33:17 +00:00

98 lines
2.9 KiB
Go

// Copyright 2026 The Gitea Authors. All rights reserved.
// SPDX-License-Identifier: MIT
//go:build !windows && !plan9
package process
import (
"fmt"
"os"
"os/exec"
"strings"
"syscall"
"testing"
"time"
"github.com/stretchr/testify/require"
)
// processAlive reports whether pid refers to a still-running process. Signal 0
// performs error checking without delivering a signal: a nil error (or EPERM)
// means the process exists, ESRCH means it is gone.
//
// On Linux, zombie processes (state Z in /proc/<pid>/stat) appear alive to
// kill(0) but have already terminated — their corpse lingers until the parent
// calls wait(). In a Docker container the child may be reparented to a PID 1
// that does not reap promptly, so we treat zombies as not alive.
func processAlive(pid int) bool {
err := syscall.Kill(pid, 0)
if err != nil {
return false
}
// On Linux /proc is available; check whether the process is a zombie.
if b, readErr := os.ReadFile(fmt.Sprintf("/proc/%d/stat", pid)); readErr == nil {
// Format: "pid (comm) state ..." — state follows the closing ')' of the
// command name (which may itself contain spaces and parens).
if _, after, found := strings.CutLast(string(b), ") "); found {
fields := strings.Fields(after)
if len(fields) > 0 && fields[0] == "Z" {
return false // zombie: terminated but not yet reaped
}
}
}
return true
}
func TestKillerKillsTree(t *testing.T) {
cmd, ready := newTestProcess(t, "parent", false)
require.NoError(t, cmd.Start())
t.Cleanup(func() {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
_ = cmd.Wait()
})
killer, err := NewKiller(cmd.Process)
require.NoError(t, err)
defer killer.Close()
childPID := ready()
require.True(t, processAlive(childPID), "child process should start")
require.NoError(t, killer.Kill())
_ = cmd.Wait()
require.Eventually(t, func() bool {
return !processAlive(childPID)
}, 20*time.Second, 100*time.Millisecond, "backgrounded child should be terminated")
}
func TestTreeKillCancelBeforeCaptureKillsTree(t *testing.T) {
stdin, input, err := os.Pipe()
require.NoError(t, err)
t.Cleanup(func() { _ = stdin.Close() })
t.Cleanup(func() { _ = input.Close() })
cmd := exec.CommandContext(t.Context(), "/bin/sh", "-c", "exec 3<&0; cat <&3 & echo $!; read value")
cmd.Stdin = stdin
cmd.SysProcAttr = SysProcAttr("", false)
output, err := cmd.StdoutPipe()
require.NoError(t, err)
treeKill := NewTreeKill(cmd)
require.NoError(t, cmd.Start())
t.Cleanup(func() {
_ = syscall.Kill(-cmd.Process.Pid, syscall.SIGKILL)
_ = cmd.Wait()
})
var childPID int
_, err = fmt.Fscan(output, &childPID)
require.NoError(t, err)
require.True(t, processAlive(childPID))
require.NoError(t, cmd.Cancel())
killer, err := treeKill.Capture(cmd.Process)
require.NoError(t, err)
defer killer.Close()
require.Error(t, cmd.Wait())
require.Eventually(t, func() bool { return !processAlive(childPID) }, 5*time.Second, 10*time.Millisecond)
}