1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
//! A sheep that cannot spawn, reported the same way whether it was named
//! or given a path.
use super::*;
#[cfg(unix)]
/// The setup the two cases below share: a valid-shell script written at
/// `0o644`, a `--format json` start by path that must fail `spawn_failed`,
/// and a poll until the sheep sits registered `errored`. Returns the sheep's
/// name, which is the script's stem.
///
/// The daemon this autostarts is the one the caller's own verb reuses.
#[track_caller]
fn a_sheep_that_cannot_spawn(
dir: &TempDir,
script_name: &str,
body: &str,
guard: &mut DaemonGuard,
) -> String {
let script = dir.path().join(script_name);
std::fs::write(&script, body).unwrap();
let mut perms = std::fs::metadata(&script).unwrap().permissions();
perms.set_mode(0o644);
std::fs::set_permissions(&script, perms).unwrap();
let by_path = shep(dir.path())
.arg("--format")
.arg("json")
.arg("start")
.arg(&script)
.output()
.unwrap();
guard.adopt_home(dir.path());
assert_json_error(&by_path, 7, "spawn_failed");
// Must be `errored` in the flock, or the caller's second command takes
// `resolve_target`'s path arm instead of `resume`'s.
let flock = poll_flock(dir.path(), |info| info["status"] == "errored");
assert_eq!(
flock["status"], "errored",
"the by-path failure must leave the sheep registered as errored: {flock}"
);
script.file_stem().unwrap().to_str().unwrap().to_string()
}
#[cfg(unix)]
/// `Response::Restarted` has no per-id error slot, so a respawn that cannot
/// spawn answers `Ok` with an `errored` row rather than an RPC error;
/// `resume`'s `any_restart_failed` check is what closes that gap.
///
/// The script is valid shell but not executable (`0o644`), so every spawn of
/// it fails `EACCES` whichever request drove it.
#[test]
fn starting_an_errored_sheep_by_name_reports_the_same_failure_as_by_path() {
let dir = tempfile::tempdir().unwrap();
let mut guard = DaemonGuard::default();
let name = a_sheep_that_cannot_spawn(&dir, "broken.sh", "#!/bin/sh\necho hi\n", &mut guard);
// By name, same broken script, same failure.
let by_name = shep(dir.path())
.arg("--format")
.arg("json")
.arg("start")
.arg(&name)
.output()
.unwrap();
assert_json_error(&by_name, 7, "spawn_failed");
graceful_kill(dir.path());
}
#[cfg(unix)]
/// Same `Response::Restarted` gap as the sibling above. `restart` still prints
/// its table, being a multi-target verb; the exit code and the stderr line are
/// what change.
///
/// The script is valid shell at `0o644`, so every spawn fails `EACCES`, and it
/// has no extension: `.sh` maps to `sh` through the interpreter mapping, which
/// would run a non-executable file and delete the premise.
#[test]
fn restarting_a_sheep_that_cannot_spawn_reports_it_rather_than_exiting_zero() {
let dir = tempfile::tempdir().unwrap();
let mut guard = DaemonGuard::default();
let name = a_sheep_that_cannot_spawn(&dir, "noexec", "#!/bin/sh\nsleep 30\n", &mut guard);
let restarted = shep(dir.path())
.arg("--format")
.arg("json")
.arg("restart")
.arg(&name)
.output()
.unwrap();
assert_json_error(&restarted, 7, "spawn_failed");
graceful_kill(dir.path());
}
#[cfg(unix)]
/// The missing-node sentence, produced for real rather than quoted.
///
/// It needs a `PATH` with no node on it, which a unit test could only get by
/// mutating its own process. `docs/migration.md` quotes this sentence, and
/// this is what holds the quote to the `format!` that produces it.
#[test]
fn a_js_flockfile_without_node_says_so_and_says_what_to_do() {
let dir = tempfile::tempdir().unwrap();
let flockfile = dir.path().join("Flockfile.js");
// Declares a real app, so the only thing that can fail is the missing
// interpreter: with node present this Flockfile is valid.
std::fs::write(
&flockfile,
"module.exports = { app: [{ name: 'web', script: './server.js' }] };\n",
)
.unwrap();
let mut guard = DaemonGuard::default();
// An empty PATH for the child only, so `node` cannot be found and the
// parent's environment is untouched.
let output = shep(dir.path())
.env("PATH", "")
.arg("start")
.arg("--flockfile")
.arg(&flockfile)
.output()
.unwrap();
// `start` autostarts a shepherd before it ever opens the Flockfile, so
// this case leaves one behind even though it fails.
guard.adopt_home(dir.path());
let stderr = String::from_utf8_lossy(&output.stderr);
assert!(
!output.status.success(),
"a Flockfile that cannot be read must not succeed: {stderr}"
);
assert!(
stderr.contains("node was not found on PATH"),
"the message names the cause: {stderr}"
);
assert!(
stderr.contains("install node, or convert"),
"and what to do about it: {stderr}"
);
assert!(
!stderr.contains('\u{2014}') && !stderr.contains('\u{2013}'),
"no em or en dash in copy a user reads: {stderr}"
);
graceful_kill(dir.path());
}