evfmt 0.2.0

Emoji Variation Formatter
Documentation
use super::*;
use std::io;

fn operation(id: OperationId, value: &str) -> OrderedOperation {
    OrderedOperation {
        id,
        value: value.to_owned(),
    }
}

#[allow(clippy::panic)]
fn assert_parse_error<T>(result: Result<T, CliParseError>, expected: &str) {
    let Err(error) = result else {
        panic!("parse should fail");
    };
    assert!(
        error.to_string().contains(expected),
        "expected error containing {expected:?}, got {error}"
    );
}

#[test]
fn exit_status_codes_match_cli_contract() {
    assert_eq!(ExitStatus::Success.code(), 0);
    assert_eq!(ExitStatus::CheckFoundChanges.code(), 1);
    assert_eq!(ExitStatus::UsageOrIoError.code(), 2);
}

#[test]
fn ignore_settings_apply_label_updates() {
    let mut settings = IgnoreSettings::from_labels(&[IgnoreLabel::Git]);

    assert!(settings.git);
    assert!(!settings.evfmt);
    assert!(!settings.hidden);

    settings.enable(&[IgnoreLabel::Evfmt, IgnoreLabel::Hidden]);
    assert!(settings.git);
    assert!(settings.evfmt);
    assert!(settings.hidden);

    settings.disable(&[IgnoreLabel::Git, IgnoreLabel::Hidden]);
    assert!(!settings.git);
    assert!(settings.evfmt);
    assert!(!settings.hidden);
}

#[test]
#[allow(clippy::unwrap_used)]
fn variation_set_lists_parse_shortcuts_and_comma_lists() {
    let all = parse_variation_set_list(UpdateKind::Add, "all").unwrap();
    assert!(all.contains('#'));
    assert!(all.contains_keycap('#'));
    assert!(all.contains('\u{00A9}'));

    assert_eq!(
        parse_variation_set_list(UpdateKind::Set, "none").unwrap(),
        VariationSet::none()
    );

    let set =
        parse_variation_set_list(UpdateKind::Add, " ascii, u(00A9), k(0023), *\u{FE0F} ").unwrap();
    assert!(set.contains('#'));
    assert!(set.contains('*'));
    assert!(set.contains('\u{00A9}'));
    assert!(set.contains_keycap('#'));
    assert!(!set.contains('\u{2728}'));

    let text_defaults = parse_variation_set_list(UpdateKind::Add, "text-defaults").unwrap();
    assert!(text_defaults.contains('\u{00A9}'));
    assert!(!text_defaults.contains('\u{2728}'));

    let emoji_defaults = parse_variation_set_list(UpdateKind::Add, "emoji-defaults").unwrap();
    assert!(emoji_defaults.contains('\u{2728}'));
    assert!(!emoji_defaults.contains('\u{00A9}'));

    let arrows = parse_variation_set_list(UpdateKind::Add, "arrows").unwrap();
    assert!(arrows.contains('\u{2194}'));
    assert!(!arrows.contains('\u{2660}'));

    let keycap_chars = parse_variation_set_list(UpdateKind::Add, "keycap-chars").unwrap();
    assert!(!keycap_chars.contains('#'));
    assert!(keycap_chars.contains_keycap('#'));

    let keycap_emojis = parse_variation_set_list(UpdateKind::Add, "keycap-emojis").unwrap();
    assert!(keycap_emojis.contains_keycap('#'));
    assert!(!keycap_emojis.contains_keycap('\u{00A9}'));
}

#[test]
fn variation_set_lists_reject_invalid_shortcut_usage() {
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Add, "none"),
        "`none` is only allowed",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "all,ascii"),
        "`all` and `none` must appear alone",
    );
    assert_parse_error(parse_variation_set_list(UpdateKind::Set, ""), "empty list");
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "ascii,"),
        "empty list item",
    );
}

