canic-cli 0.111.0

Operator CLI for Canic fleet setup, builds, evidence, catalog, backup, and restore workflows
Documentation
use super::*;
use canic_host::canister_build::{BuildLockPhase, KernelBuildLock};

#[test]
fn checking_waiting_and_completion_share_one_bounded_live_line() {
    let mut painter = WaitPainter::new(true);
    let mut bytes = vec![];
    painter.checking(&mut bytes, 20).unwrap();
    assert!(painter.painted_width < 20);
    assert!(!bytes.contains(&b'\n'));
    painter.update(&mut bytes, &wait(1), 20).unwrap();
    bytes.clear();
    painter.checking(&mut bytes, 10).unwrap();
    assert!(bytes.starts_with(b"\r\x1b[2K"));
    assert!(painter.painted_width < 10);
    bytes.clear();
    painter
        .checked(
            &mut bytes,
            Duration::from_secs(5),
            Duration::from_secs(1),
            8,
        )
        .unwrap();
    assert!(bytes.starts_with(b"\r\x1b[2K"));
    assert_eq!(painter.painted_width, 0);
    assert!(
        String::from_utf8(bytes)
            .unwrap()
            .ends_with("Checks: 5.00s | lock wait: 1.00s\n")
    );
}

#[test]
fn redirected_checking_is_once_per_phase_and_never_emits_cursor_controls() {
    let mut painter = WaitPainter::new(false);
    let mut bytes = vec![];
    painter.checking(&mut bytes, 20).unwrap();
    let first = bytes.len();
    painter.checking(&mut bytes, 20).unwrap();
    assert_eq!(bytes.len(), first);
    painter.update(&mut bytes, &wait(1), 20).unwrap();
    painter.checking(&mut bytes, 20).unwrap();
    painter
        .checked(
            &mut bytes,
            Duration::from_secs(5),
            Duration::from_secs(1),
            20,
        )
        .unwrap();
    assert!(!bytes.contains(&b'\x1b'));
    let output = String::from_utf8(bytes).unwrap();
    assert_eq!(
        output.matches("Checking build inputs/outputs...").count(),
        2
    );
    assert_eq!(output.matches("Checks:").count(), 1);
}

fn wait(seconds: u64) -> BuildLockWait {
    BuildLockWait {
        elapsed: Duration::from_secs(seconds),
        inspection: BuildLockInspection {
            lock_path: "/tmp/work space/.canic/locks/complete-build-reuse.lock".into(),
            recorded_owner: None,
            kernel: KernelBuildLock::Unavailable,
            owner_visibility: BuildProcessVisibility::Unavailable,
            processes: vec![],
            process_snapshot_complete: false,
        },
    }
}

#[test]
fn redirected_output_is_sparse_escape_free_and_finishes_with_an_outcome() {
    let mut painter = WaitPainter::new(false);
    let mut bytes = vec![];
    painter.update(&mut bytes, &wait(1), 80).unwrap();
    let first = bytes.len();
    for second in 2..31 {
        painter.update(&mut bytes, &wait(second), 80).unwrap();
    }
    assert_eq!(bytes.len(), first);
    painter.update(&mut bytes, &wait(31), 80).unwrap();
    assert!(bytes.len() > first);
    painter
        .finish(
            &mut bytes,
            "cancelled (owner unaffected)",
            Duration::from_secs(32),
            80,
        )
        .unwrap();
    assert!(!bytes.contains(&b'\x1b'));
    assert!(
        String::from_utf8(bytes)
            .unwrap()
            .ends_with("Build reuse lock cancelled (owner unaffected) after 32.00s\n")
    );
}

#[test]
fn live_wait_is_one_bounded_line_and_clears_before_summary_even_after_resize() {
    let mut painter = WaitPainter::new(true);
    let mut bytes = vec![];
    painter.update(&mut bytes, &wait(1), 80).unwrap();
    bytes.clear();
    painter.update(&mut bytes, &wait(2), 30).unwrap();
    assert!(!bytes.contains(&b'\n'));
    assert!(painter.painted_width < 30);
    assert!(bytes.starts_with(b"\r\x1b[2K\x1b[1A"));
    bytes.clear();
    painter
        .finish(&mut bytes, "acquired", Duration::from_secs(3), 30)
        .unwrap();
    assert!(bytes.starts_with(b"\r\x1b[2KBuild reuse lock acquired"));
    assert_eq!(painter.painted_width, 0);
}

#[test]
fn advisory_paths_are_escaped_and_shell_inspection_preserves_quotes() {
    let mut wait = wait(1);
    wait.inspection.recorded_owner = Some(BuildLockOwner {
        pid: 42,
        profile: "fast".into(),
        workspace: "/tmp/work\nspace\x1b".into(),
        started_at_unix_seconds: 123,
        identity: None,
        phase: BuildLockPhase::BuildingArtifacts,
        phase_started_at_unix_seconds: 124,
    });
    let rendered = render_inspection(&wait.inspection);
    assert!(!rendered.contains('\x1b'));
    assert!(rendered.contains(r"work\nspace\u{1b}"));
    assert!(rendered.contains("1970-01-01T00:02:03"));
    assert_eq!(
        inspection_command(Path::new("/tmp/a'b")),
        Some("canic diagnostic build-lock --lock '/tmp/a'\\''b'".into())
    );
    assert!(inspection_command(Path::new("/tmp/a\nb")).is_none());
}

#[test]
fn actual_phase_change_is_reported_immediately_in_redirected_output() {
    let mut painter = WaitPainter::new(false);
    let mut bytes = vec![];
    let mut wait = wait(1);
    painter.update(&mut bytes, &wait, 80).unwrap();
    bytes.clear();
    wait.inspection.recorded_owner = Some(BuildLockOwner {
        pid: 42,
        profile: "fast".into(),
        workspace: "/tmp/app".into(),
        started_at_unix_seconds: 123,
        identity: None,
        phase: BuildLockPhase::BuildingArtifacts,
        phase_started_at_unix_seconds: 124,
    });
    painter.update(&mut bytes, &wait, 80).unwrap();
    assert!(!bytes.is_empty());
    assert_eq!(
        painter.last_owner.unwrap().phase,
        BuildLockPhase::BuildingArtifacts
    );
}