use serde::Serialize;
use serde_json::Value;
use crate::core::command::Command;
use crate::core::report::{ReportState, is_full_snapshot_message};
use crate::core::status::PrinterStatus;
use crate::core::verify::{self, EffectStatus};
#[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize)]
#[serde(rename_all = "snake_case")]
pub enum VerifyStage {
Ack,
Effect,
}
#[derive(Debug, Clone, PartialEq, Eq, Serialize)]
#[serde(tag = "outcome", rename_all = "snake_case")]
pub enum CommandOutcome {
Verified,
Rejected { reason: String },
Unverified { stage: VerifyStage },
}
pub struct VerifySession {
cmd: Command,
seq: String,
state: ReportState,
acked: bool,
baseline_error: Option<i64>,
}
impl VerifySession {
pub fn new(cmd: Command, seq: impl Into<String>) -> Self {
Self {
cmd,
seq: seq.into(),
state: ReportState::new(),
acked: false,
baseline_error: None,
}
}
pub fn observe(&mut self, message: Value) -> Option<CommandOutcome> {
let full = is_full_snapshot_message(&message);
self.state.apply(message);
if self.baseline_error.is_none() && full {
self.baseline_error = Some(
PrinterStatus::from_state(self.state.get())
.print_error
.unwrap_or(0),
);
}
let cat = self.cmd.category();
if !self.acked {
let echoed = self
.state
.pointer(&format!("/{cat}/sequence_id"))
.and_then(|v| v.as_str())
== Some(self.seq.as_str());
if let (true, Some(result)) = (
echoed,
self.state
.pointer(&format!("/{cat}/result"))
.and_then(|v| v.as_str()),
) {
if !result.eq_ignore_ascii_case("success") {
let reason = self
.state
.pointer(&format!("/{cat}/reason"))
.and_then(|v| v.as_str())
.unwrap_or(result)
.to_string();
return Some(CommandOutcome::Rejected { reason });
}
self.acked = true;
if !verify::has_observable_effect(&self.cmd) {
return Some(CommandOutcome::Verified);
}
}
}
if self.acked {
let status = PrinterStatus::from_state(self.state.get());
match verify::evaluate(&self.cmd, &status, self.baseline_error) {
EffectStatus::Observed => return Some(CommandOutcome::Verified),
EffectStatus::NewError(code) => {
return Some(CommandOutcome::Rejected {
reason: format!(
"device reported error 0x{code:08X} after the command; \
the effect was not observed (state unchanged)"
),
});
}
EffectStatus::Pending => {}
}
}
None
}
pub fn timed_out(&self) -> CommandOutcome {
CommandOutcome::Unverified {
stage: if self.acked {
VerifyStage::Effect
} else {
VerifyStage::Ack
},
}
}
}
#[cfg(test)]
mod tests {
use super::*;
use crate::core::command::{Command, ProjectFile, SpeedLevel};
use crate::core::fake::FakePrinter;
fn project() -> Command {
Command::ProjectFile(ProjectFile::new("ftp:///model/x.gcode.3mf", 1, "x"))
}
fn run(cmd: Command, seq: &str, msgs: Vec<Value>) -> CommandOutcome {
let mut s = VerifySession::new(cmd, seq);
for m in msgs {
if let Some(o) = s.observe(m) {
return o;
}
}
s.timed_out()
}
#[test]
fn print_start_acked_and_running_is_verified() {
let mut p = FakePrinter::idle();
let msgs = vec![
p.snapshot(),
p.ack(&project(), "1", true, "success"),
p.effect_delta(&project()), ];
assert_eq!(run(project(), "1", msgs), CommandOutcome::Verified);
}
#[test]
fn rejected_ack_is_rejected_with_reason() {
let p = FakePrinter::idle();
let msgs = vec![
p.snapshot(),
p.ack(&project(), "1", false, "print_id error"),
];
assert_eq!(
run(project(), "1", msgs),
CommandOutcome::Rejected {
reason: "print_id error".to_string()
}
);
}
#[test]
fn acked_but_no_effect_times_out_as_unverified_effect() {
let p = FakePrinter::idle();
let msgs = vec![p.snapshot(), p.ack(&project(), "1", true, "success")];
assert_eq!(
run(project(), "1", msgs),
CommandOutcome::Unverified {
stage: VerifyStage::Effect
}
);
}
#[test]
fn never_acked_times_out_as_unverified_ack() {
let p = FakePrinter::idle();
let msgs = vec![p.snapshot()];
assert_eq!(
run(project(), "1", msgs),
CommandOutcome::Unverified {
stage: VerifyStage::Ack
}
);
}
#[test]
fn new_print_error_after_command_is_rejected() {
let mut p = FakePrinter::idle();
let msgs = vec![
p.snapshot(),
p.ack(&project(), "1", true, "success"),
p.new_error_delta(0x0500C010), ];
assert!(matches!(
run(project(), "1", msgs),
CommandOutcome::Rejected { .. }
));
}
#[test]
fn preexisting_error_is_not_blamed_on_the_command() {
let p = FakePrinter::with_error(0x0500C010);
let msgs = vec![p.snapshot(), p.ack(&project(), "1", true, "success")];
assert_eq!(
run(project(), "1", msgs),
CommandOutcome::Unverified {
stage: VerifyStage::Effect
}
);
}
#[test]
fn ack_only_command_is_verified_on_ack_without_effect() {
let cmd = Command::GcodeLine("G28".to_string());
let p = FakePrinter::idle();
let msgs = vec![p.snapshot(), p.ack(&cmd, "1", true, "success")];
assert_eq!(run(cmd, "1", msgs), CommandOutcome::Verified);
}
#[test]
fn effect_verified_via_spd_lvl_for_print_speed() {
let cmd = Command::PrintSpeed(SpeedLevel::Sport);
let mut p = FakePrinter::idle();
let msgs = vec![
p.snapshot(),
p.ack(&cmd, "1", true, "success"),
p.effect_delta(&cmd), ];
assert_eq!(run(cmd, "1", msgs), CommandOutcome::Verified);
}
}