use std::collections::BTreeMap;
use serde_json::json;
use super::{
ArgumentValue, ArgvSlot, CommandLineContract, CommandParameterContract,
DeclaredCommandContract, EnvBindingContract, FillPiece, FillTemplate, RenderError,
};
type TestResult = Result<(), Box<dyn std::error::Error>>;
fn hole(parameter: &str) -> FillTemplate {
FillTemplate {
pieces: vec![FillPiece::Hole {
parameter: parameter.to_owned(),
}],
}
}
fn mixed(prefix: &str, parameter: &str, suffix: &str) -> FillTemplate {
FillTemplate {
pieces: vec![
FillPiece::Literal {
text: prefix.to_owned(),
},
FillPiece::Hole {
parameter: parameter.to_owned(),
},
FillPiece::Literal {
text: suffix.to_owned(),
},
],
}
}
fn slot(fill: FillTemplate, label: &str, admits_leading_dash: bool) -> ArgvSlot {
ArgvSlot {
fill,
label: label.to_owned(),
admits_leading_dash,
}
}
fn parameter(name: &str, default: Option<&str>) -> CommandParameterContract {
CommandParameterContract {
name: name.to_owned(),
default: default.map(str::to_owned),
}
}
fn contract(
parameters: Vec<CommandParameterContract>,
program: &[&str],
args: Vec<ArgvSlot>,
) -> DeclaredCommandContract {
let mut slots: Vec<ArgvSlot> = program
.iter()
.map(|word| slot(FillTemplate::literal((*word).to_owned()), word, true))
.collect();
slots.extend(args);
DeclaredCommandContract {
name: "probe".to_owned(),
parameters,
lines: vec![CommandLineContract { slots }],
env: Vec::new(),
cwd: None,
prior_form_refusal: None,
}
}
fn supplied(pairs: &[(&str, ArgumentValue)]) -> BTreeMap<String, ArgumentValue> {
pairs
.iter()
.map(|(name, value)| ((*name).to_owned(), value.clone()))
.collect()
}
#[test]
fn a_value_carrying_shell_metacharacters_arrives_as_one_argv_element() -> TestResult {
let command = contract(
vec![parameter("value", None)],
&["echo"],
vec![slot(hole("value"), "value", true)],
);
let rendered = command.render(&supplied(&[(
"value",
ArgumentValue::scalar("$(boom); rm -rf /"),
)]))?;
assert_eq!(
rendered.argv_lines,
vec![vec!["echo".to_owned(), "$(boom); rm -rf /".to_owned()]],
"a hostile value must be one inert element, never re-split"
);
Ok(())
}
#[test]
fn a_value_containing_a_space_stays_one_element() -> TestResult {
let command = contract(
vec![parameter("value", None)],
&["echo"],
vec![slot(hole("value"), "value", true)],
);
let rendered = command.render(&supplied(&[("value", ArgumentValue::scalar("a b c"))]))?;
assert_eq!(
rendered.argv_lines,
vec![vec!["echo".to_owned(), "a b c".to_owned()]]
);
Ok(())
}
#[test]
fn a_multi_line_body_renders_one_argv_per_line_in_order() -> TestResult {
let command = DeclaredCommandContract {
name: "probe".to_owned(),
parameters: vec![parameter("tag", None)],
lines: vec![
CommandLineContract {
slots: vec![
slot(FillTemplate::literal("git"), "git", true),
slot(FillTemplate::literal("fetch"), "fetch", true),
],
},
CommandLineContract {
slots: vec![
slot(FillTemplate::literal("git"), "git", true),
slot(FillTemplate::literal("tag"), "tag", true),
slot(FillTemplate::literal("--"), "--", true),
slot(hole("tag"), "tag", true),
],
},
],
env: Vec::new(),
cwd: None,
prior_form_refusal: None,
};
let rendered = command.render(&supplied(&[("tag", ArgumentValue::scalar("v1.0"))]))?;
assert_eq!(
rendered.argv_lines,
vec![
vec!["git".to_owned(), "fetch".to_owned()],
vec![
"git".to_owned(),
"tag".to_owned(),
"--".to_owned(),
"v1.0".to_owned()
],
]
);
Ok(())
}
#[test]
fn a_default_is_literal_text() -> TestResult {
let command = contract(
vec![parameter("range", Some("main..HEAD"))],
&["git", "log"],
vec![slot(hole("range"), "range", true)],
);
let rendered = command.render(&BTreeMap::new())?;
assert_eq!(
rendered.argv_lines,
vec![vec![
"git".to_owned(),
"log".to_owned(),
"main..HEAD".to_owned()
]]
);
Ok(())
}
#[test]
fn a_supplied_value_beats_the_declared_default() -> TestResult {
let command = contract(
vec![parameter("who", Some("nobody"))],
&["echo"],
vec![slot(hole("who"), "who", true)],
);
let rendered = command.render(&supplied(&[("who", ArgumentValue::scalar("world"))]))?;
assert_eq!(
rendered.argv_lines,
vec![vec!["echo".to_owned(), "world".to_owned()]]
);
Ok(())
}
#[test]
fn a_parameter_with_no_value_and_no_default_refuses_by_name() {
let command = contract(
vec![parameter("who", None)],
&["echo"],
vec![slot(hole("who"), "who", true)],
);
assert_eq!(
command.render(&BTreeMap::new()),
Err(RenderError::ArgumentMissing {
command: "probe".to_owned(),
parameter: "who".to_owned(),
})
);
}
#[test]
fn a_value_the_command_does_not_declare_refuses_by_name() {
let command = contract(Vec::new(), &["echo"], Vec::new());
assert_eq!(
command.render(&supplied(&[("stray", ArgumentValue::scalar("x"))])),
Err(RenderError::ArgumentUndeclared {
command: "probe".to_owned(),
parameter: "stray".to_owned(),
})
);
}
#[test]
fn a_list_value_refuses_naming_both_shapes() {
let command = contract(
vec![parameter("paths", None)],
&["git"],
vec![slot(hole("paths"), "paths", true)],
);
assert_eq!(
command.render(&supplied(&[(
"paths",
ArgumentValue::list(["a.rs", "b.rs"])
)])),
Err(RenderError::ArgumentTypeMismatch {
command: "probe".to_owned(),
parameter: "paths".to_owned(),
observed: "[a.rs, b.rs]".to_owned(),
declared: "single value",
supplied: "list",
})
);
}
#[test]
fn a_leading_dash_operand_refuses_where_the_program_still_reads_options() {
let command = contract(
vec![parameter("name", None)],
&["git", "tag"],
vec![slot(hole("name"), "name", false)],
);
let Err(error) = command.render(&supplied(&[("name", ArgumentValue::scalar("-n"))])) else {
panic_free_failure("a leading-dash operand must refuse");
return;
};
assert_eq!(
error,
RenderError::LeadingDashOperand {
command: "probe".to_owned(),
argument: "name".to_owned(),
element: "-n".to_owned(),
marker: "--",
}
);
}
#[test]
fn a_literally_opened_element_is_not_dash_guarded() -> TestResult {
let command = contract(
vec![parameter("count", None)],
&["grep"],
vec![
slot(FillTemplate::literal("--max-count"), "--max-count", true),
slot(hole("count"), "count", true),
],
);
let rendered = command.render(&supplied(&[("count", ArgumentValue::scalar("-3"))]))?;
assert_eq!(
rendered.argv_lines,
vec![vec![
"grep".to_owned(),
"--max-count".to_owned(),
"-3".to_owned()
]]
);
Ok(())
}
#[test]
fn the_same_bytes_pass_once_the_end_of_options_marker_stands_before_them() -> TestResult {
let command = contract(
vec![parameter("name", None)],
&["git", "tag"],
vec![
slot(FillTemplate::literal("--"), "--", true),
slot(hole("name"), "name", true),
],
);
let rendered = command.render(&supplied(&[("name", ArgumentValue::scalar("-n"))]))?;
assert_eq!(
rendered.argv_lines,
vec![vec![
"git".to_owned(),
"tag".to_owned(),
"--".to_owned(),
"-n".to_owned()
]]
);
Ok(())
}
#[test]
fn env_bindings_carry_their_literal_values() -> TestResult {
let mut command = contract(Vec::new(), &["true"], Vec::new());
command.env = vec![EnvBindingContract {
name: "GIT_PAGER".to_owned(),
value: "cat".to_owned(),
}];
command.cwd = Some("{workspace_root}".to_owned());
let rendered = command.render(&BTreeMap::new())?;
assert_eq!(
rendered.env,
vec![("GIT_PAGER".to_owned(), "cat".to_owned())]
);
assert_eq!(rendered.cwd, Some("{workspace_root}".to_owned()));
Ok(())
}
#[test]
fn json_values_take_their_one_obvious_argument_form() -> TestResult {
assert_eq!(
ArgumentValue::from_json("p", &json!("text"))?,
ArgumentValue::scalar("text")
);
assert_eq!(
ArgumentValue::from_json("p", &json!(7))?,
ArgumentValue::scalar("7")
);
assert_eq!(
ArgumentValue::from_json("p", &json!(true))?,
ArgumentValue::scalar("true")
);
assert_eq!(
ArgumentValue::from_json("p", &json!(["a", 2]))?,
ArgumentValue::list(["a", "2"])
);
Ok(())
}
#[test]
fn json_values_with_no_argument_form_refuse_by_name() {
for (value, kind) in [
(json!(null), "null"),
(json!({ "a": 1 }), "an object"),
(json!([[1]]), "a nested array"),
] {
assert_eq!(
ArgumentValue::from_json("p", &value),
Err(RenderError::UnrepresentableValue {
parameter: "p".to_owned(),
kind,
})
);
}
assert_eq!(
ArgumentValue::from_json("p", &json!("has\0nul")),
Err(RenderError::InteriorNul {
parameter: "p".to_owned(),
})
);
}
#[test]
fn the_contract_round_trips_through_json() -> TestResult {
let mut command = contract(
vec![parameter("who", Some("world"))],
&["echo"],
vec![slot(mixed("hello ", "who", "!"), "who", false)],
);
command.cwd = Some("{workspace_root}".to_owned());
command.env = vec![EnvBindingContract {
name: "GIT_PAGER".to_owned(),
value: "cat".to_owned(),
}];
let encoded = serde_json::to_string(&command)?;
let decoded: DeclaredCommandContract = serde_json::from_str(&encoded)?;
assert_eq!(decoded, command);
Ok(())
}
#[test]
fn every_distinguishing_edit_moves_the_identity_bytes() -> TestResult {
use crate::contract::CommandBodyCapture;
let base = contract(
vec![parameter("who", None)],
&["echo"],
vec![slot(hole("who"), "who", true)],
);
let mut seen = std::collections::BTreeSet::new();
let mut record = |capture: CommandBodyCapture,
command: &DeclaredCommandContract|
-> Result<(), Box<dyn std::error::Error>> {
let mut bytes = Vec::new();
crate::declared_command::encode_identity(&mut bytes, command);
bytes.push(u8::from(matches!(capture, CommandBodyCapture::Json)));
assert!(seen.insert(bytes), "two distinct commands hashed alike");
Ok(())
};
record(CommandBodyCapture::Text, &base)?;
record(CommandBodyCapture::Json, &base)?;
let mut edited = base.clone();
edited.lines[0].slots[0].fill = FillTemplate::literal("printf");
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.lines[0]
.slots
.push(slot(FillTemplate::literal("--"), "--", true));
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.lines[0]
.slots
.insert(1, slot(FillTemplate::literal("--"), "--", true));
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.lines[0].slots[1].admits_leading_dash = false;
record(CommandBodyCapture::Text, &edited)?;
let extra = CommandLineContract {
slots: vec![slot(FillTemplate::literal("true"), "true", true)],
};
let mut edited = base.clone();
edited.lines.push(extra.clone());
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.lines.insert(0, extra);
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.env = vec![EnvBindingContract {
name: "A".to_owned(),
value: "1".to_owned(),
}];
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base.clone();
edited.cwd = Some("/srv".to_owned());
record(CommandBodyCapture::Text, &edited)?;
let mut edited = base;
edited.parameters[0].default = Some("world".to_owned());
record(CommandBodyCapture::Text, &edited)?;
Ok(())
}
fn panic_free_failure(reason: &str) {
assert!(reason.is_empty(), "{reason}");
}
fn prior_form_specimen() -> serde_json::Value {
json!({
"name": "probe_release_tree_sh",
"parameters": [
{"name": "tree", "list": false},
{"name": "version", "list": false}
],
"program": ["sh"],
"args": [
{
"fill": {"pieces": [
{"kind": "hole", "parameter": "tree"},
{"kind": "literal", "text": "/workflows/release_ceremony/probe-release-tree.sh"}
]},
"label": "script",
"admits_leading_dash": true
},
{
"fill": {"pieces": [{"kind": "hole", "parameter": "tree"}]},
"label": "release_tree",
"admits_leading_dash": true
},
{
"fill": {"pieces": [{"kind": "hole", "parameter": "version"}]},
"label": "release_version",
"admits_leading_dash": true
}
],
"timeout_ms": 300_000,
"timeout_owner": "release"
})
}
#[test]
fn the_prior_archive_form_reads_and_renders_the_same_argv() -> TestResult {
let command: DeclaredCommandContract = serde_json::from_value(prior_form_specimen())?;
assert_eq!(command.name, "probe_release_tree_sh");
assert_eq!(command.prior_form_refusal, None);
let rendered = command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::scalar("0.27.0")),
]))?;
assert_eq!(
rendered.argv_lines,
vec![vec![
"sh".to_owned(),
"/srv/release/workflows/release_ceremony/probe-release-tree.sh".to_owned(),
"/srv/release".to_owned(),
"0.27.0".to_owned(),
]]
);
assert!(rendered.env.is_empty());
assert_eq!(rendered.cwd, None);
Ok(())
}
#[test]
fn a_prior_hardened_path_becomes_a_literal_path_binding() -> TestResult {
let mut specimen = prior_form_specimen();
specimen["hardened_path"] = json!({
"pieces": [{"kind": "literal", "text": "/usr/bin:/bin"}]
});
let command: DeclaredCommandContract = serde_json::from_value(specimen)?;
let rendered = command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::scalar("0.27.0")),
]))?;
assert_eq!(
rendered.env,
vec![("PATH".to_owned(), "/usr/bin:/bin".to_owned())]
);
Ok(())
}
#[test]
fn a_prior_interpolating_env_binding_reads_but_refuses_to_render() -> TestResult {
let mut specimen = prior_form_specimen();
specimen["env"] = json!([{
"name": "DECLARED_WHO",
"value": {"pieces": [{"kind": "hole", "parameter": "version"}]}
}]);
let command: DeclaredCommandContract = serde_json::from_value(specimen)?;
let Some(construct) = &command.prior_form_refusal else {
panic_free_failure("the untranslatable binding must be carried by name");
return Ok(());
};
assert!(construct.contains("DECLARED_WHO"), "{construct}");
let Err(error) = command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::scalar("0.27.0")),
])) else {
panic_free_failure("a prior-form construct with no translation must not execute");
return Ok(());
};
let RenderError::PriorFormUnrenderable { command, construct } = &error else {
panic_free_failure(&format!(
"the refusal must name the prior form, got {error:?}"
));
return Ok(());
};
assert_eq!(command, "probe_release_tree_sh");
assert!(construct.contains("DECLARED_WHO"), "{construct}");
assert!(error.to_string().contains("redeploy"), "{error}");
Ok(())
}
#[test]
fn a_prior_interpolating_parameter_default_reads_but_refuses_to_render() -> TestResult {
let mut specimen = prior_form_specimen();
specimen["parameters"] = json!([
{"name": "tree", "list": false},
{
"name": "version",
"list": false,
"default": {"pieces": [
{"kind": "hole", "parameter": "tree"},
{"kind": "literal", "text": "..HEAD"}
]}
}
]);
let command: DeclaredCommandContract = serde_json::from_value(specimen)?;
let Some(construct) = &command.prior_form_refusal else {
panic_free_failure("the untranslatable default must be carried by name");
return Ok(());
};
assert_eq!(construct, "the default of parameter `version`");
let outcome = command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::scalar("0.27.0")),
]));
assert_eq!(
outcome,
Err(RenderError::PriorFormUnrenderable {
command: "probe_release_tree_sh".to_owned(),
construct: "the default of parameter `version`".to_owned(),
})
);
Ok(())
}
#[test]
fn a_prior_list_parameter_reads_but_refuses_to_render() -> TestResult {
let mut specimen = prior_form_specimen();
specimen["parameters"] = json!([
{"name": "tree", "list": false},
{"name": "version", "list": true}
]);
let command: DeclaredCommandContract = serde_json::from_value(specimen)?;
let Some(construct) = &command.prior_form_refusal else {
panic_free_failure("the list parameter must be carried by name");
return Ok(());
};
assert_eq!(construct, "the list parameter `version`");
let expected = Err(RenderError::PriorFormUnrenderable {
command: "probe_release_tree_sh".to_owned(),
construct: "the list parameter `version`".to_owned(),
});
assert_eq!(
command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::list(["0.27.0", "0.27.1"])),
])),
expected
);
assert_eq!(
command.render(&supplied(&[
("tree", ArgumentValue::scalar("/srv/release")),
("version", ArgumentValue::scalar("0.27.0")),
])),
expected
);
Ok(())
}
#[test]
fn an_entry_carrying_both_archive_forms_refuses_rather_than_choosing() {
let mut specimen = prior_form_specimen();
specimen["lines"] = json!([{
"slots": [{
"fill": {"pieces": [{"kind": "literal", "text": "true"}]},
"label": "true",
"admits_leading_dash": true
}]
}]);
let outcome: Result<DeclaredCommandContract, _> = serde_json::from_value(specimen);
let Err(error) = outcome else {
panic_free_failure("an entry carrying both forms must not read as either one");
return;
};
let reported = error.to_string();
assert!(reported.contains("program"), "{reported}");
assert!(reported.contains("lines"), "{reported}");
}
#[test]
fn a_current_form_entry_with_a_stray_prior_key_refuses() -> TestResult {
let command = contract(
vec![parameter("name", None)],
&["printf"],
vec![slot(hole("name"), "name", true)],
);
let mut wire = serde_json::to_value(&command)?;
let Some(object) = wire.as_object_mut() else {
panic_free_failure("a serialized contract is a JSON object");
return Ok(());
};
object.insert("hardened_path".to_owned(), json!({"pieces": []}));
let outcome: Result<DeclaredCommandContract, _> = serde_json::from_value(wire);
let Err(error) = outcome else {
panic_free_failure("a stray key must not be read past in silence");
return Ok(());
};
assert!(error.to_string().contains("hardened_path"), "{error}");
Ok(())
}
#[test]
fn a_body_reading_an_undeclared_parameter_refuses_by_name() {
let command = contract(
Vec::new(),
&["echo"],
vec![slot(mixed("prefix-", "ghost", "-suffix"), "ghost", true)],
);
assert_eq!(
command.render(&BTreeMap::new()),
Err(RenderError::UnboundHole {
command: "probe".to_owned(),
parameter: "ghost".to_owned(),
})
);
}
#[test]
fn the_current_form_round_trips_unchanged() -> TestResult {
let command = contract(
vec![parameter("name", None)],
&["printf", "--", "%s"],
vec![slot(hole("name"), "name", true)],
);
let wire = serde_json::to_value(&command)?;
assert!(
wire.get("prior_form_refusal").is_none(),
"an emitted contract must not carry the reader's marker on the wire"
);
let read: DeclaredCommandContract = serde_json::from_value(wire)?;
assert_eq!(read, command);
Ok(())
}