use super::*;
fn samples() -> Vec<ClientFailure> {
let mut all: Vec<ClientFailure> = vec![
ClientFailure::DidNotStart,
ClientFailure::OtherAccount,
ClientFailure::MayNotHaveLanded,
ClientFailure::NoCommonVersion,
ClientFailure::OtherRelease,
ClientFailure::RepositoryFull(String::from("/work/repo")),
ClientFailure::RepositoryReadOnly(String::from("/work/repo")),
ClientFailure::BranchKeepsChanging { branch: String::from("main"), times: 5 },
ClientFailure::BranchMoved { branch: String::from("main") },
ClientFailure::BranchMovedMessageSaved { branch: String::from("main"), file: String::from("/work/repo/.git/cli-git-saved-message-ab12"), amend: false },
ClientFailure::BranchMovedMessageSaved { branch: String::from("main"), file: String::from("/work/repo/.git/cli-git-saved-message-ab12"), amend: true },
ClientFailure::WaitedTooLong { minutes: 10 },
ClientFailure::Refused,
ClientFailure::IndexInUse(String::from("the editor (process 4242)")),
ClientFailure::Unavailable,
ClientFailure::ProgramMissing(String::from("/opt/cli-git/cli-git-service")),
ClientFailure::CouldNotStart(String::from("the runtime directory has no space left")),
ClientFailure::UnitInTheWay(String::from("/home/a/.config/systemd/user/cli-git.socket")),
ClientFailure::WorkNotHandedOver(String::from("the work directory cannot be written")),
ClientFailure::ContentPruned,
];
for platform in [Platform::Windows, Platform::Bsd, Platform::LinuxWithoutUserServices, Platform::Other] {
all.push(ClientFailure::NoActivation(platform));
}
return all;
}
fn variant_index(failure: &ClientFailure) -> usize {
return match failure {
ClientFailure::DidNotStart => 0,
ClientFailure::NoActivation(_) => 1,
ClientFailure::OtherAccount => 2,
ClientFailure::MayNotHaveLanded => 3,
ClientFailure::NoCommonVersion => 4,
ClientFailure::RepositoryFull(_) => 5,
ClientFailure::RepositoryReadOnly(_) => 6,
ClientFailure::BranchKeepsChanging { .. } => 7,
ClientFailure::WaitedTooLong { .. } => 8,
ClientFailure::Refused => 9,
ClientFailure::IndexInUse(_) => 10,
ClientFailure::Unavailable => 11,
ClientFailure::ProgramMissing(_) => 12,
ClientFailure::CouldNotStart(_) => 13,
ClientFailure::UnitInTheWay(_) => 15,
ClientFailure::WorkNotHandedOver(_) => 14,
ClientFailure::BranchMoved { .. } => 16,
ClientFailure::BranchMovedMessageSaved { .. } => 17,
ClientFailure::OtherRelease => 18,
ClientFailure::ContentPruned => 19,
};
}
fn without_commands(text: &str) -> String {
let mut plain: String = String::new();
let mut quoted: bool = false;
for character in text.chars() {
if character == '`' {
quoted = !quoted;
} else if !quoted {
plain.push(character);
}
}
return plain;
}
fn contains_whole(text: &str, word: &str) -> bool {
let haystack: String = text.to_lowercase();
let needle: String = word.to_lowercase();
let mut from: usize = 0;
while let Some(at) = haystack[from..].find(needle.as_str()) {
let start: usize = from + at;
let end: usize = start + needle.len();
let before_ok: bool = haystack[..start].chars().next_back().is_none_or(|c| { return !c.is_alphanumeric(); });
let after_ok: bool = haystack[end..].chars().next().is_none_or(|c| { return !c.is_alphanumeric(); });
if before_ok && after_ok {
return true;
}
from = start + 1;
}
return false;
}
#[test]
fn every_failure_variant_is_rendered() {
let covered: std::collections::BTreeSet<usize> = samples().iter().map(variant_index).collect();
let expected: usize = variant_index(&ClientFailure::ContentPruned) + 1;
assert_eq!(covered.len(), expected);
}
#[test]
fn no_message_leaks_an_implementation_term() {
for failure in samples() {
let plain: String = without_commands(render(&failure).as_str());
for term in DENYLIST {
assert!(!contains_whole(plain.as_str(), term), "{failure:?} leaks {term:?}: {plain}");
}
}
for notice in [STARTING_NOTICE, UPDATING_NOTICE] {
for term in DENYLIST {
assert!(!contains_whole(notice, term), "{notice:?} leaks {term:?}");
}
}
for (number, of) in [(1, 1), (2, 5), (9, 9)] {
let notice: String = position_notice(number, of);
for term in DENYLIST {
assert!(!contains_whole(notice.as_str(), term), "{notice:?} leaks {term:?}");
}
}
}
#[test]
fn the_denylist_catches_a_leak_and_ignores_inner_words() {
assert!(contains_whole("The daemon refused the request id.", "request id"));
assert!(contains_whole("Socket closed (EPIPE).", "socket"));
assert!(!contains_whole("Please avoid the voided path.", "oid"));
assert!(!contains_whole("The rapid pipeline.", "pid"));
assert!(contains_whole("process pid 42", "pid"));
}
#[test]
fn every_message_gives_an_action() {
let verbs = ["run", "use", "install", "free", "make", "wait", "update", "raise", "ask", "turn", "check", "commit"];
for failure in samples() {
let text: String = render(&failure);
let has_command: bool = text.contains('`');
let has_verb: bool = verbs.iter().any(|verb| { return contains_whole(text.as_str(), verb); });
assert!(has_command || has_verb, "{failure:?} gives no action: {text}");
assert!(!text.trim().is_empty());
}
}
#[test]
fn messages_quote_their_constants_and_facts() {
assert!(render(&ClientFailure::UnitInTheWay(String::from("/home/a/.config/systemd/user/cli-git.socket"))).contains("/home/a/.config/systemd/user/cli-git.socket"));
assert!(render(&ClientFailure::DidNotStart).contains(LOG_COMMAND));
assert!(render(&ClientFailure::Unavailable).contains(LOG_COMMAND));
assert!(render(&ClientFailure::OtherRelease).contains(RESTART_COMMAND));
let unsure: String = render(&ClientFailure::MayNotHaveLanded);
for command in CHECK_COMMANDS {
assert!(unsure.contains(command), "{unsure}");
}
assert!(render(&ClientFailure::NoActivation(Platform::LinuxWithoutUserServices)).contains(LINGER_COMMAND));
let branch: String = render(&ClientFailure::BranchKeepsChanging { branch: String::from("topic"), times: 7 });
assert!(branch.contains("topic") && branch.contains('7'), "{branch}");
assert!(render(&ClientFailure::BranchMoved { branch: String::from("topic") }).contains("topic"));
let saved: String = render(&ClientFailure::BranchMovedMessageSaved { branch: String::from("topic"), file: String::from("/r/.git/saved"), amend: true });
assert!(saved.contains("`git commit --amend -F /r/.git/saved`") && saved.contains("topic"), "{saved}");
let plain: String = render(&ClientFailure::BranchMovedMessageSaved { branch: String::from("topic"), file: String::from("/r/.git/saved"), amend: false });
assert!(plain.contains("`git commit -F /r/.git/saved`"), "{plain}");
assert!(render(&ClientFailure::RepositoryFull(String::from("/work/repo"))).contains("/work/repo"));
assert!(render(&ClientFailure::WaitedTooLong { minutes: 13 }).contains("13 minutes"));
assert!(render(&ClientFailure::IndexInUse(String::from("the editor"))).contains("the editor"));
}
#[test]
fn only_the_unsure_failure_does_not_claim_nothing_changed() {
for failure in samples() {
let claims: bool = render(&failure).contains(NOTHING_CHANGED);
let may_have_landed: bool = matches!(failure, ClientFailure::MayNotHaveLanded | ClientFailure::WorkNotHandedOver(_));
assert_eq!(!claims, may_have_landed, "{failure:?}");
}
}
#[test]
fn position_notices_count_the_commits_ahead() {
assert_eq!(position_notice(1, 4), "This commit is next.");
assert_eq!(position_notice(2, 4), "Waiting for 1 earlier commit.");
assert_eq!(position_notice(4, 4), "Waiting for 3 earlier commits.");
}