tests: avoid leaking child processes from agent/proxyprocess package

This commit is contained in:
R.B. Boyer 2019-03-04 14:50:03 -06:00 committed by R.B. Boyer
parent a99f7aaa25
commit 5bea49ecb0
3 changed files with 154 additions and 27 deletions

View File

@ -30,8 +30,11 @@ func TestDaemonStartStop(t *testing.T) {
uuid, err := uuid.GenerateUUID() uuid, err := uuid.GenerateUUID()
require.NoError(err) require.NoError(err)
cmd, destroy := helperProcess("start-stop", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("start-stop", path), Command: cmd,
ProxyID: "tubes", ProxyID: "tubes",
ProxyToken: uuid, ProxyToken: uuid,
Logger: testLogger, Logger: testLogger,
@ -78,8 +81,11 @@ func TestDaemonRestart(t *testing.T) {
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
cmd, destroy := helperProcess("restart", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("restart", path), Command: cmd,
Logger: testLogger, Logger: testLogger,
} }
require.NoError(d.Start()) require.NoError(d.Start())
@ -117,7 +123,8 @@ func TestDaemonLaunchesNewProcessGroup(t *testing.T) {
// Start the parent process wrapping a start-stop test. The parent is acting // Start the parent process wrapping a start-stop test. The parent is acting
// as our "agent". We need an extra indirection to be able to kill the "agent" // as our "agent". We need an extra indirection to be able to kill the "agent"
// and still be running the test process. // and still be running the test process.
parentCmd := helperProcess("parent", pidPath, "start-stop", path) parentCmd, destroy := helperProcess("parent", pidPath, "start-stop", path)
defer destroy()
// We MUST run this as a separate process group otherwise the Kill below will // We MUST run this as a separate process group otherwise the Kill below will
// kill this test process (and possibly your shell/editor that launched it!) // kill this test process (and possibly your shell/editor that launched it!)
@ -186,7 +193,9 @@ func TestDaemonLaunchesNewProcessGroup(t *testing.T) {
// Start a new parent that will "adopt" the existing child even though it will // Start a new parent that will "adopt" the existing child even though it will
// not be an actual child process. // not be an actual child process.
fosterCmd := helperProcess("parent", pidPath, "start-stop", path) fosterCmd, destroy := helperProcess("parent", pidPath, "start-stop", path)
defer destroy()
// Don't care about it being same process group this time as we will just kill // Don't care about it being same process group this time as we will just kill
// it normally. // it normally.
require.NoError(fosterCmd.Start()) require.NoError(fosterCmd.Start())
@ -246,6 +255,21 @@ func TestDaemonLaunchesNewProcessGroup(t *testing.T) {
// even harder! // even harder!
// Let defer clean up the child process(es) // Let defer clean up the child process(es)
// Get the NEW child PID
bs, err = ioutil.ReadFile(pidPath)
require.NoError(err)
pid, err = strconv.Atoi(string(bs))
require.NoError(err)
proc2, err := os.FindProcess(pid)
require.NoError(err)
// Always cleanup child process after
defer func() {
if proc2 != nil {
proc2.Kill()
}
}()
} }
func TestDaemonStop_kill(t *testing.T) { func TestDaemonStop_kill(t *testing.T) {
@ -257,8 +281,11 @@ func TestDaemonStop_kill(t *testing.T) {
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
cmd, destroy := helperProcess("stop-kill", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("stop-kill", path), Command: cmd,
ProxyToken: "hello", ProxyToken: "hello",
Logger: testLogger, Logger: testLogger,
gracefulWait: 200 * time.Millisecond, gracefulWait: 200 * time.Millisecond,
@ -316,14 +343,19 @@ func TestDaemonStop_killAdopted(t *testing.T) {
// ensure we are exercising that code path. // ensure we are exercising that code path.
// Start the "child" process // Start the "child" process
childCmd := helperProcess("stop-kill", path) childCmd, destroy := helperProcess("stop-kill", path)
defer destroy()
require.NoError(childCmd.Start()) require.NoError(childCmd.Start())
go func() { childCmd.Wait() }() // Prevent it becoming a zombie when killed go func() { childCmd.Wait() }() // Prevent it becoming a zombie when killed
defer func() { childCmd.Process.Kill() }() defer func() { childCmd.Process.Kill() }()
// Create the Daemon // Create the Daemon
cmd, destroy := helperProcess("stop-kill", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("stop-kill", path), Command: cmd,
ProxyToken: "hello", ProxyToken: "hello",
Logger: testLogger, Logger: testLogger,
gracefulWait: 200 * time.Millisecond, gracefulWait: 200 * time.Millisecond,
@ -380,8 +412,11 @@ func TestDaemonStart_pidFile(t *testing.T) {
uuid, err := uuid.GenerateUUID() uuid, err := uuid.GenerateUUID()
require.NoError(err) require.NoError(err)
cmd, destroy := helperProcess("start-once", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("start-once", path), Command: cmd,
ProxyToken: uuid, ProxyToken: uuid,
Logger: testLogger, Logger: testLogger,
PidPath: pidPath, PidPath: pidPath,
@ -422,8 +457,11 @@ func TestDaemonRestart_pidFile(t *testing.T) {
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
pidPath := filepath.Join(td, "pid") pidPath := filepath.Join(td, "pid")
cmd, destroy := helperProcess("restart", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("restart", path), Command: cmd,
Logger: testLogger, Logger: testLogger,
PidPath: pidPath, PidPath: pidPath,
} }
@ -618,8 +656,11 @@ func TestDaemonUnmarshalSnapshot(t *testing.T) {
uuid, err := uuid.GenerateUUID() uuid, err := uuid.GenerateUUID()
require.NoError(err) require.NoError(err)
cmd, destroy := helperProcess("start-stop", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("start-stop", path), Command: cmd,
ProxyToken: uuid, ProxyToken: uuid,
Logger: testLogger, Logger: testLogger,
} }
@ -676,8 +717,11 @@ func TestDaemonUnmarshalSnapshot_notRunning(t *testing.T) {
uuid, err := uuid.GenerateUUID() uuid, err := uuid.GenerateUUID()
require.NoError(err) require.NoError(err)
cmd, destroy := helperProcess("start-stop", path)
defer destroy()
d := &Daemon{ d := &Daemon{
Command: helperProcess("start-stop", path), Command: cmd,
ProxyToken: uuid, ProxyToken: uuid,
Logger: testLogger, Logger: testLogger,
} }

View File

@ -43,7 +43,11 @@ func TestManagerRun_initialSync(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy := helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
// Start the manager // Start the manager
go m.Run() go m.Run()
@ -78,7 +82,11 @@ func TestManagerRun_syncNew(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy := helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
// We should see the path appear shortly // We should see the path appear shortly
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
@ -91,7 +99,11 @@ func TestManagerRun_syncNew(t *testing.T) {
// Add another proxy // Add another proxy
path = path + "2" path = path + "2"
testStateProxy(t, state, "db", helperProcess("restart", path))
cmd, destroy = helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "db", cmd)
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { if err == nil {
@ -117,7 +129,11 @@ func TestManagerRun_syncDelete(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
id := testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy := helperProcess("restart", path)
defer destroy()
id := testStateProxy(t, state, "web", cmd)
// We should see the path appear shortly // We should see the path appear shortly
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
@ -157,7 +173,11 @@ func TestManagerRun_syncUpdate(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy := helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
// We should see the path appear shortly // We should see the path appear shortly
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
@ -171,7 +191,12 @@ func TestManagerRun_syncUpdate(t *testing.T) {
// Update the proxy with a new path // Update the proxy with a new path
oldPath := path oldPath := path
path = path + "2" path = path + "2"
testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy = helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
_, err := os.Stat(path) _, err := os.Stat(path)
if err == nil { if err == nil {
@ -204,7 +229,11 @@ func TestManagerRun_daemonLogs(t *testing.T) {
// Create the service and calculate the log paths // Create the service and calculate the log paths
path := filepath.Join(m.DataDir, "notify") path := filepath.Join(m.DataDir, "notify")
id := testStateProxy(t, state, "web", helperProcess("output", path))
cmd, destroy := helperProcess("output", path)
defer destroy()
id := testStateProxy(t, state, "web", cmd)
stdoutPath := logPath(logDir, id, "stdout") stdoutPath := logPath(logDir, id, "stdout")
stderrPath := logPath(logDir, id, "stderr") stderrPath := logPath(logDir, id, "stderr")
@ -244,7 +273,11 @@ func TestManagerRun_daemonPid(t *testing.T) {
// Create the service and calculate the log paths // Create the service and calculate the log paths
path := filepath.Join(m.DataDir, "notify") path := filepath.Join(m.DataDir, "notify")
id := testStateProxy(t, state, "web", helperProcess("output", path))
cmd, destroy := helperProcess("output", path)
defer destroy()
id := testStateProxy(t, state, "web", cmd)
pidPath := pidPath(pidDir, id) pidPath := pidPath(pidDir, id)
// Start the manager // Start the manager
@ -280,7 +313,11 @@ func TestManagerPassesEnvironment(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "env-variables") path := filepath.Join(td, "env-variables")
testStateProxy(t, state, "environTest", helperProcess("environ", path))
cmd, destroy := helperProcess("environ", path)
defer destroy()
testStateProxy(t, state, "environTest", cmd)
//Run the manager //Run the manager
go m.Run() go m.Run()
@ -331,7 +368,11 @@ func TestManagerPassesProxyEnv(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "env-variables") path := filepath.Join(td, "env-variables")
testStateProxy(t, state, "environTest", helperProcess("environ", path))
cmd, destroy := helperProcess("environ", path)
defer destroy()
testStateProxy(t, state, "environTest", cmd)
//Run the manager //Run the manager
go m.Run() go m.Run()
@ -378,7 +419,11 @@ func TestManagerRun_snapshotRestore(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
testStateProxy(t, state, "web", helperProcess("start-stop", path))
cmd, destroy := helperProcess("start-stop", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
// Set a low snapshot period so we get a snapshot // Set a low snapshot period so we get a snapshot
m.SnapshotPeriod = 10 * time.Millisecond m.SnapshotPeriod = 10 * time.Millisecond
@ -427,7 +472,12 @@ func TestManagerRun_snapshotRestore(t *testing.T) {
// Add a second proxy so that we can determine when we're up // Add a second proxy so that we can determine when we're up
// and running. // and running.
path2 := filepath.Join(td, "file2") path2 := filepath.Join(td, "file2")
testStateProxy(t, state, "db", helperProcess("start-stop", path2))
cmd, destroy = helperProcess("start-stop", path2)
defer destroy()
testStateProxy(t, state, "db", cmd)
retry.Run(t, func(r *retry.R) { retry.Run(t, func(r *retry.R) {
_, err := os.Stat(path2) _, err := os.Stat(path2)
if err == nil { if err == nil {
@ -465,7 +515,11 @@ func TestManagerRun_rootDisallow(t *testing.T) {
td, closer := testTempDir(t) td, closer := testTempDir(t)
defer closer() defer closer()
path := filepath.Join(td, "file") path := filepath.Join(td, "file")
testStateProxy(t, state, "web", helperProcess("restart", path))
cmd, destroy := helperProcess("restart", path)
defer destroy()
testStateProxy(t, state, "web", cmd)
// Start the manager // Start the manager
go m.Run() go m.Run()

View File

@ -43,14 +43,19 @@ const helperProcessSentinel = "WANT_HELPER_PROCESS"
// helperProcess returns an *exec.Cmd that can be used to execute the // helperProcess returns an *exec.Cmd that can be used to execute the
// TestHelperProcess function below. This can be used to test multi-process // TestHelperProcess function below. This can be used to test multi-process
// interactions. // interactions.
func helperProcess(s ...string) *exec.Cmd { func helperProcess(s ...string) (*exec.Cmd, func()) {
cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel} cs := []string{"-test.run=TestHelperProcess", "--", helperProcessSentinel}
cs = append(cs, s...) cs = append(cs, s...)
cmd := exec.Command(os.Args[0], cs...) cmd := exec.Command(os.Args[0], cs...)
cmd.Stdout = os.Stdout cmd.Stdout = os.Stdout
cmd.Stderr = os.Stderr cmd.Stderr = os.Stderr
return cmd destroy := func() {
if p := cmd.Process; p != nil {
p.Kill()
}
}
return cmd, destroy
} }
// This is not a real test. This is just a helper process kicked off by tests // This is not a real test. This is just a helper process kicked off by tests
@ -77,6 +82,8 @@ func TestHelperProcess(t *testing.T) {
// While running, this creates a file in the given directory (args[0]) // While running, this creates a file in the given directory (args[0])
// and deletes it only when it is stopped. // and deletes it only when it is stopped.
case "start-stop": case "start-stop":
limitProcessLifetime(2 * time.Minute)
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt, syscall.SIGTERM) signal.Notify(ch, os.Interrupt, syscall.SIGTERM)
defer signal.Stop(ch) defer signal.Stop(ch)
@ -98,6 +105,8 @@ func TestHelperProcess(t *testing.T) {
// exists. When that file is removed, this process exits. This can be // exists. When that file is removed, this process exits. This can be
// used to test restarting. // used to test restarting.
case "restart": case "restart":
limitProcessLifetime(2 * time.Minute)
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt) signal.Notify(ch, os.Interrupt)
defer signal.Stop(ch) defer signal.Stop(ch)
@ -127,6 +136,8 @@ func TestHelperProcess(t *testing.T) {
} }
} }
case "stop-kill": case "stop-kill":
limitProcessLifetime(2 * time.Minute)
// Setup listeners so it is ignored // Setup listeners so it is ignored
ch := make(chan os.Signal, 1) ch := make(chan os.Signal, 1)
signal.Notify(ch, os.Interrupt) signal.Notify(ch, os.Interrupt)
@ -142,6 +153,8 @@ func TestHelperProcess(t *testing.T) {
} }
// Check if the external process can access the enivironmental variables // Check if the external process can access the enivironmental variables
case "environ": case "environ":
limitProcessLifetime(2 * time.Minute)
stop := make(chan os.Signal, 1) stop := make(chan os.Signal, 1)
signal.Notify(stop, os.Interrupt) signal.Notify(stop, os.Interrupt)
defer signal.Stop(stop) defer signal.Stop(stop)
@ -172,6 +185,8 @@ func TestHelperProcess(t *testing.T) {
<-stop <-stop
case "output": case "output":
limitProcessLifetime(2 * time.Minute)
fmt.Fprintf(os.Stdout, "hello stdout\n") fmt.Fprintf(os.Stdout, "hello stdout\n")
fmt.Fprintf(os.Stderr, "hello stderr\n") fmt.Fprintf(os.Stderr, "hello stderr\n")
@ -199,12 +214,17 @@ func TestHelperProcess(t *testing.T) {
// If the PID file already exists, it will "adopt" the child rather than // If the PID file already exists, it will "adopt" the child rather than
// launch a new one. // launch a new one.
case "parent": case "parent":
limitProcessLifetime(2 * time.Minute)
// We will write the PID for the child to the file in the first argument // We will write the PID for the child to the file in the first argument
// then pass rest of args through to command. // then pass rest of args through to command.
pidFile := args[0] pidFile := args[0]
cmd, destroyChild := helperProcess(args[1:]...)
defer destroyChild()
d := &Daemon{ d := &Daemon{
Command: helperProcess(args[1:]...), Command: cmd,
Logger: testLogger, Logger: testLogger,
PidPath: pidFile, PidPath: pidFile,
} }
@ -251,3 +271,12 @@ func TestHelperProcess(t *testing.T) {
os.Exit(2) os.Exit(2)
} }
} }
// limitProcessLifetime installs a background goroutine that self-exits after
// the specified duration elapses to prevent leaking processes from tests that
// may spawn them.
func limitProcessLifetime(dur time.Duration) {
go time.AfterFunc(dur, func() {
os.Exit(99)
})
}