Signals end every command, not only ssh to and ssh install (closes #48)
check / check (push) Successful in 2m35s
check / check (push) Successful in 2m35s
SIGINT, SIGTERM and SIGHUP were caught for the whole run, but only the ssh and sftp children acted on them: the mnemonic prompt waited for Enter, and an interrupted `age encrypt -o` went on to put the encryption of the cut-off input in place. Now only `ssh to` and `ssh install` catch them, from once the mnemonic is read until their cleanup has run; everywhere else they end the tool at once. Tests cover an interrupted `age encrypt -o` and the install working directory on a signal. Model: opus-5-5
This commit is contained in:
@@ -20,13 +20,13 @@ import (
|
||||
|
||||
// runAsTool, set in the environment of a re-executed test binary, tells
|
||||
// TestMain to run the tool through Main rather than the suite, so the
|
||||
// signal test can drive the real signal path in a process it can send a
|
||||
// signal to.
|
||||
// signal tests can drive the real signal path in a process they can
|
||||
// send a signal to.
|
||||
const runAsTool = "KEYFUNC_TEST_RUN_AS_TOOL"
|
||||
|
||||
// TestMain re-executes the test binary as the tool when runAsTool is
|
||||
// set, and otherwise runs the suite. The signal test starts the tool
|
||||
// this way, as a subprocess it can signal and watch clean up.
|
||||
// set, and otherwise runs the suite. The signal tests start the tool
|
||||
// this way, as a subprocess they can signal and watch end.
|
||||
func TestMain(m *testing.M) {
|
||||
if os.Getenv(runAsTool) == "1" {
|
||||
os.Exit(cli.Main())
|
||||
@@ -201,6 +201,16 @@ fi
|
||||
sleep 5
|
||||
`
|
||||
|
||||
// stalled is a stand-in for the system sftp that notes it has started
|
||||
// and then blocks, so a test can signal the tool while sftp is running
|
||||
// and watch it remove its working directory. The exec keeps the shell
|
||||
// from leaving a sleep behind that holds the output the tool reads sftp
|
||||
// through.
|
||||
const stalled = `
|
||||
touch "$KEYFUNC_TEST_STARTED"
|
||||
exec sleep 5
|
||||
`
|
||||
|
||||
// pretended is where a stand-in writes down what it was asked to do.
|
||||
type pretended struct {
|
||||
// home stands in for the home directory on the host.
|
||||
@@ -510,7 +520,7 @@ func TestASignalTakesTheAgentDirectoryDown(t *testing.T) {
|
||||
// ssh that blocks, waits until the agent is up and ssh is running
|
||||
// against it, sends the tool the signal, and requires the agent socket
|
||||
// and its directory to be gone once the tool has ended. The subprocess
|
||||
// goes through Main and its signal handling, so with that handling
|
||||
// goes through Main and the command's signal handling, so with that handling
|
||||
// removed the signal kills the tool outright, no deferred cleanup runs,
|
||||
// the directory is left behind, and the check fails.
|
||||
func signalEndsTheTool(t *testing.T, name string, signal os.Signal) {
|
||||
@@ -577,6 +587,56 @@ func waitForSocket(t *testing.T, noted string) string {
|
||||
return socket
|
||||
}
|
||||
|
||||
func TestASignalTakesTheInstallWorkingDirectoryDown(t *testing.T) {
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
for _, ending := range []os.Signal{
|
||||
syscall.SIGTERM, syscall.SIGINT, syscall.SIGHUP,
|
||||
} {
|
||||
signalEndsTheInstall(t, ending.String(), ending)
|
||||
}
|
||||
}
|
||||
|
||||
// signalEndsTheInstall runs "ssh install" as a subprocess against a
|
||||
// stand-in sftp that blocks, with a temporary directory of the test's
|
||||
// own, waits until sftp is running, sends the tool the signal, and
|
||||
// requires the working directory the tool made there to be gone once
|
||||
// the tool has ended.
|
||||
func signalEndsTheInstall(t *testing.T, name string, signal os.Signal) {
|
||||
t.Helper()
|
||||
|
||||
temporary := t.TempDir()
|
||||
started := filepath.Join(t.TempDir(), "started")
|
||||
t.Setenv("KEYFUNC_TEST_STARTED", started)
|
||||
standIn(t, "sftp", stalled)
|
||||
|
||||
//nolint:gosec // the binary is this test's own, re-run as the tool
|
||||
command := exec.CommandContext(
|
||||
t.Context(), os.Args[0], subcommand, installing, host,
|
||||
)
|
||||
|
||||
command.Env = append(os.Environ(), runAsTool+"=1", "TMPDIR="+temporary)
|
||||
require.NoError(t, command.Start())
|
||||
|
||||
// sftp is started only once the working directory has been made.
|
||||
require.Eventually(t, func() bool {
|
||||
_, err := os.Stat(started)
|
||||
|
||||
return err == nil
|
||||
}, 5*time.Second, 5*time.Millisecond)
|
||||
|
||||
working, err := os.ReadDir(temporary)
|
||||
require.NoError(t, err)
|
||||
require.Len(t, working, 1, name)
|
||||
|
||||
require.NoError(t, command.Process.Signal(signal))
|
||||
waitForTool(t, name, command)
|
||||
|
||||
left, err := os.ReadDir(temporary)
|
||||
require.NoError(t, err)
|
||||
require.Empty(t, left, name)
|
||||
}
|
||||
|
||||
func TestTheMnemonicIsNotHandedToSFTP(t *testing.T) {
|
||||
t.Setenv(mnemonic.CommandVariable, "echo "+example())
|
||||
t.Setenv(mnemonic.Variable, example())
|
||||
|
||||
Reference in New Issue
Block a user