use super::*;
mod slow {
const PROBE_BUDGET: Duration = Duration::from_secs(30);
use super::*;
#[cfg(unix)]
fn answering_a_restart(request: &Request) -> Response {
match request {
Request::Restart { .. } => Response::Restarted {
accepted: Vec::new(),
refused: Vec::new(),
},
_ => Response::Flock(Vec::new()),
}
}
#[cfg(unix)]
#[tokio::test]
async fn a_dog_whose_disk_binary_cannot_connect_is_warned_about_before_the_restart() {
let dir = tempfile::tempdir().unwrap();
let paths = adopted_dog(dir.path(), "log-rotate", &below_floor_answer());
let sock = shep_client::testing::control_address(dir.path());
let (client, _daemon) =
fake_client_replying_err(&sock, RpcErrorCode::NotFound, "no such sheep").await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["log-rotate".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
let text = String::from_utf8(err).unwrap();
let warning = text
.find("dog_binary_skew")
.unwrap_or_else(|| panic!("the restart must warn first: {text}"));
let refusal = text
.find("no such sheep")
.unwrap_or_else(|| panic!("the restart must still be attempted: {text}"));
assert!(
warning < refusal,
"the warning must reach the operator before the restart does: {text}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn the_restart_warning_names_both_numbers_and_both_ways_out() {
let dir = tempfile::tempdir().unwrap();
let paths = adopted_dog(dir.path(), "log-rotate", &below_floor_answer());
let sock = shep_client::testing::control_address(dir.path());
let (client, _envelopes) = fake_client_answering(&sock, answering_a_restart).await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["log-rotate".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
let text = String::from_utf8(err).unwrap();
let disk = shep_core::protocol::MIN_SUPPORTED
.saturating_sub(1)
.to_string();
let floor = shep_core::protocol::MIN_SUPPORTED.to_string();
assert!(
text.contains(&disk) && text.contains(&floor),
"the warning names both numbers: {text}"
);
assert!(
text.contains("Run a shep that accepts protocol") && text.contains("reinstall the dog"),
"the warning names both ways out, and picks neither: {text}"
);
assert!(
text.contains("log-rotate"),
"the warning names the dog it is about: {text}"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_dog_whose_disk_binary_is_current_restarts_in_silence() {
let dir = tempfile::tempdir().unwrap();
let answer = format!(
"echo 'shep-log-rotate 0.1.3'\necho 'shep-protocol: {}'",
shep_client::PROTOCOL_VERSION
);
let paths = adopted_dog(dir.path(), "log-rotate", &answer);
let sock = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_answering(&sock, answering_a_restart).await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["log-rotate".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
assert_eq!(
String::from_utf8(err).unwrap(),
"",
"a healthy dog restarts with nothing said about it"
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), envelopes.recv())
.await
.expect("restart must reach the wire; it hung instead of sending a request")
.unwrap()
.body,
Request::Restart {
selector: SelectorSpec::Name("log-rotate".into()),
},
"and it is still restarted"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_dog_that_answers_nothing_restarts_in_silence() {
let dir = tempfile::tempdir().unwrap();
let paths = adopted_dog(
dir.path(),
"log-rotate",
"echo 'shep-log-rotate does not understand --version' >&2\nexit 2",
);
let sock = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_answering(&sock, answering_a_restart).await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["log-rotate".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
assert_eq!(
String::from_utf8(err).unwrap(),
"",
"a dog that does not answer is not a dog that is stale"
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), envelopes.recv())
.await
.expect("restart must reach the wire; it hung instead of sending a request")
.unwrap()
.body,
Request::Restart {
selector: SelectorSpec::Name("log-rotate".into()),
},
"and it is still restarted"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_dog_that_names_no_protocol_restarts_in_silence() {
let dir = tempfile::tempdir().unwrap();
let paths = adopted_dog(dir.path(), "log-rotate", "echo 'shep-log-rotate 0.1.3'");
let sock = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_answering(&sock, answering_a_restart).await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["log-rotate".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
assert_eq!(
String::from_utf8(err).unwrap(),
"",
"an unstated protocol is unknown, and unknown is not stale"
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), envelopes.recv())
.await
.expect("restart must reach the wire; it hung instead of sending a request")
.unwrap()
.body,
Request::Restart {
selector: SelectorSpec::Name("log-rotate".into()),
},
"and it is still restarted"
);
}
#[cfg(unix)]
#[tokio::test]
async fn a_built_in_dog_is_never_asked_what_its_binary_speaks() {
let dir = tempfile::tempdir().unwrap();
let paths = adopted_dog(dir.path(), "log-rotate", &below_floor_answer());
crate::commands::shep_toml::ShepToml::edit(&paths.daemon_config, |cfg| {
cfg.enable_dog("metrics").unwrap();
})
.unwrap();
let sock = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_answering(&sock, answering_a_restart).await;
let mut out = Vec::new();
let mut err = Vec::new();
{
let mut streams = Streams::bare(&mut out, &mut err, Format::Table);
let args = SelectorArgs {
selectors: vec!["metrics".into()],
};
let _ = restart_within(&client, &mut streams, Some(&paths), &args, PROBE_BUDGET).await;
}
assert_eq!(
String::from_utf8(err).unwrap(),
"",
"a built-in dog has no binary of its own to be stale"
);
assert_eq!(
tokio::time::timeout(Duration::from_secs(5), envelopes.recv())
.await
.expect("restart must reach the wire; it hung instead of sending a request")
.unwrap()
.body,
Request::Restart {
selector: SelectorSpec::Name("metrics".into()),
},
"and it is still restarted"
);
}
fn held_pipe_module(budget: Duration) -> String {
format!(
"require('child_process')\
.spawn(process.execPath, ['-e', 'setTimeout(()=>{{}},{})'], \
{{ detached: true, stdio: 'inherit' }})\
.unref(); \
module.exports = {{ app: [] }};",
(budget * 3).as_millis()
)
}
#[test]
fn a_js_flockfile_leaving_a_process_on_the_pipe_says_that_instead() {
if !node_available() {
return;
}
let dir = tempfile::tempdir().unwrap();
let path = dir.path().join("flock.js");
const BUDGETS: [Duration; 3] = [
Duration::from_secs(5),
Duration::from_secs(20),
Duration::from_secs(80),
];
for (attempt, budget) in BUDGETS.iter().copied().enumerate() {
std::fs::write(&path, held_pipe_module(budget)).unwrap();
let err = evaluate_js_flockfile(&path, budget).unwrap_err();
let message = err.to_string();
if message.contains("still running") && attempt + 1 < BUDGETS.len() {
continue;
}
assert_eq!(target_exit_code(&err), ExitCode::InvalidConfig);
assert!(
message.contains("left behind still holds the output"),
"got: {message}"
);
assert!(
!message.contains("killed"),
"node exited on its own, so nothing was killed: {message}"
);
return;
}
}
}