use std::fmt;
use std::time::Duration;
use crate::wait::condition::WaitCondition;
use crate::{ExitStatus, Snapshot};
#[non_exhaustive]
#[derive(thiserror::Error)]
pub enum WaitError {
#[error("invalid regex pattern '{pattern}': {source}")]
InvalidRegex {
pattern: String,
#[source]
source: crate::InvalidRegexSource,
},
#[error("timed out after {}ms waiting for {condition}", elapsed.as_millis())]
Timeout {
condition: WaitCondition,
elapsed: Duration,
snapshot: Box<Snapshot>,
},
#[error("session closed after {}ms waiting for {condition}", elapsed.as_millis())]
SessionClosed {
condition: WaitCondition,
elapsed: Duration,
snapshot: Box<Snapshot>,
},
#[error("process exited before matching {condition}")]
ProcessExitedBeforeMatch {
condition: WaitCondition,
exit_status: ExitStatus,
snapshot: Box<Snapshot>,
},
#[error("failed to poll exit status: {source}")]
ExitStatus {
snapshot: Box<Snapshot>,
#[source]
source: tastty::Error,
},
#[error("nested any_of is not supported: flatten into a single any_of")]
NestedAnyOf,
#[error(
"stable settle {}ms below floor {}ms (poll {}ms): increase settle or decrease poll",
settle.as_millis(),
floor.as_millis(),
poll.as_millis()
)]
SettleBelowPollFloor {
settle: Duration,
poll: Duration,
floor: Duration,
},
}
const SNAPSHOT_PREVIEW_LINES: usize = 5;
const SNAPSHOT_PREVIEW_LINE_WIDTH: usize = 120;
impl fmt::Debug for WaitError {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
match self {
Self::InvalidRegex { pattern, source } => f
.debug_struct("InvalidRegex")
.field("pattern", pattern)
.field("source", source)
.finish(),
Self::Timeout {
condition,
elapsed,
snapshot,
} => f
.debug_struct("Timeout")
.field("condition", condition)
.field("elapsed", elapsed)
.field("snapshot", &SnapshotPreview(snapshot))
.finish(),
Self::SessionClosed {
condition,
elapsed,
snapshot,
} => f
.debug_struct("SessionClosed")
.field("condition", condition)
.field("elapsed", elapsed)
.field("snapshot", &SnapshotPreview(snapshot))
.finish(),
Self::ProcessExitedBeforeMatch {
condition,
exit_status,
snapshot,
} => f
.debug_struct("ProcessExitedBeforeMatch")
.field("condition", condition)
.field("exit_status", exit_status)
.field("snapshot", &SnapshotPreview(snapshot))
.finish(),
Self::ExitStatus { snapshot, source } => f
.debug_struct("ExitStatus")
.field("snapshot", &SnapshotPreview(snapshot))
.field("source", source)
.finish(),
Self::NestedAnyOf => f.debug_struct("NestedAnyOf").finish(),
Self::SettleBelowPollFloor {
settle,
poll,
floor,
} => f
.debug_struct("SettleBelowPollFloor")
.field("settle", settle)
.field("poll", poll)
.field("floor", floor)
.finish(),
}
}
}
struct SnapshotPreview<'a>(&'a Snapshot);
impl fmt::Debug for SnapshotPreview<'_> {
fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result {
let preview: Vec<String> = self
.0
.lines()
.into_iter()
.filter_map(|line| {
let trimmed = line.trim_end();
if trimmed.is_empty() {
None
} else {
Some(truncate_chars(trimmed, SNAPSHOT_PREVIEW_LINE_WIDTH))
}
})
.take(SNAPSHOT_PREVIEW_LINES)
.collect();
f.debug_struct("Snapshot")
.field("size", &self.0.size())
.field("cursor", &self.0.cursor())
.field("lines", &preview)
.finish()
}
}
fn truncate_chars(s: &str, max_chars: usize) -> String {
if s.chars().count() <= max_chars {
return s.to_string();
}
let mut out: String = s.chars().take(max_chars.saturating_sub(3)).collect();
out.push_str("...");
out
}
#[cfg(test)]
mod tests {
use super::*;
use crate::snapshot::snapshot_from_screen;
use tastty::{Parser, TerminalSize};
fn build_snapshot(rows: u16, cols: u16, fill: u8) -> Box<Snapshot> {
let mut parser = Parser::new(TerminalSize { rows, cols }, 0);
let payload = vec![fill; rows as usize * cols as usize];
parser.process(&payload);
Box::new(snapshot_from_screen(parser.screen()))
}
#[test]
fn debug_for_timeout_bounds_snapshot_output() {
let err = WaitError::Timeout {
condition: WaitCondition::text("ready"),
elapsed: Duration::from_millis(250),
snapshot: build_snapshot(100, 200, b'X'),
};
let formatted = format!("{err:?}");
assert!(
formatted.len() < 4096,
"Debug output should be under 4 KB but was {} bytes",
formatted.len()
);
assert!(formatted.contains("Timeout"));
assert!(formatted.contains("Snapshot"));
assert!(formatted.contains("size"));
assert!(formatted.contains("cursor"));
}
#[test]
fn debug_for_nested_any_of_renders_unit_variant() {
let formatted = format!("{:?}", WaitError::NestedAnyOf);
assert_eq!(formatted, "NestedAnyOf");
}
#[test]
fn debug_for_settle_below_poll_floor_renders_all_fields() {
let err = WaitError::SettleBelowPollFloor {
settle: Duration::from_millis(10),
poll: Duration::from_millis(50),
floor: Duration::from_millis(150),
};
let formatted = format!("{err:?}");
assert!(formatted.contains("SettleBelowPollFloor"));
assert!(formatted.contains("settle"));
assert!(formatted.contains("poll"));
assert!(formatted.contains("floor"));
}
#[test]
fn truncate_chars_appends_ellipsis_when_over_limit() {
let truncated = truncate_chars(&"a".repeat(200), 50);
assert_eq!(truncated.chars().count(), 50);
assert!(truncated.ends_with("..."));
}
#[test]
fn truncate_chars_passes_through_short_input() {
assert_eq!(truncate_chars("hello", 50), "hello");
}
}