use crate::render::Buffer;
#[derive(Debug, Clone)]
pub enum AssertionResult {
Pass,
Fail(String),
}
impl AssertionResult {
pub fn is_pass(&self) -> bool {
matches!(self, AssertionResult::Pass)
}
pub fn is_fail(&self) -> bool {
matches!(self, AssertionResult::Fail(_))
}
pub fn unwrap(self) {
if let AssertionResult::Fail(msg) = self {
panic!("Assertion failed: {}", msg);
}
}
}
pub trait Assertion {
fn check(&self, buffer: &Buffer) -> AssertionResult;
fn description(&self) -> String;
}
#[cfg(test)]
pub struct ContainsText {
text: String,
}
#[cfg(test)]
impl ContainsText {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
#[cfg(test)]
impl Assertion for ContainsText {
fn check(&self, buffer: &Buffer) -> AssertionResult {
let screen = buffer_to_string(buffer);
if screen.contains(&self.text) {
AssertionResult::Pass
} else {
AssertionResult::Fail(format!(
"Expected screen to contain '{}', but it didn't.\nScreen:\n{}",
self.text, screen
))
}
}
fn description(&self) -> String {
format!("Screen contains '{}'", self.text)
}
}
#[cfg(test)]
pub struct NotContainsText {
text: String,
}
#[cfg(test)]
impl NotContainsText {
pub fn new(text: impl Into<String>) -> Self {
Self { text: text.into() }
}
}
#[cfg(test)]
impl Assertion for NotContainsText {
fn check(&self, buffer: &Buffer) -> AssertionResult {
let screen = buffer_to_string(buffer);
if !screen.contains(&self.text) {
AssertionResult::Pass
} else {
AssertionResult::Fail(format!(
"Expected screen NOT to contain '{}', but it did.\nScreen:\n{}",
self.text, screen
))
}
}
fn description(&self) -> String {
format!("Screen does not contain '{}'", self.text)
}
}
#[cfg(test)]
pub struct LineContains {
line: u16,
text: String,
}
#[cfg(test)]
impl LineContains {
pub fn new(line: u16, text: impl Into<String>) -> Self {
Self {
line,
text: text.into(),
}
}
}
#[cfg(test)]
impl Assertion for LineContains {
fn check(&self, buffer: &Buffer) -> AssertionResult {
let line_text = get_line(buffer, self.line);
if line_text.contains(&self.text) {
AssertionResult::Pass
} else {
AssertionResult::Fail(format!(
"Expected line {} to contain '{}', but got: '{}'",
self.line, self.text, line_text
))
}
}
fn description(&self) -> String {
format!("Line {} contains '{}'", self.line, self.text)
}
}
#[cfg(test)]
pub struct CellEquals {
x: u16,
y: u16,
expected: char,
}
#[cfg(test)]
impl CellEquals {
pub fn new(x: u16, y: u16, expected: char) -> Self {
Self { x, y, expected }
}
}
#[cfg(test)]
impl Assertion for CellEquals {
fn check(&self, buffer: &Buffer) -> AssertionResult {
if let Some(cell) = buffer.get(self.x, self.y) {
if cell.symbol == self.expected {
AssertionResult::Pass
} else {
AssertionResult::Fail(format!(
"Expected cell ({}, {}) to be '{}', but got '{}'",
self.x, self.y, self.expected, cell.symbol
))
}
} else {
AssertionResult::Fail(format!("Cell ({}, {}) is out of bounds", self.x, self.y))
}
}
fn description(&self) -> String {
format!("Cell ({}, {}) equals '{}'", self.x, self.y, self.expected)
}
}
#[cfg(test)]
pub struct ScreenEquals {
expected: String,
}
#[cfg(test)]
impl ScreenEquals {
pub fn new(expected: impl Into<String>) -> Self {
Self {
expected: expected.into(),
}
}
}
#[cfg(test)]
impl Assertion for ScreenEquals {
fn check(&self, buffer: &Buffer) -> AssertionResult {
let actual = buffer_to_string(buffer);
let expected_trimmed = self.expected.trim();
let actual_trimmed = actual.trim();
if actual_trimmed == expected_trimmed {
AssertionResult::Pass
} else {
AssertionResult::Fail(format!(
"Screen does not match expected.\nExpected:\n{}\n\nActual:\n{}",
expected_trimmed, actual_trimmed
))
}
}
fn description(&self) -> String {
"Screen matches expected text".to_string()
}
}
#[cfg(test)]
fn buffer_to_string(buffer: &Buffer) -> String {
let mut lines = Vec::new();
for y in 0..buffer.height() {
let mut line = String::new();
for x in 0..buffer.width() {
if let Some(cell) = buffer.get(x, y) {
line.push(cell.symbol);
} else {
line.push(' ');
}
}
lines.push(line.trim_end().to_string());
}
while lines.last().map(|l| l.is_empty()).unwrap_or(false) {
lines.pop();
}
lines.join("\n")
}
#[cfg(test)]
fn get_line(buffer: &Buffer, row: u16) -> String {
if row >= buffer.height() {
return String::new();
}
let mut line = String::new();
for x in 0..buffer.width() {
if let Some(cell) = buffer.get(x, row) {
line.push(cell.symbol);
} else {
line.push(' ');
}
}
line.trim_end().to_string()
}
#[cfg(test)]
mod tests {
use super::*;
use crate::render::Cell;
fn make_buffer(text: &str) -> Buffer {
let lines: Vec<&str> = text.lines().collect();
let height = lines.len() as u16;
let width = lines.iter().map(|l| l.len()).max().unwrap_or(0) as u16;
let mut buffer = Buffer::new(width.max(1), height.max(1));
for (y, line) in lines.iter().enumerate() {
for (x, ch) in line.chars().enumerate() {
buffer.set(x as u16, y as u16, Cell::new(ch));
}
}
buffer
}
#[test]
fn test_contains_text_pass() {
let buffer = make_buffer("Hello, World!");
let assertion = ContainsText::new("World");
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_contains_text_fail() {
let buffer = make_buffer("Hello, World!");
let assertion = ContainsText::new("Goodbye");
assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_not_contains_text() {
let buffer = make_buffer("Hello, World!");
let assertion = NotContainsText::new("Goodbye");
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_line_contains() {
let buffer = make_buffer("Line 1\nLine 2\nLine 3");
let assertion = LineContains::new(1, "Line 2");
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_cell_equals() {
let buffer = make_buffer("ABC");
let assertion = CellEquals::new(1, 0, 'B');
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_assertion_result_clone() {
let result = AssertionResult::Pass;
let cloned = result.clone();
assert!(cloned.is_pass());
let fail = AssertionResult::Fail("error".to_string());
let fail_cloned = fail.clone();
assert!(fail_cloned.is_fail());
}
#[test]
fn test_assertion_result_debug() {
let pass = AssertionResult::Pass;
let debug = format!("{:?}", pass);
assert!(debug.contains("Pass"));
let fail = AssertionResult::Fail("test error".to_string());
let debug = format!("{:?}", fail);
assert!(debug.contains("Fail"));
assert!(debug.contains("test error"));
}
#[test]
#[should_panic(expected = "Assertion failed")]
fn test_assertion_result_unwrap_fail() {
let result = AssertionResult::Fail("test failure".to_string());
result.unwrap();
}
#[test]
fn test_assertion_result_unwrap_pass() {
let result = AssertionResult::Pass;
result.unwrap(); }
#[test]
fn test_contains_text_description() {
let assertion = ContainsText::new("test");
assert_eq!(assertion.description(), "Screen contains 'test'");
}
#[test]
fn test_not_contains_text_description() {
let assertion = NotContainsText::new("test");
assert_eq!(assertion.description(), "Screen does not contain 'test'");
}
#[test]
fn test_not_contains_text_fail() {
let buffer = make_buffer("Hello, World!");
let assertion = NotContainsText::new("Hello");
assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_line_contains_description() {
let assertion = LineContains::new(5, "text");
assert_eq!(assertion.description(), "Line 5 contains 'text'");
}
#[test]
fn test_line_contains_fail() {
let buffer = make_buffer("Line 1\nLine 2\nLine 3");
let assertion = LineContains::new(1, "foo");
assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_line_contains_out_of_bounds() {
let buffer = make_buffer("Line 1\nLine 2");
let assertion = LineContains::new(10, "text"); assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_cell_equals_description() {
let assertion = CellEquals::new(5, 10, 'X');
assert_eq!(assertion.description(), "Cell (5, 10) equals 'X'");
}
#[test]
fn test_cell_equals_fail() {
let buffer = make_buffer("ABC");
let assertion = CellEquals::new(0, 0, 'Z');
assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_cell_equals_out_of_bounds() {
let buffer = make_buffer("ABC");
let assertion = CellEquals::new(100, 100, 'X');
let result = assertion.check(&buffer);
assert!(result.is_fail());
}
#[test]
fn test_screen_equals_pass() {
let buffer = make_buffer("Hello\nWorld");
let assertion = ScreenEquals::new("Hello\nWorld");
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_screen_equals_fail() {
let buffer = make_buffer("Hello\nWorld");
let assertion = ScreenEquals::new("Goodbye\nWorld");
assert!(assertion.check(&buffer).is_fail());
}
#[test]
fn test_screen_equals_description() {
let assertion = ScreenEquals::new("test");
assert_eq!(assertion.description(), "Screen matches expected text");
}
#[test]
fn test_screen_equals_trims_whitespace() {
let buffer = make_buffer("Hello");
let assertion = ScreenEquals::new(" Hello ");
assert!(assertion.check(&buffer).is_pass());
}
#[test]
fn test_buffer_to_string_empty() {
let buffer = Buffer::new(5, 5);
let s = buffer_to_string(&buffer);
assert!(s.is_empty() || s.chars().all(|c| c.is_whitespace() || c == '\n'));
}
#[test]
fn test_get_line_out_of_bounds() {
let buffer = Buffer::new(10, 5);
let line = get_line(&buffer, 100);
assert!(line.is_empty());
}
#[test]
fn test_multiline_buffer() {
let buffer = make_buffer("Line A\nLine B\nLine C");
let assertion = ContainsText::new("Line B");
assert!(assertion.check(&buffer).is_pass());
let assertion = LineContains::new(0, "Line A");
assert!(assertion.check(&buffer).is_pass());
let assertion = LineContains::new(2, "Line C");
assert!(assertion.check(&buffer).is_pass());
}
}