use super::*;
#[test]
fn every_format_survives_the_second_deserialize_resolve_target_now_runs() {
let dir = tempfile::tempdir().unwrap();
let sources = [
(
"Flockfile.toml",
"[[app]]\nname = \"web\"\nscript = \"./srv\"\nargs = [\"-p\", \"80\"]\n\
[app.env]\nMODE = \"live\"\n",
),
(
"Flockfile.yaml",
"app:\n - name: web\n script: ./srv\n args: ['-p', '80']\n\
\n env:\n MODE: live\n",
),
(
"Flockfile.json",
"{\"app\":[{\"name\":\"web\",\"script\":\"./srv\",\
\"args\":[\"-p\",\"80\"],\"env\":{\"MODE\":\"live\"}}]}",
),
(
"Flockfile.json5",
"{app:[{name:'web',script:'./srv',args:['-p','80'],env:{MODE:'live'}}]}",
),
];
for (filename, source) in sources {
let path = dir.path().join(filename);
std::fs::write(&path, source).unwrap();
let target = path.to_str().unwrap();
let format = FlockFormat::from_path(&path).expect("a recognised extension");
let first = Flockfile::parse(source, format).expect("the validating pass accepts it");
let apps = resolve_target(target, None, b"", false)
.unwrap_or_else(|err| panic!("{filename} was refused after the first pass: {err}"));
assert_eq!(
apps.len(),
first.apps.len(),
"{filename}: both passes see the same apps"
);
assert_eq!(apps[0].name, "web", "{filename}");
assert_eq!(
apps[0].args,
vec!["-p".to_string(), "80".to_string()],
"{filename}"
);
assert_eq!(
apps[0].env.get("MODE").map(String::as_str),
Some("live"),
"{filename}: a nested table survives both passes"
);
}
}
#[tokio::test]
async fn a_discovered_flockfile_applies_to_an_app_the_flock_already_has() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let flockfile = dir.path().join("Flockfile.toml");
std::fs::write(
&flockfile,
"[[app]]\nname = \"zam\"\nscript = \"./srv\"\nmax_restarts = 99\n",
)
.unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) =
fake_client_answering(&path, a_daemon_for(a_clustered_flock(&[0, 1, 2]), &[])).await;
let args = StartArgs {
targets: Vec::new(),
name: None,
fold: None,
cwd: None,
interpreter: None,
flockfile: false,
no_log_timestamps: false,
reset: None,
};
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
start(
&client,
&mut streams,
&args,
Some(flockfile.as_path()),
&BTreeMap::new(),
)
.await
};
assert_eq!(code, ExitCode::Success);
assert_eq!(
applies(&mut envelopes),
vec![vec!["zam".to_string()]],
"a discovered load applies exactly as an explicit one does"
);
}
#[tokio::test]
async fn start_by_name_sends_no_apply_config_even_with_a_flockfile_present() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let flockfile = dir.path().join("Flockfile.toml");
std::fs::write(
&flockfile,
"[[app]]\nname = \"zam\"\nscript = \"./from-the-repository\"\n",
)
.unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) =
fake_client_answering(&path, a_daemon_for(a_clustered_flock(&[0, 1, 2]), &[])).await;
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
start(
&client,
&mut streams,
&start_args("zam"),
Some(&flockfile),
&BTreeMap::new(),
)
.await
};
assert_eq!(code, ExitCode::Success, "the name resolved to the flock");
assert!(
applies(&mut envelopes).is_empty(),
"a name target applies nothing"
);
}
#[tokio::test]
async fn a_start_never_respawns_a_row_that_was_already_up() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) =
fake_client_answering(&path, a_daemon_for(a_clustered_flock(&[0]), &[])).await;
let (code, printed, said) = start_against(&client, "all").await;
assert_eq!(code, ExitCode::Success);
let _ = printed;
assert_eq!(
respawns(&mut envelopes),
vec![SelectorSpec::Id(1), SelectorSpec::Id(2)],
"the two that were down, and not the one that was up"
);
assert!(said.contains("already"), "the live one is reported: {said}");
}
#[tokio::test]
async fn the_already_up_notice_quotes_the_operators_own_token() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, _envelopes) =
fake_client_answering(&path, a_daemon_for(a_clustered_flock(&[0]), &[])).await;
let (code, _, said) = start_against(&client, "0").await;
assert_eq!(code, ExitCode::Success);
assert!(
said.contains("`shep restart 0`"),
"the one row the operator named: {said}"
);
assert!(
!said.contains("`shep restart zam`"),
"never the name, which would replace every instance of it: {said}"
);
}
#[tokio::test]
async fn a_row_that_cannot_spawn_does_not_abandon_the_rows_after_it() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) =
fake_client_answering(&path, a_daemon_for(a_clustered_flock(&[]), &[0])).await;
let (code, printed, said) = start_against(&client, "all").await;
assert_eq!(code, ExitCode::SpawnFailed, "the first failure is the code");
assert_eq!(
respawns(&mut envelopes),
vec![
SelectorSpec::Id(0),
SelectorSpec::Id(1),
SelectorSpec::Id(2)
],
"every row is attempted, not just the ones before the failure"
);
assert!(
said.contains("id 0"),
"and the failure names the row, not just the app: {said}"
);
assert!(
printed.is_empty(),
"a failed verb leaves stdout empty even though rows 1 and 2 came \
up, the rule `cli_e2e`'s assert_json_error pins crate-wide: \
{printed}"
);
}
#[tokio::test]
async fn a_flockfile_naming_a_clustered_app_resumes_every_instance() {
use shep_client::testing::fake_client_answering;
let dir = tempfile::tempdir().unwrap();
let flockfile = dir.path().join("flock.toml");
std::fs::write(
&flockfile,
"[[app]]\nname = \"zam\"\nscript = \"./zam\"\ninstances = 3\n",
)
.unwrap();
let socket = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) =
fake_client_answering(&socket, a_daemon_for(a_clustered_flock(&[]), &[])).await;
let (code, _, _) = start_against(&client, flockfile.to_str().unwrap()).await;
assert_eq!(code, ExitCode::Success);
assert_eq!(
respawns(&mut envelopes),
vec![
SelectorSpec::Id(0),
SelectorSpec::Id(1),
SelectorSpec::Id(2)
],
"every row the name has, not the first one"
);
}
#[tokio::test]
async fn a_target_that_matches_nothing_is_a_usage_error_naming_what_was_tried() {
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_capturing_envelopes(&path).await;
let mut out = Vec::new();
let mut err = Vec::new();
let code = {
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
start(
&client,
&mut streams,
&start_args("./does-not-exist"),
None,
&BTreeMap::new(),
)
.await
};
assert_eq!(code, ExitCode::Usage);
assert!(
envelopes.try_recv().is_err(),
"a target that can only be a path costs no round trip, and an \
unresolvable one must never become a Start"
);
assert!(String::from_utf8(err).unwrap().contains("./does-not-exist"));
}
#[tokio::test]
async fn a_selector_reaches_the_wire_in_its_compiled_form() {
let dir = tempfile::tempdir().unwrap();
let path = shep_client::testing::control_address(dir.path());
let (client, mut envelopes) = fake_client_capturing_envelopes(&path).await;
let paths = ShepPaths::resolve(&|_| None, dir.path());
#[derive(Clone, Copy, Debug)]
enum Verb {
Stop,
Restart,
Reload,
Delete,
}
for verb in [Verb::Stop, Verb::Restart, Verb::Reload, Verb::Delete] {
for (input, expected) in [
("all", SelectorSpec::All),
("7", SelectorSpec::Id(7)),
("web", SelectorSpec::Name("web".into())),
("/^web-/", SelectorSpec::Regex("^web-".into())),
("fold:api", SelectorSpec::Fold("api".into())),
] {
let mut out = Vec::new();
let mut err = Vec::new();
let mut streams = Streams {
out: &mut out,
err: &mut err,
style: crate::style::Presentation::BARE,
fmt: Format::Table,
};
let args = SelectorArgs {
selectors: vec![input.into()],
};
let expected_body = match verb {
Verb::Stop => Request::Stop { selector: expected },
Verb::Restart => Request::Restart { selector: expected },
Verb::Reload => Request::Reload { selector: expected },
Verb::Delete => Request::Delete { selector: expected },
};
let _ = match verb {
Verb::Stop => stop(&client, &mut streams, &args).await,
Verb::Restart => restart(&client, &mut streams, &paths, &args).await,
Verb::Reload => reload(&client, &mut streams, &args).await,
Verb::Delete => delete(&client, &mut streams, &args).await,
};
let sent = envelopes.recv().await.unwrap();
assert_eq!(sent.body, expected_body, "verb={verb:?} input={input}");
let expected_deadline = match verb {
Verb::Stop | Verb::Delete => DEFAULT_DEADLINE,
Verb::Restart => START_DEADLINE,
Verb::Reload => RELOAD_DEADLINE,
};
assert_eq!(
sent.deadline_ms,
Some(u64::try_from(expected_deadline.as_millis()).unwrap()),
"verb={verb:?} input={input} must ask for its own budget"
);
}
}
}