use std::path::{Path, PathBuf};
use shep_core::paths::ShepPaths;
use super::adopt::adopt;
use super::enable::enable;
use super::test_support::streams;
use crate::cli::AdoptArgs;
use crate::exit::ExitCode;
fn write_dog(dir: &Path, manifest: &str, extra: &str) -> (PathBuf, PathBuf) {
let binary = dir.join("shep-otel");
let count = dir.join("manifest-runs");
let script = format!(
"#!/bin/sh\nif [ \"$1\" = --shep-manifest ]; then printf x >> '{count}'; cat <<'EOF'\n\
{manifest}\nEOF\nexit 0\nfi\n{extra}\nexit 0\n",
count = count.display(),
);
std::fs::write(&binary, script).unwrap();
let mut mode = std::fs::metadata(&binary).unwrap().permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut mode, 0o755);
std::fs::set_permissions(&binary, mode).unwrap();
(binary, count)
}
fn runs(count: &Path) -> usize {
std::fs::read_to_string(count).map_or(0, |text| text.len())
}
async fn adopt_otel(paths: &ShepPaths, binary: &Path) -> (ExitCode, String) {
let args = AdoptArgs {
yes: true,
name: Some("otel".to_string()),
path: binary.to_path_buf(),
};
let mut out = Vec::new();
let mut err = Vec::new();
let code = adopt(&mut streams(&mut out, &mut err), paths, &args).await;
(code, String::from_utf8(err).unwrap())
}
const MANIFEST: &str = r#"{"manifest":1,"name":"otel","version":"0.2.0","protocol":12}"#;
#[tokio::test]
async fn adopting_a_manifest_dog_reads_it_once_and_adopting_it_again_reads_the_cache() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, count) = write_dog(dir.path(), MANIFEST, "");
let (first, _) = adopt_otel(&paths, &binary).await;
assert_eq!(first, ExitCode::Success);
assert_eq!(runs(&count), 1, "the first adopt asks the dog");
let (second, _) = adopt_otel(&paths, &binary).await;
assert_eq!(second, ExitCode::Success);
assert_eq!(runs(&count), 1, "the same binary is read from the cache");
}
#[tokio::test]
async fn rebuilding_the_binary_probes_it_again() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, count) = write_dog(dir.path(), MANIFEST, "");
adopt_otel(&paths, &binary).await;
let rebuilt = MANIFEST.replace("0.2.0", "0.3.0");
write_dog(dir.path(), &rebuilt, "");
let (code, _) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert_eq!(runs(&count), 2, "a changed binary is asked again");
}
#[tokio::test]
async fn a_dog_with_no_usable_manifest_adopts_through_the_old_flags() {
let version =
"if [ \"$1\" = --version ]; then printf 'shep-otel 0.1.0\\nshep-channel: true\\n'; fi";
for manifest in ["", "not json at all", r#"{"name":"otel"}"#] {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, count) = write_dog(dir.path(), manifest, version);
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success, "{manifest:?}: {notices}");
assert!(notices.contains("reports version 0.1.0"), "{notices}");
let written = std::fs::read_to_string(&paths.daemon_config).unwrap();
assert!(written.contains("channel_dogs"), "{manifest:?}: {written}");
adopt_otel(&paths, &binary).await;
assert_eq!(
runs(&count),
2,
"an answer that is no manifest is not cached"
);
}
}
#[tokio::test]
async fn a_dog_that_fails_the_manifest_flag_adopts_through_the_old_flags() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let binary = dir.path().join("shep-otel");
let script = "#!/bin/sh\ncase \"$1\" in\n--shep-manifest) echo '{\"manifest\":1,\"name\":\"otel\",\"version\":\"9\",\"protocol\":12}'; exit 3;;\n--version) printf 'shep-otel 0.1.0\\n';;\nesac\nexit 0\n";
std::fs::write(&binary, script).unwrap();
let mut mode = std::fs::metadata(&binary).unwrap().permissions();
std::os::unix::fs::PermissionsExt::set_mode(&mut mode, 0o755);
std::fs::set_permissions(&binary, mode).unwrap();
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(notices.contains("reports version 0.1.0"), "{notices}");
}
#[tokio::test]
async fn the_manifest_supplies_the_version_and_the_channel_ask() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let manifest = r#"{"manifest":1,"name":"otel","version":"0.2.0","protocol":12,"channel":true}"#;
let (binary, _) = write_dog(dir.path(), manifest, "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(
notices.contains("reports version 0.2.0, shep protocol 12"),
"{notices}"
);
let written = std::fs::read_to_string(&paths.daemon_config).unwrap();
assert!(written.contains("channel_dogs"), "{written}");
}
#[tokio::test]
async fn a_manifest_protocol_below_the_floor_refuses_the_adopt() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let manifest = r#"{"manifest":1,"name":"otel","version":"0.2.0","protocol":1}"#;
let (binary, _) = write_dog(dir.path(), manifest, "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::InvalidConfig);
assert!(notices.contains("built for shep protocol 1"), "{notices}");
assert!(!paths.daemon_config.exists());
}
fn manifest_depending_on(depends: &str) -> String {
format!(r#"{{"manifest":1,"name":"otel","version":"0.2.0","protocol":12,"depends":{depends}}}"#)
}
const NEEDS_LOG_ROTATE: &str = r#"{"dogs":[{"name":"log-rotate","version":">=0.1"}]}"#;
#[tokio::test]
async fn adopt_warns_of_a_dependency_that_is_not_adopted_and_still_adopts() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, _) = write_dog(dir.path(), &manifest_depending_on(NEEDS_LOG_ROTATE), "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(
notices.contains("`otel` depends on `log-rotate`, which is not adopted"),
"{notices}"
);
assert!(
std::fs::read_to_string(&paths.daemon_config)
.unwrap()
.contains("otel")
);
}
#[tokio::test]
async fn adopt_stays_quiet_about_a_dependency_that_is_adopted_or_built_in() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (rotate, _) = write_dog(dir.path(), "", "");
let rotate = {
let renamed = dir.path().join("shep-log-rotate");
std::fs::rename(rotate, &renamed).unwrap();
renamed
};
let args = AdoptArgs {
yes: true,
name: Some("log-rotate".to_string()),
path: rotate,
};
let mut out = Vec::new();
let mut err = Vec::new();
adopt(&mut streams(&mut out, &mut err), &paths, &args).await;
let depends =
r#"{"dogs":[{"name":"log-rotate","version":">=0.1"},{"name":"metrics","version":"*"}]}"#;
let (binary, _) = write_dog(dir.path(), &manifest_depending_on(depends), "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(!notices.contains("depends on"), "{notices}");
}
#[tokio::test]
async fn adopt_warns_of_a_dog_that_needs_a_newer_shep_and_still_adopts() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, _) = write_dog(
dir.path(),
&manifest_depending_on(r#"{"shep":"999.0.0"}"#),
"",
);
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(
notices.contains(&format!(
"`otel` needs shep 999.0.0 or newer, and this is shep {}",
env!("CARGO_PKG_VERSION")
)),
"{notices}"
);
assert!(
std::fs::read_to_string(&paths.daemon_config)
.unwrap()
.contains("otel")
);
}
#[tokio::test]
async fn adopt_stays_quiet_when_this_shep_is_new_enough() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let (binary, _) = write_dog(
dir.path(),
&manifest_depending_on(r#"{"shep":"0.1.0"}"#),
"",
);
let (_, notices) = adopt_otel(&paths, &binary).await;
assert!(!notices.contains("needs shep"), "{notices}");
}
fn manifest_with_verb(description: &str, examples: &[&str]) -> String {
let examples = examples
.iter()
.map(|example| format!("\"{example}\""))
.collect::<Vec<_>>()
.join(",");
let verb = format!(r#"{{"description":"{description}","examples":[{examples}]}}"#);
format!(r#"{{"manifest":1,"name":"otel","version":"0.2.0","protocol":12,"verb":{verb}}}"#)
}
#[tokio::test]
async fn a_long_description_is_noticed_and_still_adopted() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let manifest = manifest_with_verb(
"export metrics to a collector over the network every interval",
&[
"shep otel status",
"shep otel status --full",
"shep otel flush",
"shep otel once",
],
);
let (binary, _) = write_dog(dir.path(), &manifest, "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(
notices.contains("reports version 0.2.0, shep protocol 12"),
"the manifest was not read: {notices}"
);
assert!(
notices.contains("more in its manifest `verb` than shep's help holds"),
"{notices}"
);
assert!(notices.contains("61 columns"), "{notices}");
assert!(notices.contains("4 examples"), "{notices}");
assert!(
std::fs::read_to_string(&paths.daemon_config)
.unwrap()
.contains("otel")
);
}
#[tokio::test]
async fn a_verb_inside_the_help_budget_is_said_nothing_about() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let manifest = manifest_with_verb(
"export metrics to a collector",
&["shep otel status", "shep otel status --full"],
);
let (binary, _) = write_dog(dir.path(), &manifest, "");
let (code, notices) = adopt_otel(&paths, &binary).await;
assert_eq!(code, ExitCode::Success);
assert!(
notices.contains("reports version 0.2.0, shep protocol 12"),
"the manifest was not read: {notices}"
);
assert!(!notices.contains("manifest `verb`"), "{notices}");
}
#[tokio::test]
async fn enable_of_an_adopted_dog_gives_both_warnings_and_still_enables() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let depends = r#"{"dogs":[{"name":"log-rotate","version":">=0.1"}],"shep":"999.0.0"}"#;
let (binary, _) = write_dog(dir.path(), &manifest_depending_on(depends), "");
adopt_otel(&paths, &binary).await;
let mut out = Vec::new();
let mut err = Vec::new();
let code = enable(&mut streams(&mut out, &mut err), &paths, "otel", true).await;
assert_eq!(code, ExitCode::Success);
let notices = String::from_utf8(err).unwrap();
assert!(notices.contains("depends on `log-rotate`"), "{notices}");
assert!(notices.contains("needs shep 999.0.0 or newer"), "{notices}");
}
#[tokio::test]
async fn enable_of_a_built_in_dog_says_nothing_of_dependencies() {
let dir = tempfile::tempdir().unwrap();
let paths = ShepPaths::resolve(&|_| None, dir.path());
let mut out = Vec::new();
let mut err = Vec::new();
let code = enable(&mut streams(&mut out, &mut err), &paths, "metrics", true).await;
assert_eq!(code, ExitCode::Success);
let notices = String::from_utf8(err).unwrap();
assert!(!notices.contains("depends on"), "{notices}");
assert!(!notices.contains("needs shep"), "{notices}");
}