use std::collections::BTreeSet;
#[derive(Clone, Debug, Eq, PartialEq)]
pub struct DeclaringVersion {
pub content_hash: String,
pub workflow_types: Vec<String>,
pub route_active: bool,
pub body: usize,
}
#[must_use]
pub fn ambiguous_body_refusal(
action: &str,
task_queue: &str,
declaring: &[DeclaringVersion],
) -> String {
let distinct: BTreeSet<usize> = declaring.iter().map(|version| version.body).collect();
let live: BTreeSet<usize> = declaring
.iter()
.filter(|version| version.route_active)
.map(|version| version.body)
.collect();
let remedy = remedy_for(declaring, &live);
format!(
"terminal:action `{action}` on task queue `{task_queue}` declares {bodies} different \
bodies across {versions} retained package versions; refusing to guess which deploy this \
run meant. {remedy}",
bodies = distinct.len(),
versions = declaring.len(),
)
}
fn remedy_for(declaring: &[DeclaringVersion], live: &BTreeSet<usize>) -> String {
if live.len() > 1 {
return format!(
"Every disagreeing version is route-active, so there is no superseded version to \
retire — {live_versions} are all reachable by new starts and declare different \
commands for the same action name. Give the action a distinct name in each document, \
or declare the same body in both.",
live_versions = route_active_names(declaring),
);
}
let superseded = unload_commands(declaring);
if superseded.is_empty() {
return "No superseded version is retained, so there is nothing to unload; the \
disagreement is inside the route-active set and needs the documents themselves \
reconciled."
.to_owned();
}
if live.is_empty() {
return format!(
"No retained version is route-active, so deploy the document you mean first — that \
makes its version the one new starts reach — then retire the rest and start the run \
again: {superseded}.",
superseded = superseded.join(", "),
);
}
format!(
"The route-active version already carries the body new starts use; retiring the \
superseded ones is what leaves a single body. Redeploying adds a version rather than \
removing one, so unload instead, then start the run again: {superseded}. Kept, because \
new starts route to it: {kept}.",
superseded = superseded.join(", "),
kept = route_active_names(declaring),
)
}
fn unload_commands(declaring: &[DeclaringVersion]) -> Vec<String> {
declaring
.iter()
.filter(|version| !version.route_active)
.flat_map(|version| {
version.workflow_types.iter().map(move |workflow_type| {
format!(
"`aion unload {workflow_type} {hash}`",
hash = version.content_hash
)
})
})
.collect()
}
fn route_active_names(declaring: &[DeclaringVersion]) -> String {
let names: Vec<String> = declaring
.iter()
.filter(|version| version.route_active)
.flat_map(|version| {
version.workflow_types.iter().map(move |workflow_type| {
format!("{workflow_type}@{hash}", hash = version.content_hash)
})
})
.collect();
if names.is_empty() {
return "no version".to_owned();
}
names.join(", ")
}
#[cfg(test)]
mod tests {
use super::{DeclaringVersion, ambiguous_body_refusal};
const OLD: &str = "1111111111111111111111111111111111111111111111111111111111111111";
const NEW: &str = "2222222222222222222222222222222222222222222222222222222222222222";
fn version(hash: &str, types: &[&str], route_active: bool, body: usize) -> DeclaringVersion {
DeclaringVersion {
content_hash: hash.to_owned(),
workflow_types: types.iter().map(|name| (*name).to_owned()).collect(),
route_active,
body,
}
}
#[test]
fn the_remedy_names_the_superseded_version_as_a_runnable_command() {
let refusal = ambiguous_body_refusal(
"find_repositories",
"local",
&[
version(OLD, &["git_status_sweep"], false, 0),
version(NEW, &["git_status_sweep"], true, 1),
],
);
assert!(refusal.starts_with("terminal:"), "{refusal}");
assert!(
refusal.contains(&format!("`aion unload git_status_sweep {OLD}`")),
"the superseded version must be named as a command: {refusal}"
);
assert!(
!refusal.contains(&format!("`aion unload git_status_sweep {NEW}`")),
"the route-active version cannot be unloaded and must not be told to: {refusal}"
);
assert!(
refusal.contains(&format!("git_status_sweep@{NEW}")),
"the kept version must be identified: {refusal}"
);
}
#[test]
fn the_remedy_never_tells_the_operator_to_redeploy_into_the_problem() {
let refusal = ambiguous_body_refusal(
"find_repositories",
"local",
&[
version(OLD, &["sweep"], false, 0),
version(NEW, &["sweep"], true, 1),
],
);
assert!(
!refusal.contains("redeploy so one body remains"),
"{refusal}"
);
assert!(
refusal.contains("Redeploying adds a version rather than removing one"),
"the message must say why the obvious move is wrong: {refusal}"
);
}
#[test]
fn a_version_with_several_entry_types_earns_one_command_per_type() {
let refusal = ambiguous_body_refusal(
"build",
"local",
&[
version(OLD, &["alpha", "beta"], false, 0),
version(NEW, &["alpha"], true, 1),
],
);
assert!(
refusal.contains(&format!("`aion unload alpha {OLD}`")),
"{refusal}"
);
assert!(
refusal.contains(&format!("`aion unload beta {OLD}`")),
"{refusal}"
);
}
#[test]
fn two_route_active_packages_are_told_to_reconcile_not_to_unload() {
let refusal = ambiguous_body_refusal(
"build",
"local",
&[
version(OLD, &["alpha"], true, 0),
version(NEW, &["beta"], true, 1),
],
);
assert!(
!refusal.contains("aion unload"),
"unloading a route-active version is refused, so it must not be prescribed: {refusal}"
);
assert!(
refusal.contains("distinct name"),
"the remedy for a live collision is an authoring change: {refusal}"
);
assert!(refusal.contains(&format!("alpha@{OLD}")), "{refusal}");
assert!(refusal.contains(&format!("beta@{NEW}")), "{refusal}");
}
#[test]
fn with_nothing_route_active_the_deploy_comes_before_the_unload() -> Result<(), String> {
let refusal = ambiguous_body_refusal(
"build",
"local",
&[
version(OLD, &["alpha"], false, 0),
version(NEW, &["alpha"], false, 1),
],
);
let deploy = refusal
.find("deploy the document you mean first")
.ok_or_else(|| format!("the deploy step must be named: {refusal}"))?;
let unload = refusal
.find("aion unload")
.ok_or_else(|| format!("the unload step must be named: {refusal}"))?;
assert!(
deploy < unload,
"deploy must be prescribed before unload: {refusal}"
);
Ok(())
}
#[test]
fn the_counts_report_bodies_and_versions_separately() {
let refusal = ambiguous_body_refusal(
"build",
"local",
&[
version(OLD, &["alpha"], false, 0),
version(NEW, &["alpha"], false, 0),
version(
"3333333333333333333333333333333333333333333333333333333333333333",
&["alpha"],
true,
1,
),
],
);
assert!(
refusal.contains("declares 2 different bodies across 3 retained package versions"),
"{refusal}"
);
}
}