36b3af2e3d
PR Check / Code Quality: Format (push) Failing after 1s
PR Check / Code Quality: Lint (darwin) (push) Failing after 0s
PR Check / Code Quality: Lint (freebsd) (push) Failing after 1s
PR Check / Code Quality: Lint (windows) (push) Failing after 1s
PR Check / Code Quality: Lint (linux) (push) Failing after 1s
PR Check / Security: Vulnerability Scan (push) Failing after 0s
Update Documentation / update-docs (push) Failing after 2s
PR Check / Code Quality: Vendor (push) Failing after 1s
PR Check / Code Quality: Coverage (push) Failing after 0s
PR Check / Tests: Unit (macos-latest) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04) (push) Has been cancelled
PR Check / Tests: Unit (ubuntu-24.04-arm) (push) Has been cancelled
PR Check / Tests: Unit (windows-latest) (push) Has been cancelled
191 lines
6.2 KiB
Go
191 lines
6.2 KiB
Go
//go:build linux || darwin || freebsd || windows
|
|
|
|
package target
|
|
|
|
import (
|
|
"net"
|
|
"os"
|
|
"os/exec"
|
|
"runtime"
|
|
"slices"
|
|
"testing"
|
|
"time"
|
|
)
|
|
|
|
// Integration smoke tests for the target package's platform-specific
|
|
// resolution paths. They spin up real OS state (a sleeper child, a TCP
|
|
// listener) the test can identify by PID, then confirm the resolver finds it.
|
|
|
|
// startSleeper spawns a long-running child process suitable for name-resolution
|
|
// assertions. Returns the child's PID and process name; the caller must invoke
|
|
// the returned cleanup func to kill it.
|
|
func startSleeper(t *testing.T) (pid int, name string, cleanup func()) {
|
|
t.Helper()
|
|
|
|
var cmd *exec.Cmd
|
|
switch runtime.GOOS {
|
|
case "windows":
|
|
// `ping -n N 127.0.0.1` is a reliable cross-version Windows sleeper
|
|
// that doesn't require a TTY (unlike `timeout /nobreak`).
|
|
cmd = exec.Command("ping", "-n", "60", "127.0.0.1")
|
|
name = "ping"
|
|
default:
|
|
cmd = exec.Command("sleep", "60")
|
|
name = "sleep"
|
|
}
|
|
|
|
if err := cmd.Start(); err != nil {
|
|
t.Skipf("could not spawn %s for integration test: %v", name, err)
|
|
}
|
|
return cmd.Process.Pid, name, func() {
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}
|
|
}
|
|
|
|
// TestIntegration_ResolveNameFindsSpawnedChild proves the name resolver talks
|
|
// to the real OS process table. We can't ResolveName against the test binary
|
|
// itself because every platform's name resolver excludes self+ancestors —
|
|
// that's a feature (so `witr bash` doesn't always match the user's own
|
|
// shell), but it forces this test to use a spawned child instead.
|
|
func TestIntegration_ResolveNameFindsSpawnedChild(t *testing.T) {
|
|
childPID, name, cleanup := startSleeper(t)
|
|
defer cleanup()
|
|
|
|
// Give the OS a beat to register the child. ToolHelp32 / /proc updates
|
|
// are usually instant, but CI runners can be sluggish.
|
|
var pids []int
|
|
var lastErr error
|
|
for i := 0; i < 10; i++ {
|
|
var err error
|
|
pids, err = ResolveName(name, true)
|
|
if err == nil && slices.Contains(pids, childPID) {
|
|
return
|
|
}
|
|
lastErr = err
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
t.Errorf("ResolveName(%q) did not find spawned child PID %d after retries; got %v (last err: %v)",
|
|
name, childPID, pids, lastErr)
|
|
}
|
|
|
|
// TestIntegration_ResolveNameFindsGrep is a regression test for the removed
|
|
// "grep" name-matching exclusion. witr reads the process table directly (no
|
|
// `ps | grep` pipeline), so a real grep process must be resolvable by name —
|
|
// previously it was silently filtered out on every Unix platform.
|
|
func TestIntegration_ResolveNameFindsGrep(t *testing.T) {
|
|
if runtime.GOOS == "windows" {
|
|
t.Skip("grep is not available on Windows")
|
|
}
|
|
if _, err := exec.LookPath("grep"); err != nil {
|
|
t.Skipf("grep not found in PATH: %v", err)
|
|
}
|
|
|
|
// grep with no file argument blocks reading stdin; keeping the pipe open
|
|
// holds it alive for the duration of the lookup.
|
|
cmd := exec.Command("grep", "witr-regression-pattern")
|
|
stdin, err := cmd.StdinPipe()
|
|
if err != nil {
|
|
t.Fatalf("StdinPipe: %v", err)
|
|
}
|
|
if err := cmd.Start(); err != nil {
|
|
t.Skipf("could not spawn grep: %v", err)
|
|
}
|
|
childPID := cmd.Process.Pid
|
|
defer func() {
|
|
_ = stdin.Close()
|
|
_ = cmd.Process.Kill()
|
|
_, _ = cmd.Process.Wait()
|
|
}()
|
|
|
|
var pids []int
|
|
var lastErr error
|
|
for i := 0; i < 10; i++ {
|
|
pids, lastErr = ResolveName("grep", true)
|
|
if lastErr == nil && slices.Contains(pids, childPID) {
|
|
return
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
t.Errorf("ResolveName(%q) did not find spawned grep PID %d after retries; got %v (last err: %v)",
|
|
"grep", childPID, pids, lastErr)
|
|
}
|
|
|
|
// TestIntegration_ResolvePortFindsLoopbackListener binds a real TCP listener
|
|
// on a random localhost port and asserts ResolvePort attributes it to the
|
|
// test process. This drives the platform's port-resolution machinery end to
|
|
// end (netstat/lsof/sockstat/Win32 netstat plus /proc fd scan).
|
|
func TestIntegration_ResolvePortFindsLoopbackListener(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("net.Listen: %v", err)
|
|
}
|
|
defer ln.Close()
|
|
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
self := os.Getpid()
|
|
|
|
pids, err := ResolvePort(port)
|
|
if err != nil {
|
|
t.Fatalf("ResolvePort(%d): %v", port, err)
|
|
}
|
|
if !slices.Contains(pids, self) {
|
|
t.Errorf("ResolvePort(%d) = %v, missing self PID %d", port, pids, self)
|
|
}
|
|
}
|
|
|
|
// TestIntegration_ResolvePortNonexistent asserts the resolver errors cleanly
|
|
// on a port nothing has bound. Picking a random ephemeral port and closing
|
|
// the listener gives us a port we know nobody else owns.
|
|
func TestIntegration_ResolvePortNonexistent(t *testing.T) {
|
|
ln, err := net.Listen("tcp", "127.0.0.1:0")
|
|
if err != nil {
|
|
t.Fatalf("net.Listen: %v", err)
|
|
}
|
|
port := ln.Addr().(*net.TCPAddr).Port
|
|
ln.Close() // free the port immediately
|
|
|
|
// The kernel may keep the port in TIME_WAIT briefly. Loop a few times
|
|
// until the port is genuinely unowned.
|
|
for i := 0; i < 10; i++ {
|
|
_, err := ResolvePort(port)
|
|
if err != nil {
|
|
return // expected
|
|
}
|
|
time.Sleep(50 * time.Millisecond)
|
|
}
|
|
t.Errorf("ResolvePort(%d) returned nil error for an unbound port", port)
|
|
}
|
|
|
|
// TestIntegration_ResolveFileSelf creates a file, holds it open, and confirms
|
|
// the platform's file resolver attributes it to the test process — exercising
|
|
// /proc/fd (Linux), lsof (macOS), fstat (FreeBSD), and the Restart Manager
|
|
// (Windows). macOS/FreeBSD lean on external tools that may be absent or quirky
|
|
// in minimal CI images, so a miss there is tolerated rather than failed.
|
|
func TestIntegration_ResolveFileSelf(t *testing.T) {
|
|
f, err := os.CreateTemp("", "witr-file-*.tmp")
|
|
if err != nil {
|
|
t.Fatalf("CreateTemp: %v", err)
|
|
}
|
|
defer func() { _ = os.Remove(f.Name()) }()
|
|
defer f.Close()
|
|
if _, err := f.WriteString("witr"); err != nil {
|
|
t.Fatalf("write temp file: %v", err)
|
|
}
|
|
|
|
self := os.Getpid()
|
|
var pids []int
|
|
var lastErr error
|
|
for i := 0; i < 10; i++ {
|
|
pids, lastErr = ResolveFile(f.Name())
|
|
if lastErr == nil && slices.Contains(pids, self) {
|
|
return
|
|
}
|
|
time.Sleep(100 * time.Millisecond)
|
|
}
|
|
if runtime.GOOS == "darwin" || runtime.GOOS == "freebsd" {
|
|
t.Skipf("ResolveFile on %s did not attribute the file to self (got %v, err %v); tolerated", runtime.GOOS, pids, lastErr)
|
|
}
|
|
t.Errorf("ResolveFile(%q) did not find self PID %d; got %v (last err: %v)", f.Name(), self, pids, lastErr)
|
|
}
|