use super::{FilePrinter, PrintOutcome, PrintProgress, PrinterDevice};
use crate::ticket::PrintTicket;
use std::fmt::Debug;
use std::ops::ControlFlow;
use std::panic::{self, AssertUnwindSafe};
use std::path::Path;
use std::process::{Command, Stdio};
use std::thread;
use std::time::{Duration, Instant};
fn print_recording<P>(
printer: &P,
path: &Path,
mut on_progress: impl FnMut(&PrintProgress) -> ControlFlow<()>,
) -> (Result<PrintOutcome, P::Error>, Vec<PrintProgress>)
where
P: FilePrinter<Options = PrintTicket>,
{
let mut reports = Vec::new();
let outcome = printer.print_with_progress(path, Default::default(), &mut |progress| {
reports.push(*progress);
on_progress(progress)
});
(outcome, reports)
}
fn with_print_jobs(device: &PrinterDevice, pipeline: &str) -> String {
let output = Command::new("powershell")
.stdin(Stdio::null())
.args([
"-NoProfile",
"-Command",
&format!(
"Get-PrintJob -PrinterName '{}' | {pipeline}",
device.name().replace('\'', "''")
),
])
.output()
.unwrap();
assert!(output.status.success(), "{pipeline} failed: {output:?}");
String::from_utf8(output.stdout).unwrap()
}
fn remove_print_jobs(device: &PrinterDevice) {
with_print_jobs(device, "Remove-PrintJob");
}
fn pause_printer(device: &PrinterDevice) {
let status = Command::new("powershell")
.stdin(Stdio::null())
.args([
"-NoProfile",
"-Command",
&format!(
"Get-CimInstance -ClassName Win32_Printer -Filter \"Name='{}'\" | Invoke-CimMethod -MethodName Pause",
device.name().replace('\'', "''")
),
])
.status()
.unwrap();
assert!(status.success(), "pausing the printer failed: {status:?}");
}
fn assert_queue_drains(device: &PrinterDevice) {
let deadline = Instant::now() + Duration::from_secs(30);
loop {
let jobs = with_print_jobs(device, "Format-List -Property Id, JobStatus");
if jobs.trim().is_empty() {
return;
}
assert!(
Instant::now() < deadline,
"jobs are left in the queue:\n{jobs}"
);
thread::sleep(Duration::from_secs(1));
}
}
pub fn assert_reports_progress<P>(printer: &P, path: &Path)
where
P: FilePrinter<Options = PrintTicket>,
P::Error: Debug,
{
let mut reported = 0;
let (outcome, reports) = print_recording(printer, path, |_| {
reported += 1;
if reported == 2 {
thread::sleep(Duration::from_secs(2));
}
ControlFlow::Continue(())
});
eprintln!("{reports:?} -> {outcome:?}");
assert_eq!(outcome.unwrap(), PrintOutcome::Completed);
let total = reports.first().expect("progress is reported").total();
assert!(total > 0);
assert!(reports.iter().all(|progress| progress.total() == total));
assert_eq!(reports.first().unwrap().current(), 0);
assert!(reports
.windows(2)
.all(|pair| pair[0].current() <= pair[1].current()));
assert_eq!(reports.last().unwrap().current(), total);
}
pub fn assert_cancels_on_request<P>(printer: &P, device: &PrinterDevice, path: &Path)
where
P: FilePrinter<Options = PrintTicket>,
P::Error: Debug,
{
pause_printer(device);
let (outcome, reports) = print_recording(printer, path, |_| ControlFlow::Break(()));
eprintln!("{reports:?} -> {outcome:?}");
assert_eq!(outcome.unwrap(), PrintOutcome::Canceled);
assert_eq!(reports.len(), 1);
assert_queue_drains(device);
}
pub fn assert_observes_external_cancel<P>(printer: &P, device: &PrinterDevice, path: &Path)
where
P: FilePrinter<Options = PrintTicket>,
P::Error: Debug,
{
let mut removed = false;
let (outcome, reports) = print_recording(printer, path, |progress| {
if !removed && progress.current() > 0 {
removed = true;
remove_print_jobs(device);
}
ControlFlow::Continue(())
});
eprintln!("{reports:?} -> {outcome:?}");
outcome.unwrap();
}
pub fn assert_failure_leaves_no_job<P>(printer: &P, device: &PrinterDevice, path: &Path)
where
P: FilePrinter<Options = PrintTicket>,
{
pause_printer(device);
let failed = panic::catch_unwind(AssertUnwindSafe(|| {
let _ = printer.print_with_progress(path, Default::default(), &mut |progress| {
assert_eq!(progress.current(), 0, "fail after the first page");
ControlFlow::Continue(())
});
}));
assert!(failed.is_err());
assert_queue_drains(device);
}