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;
}
}
}
async fn restart_kelpie(entry_env: &str, spawned_env: &[(&str, &str)]) -> (String, Vec<Request>) {
use crate::commands::dogs::entry_drift_support::{home_and_spawned, shepherd_running};
let dir = tempfile::tempdir().unwrap();
let (paths, spawned) = home_and_spawned(dir.path(), entry_env, spawned_env);
let restarted = shep_core::protocol::ProcessInfo::builder(
1,
"kelpie",
shep_core::status::ProcStatus::Online,
)
.dog(Some(shep_core::protocol::DogSource::Adopted {
path: spawned.script.clone(),
channel: false,
grants: None,
}))
.build();
let base = shepherd_running(spawned, shep_core::status::ProcStatus::Online);
let sock = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_answering(&sock, move |req| match req {
Request::Restart { .. } => Response::Restarted {
accepted: vec![restarted.clone()],
refused: Vec::new(),
},
other => base(other),
})
.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!["kelpie".into()],
};
let _ = restart_within(
&client,
&mut streams,
Some(&paths),
&args,
Duration::from_millis(1),
)
.await;
}
let sent = std::iter::from_fn(|| envelopes.try_recv().ok())
.map(|envelope| envelope.body)
.collect();
(String::from_utf8(err).unwrap(), sent)
}
#[tokio::test]
async fn a_restart_of_a_dog_whose_entry_changed_restarts_and_says_it_kept_the_old_one() {
let (text, sent) = restart_kelpie("PADDOCK_KEY = \"{{secret:paddock-key}}\"", &[]).await;
assert!(
text.contains("`kelpie`")
&& text.contains("shep disable kelpie")
&& text.contains("shep enable kelpie"),
"the dog and the way to apply its entry must be named: {text}"
);
assert!(
!text.contains("paddock-key"),
"a value, or a reference standing for one, must never be printed: {text}"
);
assert!(
sent.iter()
.any(|req| matches!(req, Request::Restart { .. })),
"a notice is not a refusal: {sent:?}"
);
}
#[tokio::test]
async fn a_restart_of_a_dog_whose_entry_is_unchanged_says_nothing_about_it() {
let (text, sent) = restart_kelpie(
"PADDOCK_KEY = \"{{secret:paddock-key}}\"",
&[("PADDOCK_KEY", "{{secret:paddock-key}}")],
)
.await;
assert_eq!(text, "");
assert!(
sent.iter()
.any(|req| matches!(req, Request::Restart { .. }))
);
}