#[test]
fn variation_set_items_report_specific_errors() {
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "arowws"),
        "unknown variation set preset `arowws`; did you mean `arrows`?",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "card_suit"),
        "did you mean `card-suits`?",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "u(110000)"),
        "invalid code point item",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "u(00ag)"),
        "invalid code point item",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "u(00A9"),
        "invalid code point item",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "u(0041)"),
        "not eligible for emoji variation selectors",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "A"),
        "not eligible for emoji variation selectors",
    );
    assert_parse_error(
        parse_variation_set_list(UpdateKind::Set, "\u{00A9}#"),
        "invalid variation set item",
    );
}

#[test]
#[allow(clippy::unwrap_used)]
fn ignore_lists_parse_shortcuts_and_labels() {
    assert_eq!(
        parse_ignore_list(UpdateKind::Set, "none").unwrap(),
        Vec::<IgnoreLabel>::new()
    );
    assert_eq!(
        parse_ignore_list(UpdateKind::Remove, "all").unwrap(),
        [IgnoreLabel::Git, IgnoreLabel::Evfmt, IgnoreLabel::Hidden]
    );
    assert_eq!(
        parse_ignore_list(UpdateKind::Add, " git, hidden ").unwrap(),
        [IgnoreLabel::Git, IgnoreLabel::Hidden]
    );
}

#[test]
fn ignore_lists_reject_invalid_labels() {
    assert_parse_error(
        parse_ignore_list(UpdateKind::Add, "none"),
        "`none` is only allowed with `--set-ignore`",
    );
    assert_parse_error(
        parse_ignore_list(UpdateKind::Set, "hdden"),
        "did you mean `hidden`?",
    );
    assert_parse_error(parse_ignore_list(UpdateKind::Set, "  "), "empty list");
    assert_parse_error(
        parse_ignore_list(UpdateKind::Set, "git,"),
        "empty list item",
    );
}

#[test]
#[allow(clippy::unwrap_used)]
fn runtime_settings_start_with_documented_policy_defaults() {
    let settings = build_runtime_settings(&[]).unwrap();

    assert_eq!(
        formatter::format_text("\u{2728}\u{FE0F}", &settings.policy),
        FormatResult::Changed("\u{2728}".to_owned())
    );
    assert_eq!(
        formatter::format_text("\u{00A9}", &settings.policy),
        FormatResult::Changed("\u{00A9}\u{FE0F}".to_owned())
    );
}

#[test]
#[allow(clippy::unwrap_used)]
fn policy_operations_apply_to_their_target_in_order() {
    let settings = build_runtime_settings(&[
        operation(OperationId::SetPreferBare, "none"),
        operation(OperationId::AddPreferBare, "rights-marks"),
        operation(OperationId::RemovePreferBare, "u(00AE)"),
        operation(OperationId::SetBareAsText, "all"),
        operation(OperationId::RemoveBareAsText, "ascii"),
        operation(OperationId::AddBareAsText, "card-suits"),
        operation(OperationId::SetIgnore, "none"),
    ])
    .unwrap();

    assert_eq!(
        formatter::format_text("\u{00A9}", &settings.policy),
        FormatResult::Unchanged
    );
    assert_eq!(
        formatter::format_text("\u{00AE}", &settings.policy),
        FormatResult::Changed("\u{00AE}\u{FE0E}".to_owned())
    );
    assert_eq!(
        formatter::format_text("#", &settings.policy),
        FormatResult::Changed("#\u{FE0F}".to_owned())
    );
    assert_eq!(
        formatter::format_text("\u{2660}", &settings.policy),
        FormatResult::Changed("\u{2660}\u{FE0E}".to_owned())
    );
}

#[test]
#[allow(clippy::unwrap_used)]
fn ignore_operations_apply_left_to_right() {
    let settings = build_runtime_settings(&[
        operation(OperationId::SetIgnore, "none"),
        operation(OperationId::AddIgnore, "git,hidden"),
        operation(OperationId::RemoveIgnore, "hidden"),
        operation(OperationId::AddPreferBare, "rights-marks"),
    ])
    .unwrap();

    assert!(settings.ignore.git);
    assert!(!settings.ignore.evfmt);
    assert!(!settings.ignore.hidden);
}

