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 {
text.chars()
.filter(|c| !c.is_control())
.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
}
pub async fn repaint<D: Daemon>(
daemon: &D,
sheep: &str,
painted: &str,
state: &State,
) -> Result<(), Error> {
if text(state) == painted {
return Ok(());
}
publish(daemon, sheep, state).await
}
#[cfg(test)]
mod tests {
use super::*;
fn target(watch: Watch, deployed: Option<&str>) -> State {
State {
deployed: deployed.map(str::to_owned),
watch,
..crate::fixtures::state()
}
}
#[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 set_smit(&self, sheep: &str, text: &str) -> Result<(), Error> {
self.0
.borrow_mut()
.push((sheep.to_owned(), text.to_owned()));
Ok(())
}
crate::fixtures::daemon_methods!(unimplemented;
dog_config, list_flock, describe, start, delete, reload, restart, save_roll,
);
}
#[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())]
);
}
#[tokio::test]
async fn a_repaint_with_nothing_new_sends_nothing() {
let daemon = Recording(std::cell::RefCell::new(Vec::new()));
let state = target(Watch::Auto, Some("a1b2c3d4e5f6"));
repaint(&daemon, "web", &text(&state), &state)
.await
.expect("repaints");
assert_eq!(daemon.0.into_inner(), vec![]);
}
#[tokio::test]
async fn a_moved_sha_is_repainted_with_the_new_text() {
let daemon = Recording(std::cell::RefCell::new(Vec::new()));
let before = text(&target(Watch::Auto, Some("a1b2c3d4e5f6")));
let now = target(Watch::Auto, Some("f6e5d4c3b2a1"));
repaint(&daemon, "web", &before, &now)
.await
.expect("repaints");
assert_eq!(
daemon.0.into_inner(),
vec![("web".to_owned(), "▲ main@f6e5d4".to_owned())]
);
}
}