standout-test 9.0.0

In-process test harness for applications built with the standout CLI framework
use crate::output_mode_flag;
use standout_render::OutputMode;
use std::fmt;
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct SnapshotCase {
    subject: String,
    axes: Vec<(String, String)>,
}
impl SnapshotCase {
    pub fn new(subject: impl Into<String>) -> Self {
        Self {
            subject: subject.into(),
            axes: Vec::new(),
        }
    }
    pub fn axis(mut self, name: impl Into<String>, value: impl Into<String>) -> Self {
        self.axes.push((name.into(), value.into()));
        self
    }
    pub fn output_mode(self, mode: OutputMode) -> Self {
        self.axis("mode", output_mode_flag(mode))
    }
    pub fn tty(self, is_tty: bool) -> Self {
        self.axis("tty", if is_tty { "on" } else { "off" })
    }
    pub fn color(self, color: bool) -> Self {
        self.axis("color", if color { "on" } else { "off" })
    }
    pub fn theme(self, name: impl Into<String>) -> Self {
        self.axis("theme", name)
    }
    pub fn entry_point(self, entry: impl Into<String>) -> Self {
        self.axis("entry", entry)
    }
    pub fn key(&self) -> String {
        let mut key = slug(&self.subject);
        for (name, value) in &self.axes {
            key.push_str("__");
            key.push_str(&slug(name));
            key.push('_');
            key.push_str(&slug(value));
        }
        key
    }
}
impl fmt::Display for SnapshotCase {
    fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
        f.write_str(&self.key())
    }
}
const DIGEST_TAG: &str = "--";
fn slug(text: &str) -> String {
    let readable = squash(text);
    if !readable.is_empty() && readable == text {
        return readable;
    }
    let base = if readable.is_empty() {
        "none"
    } else {
        &readable
    };
    format!("{}{}{:08x}", base, DIGEST_TAG, digest(text))
}
fn squash(text: &str) -> String {
    let mut out = String::with_capacity(text.len());
    for ch in text.chars() {
        if ch.is_ascii_alphanumeric() {
            out.push(ch.to_ascii_lowercase());
        } else if !out.ends_with('-') {
            out.push('-');
        }
    }
    out.trim_matches('-').to_string()
}
fn digest(text: &str) -> u32 {
    const OFFSET_BASIS: u32 = 0x811c_9dc5;
    const PRIME: u32 = 0x0100_0193;
    let mut hash = OFFSET_BASIS;
    for byte in text.as_bytes() {
        hash ^= u32::from(*byte);
        hash = hash.wrapping_mul(PRIME);
    }
    hash
}
#[macro_export]
macro_rules! assert_page_snapshot {
    ($result:expr, $case:expr $(,)?) => {{
        let case = $case;
        ::insta::assert_snapshot!(case.key(), $result.stdout_plain());
    }};
}
#[cfg(test)]
mod tests {
    use super::*;
    #[test]
    fn key_is_the_subject_when_no_axis_is_recorded() {
        assert_eq!(SnapshotCase::new("help").key(), "help");
    }
    #[test]
    fn key_appends_axes_in_declaration_order() {
        let case = SnapshotCase::new("help")
            .output_mode(OutputMode::Text)
            .tty(false)
            .theme("default");
        assert_eq!(case.key(), "help__mode_text__tty_off__theme_default");
    }
    #[test]
    fn key_slugifies_argv_shaped_values() {
        let case = SnapshotCase::new("Help Page")
            .entry_point("--help")
            .output_mode(OutputMode::TermDebug);
        assert_eq!(
            case.key(),
            "help-page--f5f24815__entry_help--7fb28c5c__mode_term-debug"
        );
    }
    #[test]
    fn cases_differing_in_one_axis_differ_in_one_segment() {
        let dark = SnapshotCase::new("help").theme("dark").key();
        let light = SnapshotCase::new("help").theme("light").key();
        assert_ne!(dark, light);
        assert_eq!(dark.replace("dark", "light"), light);
    }
    #[test]
    fn values_that_squash_alike_still_key_apart() {
        let pairs = [
            ("--help", "help"),
            ("dark mode", "dark-mode"),
            ("", "none"),
            ("-h", "h"),
            ("--help", "help--7fb28c5c"),
            ("--help", "help-7fb28c5c"),
        ];
        for (left, right) in pairs {
            let left_key = SnapshotCase::new("help").entry_point(left).key();
            let right_key = SnapshotCase::new("help").entry_point(right).key();
            assert_ne!(
                left_key, right_key,
                "{:?} and {:?} must not share a snapshot name",
                left, right
            );
        }
    }
    #[test]
    fn the_axis_name_value_boundary_is_unambiguous() {
        let split_in_the_name = SnapshotCase::new("help").axis("group-1", "test").key();
        let split_in_the_value = SnapshotCase::new("help").axis("group", "1-test").key();
        assert_eq!(split_in_the_name, "help__group-1_test");
        assert_eq!(split_in_the_value, "help__group_1-test");
        assert_ne!(split_in_the_name, split_in_the_value);
    }
    #[test]
    fn a_slug_spells_the_keys_punctuation_only_in_its_reserved_tag() {
        for text in ["--help", "dark mode", "", "a__b", "x--y", "Group_1", "help"] {
            let slugged = slug(text);
            let squashed = squash(text);
            let expected_readable = if squashed.is_empty() {
                "none"
            } else {
                &squashed
            };
            let readable = match slugged.split_once(DIGEST_TAG) {
                Some((readable, tag)) => {
                    assert_eq!(
                        tag,
                        format!("{:08x}", digest(text)),
                        "{text:?} → {slugged:?}"
                    );
                    readable
                }
                None => slugged.as_str(),
            };
            assert_eq!(readable, expected_readable, "{text:?} → {slugged:?}");
            assert!(!slugged.contains('_'), "{text:?} → {slugged:?}");
            assert!(!readable.contains(DIGEST_TAG), "{text:?} → {slugged:?}");
        }
    }
    #[test]
    fn a_canonical_value_keys_without_a_digest() {
        let case = SnapshotCase::new("help")
            .output_mode(OutputMode::Text)
            .tty(true)
            .theme("solarized-dark");
        assert_eq!(case.key(), "help__mode_text__tty_on__theme_solarized-dark");
    }
    #[test]
    fn an_axis_value_that_slugs_away_keeps_the_key_unambiguous() {
        assert_eq!(
            SnapshotCase::new("help").theme("").key(),
            "help__theme_none--811c9dc5"
        );
    }
    #[test]
    fn the_digest_is_a_fixed_value_not_a_toolchain_detail() {
        assert_eq!(digest(""), 0x811c_9dc5);
        assert_eq!(format!("{:08x}", digest("--help")), "7fb28c5c");
    }
    #[test]
    fn display_renders_the_key() {
        let case = SnapshotCase::new("help").tty(true);
        assert_eq!(case.to_string(), case.key());
    }
}