use shep_client::shep_core::protocol::Smit;
use crate::daemon::Daemon;
use crate::error::Error;
use crate::state::{State, Watch};
const ABBREVIATED: usize = 6;
#[must_use]
pub fn text(state: &State) -> String {
let mark = match state.watch {
Watch::Auto => '▲',
Watch::Manual => '⏸',
};
let sha = state.deployed.as_deref().map_or("none", |sha| {
sha.get(..ABBREVIATED).unwrap_or(sha)
});
format!("{mark} {}@{sha}", state.branch)
}
fn fit(text: String) -> String {
if text.chars().count() <= Smit::MAX_CHARS {
return text;
}
text.chars().take(Smit::MAX_CHARS).collect()
}
pub async fn publish<D: Daemon>(daemon: &D, sheep: &str, state: &State) -> Result<(), Error> {
daemon.set_smit(sheep, &fit(text(state))).await
}
#[cfg(test)]
mod tests {
use super::*;
fn target(watch: Watch, deployed: Option<&str>) -> State {
State {
remote: "https://example.com/x".to_owned(),
branch: "main".to_owned(),
deployed: deployed.map(str::to_owned),
failed: None,
verify: crate::state::Verify::default(),
watch,
origin_cwd: None,
origin_script: None,
checkout: std::path::PathBuf::from("/srv/x"),
}
}
#[test]
fn watched_and_manual_are_told_apart_at_a_glance() {
assert_eq!(
text(&target(Watch::Auto, Some("a1b2c3d4e5f6"))),
"▲ main@a1b2c3"
);
assert_eq!(
text(&target(Watch::Manual, Some("a1b2c3d4e5f6"))),
"⏸ main@a1b2c3"
);
}
#[test]
fn the_branch_is_named_not_assumed() {
let mut state = target(Watch::Auto, Some("a1b2c3d4e5f6"));
state.branch = "stable".to_owned();
assert_eq!(text(&state), "▲ stable@a1b2c3");
}
#[test]
fn a_target_with_no_deploy_yet_says_so() {
assert_eq!(text(&target(Watch::Auto, None)), "▲ main@none");
}
#[test]
fn a_short_sha_degrades_rather_than_panicking() {
assert_eq!(text(&target(Watch::Auto, Some("abc"))), "▲ main@abc");
}
#[test]
fn a_long_branch_name_is_shortened_to_fit() {
let mut state = target(Watch::Auto, Some("a1b2c3d4e5f6"));
state.branch = "x".repeat(100);
let shortened = fit(text(&state));
assert_eq!(shortened.chars().count(), Smit::MAX_CHARS);
assert!(shortened.parse::<Smit>().is_ok(), "{shortened}");
}
#[test]
fn fit_agrees_with_smit_about_where_the_limit_is() {
let at = "x".repeat(Smit::MAX_CHARS);
let under = "x".repeat(Smit::MAX_CHARS - 1);
let over = "x".repeat(Smit::MAX_CHARS + 1);
assert!(under.parse::<Smit>().is_ok(), "one under must be accepted");
assert!(at.parse::<Smit>().is_ok(), "exactly at must be accepted");
assert!(over.parse::<Smit>().is_err(), "one over must be refused");
assert_eq!(fit(under.clone()), under, "under the limit is untouched");
assert_eq!(fit(at.clone()), at, "at the limit is untouched");
assert_eq!(
fit(over).chars().count(),
Smit::MAX_CHARS,
"one over is shortened to exactly the limit"
);
}
struct Recording(std::cell::RefCell<Vec<(String, String)>>);
impl Daemon for Recording {
async fn dog_config(&self, _name: &str) -> Result<String, Error> {
unimplemented!()
}
async fn list_flock(
&self,
) -> Result<Vec<shep_client::shep_core::protocol::ProcessInfo>, Error> {
unimplemented!()
}
async fn describe(
&self,
_sheep: &str,
) -> Result<Vec<shep_client::shep_core::protocol::ProcessInfo>, Error> {
unimplemented!()
}
async fn start(
&self,
_apps: Vec<shep_client::shep_core::config::AppConfig>,
) -> Result<(), Error> {
unimplemented!()
}
async fn delete(&self, _id: u32) -> Result<(), Error> {
unimplemented!()
}
async fn reload(&self, _sheep: &str) -> Result<(), Error> {
unimplemented!()
}
async fn restart(&self, _sheep: &str) -> Result<(), Error> {
unimplemented!()
}
async fn save_roll(&self) -> Result<std::path::PathBuf, Error> {
unimplemented!()
}
async fn set_smit(&self, sheep: &str, text: &str) -> Result<(), Error> {
self.0
.borrow_mut()
.push((sheep.to_owned(), text.to_owned()));
Ok(())
}
}
#[tokio::test]
async fn publish_sends_the_named_sheeps_own_text() {
let daemon = Recording(std::cell::RefCell::new(Vec::new()));
let state = target(Watch::Auto, Some("a1b2c3d4e5f6"));
publish(&daemon, "web", &state).await.expect("publishes");
assert_eq!(
daemon.0.into_inner(),
vec![("web".to_owned(), "▲ main@a1b2c3".to_owned())]
);
}
}