ic-backup 0.64.0

Host-side snapshot backup and same-release recovery for Internet Computer canisters
Documentation
//! Fresh explicit dependency regressions adapted from Canic phase and restore ordering.

use super::*;
use crate::model::attempt_journal::AttemptBudgetRecord;
use crate::model::operation_plan::{
    OperationPlanRecord, OperationPlanRequest, PlanBudgetRecord, PlannedOperationRequest,
};

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_rejects_invalid_dependencies_and_caller_supplied_order() {
    let value = serde_json::to_value(diamond()).expect("json");
    for (path, entry) in [
        ("/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());
}

#[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")
    );
}