#[test]
fn update_kind_helpers_classify_operation_ids() {
    assert_eq!(
        OperationId::SetPreferBare.runtime_operation(),
        RuntimeOperation::PreferBare(UpdateKind::Set)
    );
    assert_eq!(
        OperationId::AddBareAsText.runtime_operation(),
        RuntimeOperation::BareAsText(UpdateKind::Add)
    );
    assert_eq!(
        OperationId::RemoveIgnore.runtime_operation(),
        RuntimeOperation::Ignore(UpdateKind::Remove)
    );
}

#[test]
fn edit_distance_supports_suggestions_threshold() {
    assert_eq!(edit_distance("hdden", "hidden"), 1);
    assert_eq!(edit_distance("", "abc"), 3);
    assert_eq!(edit_distance("abc", ""), 3);
    assert_eq!(edit_distance("abc", "a"), 2);
    assert_eq!(edit_distance("abc", "ac"), 1);
    assert_eq!(edit_distance("kitten", "sitting"), 3);
    assert_eq!(suggest_name("abd", &["abc", "abe"]), Some("abc")); // spellchecker:disable-line
    assert_eq!(suggest_name("arr", &named_set_names()), Some("arrows"));
    assert_eq!(suggest_name("unrelated", &named_set_names()), None);
    assert_eq!(
        suggest_name("card-suit", &named_set_names()),
        Some("card-suits")
    );
}

#[test]
fn flag_names_match_public_options() {
    assert_eq!(OperationId::SetPreferBare.flag_name(), "--set-prefer-bare");
    assert_eq!(OperationId::AddPreferBare.flag_name(), "--add-prefer-bare");
    assert_eq!(
        OperationId::RemovePreferBare.flag_name(),
        "--remove-prefer-bare"
    );
    assert_eq!(OperationId::SetBareAsText.flag_name(), "--set-bare-as-text");
    assert_eq!(OperationId::AddBareAsText.flag_name(), "--add-bare-as-text");
    assert_eq!(
        OperationId::RemoveBareAsText.flag_name(),
        "--remove-bare-as-text"
    );
    assert_eq!(OperationId::SetIgnore.flag_name(), "--set-ignore");
    assert_eq!(OperationId::AddIgnore.flag_name(), "--add-ignore");
    assert_eq!(OperationId::RemoveIgnore.flag_name(), "--remove-ignore");
}

struct FailingWriter;

impl io::Write for FailingWriter {
    fn write(&mut self, _buf: &[u8]) -> io::Result<usize> {
        Err(io::Error::new(
            io::ErrorKind::BrokenPipe,
            "test writer failed",
        ))
    }

    fn flush(&mut self) -> io::Result<()> {
        Ok(())
    }
}

#[test]
#[allow(clippy::panic)]
fn format_lines_reports_writer_errors() {
    let mut reader = io::Cursor::new("\u{00A9}");
    let mut writer = FailingWriter;

    let result = format_lines(&mut reader, &mut writer, &Policy::default());

    let Err(ProcessLinesError::Write(error)) = result else {
        panic!("format_lines should report the writer error");
    };
    assert_eq!(error.kind(), io::ErrorKind::BrokenPipe);
}

#[test]
#[allow(clippy::unwrap_used)]
fn atomic_rewrite_reports_read_errors_from_rewrite_pass() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().join("bad.bin");
    let mut content = "\u{00A9}\n".as_bytes().to_vec();
    content.push(0xFF);
    fs::write(&path, &content).unwrap();

    let error = atomic_rewrite(&path, &Policy::default()).unwrap_err();

    assert!(error.contains("stream did not contain valid UTF-8"));
    assert_eq!(fs::read(&path).unwrap(), content);
}

#[test]
#[allow(clippy::unwrap_used)]
fn atomic_rewrite_reports_no_change_for_canonical_input() {
    let tmp = tempfile::tempdir().unwrap();
    let path = tmp.path().join("canonical.txt");
    fs::write(&path, "Hello, world!").unwrap();

    let result = atomic_rewrite(&path, &Policy::default()).unwrap();

    assert_eq!(result, (false, Vec::new()));
    assert_eq!(fs::read_to_string(&path).unwrap(), "Hello, world!");
}