diff options
author | Jesse Luehrs <doy@tozt.net> | 2019-12-03 06:26:48 -0500 |
---|---|---|
committer | Jesse Luehrs <doy@tozt.net> | 2019-12-06 21:26:07 -0500 |
commit | 9dffd607455fac176a2eece2571d3c49901175f4 (patch) | |
tree | 768af30b2787ac3e012ca6d6d5362f5efcc5a66e /tests/helpers | |
parent | b815e4dabbc3d8bb61c569126c54f18e20bce6ec (diff) | |
download | vt100-rust-9dffd607455fac176a2eece2571d3c49901175f4.tar.gz vt100-rust-9dffd607455fac176a2eece2571d3c49901175f4.zip |
start restructuring the test suite
Diffstat (limited to 'tests/helpers')
-rw-r--r-- | tests/helpers/fixtures.rs | 320 | ||||
-rw-r--r-- | tests/helpers/mod.rs | 216 |
2 files changed, 536 insertions, 0 deletions
diff --git a/tests/helpers/fixtures.rs b/tests/helpers/fixtures.rs new file mode 100644 index 0000000..8f9bfdb --- /dev/null +++ b/tests/helpers/fixtures.rs @@ -0,0 +1,320 @@ +use serde::de::Deserialize as _; +use std::io::Read as _; + +#[derive(Clone, Debug, Default, serde::Deserialize, serde::Serialize)] +pub struct FixtureCell { + contents: String, + #[serde(default, skip_serializing_if = "is_default")] + is_wide: bool, + #[serde(default, skip_serializing_if = "is_default")] + is_wide_continuation: bool, + #[serde( + default, + deserialize_with = "deserialize_color", + serialize_with = "serialize_color", + skip_serializing_if = "is_default" + )] + fgcolor: vt100::Color, + #[serde( + default, + deserialize_with = "deserialize_color", + serialize_with = "serialize_color", + skip_serializing_if = "is_default" + )] + bgcolor: vt100::Color, + #[serde(default, skip_serializing_if = "is_default")] + bold: bool, + #[serde(default, skip_serializing_if = "is_default")] + italic: bool, + #[serde(default, skip_serializing_if = "is_default")] + underline: bool, + #[serde(default, skip_serializing_if = "is_default")] + inverse: bool, +} + +impl FixtureCell { + pub fn from_cell(cell: &vt100::Cell) -> Self { + Self { + contents: cell.contents(), + is_wide: cell.is_wide(), + is_wide_continuation: cell.is_wide_continuation(), + fgcolor: cell.fgcolor(), + bgcolor: cell.bgcolor(), + bold: cell.bold(), + italic: cell.italic(), + underline: cell.underline(), + inverse: cell.inverse(), + } + } +} + +#[derive(Debug, serde::Deserialize, serde::Serialize)] +pub struct FixtureScreen { + contents: String, + cells: std::collections::BTreeMap<String, FixtureCell>, + cursor_position: (u16, u16), + #[serde(default, skip_serializing_if = "is_default")] + title: String, + #[serde(default, skip_serializing_if = "is_default")] + icon_name: String, + #[serde(default, skip_serializing_if = "is_default")] + application_keypad: bool, + #[serde(default, skip_serializing_if = "is_default")] + application_cursor: bool, + #[serde(default, skip_serializing_if = "is_default")] + hide_cursor: bool, + #[serde(default, skip_serializing_if = "is_default")] + bracketed_paste: bool, + #[serde( + default, + deserialize_with = "deserialize_mouse_protocol_mode", + serialize_with = "serialize_mouse_protocol_mode", + skip_serializing_if = "is_default" + )] + mouse_protocol_mode: vt100::MouseProtocolMode, + #[serde( + default, + deserialize_with = "deserialize_mouse_protocol_encoding", + serialize_with = "serialize_mouse_protocol_encoding", + skip_serializing_if = "is_default" + )] + mouse_protocol_encoding: vt100::MouseProtocolEncoding, +} + +impl FixtureScreen { + fn load<R: std::io::Read>(r: R) -> Self { + serde_json::from_reader(r).unwrap() + } + + #[allow(dead_code)] + pub fn from_screen(screen: &vt100::Screen) -> Self { + let mut cells = std::collections::BTreeMap::new(); + let (rows, cols) = screen.size(); + for row in 0..rows { + for col in 0..cols { + let cell = screen.cell(row, col).unwrap(); + if cell != &vt100::Cell::default() { + cells.insert( + format!("{},{}", row, col), + FixtureCell::from_cell(cell), + ); + } + } + } + Self { + contents: screen.contents(), + cells, + cursor_position: screen.cursor_position(), + title: screen.title().to_string(), + icon_name: screen.icon_name().to_string(), + application_keypad: screen.application_keypad(), + application_cursor: screen.application_cursor(), + hide_cursor: screen.hide_cursor(), + bracketed_paste: screen.bracketed_paste(), + mouse_protocol_mode: screen.mouse_protocol_mode(), + mouse_protocol_encoding: screen.mouse_protocol_encoding(), + } + } +} + +fn is_default<T: Default + PartialEq>(t: &T) -> bool { + t == &T::default() +} + +fn deserialize_color<'a, D>( + deserializer: D, +) -> std::result::Result<vt100::Color, D::Error> +where + D: serde::de::Deserializer<'a>, +{ + let val = <Option<String>>::deserialize(deserializer)?; + match val { + None => Ok(vt100::Color::Default), + Some(x) if x.starts_with('#') => { + let x = x.as_bytes(); + if x.len() != 7 { + return Err(serde::de::Error::custom("invalid rgb color")); + } + let r = + super::hex(x[1], x[2]).map_err(serde::de::Error::custom)?; + let g = + super::hex(x[3], x[4]).map_err(serde::de::Error::custom)?; + let b = + super::hex(x[5], x[6]).map_err(serde::de::Error::custom)?; + Ok(vt100::Color::Rgb(r, g, b)) + } + Some(x) => Ok(vt100::Color::Idx( + x.parse().map_err(serde::de::Error::custom)?, + )), + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_color<S>( + color: &vt100::Color, + serializer: S, +) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + let s = match color { + vt100::Color::Default => unreachable!(), + vt100::Color::Idx(n) => format!("{}", n), + vt100::Color::Rgb(r, g, b) => format!("#{:02x}{:02x}{:02x}", r, g, b), + }; + serializer.serialize_str(&s) +} + +fn deserialize_mouse_protocol_mode<'a, D>( + deserializer: D, +) -> std::result::Result<vt100::MouseProtocolMode, D::Error> +where + D: serde::de::Deserializer<'a>, +{ + let name = <String>::deserialize(deserializer)?; + match name.as_ref() { + "none" => Ok(vt100::MouseProtocolMode::None), + "press" => Ok(vt100::MouseProtocolMode::Press), + "press_release" => Ok(vt100::MouseProtocolMode::PressRelease), + "button_motion" => Ok(vt100::MouseProtocolMode::ButtonMotion), + "any_motion" => Ok(vt100::MouseProtocolMode::AnyMotion), + _ => unimplemented!(), + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_mouse_protocol_mode<S>( + mode: &vt100::MouseProtocolMode, + serializer: S, +) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + let s = match mode { + vt100::MouseProtocolMode::None => "none", + vt100::MouseProtocolMode::Press => "press", + vt100::MouseProtocolMode::PressRelease => "press_release", + vt100::MouseProtocolMode::ButtonMotion => "button_motion", + vt100::MouseProtocolMode::AnyMotion => "any_motion", + }; + serializer.serialize_str(s) +} + +fn deserialize_mouse_protocol_encoding<'a, D>( + deserializer: D, +) -> std::result::Result<vt100::MouseProtocolEncoding, D::Error> +where + D: serde::de::Deserializer<'a>, +{ + let name = <String>::deserialize(deserializer)?; + match name.as_ref() { + "default" => Ok(vt100::MouseProtocolEncoding::Default), + "utf8" => Ok(vt100::MouseProtocolEncoding::Utf8), + "sgr" => Ok(vt100::MouseProtocolEncoding::Sgr), + _ => unimplemented!(), + } +} + +#[allow(clippy::trivially_copy_pass_by_ref)] +fn serialize_mouse_protocol_encoding<S>( + encoding: &vt100::MouseProtocolEncoding, + serializer: S, +) -> Result<S::Ok, S::Error> +where + S: serde::Serializer, +{ + let s = match encoding { + vt100::MouseProtocolEncoding::Default => "default", + vt100::MouseProtocolEncoding::Utf8 => "utf8", + vt100::MouseProtocolEncoding::Sgr => "sgr", + }; + serializer.serialize_str(s) +} + +fn load_input(name: &str, i: usize) -> Option<Vec<u8>> { + let mut file = std::fs::File::open(format!( + "tests/data/fixtures/{}/{}.typescript", + name, i + )) + .ok()?; + let mut input = vec![]; + file.read_to_end(&mut input).unwrap(); + Some(input) +} + +fn load_screen(name: &str, i: usize) -> Option<FixtureScreen> { + let mut file = std::fs::File::open(format!( + "tests/data/fixtures/{}/{}.json", + name, i + )) + .ok()?; + Some(FixtureScreen::load(&mut file)) +} + +fn assert_produces(input: &[u8], expected: &FixtureScreen) { + let mut parser = vt100::Parser::default(); + parser.process(input); + + assert_eq!(parser.screen().contents(), expected.contents); + assert_eq!(parser.screen().cursor_position(), expected.cursor_position); + assert_eq!(parser.screen().title(), expected.title); + assert_eq!(parser.screen().icon_name(), expected.icon_name); + assert_eq!( + parser.screen().application_keypad(), + expected.application_keypad + ); + assert_eq!( + parser.screen().application_cursor(), + expected.application_cursor + ); + assert_eq!(parser.screen().hide_cursor(), expected.hide_cursor); + assert_eq!(parser.screen().bracketed_paste(), expected.bracketed_paste); + assert_eq!( + parser.screen().mouse_protocol_mode(), + expected.mouse_protocol_mode + ); + assert_eq!( + parser.screen().mouse_protocol_encoding(), + expected.mouse_protocol_encoding + ); + + let (rows, cols) = parser.screen().size(); + for row in 0..rows { + for col in 0..cols { + let expected_cell = expected + .cells + .get(&format!("{},{}", row, col)) + .cloned() + .unwrap_or_else(FixtureCell::default); + let got_cell = parser.screen().cell(row, col).unwrap(); + assert_eq!(got_cell.contents(), expected_cell.contents); + assert_eq!(got_cell.is_wide(), expected_cell.is_wide); + assert_eq!( + got_cell.is_wide_continuation(), + expected_cell.is_wide_continuation + ); + assert_eq!(got_cell.fgcolor(), expected_cell.fgcolor); + assert_eq!(got_cell.bgcolor(), expected_cell.bgcolor); + assert_eq!(got_cell.bold(), expected_cell.bold); + assert_eq!(got_cell.italic(), expected_cell.italic); + assert_eq!(got_cell.underline(), expected_cell.underline); + assert_eq!(got_cell.inverse(), expected_cell.inverse); + } + } +} + +#[allow(dead_code)] +pub fn fixture(name: &str) { + let mut i = 1; + let mut prev_input = vec![]; + while let Some(input) = load_input(name, i) { + super::assert_reproduces_state_from(&input, &prev_input); + prev_input.extend(input); + + let expected = load_screen(name, i).unwrap(); + assert_produces(&prev_input, &expected); + + i += 1; + } + assert!(i > 1, "couldn't find fixtures to test"); +} diff --git a/tests/helpers/mod.rs b/tests/helpers/mod.rs new file mode 100644 index 0000000..8ec0ac3 --- /dev/null +++ b/tests/helpers/mod.rs @@ -0,0 +1,216 @@ +mod fixtures; +pub use fixtures::fixture; +pub use fixtures::FixtureScreen; + +macro_rules! is { + ($got:expr, $expected:expr) => { + if ($got) != ($expected) { + eprintln!("{} != {}:", stringify!($got), stringify!($expected)); + eprintln!(" got: {:?}", $got); + eprintln!("expected: {:?}", $expected); + return false; + } + }; +} +macro_rules! ok { + ($e:expr) => { + if !($e) { + eprintln!("!{}", stringify!($e)); + return false; + } + }; +} + +pub fn compare_screens( + got: &vt100::Screen, + expected: &vt100::Screen, +) -> bool { + is!(got.contents(), expected.contents()); + is!(got.contents_formatted(), expected.contents_formatted()); + is!( + got.contents_diff(vt100::Parser::default().screen()), + expected.contents_diff(vt100::Parser::default().screen()) + ); + + let (rows, cols) = got.size(); + + for row in 0..rows { + for col in 0..cols { + let expected_cell = expected.cell(row, col); + let got_cell = got.cell(row, col); + is!(got_cell, expected_cell); + } + } + + is!(got.cursor_position(), expected.cursor_position()); + ok!(got.cursor_position().0 <= rows); + ok!(expected.cursor_position().0 <= rows); + ok!(got.cursor_position().1 <= cols); + ok!(expected.cursor_position().1 <= cols); + + is!(got.title(), expected.title()); + is!(got.icon_name(), expected.icon_name()); + + is!( + got.audible_bell_count() > 0, + expected.audible_bell_count() > 0 + ); + is!( + got.visual_bell_count() > 0, + expected.visual_bell_count() > 0 + ); + + is!(got.application_keypad(), expected.application_keypad()); + is!(got.application_cursor(), expected.application_cursor()); + is!(got.hide_cursor(), expected.hide_cursor()); + is!(got.bracketed_paste(), expected.bracketed_paste()); + is!(got.mouse_protocol_mode(), expected.mouse_protocol_mode()); + is!( + got.mouse_protocol_encoding(), + expected.mouse_protocol_encoding() + ); + + true +} + +#[allow(dead_code)] +pub fn contents_formatted_reproduces_state(input: &[u8]) -> bool { + let mut parser = vt100::Parser::default(); + parser.process(input); + contents_formatted_reproduces_screen(parser.screen()) +} + +pub fn contents_formatted_reproduces_screen(screen: &vt100::Screen) -> bool { + let empty_screen = vt100::Parser::default().screen().clone(); + + let mut new_input = screen.contents_formatted(); + new_input.extend(screen.input_mode_formatted()); + new_input.extend(screen.title_formatted()); + new_input.extend(screen.bells_diff(&empty_screen)); + let mut new_parser = vt100::Parser::default(); + new_parser.process(&new_input); + let got_screen = new_parser.screen().clone(); + + compare_screens(&got_screen, &screen) +} + +fn assert_contents_formatted_reproduces_state(input: &[u8]) { + assert!(contents_formatted_reproduces_state(input)); +} + +#[allow(dead_code)] +pub fn contents_diff_reproduces_state(input: &[u8]) -> bool { + contents_diff_reproduces_state_from(input, &[]) +} + +pub fn contents_diff_reproduces_state_from( + input: &[u8], + prev_input: &[u8], +) -> bool { + let mut parser = vt100::Parser::default(); + parser.process(prev_input); + let prev_screen = parser.screen().clone(); + parser.process(input); + + contents_diff_reproduces_state_from_screens(&prev_screen, parser.screen()) +} + +pub fn contents_diff_reproduces_state_from_screens( + prev_screen: &vt100::Screen, + screen: &vt100::Screen, +) -> bool { + let mut diff_input = screen.contents_diff(&prev_screen); + diff_input.extend(screen.input_mode_diff(&prev_screen)); + diff_input.extend(screen.title_diff(&prev_screen)); + diff_input.extend(screen.bells_diff(&prev_screen)); + + let mut diff_prev_input = prev_screen.contents_formatted(); + diff_prev_input.extend(screen.input_mode_formatted()); + diff_prev_input.extend(screen.title_formatted()); + diff_prev_input + .extend(screen.bells_diff(vt100::Parser::default().screen())); + + let mut new_parser = vt100::Parser::default(); + new_parser.process(&diff_prev_input); + new_parser.process(&diff_input); + let got_screen = new_parser.screen().clone(); + + compare_screens(&got_screen, &screen) +} + +#[allow(dead_code)] +pub fn assert_contents_diff_reproduces_state_from_screens( + prev_screen: &vt100::Screen, + screen: &vt100::Screen, +) { + assert!(contents_diff_reproduces_state_from_screens( + prev_screen, + screen, + )); +} + +fn assert_contents_diff_reproduces_state_from( + input: &[u8], + prev_input: &[u8], +) { + assert!(contents_diff_reproduces_state_from(input, prev_input)); +} + +#[allow(dead_code)] +pub fn assert_reproduces_state(input: &[u8]) { + assert_reproduces_state_from(input, &[]); +} + +pub fn assert_reproduces_state_from(input: &[u8], prev_input: &[u8]) { + let full_input: Vec<_> = + prev_input.iter().chain(input.iter()).copied().collect(); + assert_contents_formatted_reproduces_state(&full_input); + assert_contents_diff_reproduces_state_from(input, prev_input); +} + +#[allow(dead_code)] +pub fn format_bytes(bytes: &[u8]) -> String { + let mut v = vec![]; + for b in bytes { + match *b { + 10 => v.extend(b"\\n"), + 13 => v.extend(b"\\r"), + 27 => v.extend(b"\\e"), + c if c < 32 => v.extend(format!("\\x{:02x}", c).as_bytes()), + b => v.push(b), + } + } + String::from_utf8_lossy(&v).to_string() +} + +fn hex_char(c: u8) -> Result<u8, String> { + match c { + b'0' => Ok(0), + b'1' => Ok(1), + b'2' => Ok(2), + b'3' => Ok(3), + b'4' => Ok(4), + b'5' => Ok(5), + b'6' => Ok(6), + b'7' => Ok(7), + b'8' => Ok(8), + b'9' => Ok(9), + b'a' => Ok(10), + b'b' => Ok(11), + b'c' => Ok(12), + b'd' => Ok(13), + b'e' => Ok(14), + b'f' => Ok(15), + b'A' => Ok(10), + b'B' => Ok(11), + b'C' => Ok(12), + b'D' => Ok(13), + b'E' => Ok(14), + b'F' => Ok(15), + _ => Err("invalid hex char".to_string()), + } +} + +pub fn hex(upper: u8, lower: u8) -> Result<u8, String> { + Ok(hex_char(upper)? * 16 + hex_char(lower)?) +} |