use super::*;
use crate::model::attempt_journal::AttemptBudgetRecord;
use crate::model::operation_plan::{
OperationPlanRecord, OperationPlanRequest, PlanBudgetRecord, PlannedOperationRequest,
};
use crate::tests::support::schema;
fn plan(
operations: Vec<PlannedOperationRecord>,
) -> Result<OperationPlanRecord, OperationPlanError> {
let original = crate::tests::support::operation_plan::request();
OperationPlanRecord::new(OperationPlanRequest {
operations,
budget: PlanBudgetRecord::new(0, 0).unwrap(),
..original
})
}
fn node(sequence: u64, deps: &[u64]) -> PlannedOperationRecord {
PlannedOperationRecord::new(PlannedOperationRequest {
operation_sequence: sequence,
depends_on: deps.to_vec(),
target: crate::tests::support::operation_plan::APP.into(),
request: "ef".repeat(32),
budget: AttemptBudgetRecord::new(0, 0).unwrap(),
})
.expect("node")
}
fn diamond() -> OperationPlanRecord {
plan(vec![
node(0, &[]),
node(2, &[0]),
node(9, &[0]),
node(u64::MAX, &[9, 2]),
])
.expect("diamond")
}
#[test]
fn canonical_binary_hash_and_order_ignore_input_order_but_bind_exact_identities_and_edges() {
let graph = diamond();
let mut reverse = graph.operations().to_vec();
reverse.reverse();
assert_eq!(plan(reverse).expect("same declarations"), graph);
assert_eq!(
graph
.operation(u64::MAX)
.expect("exact max ID")
.depends_on(),
&[2, 9]
);
assert_eq!(
graph
.ordered_operations()
.map(PlannedOperationRecord::operation_sequence)
.collect::<Vec<_>>(),
vec![0, 2, 9, u64::MAX]
);
let changed = plan(vec![
node(0, &[]),
node(2, &[0]),
node(9, &[]),
node(u64::MAX, &[2, 9]),
])
.expect("changed edge");
assert_ne!(changed.digest(), graph.digest());
let changed = plan(vec![
node(0, &[]),
node(3, &[0]),
node(9, &[0]),
node(u64::MAX, &[3, 9]),
])
.expect("changed identity");
assert_ne!(changed.digest(), graph.digest());
let mut value = serde_json::to_value(&graph).expect("json");
value["operations"][3]["depends_on"] = serde_json::json!([9, 2]);
assert_eq!(
serde_json::from_value::<OperationPlanRecord>(value).expect("normalize edge order"),
graph
);
assert_eq!(
serde_json::from_slice::<OperationPlanRecord>(&serde_json::to_vec(&graph).expect("encode"))
.expect("roundtrip"),
graph
);
}
#[test]
fn explicit_edges_qualify_parent_first_child_first_and_smallest_ready_ties() {
let stop = plan(vec![node(5, &[10]), node(10, &[])]).expect("explicit parent before child");
assert_eq!(
stop.ordered_operations()
.map(PlannedOperationRecord::operation_sequence)
.collect::<Vec<_>>(),
vec![10, 5]
);
let start =
plan(vec![node(101, &[102]), node(102, &[])]).expect("explicit child before parent");
assert_eq!(
start
.ordered_operations()
.map(PlannedOperationRecord::operation_sequence)
.collect::<Vec<_>>(),
vec![102, 101]
);
let tie = plan(vec![node(1, &[4]), node(4, &[]), node(8, &[])]).expect("ready ties");
assert_eq!(
tie.ordered_operations()
.map(PlannedOperationRecord::operation_sequence)
.collect::<Vec<_>>(),
vec![4, 1, 8]
);
}
#[test]
fn duplicate_missing_and_cyclic_dependencies_reject_without_truncating_walks() {
assert!(matches!(
PlannedOperationRecord::new(PlannedOperationRequest {
operation_sequence: 2,
depends_on: vec![1, 1],
target: crate::tests::support::operation_plan::APP.into(),
request: "ef".repeat(32),
budget: AttemptBudgetRecord::new(0, 0).unwrap(),
}),
Err(OperationPlanError::DuplicateDependency {
operation_sequence: 2,
dependency: 1
})
));
assert!(matches!(
plan(vec![node(1, &[]), node(1, &[])]),
Err(OperationPlanError::DuplicateOperation(1))
));
assert!(matches!(
plan(vec![node(2, &[1])]),
Err(OperationPlanError::MissingDependency {
operation_sequence: 2,
dependency: 1
})
));
assert!(matches!(
plan(vec![node(1, &[1])]),
Err(OperationPlanError::Cycle)
));
assert!(matches!(
plan(vec![
node(1, &[2]),
node(2, &[3]),
node(3, &[1]),
node(4, &[])
]),
Err(OperationPlanError::Cycle)
));
assert!(matches!(
diamond().operation(1),
Err(OperationPlanError::UnknownOperation(1))
));
}
#[test]
fn decoding_requires_closed_fields_valid_dependencies_and_recomputed_order() {
let value = serde_json::to_value(diamond()).expect("json");
for (path, fields) in [
(
"",
vec![
"version",
"context",
"inventory",
"selected_targets",
"operations",
"budget",
],
),
(
"/operations/0",
vec![
"operation_sequence",
"depends_on",
"target",
"request",
"budget",
],
),
] {
schema::closed_fields::<OperationPlanRecord>(&value, path, &fields);
}
for (path, entry) in [
("/version", serde_json::json!(2)),
("/operations/0/depends_on", serde_json::json!([u64::MAX])),
("/operations/0/operation_sequence", serde_json::json!(-1)),
("/operations/1/depends_on", serde_json::json!([0, 0])),
("/operations/1/depends_on", serde_json::json!([7])),
("/operations/0/depends_on", serde_json::Value::Null),
] {
let mut bad = value.clone();
*bad.pointer_mut(path).expect("field") = entry;
assert!(serde_json::from_value::<OperationPlanRecord>(bad).is_err());
}
let mut bad = value.clone();
bad["order"] = serde_json::json!([3, 2, 1, 0]);
assert!(serde_json::from_value::<OperationPlanRecord>(bad).is_err());
schema::duplicate_versions::<OperationPlanRecord>(&value);
}
#[test]
fn exact_operation_depth_and_edge_bounds_admit_without_recursive_walks() {
assert!(matches!(
plan(vec![]),
Err(OperationPlanError::EmptyOperations)
));
let nodes = (0..MAX_PLAN_OPERATIONS)
.map(|index| {
let id = u64::try_from(index).expect("index");
if id == 0 {
node(id, &[])
} else {
node(id, &[id - 1])
}
})
.collect();
let graph = plan(nodes).expect("maximum chain");
assert_eq!(graph.ordered_operations().count(), MAX_PLAN_OPERATIONS);
assert_eq!(
serde_json::from_slice::<OperationPlanRecord>(&serde_json::to_vec(&graph).expect("encode"))
.expect("maximum decode"),
graph
);
let mut excessive = graph.operations().to_vec();
excessive.push(node(u64::MAX, &[]));
assert!(matches!(
plan(excessive),
Err(OperationPlanError::TooManyOperations)
));
let mut bad = serde_json::to_value(&graph).expect("json");
bad["operations"]
.as_array_mut()
.expect("nodes")
.push(serde_json::json!({}));
assert!(
serde_json::from_value::<OperationPlanRecord>(bad)
.expect_err("count before next node decode")
.to_string()
.contains("exceeds")
);
let dependencies: Vec<_> = (0..u64::try_from(MAX_PLAN_DEPENDENCIES).expect("bound")).collect();
let mut dense: Vec<_> = dependencies.iter().map(|id| node(*id, &[])).collect();
for id in 1024..1088 {
dense.push(node(id, &dependencies));
}
let record = plan(dense.clone()).expect("exact 65536 edges");
assert_eq!(
record
.operations()
.iter()
.map(|node| node.depends_on().len())
.sum::<usize>(),
MAX_PLAN_EDGES
);
assert_eq!(
serde_json::from_slice::<OperationPlanRecord>(
&serde_json::to_vec(&record).expect("encode")
)
.expect("edge-bound decode"),
record
);
dense.push(node(1088, &[0]));
assert!(matches!(plan(dense), Err(OperationPlanError::TooManyEdges)));
let mut bad = serde_json::to_value(&record).expect("json");
bad["operations"]
.as_array_mut()
.expect("nodes")
.push(serde_json::to_value(node(1088, &[0])).unwrap());
assert!(
serde_json::from_value::<OperationPlanRecord>(bad)
.expect_err("total bound during decoding")
.to_string()
.contains("edges")
);
let mut too_many = dependencies;
too_many.push(1024);
assert!(matches!(
PlannedOperationRecord::new(PlannedOperationRequest {
operation_sequence: 2000,
depends_on: too_many,
target: crate::tests::support::operation_plan::APP.into(),
request: "ef".repeat(32),
budget: AttemptBudgetRecord::new(0, 0).unwrap(),
}),
Err(OperationPlanError::TooManyDependencies)
));
let mut bad = serde_json::to_value(&record).expect("json");
bad["operations"][1024]["depends_on"] = serde_json::json!((0..1025).collect::<Vec<_>>());
assert!(
serde_json::from_value::<OperationPlanRecord>(bad)
.expect_err("direct edge bound")
.to_string()
.contains("dependencies")
);
}