From f336a9727676cdcb65cfbad7f227c474aa5596de Mon Sep 17 00:00:00 2001 From: neokkk Date: Sun, 24 May 2026 22:47:44 +0900 Subject: [PATCH] test: add terminal console tests --- bracket-terminal/Cargo.toml | 2 + bracket-terminal/src/consoles/console.rs | 100 +++++ .../src/consoles/flexible_console.rs | 332 ++++++++++++++++ .../src/consoles/simple_console.rs | 298 ++++++++++++++ .../src/consoles/sparse_console.rs | 366 ++++++++++++++++++ .../src/consoles/sprite_console.rs | 249 ++++++++++++ .../src/consoles/virtual_console.rs | 357 ++++++++++++++++- bracket-terminal/src/lib.rs | 4 + bracket-terminal/src/test_utils.rs | 144 +++++++ 9 files changed, 1846 insertions(+), 6 deletions(-) create mode 100644 bracket-terminal/src/test_utils.rs diff --git a/bracket-terminal/Cargo.toml b/bracket-terminal/Cargo.toml index 5f60799..3f0809c 100644 --- a/bracket-terminal/Cargo.toml +++ b/bracket-terminal/Cargo.toml @@ -52,6 +52,8 @@ webgpu = [ "wgpu", "pollster", "image", "bytemuck", "png" ] bracket-random = { path = "../bracket-random", version = "~0.8" } bracket-pathfinding = { path = "../bracket-pathfinding", version = "~0.8" } bracket-noise = { path = "../bracket-noise", version = "~0.8" } +mockall = "0.14.0" +rstest = "0.26.1" # criterion pulls in rayon, which fails to compile on wasm32. Benches don't run # on wasm anyway, so scope this to non-wasm targets. diff --git a/bracket-terminal/src/consoles/console.rs b/bracket-terminal/src/consoles/console.rs index dba1875..71e2cd9 100755 --- a/bracket-terminal/src/consoles/console.rs +++ b/bracket-terminal/src/consoles/console.rs @@ -4,6 +4,9 @@ use bracket_geometry::prelude::{Point, Rect}; use bracket_rex::prelude::XpLayer; use std::any::Any; +#[cfg(test)] +use mockall::automock; + /// The internal storage type for tiles in a simple console. #[derive(PartialEq, Copy, Clone, Debug)] pub struct Tile { @@ -25,6 +28,7 @@ pub enum CharacterTranslationMode { Unicode, } +#[cfg_attr(test, automock)] /// Trait that must be implemented by console types. pub trait Console { /// Gets the dimensions of the console in characters @@ -206,3 +210,99 @@ pub trait Console { pub fn log(message: S) { crate::hal::log(&message.to_string()); } + +#[cfg(test)] +mod tests { + use super::*; + use crate::test_utils::TestConsole; + use rstest::rstest; + + #[rstest] + #[case(0, 0, true)] + #[case(79, 0, true)] + #[case(0, 49, true)] + #[case(79, 49, true)] + #[case(40, 25, true)] + #[case(-1, 0, false)] + #[case(0, -1, false)] + #[case(80, 0, false)] + #[case(0, 50, false)] + #[case(80, 50, false)] + fn in_bounds_without_clipping(#[case] x: i32, #[case] y: i32, #[case] expected: bool) { + let console = TestConsole::new(80, 50); + assert_eq!(console.in_bounds(x, y), expected); + } + + #[rstest] + #[case(10, 10, true)] + #[case(29, 10, true)] + #[case(10, 24, true)] + #[case(29, 24, true)] + #[case(20, 15, true)] + #[case(9, 10, false)] + #[case(10, 9, false)] + #[case(30, 10, false)] + #[case(10, 25, false)] + #[case(30, 25, false)] + fn in_bounds_respects_clipping_rectangle( + #[case] x: i32, + #[case] y: i32, + #[case] expected: bool, + ) { + let console = TestConsole::new(80, 50).with_clipping(Rect::with_size(10, 10, 20, 15)); + assert_eq!(console.in_bounds(x, y), expected); + } + + #[rstest] + #[case(0, 0, true)] + #[case(79, 49, true)] + #[case(-1, 0, false)] + #[case(0, -1, false)] + #[case(80, 0, false)] + #[case(0, 50, false)] + fn in_bounds_with_large_clipping_still_respects_console_bounds( + #[case] x: i32, + #[case] y: i32, + #[case] expected: bool, + ) { + let console = TestConsole::new(80, 50).with_clipping(Rect::with_size(-10, -10, 100, 100)); + assert_eq!(console.in_bounds(x, y), expected); + } + + #[rstest] + #[case(0, 0, Some(0))] + #[case(1, 0, Some(1))] + #[case(79, 0, Some(79))] + #[case(0, 1, Some(80))] + #[case(10, 2, Some(170))] + #[case(79, 49, Some(3999))] + #[case(-1, 0, None)] + #[case(0, -1, None)] + #[case(80, 0, None)] + #[case(0, 50, None)] + #[case(80, 50, None)] + fn try_at_without_clipping(#[case] x: i32, #[case] y: i32, #[case] expected: Option) { + let console = TestConsole::new(80, 50); + assert_eq!(console.try_at(x, y), expected); + } + + #[rstest] + #[case(10, 10, Some(810))] + #[case(29, 10, Some(829))] + #[case(10, 24, Some(1930))] + #[case(29, 24, Some(1949))] + #[case(20, 15, Some(1220))] + #[case(9, 10, None)] + #[case(10, 9, None)] + #[case(30, 10, None)] + #[case(10, 25, None)] + #[case(30, 25, None)] + fn try_at_respects_clipping_rectangle( + #[case] x: i32, + #[case] y: i32, + #[case] expected: Option, + ) { + let console = TestConsole::new(80, 50).with_clipping(Rect::with_size(10, 10, 20, 15)); + assert_eq!(console.try_at(x, y), expected); + } +} diff --git a/bracket-terminal/src/consoles/flexible_console.rs b/bracket-terminal/src/consoles/flexible_console.rs index d260595..f752192 100755 --- a/bracket-terminal/src/consoles/flexible_console.rs +++ b/bracket-terminal/src/consoles/flexible_console.rs @@ -461,3 +461,335 @@ impl Console for FlexiConsole { self.is_dirty = false; } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn rgba(r: u8, g: u8, b: u8, a: u8) -> RGBA { + RGBA::from_u8(r, g, b, a) + } + + #[test] + fn init_creates_empty_sparse_console() { + let console = FlexiConsole::init(80, 50); + + assert_eq!(console.get_char_size(), (80, 50)); + assert!(console.tiles.is_empty()); + assert!(console.is_dirty); + } + + #[rstest] + #[case(0, 0, 30)] + #[case(1, 0, 31)] + #[case(9, 0, 39)] + #[case(0, 1, 20)] + #[case(0, 2, 10)] + #[case(0, 3, 0)] + #[case(9, 3, 9)] + fn at_uses_bottom_origin_storage(#[case] x: i32, #[case] y: i32, #[case] expected: usize) { + let console = FlexiConsole::init(10, 4); + assert_eq!(console.at(x, y), expected); + } + + #[test] + fn flexi_console_at_mapping_differs_from_test_console_row_major_mapping() { + let flexi = FlexiConsole::init(10, 4); + + assert_eq!(flexi.at(0, 0), 30); + assert_eq!(flexi.at(0, 3), 0); + } + + #[test] + fn print_appends_sparse_tiles_with_inverted_y_positions() { + let mut console = FlexiConsole::init(10, 4); + + console.clear_dirty(); + console.print(2, 1, "AB"); + + assert!(console.is_dirty); + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].position, PointF { x: 2.0, y: 2.0 }); + assert_eq!(console.tiles[0].glyph, 65); + assert_eq!(console.tiles[0].fg, RGBA::from_f32(1.0, 1.0, 1.0, 1.0)); + assert_eq!(console.tiles[0].bg, RGBA::from_f32(0.0, 0.0, 0.0, 1.0)); + assert_eq!(console.tiles[0].z_order, 0); + assert_eq!(console.tiles[0].rotation, 0.0); + assert_eq!(console.tiles[0].scale, PointF { x: 1.0, y: 1.0 }); + assert_eq!(console.tiles[1].position, PointF { x: 3.0, y: 2.0 }); + assert_eq!(console.tiles[1].glyph, 66); + } + + #[test] + fn print_does_not_clip_out_of_bounds_characters() { + let mut console = FlexiConsole::init(3, 1); + + console.print(1, 0, "ABCD"); + + assert_eq!(console.tiles.len(), 4); + assert_eq!(console.tiles[0].position, PointF { x: 1.0, y: 0.0 }); + assert_eq!(console.tiles[1].position, PointF { x: 2.0, y: 0.0 }); + assert_eq!(console.tiles[2].position, PointF { x: 3.0, y: 0.0 }); + assert_eq!(console.tiles[3].position, PointF { x: 4.0, y: 0.0 }); + } + + #[test] + fn print_color_appends_sparse_tiles_with_given_colors() { + let mut console = FlexiConsole::init(10, 4); + let fg = rgba(1, 2, 3, 4); + let bg = rgba(5, 6, 7, 8); + + console.print_color(1, 2, fg, bg, "XY"); + + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].position, PointF { x: 1.0, y: 1.0 }); + assert_eq!(console.tiles[0].glyph, 88); + assert_eq!(console.tiles[0].fg, fg); + assert_eq!(console.tiles[0].bg, bg); + assert_eq!(console.tiles[1].position, PointF { x: 2.0, y: 1.0 }); + assert_eq!(console.tiles[1].glyph, 89); + assert_eq!(console.tiles[1].fg, fg); + assert_eq!(console.tiles[1].bg, bg); + } + + #[test] + fn set_appends_sparse_tile_when_in_bounds() { + let mut console = FlexiConsole::init(10, 4); + let fg = rgba(11, 12, 13, 14); + let bg = rgba(21, 22, 23, 24); + + console.set(2, 1, fg, bg, 123); + + assert_eq!(console.tiles.len(), 1); + assert_eq!(console.tiles[0].position, PointF { x: 2.0, y: 2.0 }); + assert_eq!(console.tiles[0].glyph, 123); + assert_eq!(console.tiles[0].fg, fg); + assert_eq!(console.tiles[0].bg, bg); + assert_eq!(console.tiles[0].z_order, 0); + assert_eq!(console.tiles[0].rotation, 0.0); + assert_eq!(console.tiles[0].scale, PointF { x: 1.0, y: 1.0 }); + } + + #[test] + fn set_ignores_out_of_bounds_coordinates() { + let mut console = FlexiConsole::init(3, 1); + + console.set(3, 0, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 65); + console.set(-1, 0, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 66); + console.set(0, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 67); + + assert!(console.tiles.is_empty()); + } + + #[test] + fn set_bg_does_nothing_for_sparse_console() { + let mut console = FlexiConsole::init(3, 1); + + console.set_bg(1, 0, rgba(9, 8, 7, 6)); + assert!(console.tiles.is_empty()); + } + + #[test] + fn set_fancy_appends_tile_with_custom_attributes_and_inverted_y() { + let mut console = FlexiConsole::init(10, 4); + let fg = rgba(1, 2, 3, 4); + let bg = rgba(5, 6, 7, 8); + + console.set_fancy( + PointF { x: 2.5, y: 1.25 }, + 7, + 1.5, + PointF { x: 0.5, y: 2.0 }, + fg, + bg, + 200, + ); + + assert_eq!(console.tiles.len(), 1); + assert_eq!(console.tiles[0].position, PointF { x: 2.5, y: 2.75 }); + assert_eq!(console.tiles[0].z_order, 7); + assert_eq!(console.tiles[0].rotation, 1.5); + assert_eq!(console.tiles[0].scale, PointF { x: 0.5, y: 2.0 }); + assert_eq!(console.tiles[0].glyph, 200); + assert_eq!(console.tiles[0].fg, fg); + assert_eq!(console.tiles[0].bg, bg); + } + + #[test] + fn cls_clears_tiles_and_adds_single_space_tile() { + let mut console = FlexiConsole::init(10, 4); + + console.print(0, 0, "ABC"); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls(); + + assert!(console.is_dirty); + assert_eq!(console.tiles.len(), 1); + assert_eq!(console.tiles[0].glyph, 32); + assert_eq!(console.tiles[0].fg, rgba(255, 255, 255, 255)); + assert_eq!(console.tiles[0].bg, rgba(0, 0, 0, 255)); + assert_eq!(console.tiles[0].position, PointF { x: 0.0, y: 0.0 }); + assert_eq!(console.tiles[0].z_order, 0); + assert_eq!(console.tiles[0].rotation, 0.0); + } + + #[test] + fn cls_bg_turns_existing_tiles_into_default_space_tiles() { + let mut console = FlexiConsole::init(10, 4); + + console.print_color(1, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), "AB"); + + console.cls_bg(rgba(9, 8, 7, 6)); + + assert_eq!(console.tiles.len(), 2); + assert!(console.tiles.iter().all(|tile| { + tile.glyph == 32 && tile.fg == rgba(255, 255, 255, 255) && tile.bg == rgba(0, 0, 0, 255) + })); + } + + #[test] + fn fill_region_adds_one_tile_per_in_bounds_cell() { + let mut console = FlexiConsole::init(5, 5); + let fg = rgba(1, 1, 1, 255); + let bg = rgba(2, 2, 2, 255); + + console.fill_region(Rect::with_size(1, 1, 2, 3), 88, fg, bg); + + assert_eq!(console.tiles.len(), 6); + assert!( + console + .tiles + .iter() + .all(|tile| { tile.glyph == 88 && tile.fg == fg && tile.bg == bg }) + ); + } + + #[test] + fn print_centered_uses_console_width() { + let mut console = FlexiConsole::init(10, 2); + + console.print_centered(0, "ABCD"); + + assert_eq!(console.tiles.len(), 4); + assert_eq!(console.tiles[0].position, PointF { x: 3.0, y: 1.0 }); + assert_eq!(console.tiles[1].position, PointF { x: 4.0, y: 1.0 }); + assert_eq!(console.tiles[2].position, PointF { x: 5.0, y: 1.0 }); + assert_eq!(console.tiles[3].position, PointF { x: 6.0, y: 1.0 }); + } + + #[test] + fn print_right_ends_before_given_x() { + let mut console = FlexiConsole::init(10, 2); + + console.print_right(8, 0, "ABC"); + + assert_eq!(console.tiles.len(), 3); + assert_eq!(console.tiles[0].position, PointF { x: 5.0, y: 1.0 }); + assert_eq!(console.tiles[1].position, PointF { x: 6.0, y: 1.0 }); + assert_eq!(console.tiles[2].position, PointF { x: 7.0, y: 1.0 }); + } + + #[test] + fn set_offset_scales_offsets_by_console_dimensions() { + let mut console = FlexiConsole::init(10, 20); + + console.set_offset(1.0, -0.5); + + assert_eq!(console.offset_x, 0.2); + assert_eq!(console.offset_y, -0.05); + assert!(console.is_dirty); + } + + #[test] + fn set_scale_updates_scale_and_center() { + let mut console = FlexiConsole::init(10, 20); + + console.set_scale(2.5, 3, 4); + + assert_eq!(console.get_scale(), (2.5, 3, 4)); + assert!(console.is_dirty); + } + + #[test] + fn clipping_round_trip() { + let mut console = FlexiConsole::init(10, 20); + let clipping = Rect::with_size(1, 2, 3, 4); + + assert_eq!(console.get_clipping(), None); + + console.set_clipping(Some(clipping)); + assert_eq!(console.get_clipping(), Some(clipping)); + } + + #[test] + fn alpha_methods_update_existing_tiles_only() { + let mut console = FlexiConsole::init(10, 4); + + console.print("".len() as i32, 0, "AB"); + console.set_all_fg_alpha(0.25); + assert!(console.tiles.iter().all(|tile| tile.fg.a == 0.25)); + + console.set_all_bg_alpha(0.5); + assert!(console.tiles.iter().all(|tile| tile.bg.a == 0.5)); + + console.set_all_alpha(0.75, 1.0); + assert!( + console + .tiles + .iter() + .all(|tile| tile.fg.a == 0.75 && tile.bg.a == 1.0) + ); + } + + #[test] + fn set_translation_mode_changes_unicode_print_behavior() { + let mut console = FlexiConsole::init(3, 1); + + console.set_translation_mode(CharacterTranslationMode::Unicode); + console.print(0, 0, "가"); + + assert_eq!(console.tiles[0].glyph, '가' as FontCharType); + } + + #[test] + fn set_char_size_updates_dimensions_without_rebuilding_tiles() { + let mut console = FlexiConsole::init(3, 2); + + console.print(0, 0, "A"); + console.set_char_size(5, 4); + + assert_eq!(console.width, 5); + assert_eq!(console.height, 4); + assert_eq!(console.tiles.len(), 1); + assert!(console.needs_resize_internal); + } + + #[test] + fn clear_dirty_resets_dirty_flag() { + let mut console = FlexiConsole::init(3, 2); + assert!(console.is_dirty); + + console.clear_dirty(); + assert!(!console.is_dirty); + } + + #[test] + fn as_any_allows_downcasting_to_flexi_console() { + let console = FlexiConsole::init(3, 2); + assert!(console.as_any().downcast_ref::().is_some()); + } + + #[test] + fn as_any_mut_allows_mutable_downcasting_to_flexi_console() { + let mut console = FlexiConsole::init(3, 2); + assert!( + console + .as_any_mut() + .downcast_mut::() + .is_some() + ); + } +} diff --git a/bracket-terminal/src/consoles/simple_console.rs b/bracket-terminal/src/consoles/simple_console.rs index b5e31a9..6a89c8b 100755 --- a/bracket-terminal/src/consoles/simple_console.rs +++ b/bracket-terminal/src/consoles/simple_console.rs @@ -414,3 +414,301 @@ impl Console for SimpleConsole { self.is_dirty = false; } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn rgba(r: u8, g: u8, b: u8, a: u8) -> RGBA { + RGBA::from_u8(r, g, b, a) + } + + #[test] + fn init_creates_expected_default_state() { + let console = SimpleConsole::init(80, 50); + + assert_eq!(console.width, 80); + assert_eq!(console.height, 50); + assert_eq!(console.tiles.len(), 80 * 50); + assert!(console.is_dirty); + } + + #[rstest] + #[case(0, 0, 30)] + #[case(1, 0, 31)] + #[case(9, 0, 39)] + #[case(0, 1, 20)] + #[case(0, 2, 10)] + #[case(0, 3, 0)] + #[case(9, 3, 9)] + fn at_uses_bottom_origin_storage(#[case] x: i32, #[case] y: i32, #[case] expected: usize) { + let console = SimpleConsole::init(10, 4); + assert_eq!(console.at(x, y), expected); + } + + #[test] + fn simple_console_at_mapping_differs_from_test_console_row_major_mapping() { + let simple = SimpleConsole::init(10, 4); + + assert_eq!(simple.at(0, 0), 30); + assert_eq!(simple.at(0, 3), 0); + } + + #[test] + fn cls_resets_all_tiles_to_space_white_on_black() { + let mut console = SimpleConsole::init(3, 2); + + console.set(1, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 99); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls(); + assert!(console.is_dirty); + assert!(console.tiles.iter().all(|tile| { + tile.glyph == 32 && tile.fg == rgba(255, 255, 255, 255) && tile.bg == rgba(0, 0, 0, 255) + })); + } + + #[test] + fn cls_bg_resets_all_tiles_with_given_background() { + let mut console = SimpleConsole::init(3, 2); + let bg = rgba(10, 20, 30, 40); + + console.cls_bg(bg); + + assert!(console.is_dirty); + assert!(console.tiles.iter().all(|tile| { + tile.glyph == 32 && tile.fg == rgba(255, 255, 255, 255) && tile.bg == bg + })); + } + + #[test] + fn print_writes_glyphs_but_keeps_existing_colors() { + let mut console = SimpleConsole::init(5, 2); + let idx = console.at(1, 0); + let original_fg = console.tiles[idx].fg; + let original_bg = console.tiles[idx].bg; + + console.clear_dirty(); + console.print(1, 0, "ABC"); + + assert!(console.is_dirty); + assert_eq!(console.tiles[console.at(1, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(2, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(3, 0)].glyph, 67); + assert_eq!(console.tiles[idx].fg, original_fg); + assert_eq!(console.tiles[idx].bg, original_bg); + } + + #[test] + fn print_clips_out_of_bounds_characters() { + let mut console = SimpleConsole::init(3, 1); + + console.print(1, 0, "ABCD"); + + assert_eq!(console.tiles[console.at(0, 0)].glyph, 0); + assert_eq!(console.tiles[console.at(1, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(2, 0)].glyph, 66); + } + + #[test] + fn print_color_writes_glyphs_and_colors() { + let mut console = SimpleConsole::init(5, 2); + let fg = rgba(1, 2, 3, 4); + let bg = rgba(5, 6, 7, 8); + + console.print_color(1, 0, fg, bg, "XY"); + + let x = console.tiles[console.at(1, 0)]; + let y = console.tiles[console.at(2, 0)]; + + assert_eq!(x.glyph, 88); + assert_eq!(x.fg, fg); + assert_eq!(x.bg, bg); + + assert_eq!(y.glyph, 89); + assert_eq!(y.fg, fg); + assert_eq!(y.bg, bg); + } + + #[test] + fn set_writes_single_tile() { + let mut console = SimpleConsole::init(3, 2); + let fg = rgba(11, 12, 13, 14); + let bg = rgba(21, 22, 23, 24); + + console.set(2, 1, fg, bg, 123); + + let tile = console.tiles[console.at(2, 1)]; + assert_eq!(tile.glyph, 123); + assert_eq!(tile.fg, fg); + assert_eq!(tile.bg, bg); + } + + #[test] + fn set_bg_changes_only_background() { + let mut console = SimpleConsole::init(3, 2); + let idx = console.at(1, 1); + let original_glyph = console.tiles[idx].glyph; + let original_fg = console.tiles[idx].fg; + let bg = rgba(9, 8, 7, 6); + + console.set_bg(1, 1, bg); + + assert_eq!(console.tiles[idx].glyph, original_glyph); + assert_eq!(console.tiles[idx].fg, original_fg); + assert_eq!(console.tiles[idx].bg, bg); + } + + #[test] + fn fill_region_updates_each_tile_in_region() { + let mut console = SimpleConsole::init(5, 5); + let fg = rgba(1, 1, 1, 255); + let bg = rgba(2, 2, 2, 255); + + console.fill_region(Rect::with_size(1, 1, 2, 3), 88, fg, bg); + + for y in 1..4 { + for x in 1..3 { + let tile = console.tiles[console.at(x, y)]; + assert_eq!(tile.glyph, 88); + assert_eq!(tile.fg, fg); + assert_eq!(tile.bg, bg); + } + } + + assert_eq!(console.tiles[console.at(0, 0)].glyph, 0); + } + + #[test] + fn print_centered_uses_console_width() { + let mut console = SimpleConsole::init(10, 2); + + console.print_centered(0, "ABCD"); + + assert_eq!(console.tiles[console.at(3, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(4, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(5, 0)].glyph, 67); + assert_eq!(console.tiles[console.at(6, 0)].glyph, 68); + } + + #[test] + fn print_right_ends_before_given_x() { + let mut console = SimpleConsole::init(10, 2); + + console.print_right(8, 0, "ABC"); + + assert_eq!(console.tiles[console.at(5, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(6, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(7, 0)].glyph, 67); + assert_eq!(console.tiles[console.at(8, 0)].glyph, 0); + } + + #[test] + fn set_offset_scales_offsets_by_console_dimensions() { + let mut console = SimpleConsole::init(10, 20); + + console.set_offset(1.0, -0.5); + + assert_eq!(console.offset_x, 0.2); + assert_eq!(console.offset_y, -0.05); + assert!(console.is_dirty); + } + + #[test] + fn set_scale_updates_scale_and_center() { + let mut console = SimpleConsole::init(10, 20); + + console.set_scale(2.5, 3, 4); + + assert_eq!(console.get_scale(), (2.5, 3, 4)); + assert!(console.is_dirty); + } + + #[test] + fn clipping_round_trip() { + let mut console = SimpleConsole::init(10, 20); + let clipping = Rect::with_size(1, 2, 3, 4); + + assert_eq!(console.get_clipping(), None); + + console.set_clipping(Some(clipping)); + assert_eq!(console.get_clipping(), Some(clipping)); + } + + #[test] + fn alpha_methods_update_all_tiles() { + let mut console = SimpleConsole::init(3, 2); + + console.set_all_fg_alpha(0.25); + assert!(console.tiles.iter().all(|tile| tile.fg.a == 0.25)); + + console.set_all_bg_alpha(0.5); + assert!(console.tiles.iter().all(|tile| tile.bg.a == 0.5)); + + console.set_all_alpha(0.75, 1.0); + assert!( + console + .tiles + .iter() + .all(|tile| tile.fg.a == 0.75 && tile.bg.a == 1.0) + ); + } + + #[test] + fn set_translation_mode_changes_unicode_print_behavior() { + let mut console = SimpleConsole::init(3, 1); + + console.set_translation_mode(CharacterTranslationMode::Unicode); + console.print(0, 0, "가"); + + assert_eq!(console.tiles[console.at(0, 0)].glyph, '가' as FontCharType); + } + + #[test] + fn set_char_size_resizes_buffer_and_preserves_overlapping_content() { + let mut console = SimpleConsole::init(3, 2); + let fg = rgba(1, 2, 3, 4); + let bg = rgba(5, 6, 7, 8); + + console.set(1, 1, fg, bg, 77); + console.set_char_size(5, 4); + + assert_eq!(console.width, 5); + assert_eq!(console.height, 4); + assert_eq!(console.tiles.len(), 20); + assert!(console.needs_resize_internal); + + let preserved = console.tiles[console.at(1, 1)]; + assert_eq!(preserved.glyph, 77); + assert_eq!(preserved.fg, fg); + assert_eq!(preserved.bg, bg); + } + + #[test] + fn clear_dirty_resets_dirty_flag() { + let mut console = SimpleConsole::init(3, 2); + assert!(console.is_dirty); + + console.clear_dirty(); + assert!(!console.is_dirty); + } + + #[test] + fn as_any_allows_downcasting_to_simple_console() { + let console = SimpleConsole::init(3, 2); + assert!(console.as_any().downcast_ref::().is_some()); + } + + #[test] + fn as_any_mut_allows_mutable_downcasting_to_simple_console() { + let mut console = SimpleConsole::init(3, 2); + assert!( + console + .as_any_mut() + .downcast_mut::() + .is_some() + ); + } +} diff --git a/bracket-terminal/src/consoles/sparse_console.rs b/bracket-terminal/src/consoles/sparse_console.rs index 066b7b0..7ccfbb3 100755 --- a/bracket-terminal/src/consoles/sparse_console.rs +++ b/bracket-terminal/src/consoles/sparse_console.rs @@ -424,3 +424,369 @@ impl Console for SparseConsole { self.is_dirty = false; } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn rgba(r: u8, g: u8, b: u8, a: u8) -> RGBA { + RGBA::from_u8(r, g, b, a) + } + + #[test] + fn init_creates_empty_sparse_console() { + let console = SparseConsole::init(80, 50); + + assert_eq!(console.get_char_size(), (80, 50)); + assert!(console.tiles.is_empty()); + assert!(console.is_dirty); + assert!(!console.needs_resize_internal); + } + + #[rstest] + #[case(0, 0, 30)] + #[case(1, 0, 31)] + #[case(9, 0, 39)] + #[case(0, 1, 20)] + #[case(0, 2, 10)] + #[case(0, 3, 0)] + #[case(9, 3, 9)] + fn at_uses_bottom_origin_storage(#[case] x: i32, #[case] y: i32, #[case] expected: usize) { + let console = SparseConsole::init(10, 4); + assert_eq!(console.at(x, y), expected); + } + + #[test] + fn sparse_console_at_mapping_differs_from_test_console_row_major_mapping() { + let sparse = SparseConsole::init(10, 4); + + assert_eq!(sparse.at(0, 0), 30); + assert_eq!(sparse.at(0, 3), 0); + } + + #[test] + fn print_appends_sparse_tiles_with_default_colors() { + let mut console = SparseConsole::init(10, 4); + + console.clear_dirty(); + console.print(2, 1, "AB"); + + assert!(console.is_dirty); + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].idx, console.at(2, 1)); + assert_eq!(console.tiles[0].glyph, 65); + assert_eq!(console.tiles[0].fg, RGBA::from_f32(1.0, 1.0, 1.0, 1.0)); + assert_eq!(console.tiles[0].bg, RGBA::from_f32(0.0, 0.0, 0.0, 1.0)); + assert_eq!(console.tiles[1].idx, console.at(3, 1)); + assert_eq!(console.tiles[1].glyph, 66); + } + + #[test] + fn print_clips_at_right_edge() { + let mut console = SparseConsole::init(3, 1); + + console.print(1, 0, "ABCD"); + + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].idx, console.at(1, 0)); + assert_eq!(console.tiles[0].glyph, 65); + assert_eq!(console.tiles[1].idx, console.at(2, 0)); + assert_eq!(console.tiles[1].glyph, 66); + } + + #[test] + fn print_color_appends_sparse_tiles_with_given_colors() { + let mut console = SparseConsole::init(10, 4); + let fg = rgba(1, 2, 3, 4); + let bg = rgba(5, 6, 7, 8); + + console.print_color(1, 2, fg, bg, "XY"); + + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].idx, console.at(1, 2)); + assert_eq!(console.tiles[0].glyph, 88); + assert_eq!(console.tiles[0].fg, fg); + assert_eq!(console.tiles[0].bg, bg); + assert_eq!(console.tiles[1].idx, console.at(2, 2)); + assert_eq!(console.tiles[1].glyph, 89); + assert_eq!(console.tiles[1].fg, fg); + assert_eq!(console.tiles[1].bg, bg); + } + + #[test] + fn set_appends_sparse_tile_when_in_bounds() { + let mut console = SparseConsole::init(10, 4); + let fg = rgba(11, 12, 13, 14); + let bg = rgba(21, 22, 23, 24); + + console.set(2, 1, fg, bg, 123); + + assert_eq!(console.tiles.len(), 1); + assert_eq!(console.tiles[0].idx, console.at(2, 1)); + assert_eq!(console.tiles[0].glyph, 123); + assert_eq!(console.tiles[0].fg, fg); + assert_eq!(console.tiles[0].bg, bg); + } + + #[test] + fn set_ignores_out_of_bounds_coordinates() { + let mut console = SparseConsole::init(3, 1); + + console.set(3, 0, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 65); + console.set(-1, 0, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 66); + console.set(0, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 67); + + assert!(console.tiles.is_empty()); + } + + #[test] + fn set_bg_updates_existing_tile_backgrounds_with_same_idx() { + let mut console = SparseConsole::init(10, 4); + let old_bg = rgba(1, 1, 1, 255); + let new_bg = rgba(9, 8, 7, 6); + + console.set(2, 1, rgba(1, 2, 3, 4), old_bg, 65); + console.set(2, 1, rgba(5, 6, 7, 8), old_bg, 66); + console.set_bg(2, 1, new_bg); + + assert_eq!(console.tiles.len(), 2); + assert!( + console + .tiles + .iter() + .all(|tile| tile.idx == console.at(2, 1) && tile.bg == new_bg) + ); + } + + #[test] + fn set_bg_creates_space_tile_when_no_existing_tile_matches() { + let mut console = SparseConsole::init(10, 4); + let bg = rgba(9, 8, 7, 6); + + console.set_bg(2, 1, bg); + + assert_eq!(console.tiles.len(), 1); + assert_eq!(console.tiles[0].idx, console.at(2, 1)); + assert_eq!(console.tiles[0].glyph, to_cp437(' ')); + assert_eq!(console.tiles[0].fg, rgba(0, 0, 0, 255)); + assert_eq!(console.tiles[0].bg, bg); + } + + #[test] + fn set_bg_ignores_out_of_bounds_coordinates() { + let mut console = SparseConsole::init(3, 1); + + console.set_bg(3, 0, rgba(9, 8, 7, 6)); + console.set_bg(-1, 0, rgba(9, 8, 7, 6)); + console.set_bg(0, 1, rgba(9, 8, 7, 6)); + + assert!(console.tiles.is_empty()); + } + + #[test] + fn cls_clears_sparse_tiles_and_marks_dirty() { + let mut console = SparseConsole::init(10, 4); + + console.print(0, 0, "ABC"); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls(); + assert!(console.is_dirty); + assert!(console.tiles.is_empty()); + } + + #[test] + fn cls_bg_clears_sparse_tiles_and_ignores_background() { + let mut console = SparseConsole::init(10, 4); + + console.print(0, 0, "ABC"); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls_bg(rgba(1, 2, 3, 4)); + assert!(console.is_dirty); + assert!(console.tiles.is_empty()); + } + + #[test] + fn fill_region_adds_one_tile_per_in_bounds_cell() { + let mut console = SparseConsole::init(5, 5); + let fg = rgba(1, 1, 1, 255); + let bg = rgba(2, 2, 2, 255); + + console.fill_region(Rect::with_size(1, 1, 2, 3), 88, fg, bg); + + assert_eq!(console.tiles.len(), 6); + assert!( + console + .tiles + .iter() + .all(|tile| tile.glyph == 88 && tile.fg == fg && tile.bg == bg) + ); + } + + #[test] + fn print_centered_uses_console_width() { + let mut console = SparseConsole::init(10, 2); + + console.print_centered(0, "ABCD"); + + assert_eq!(console.tiles.len(), 4); + assert_eq!(console.tiles[0].idx, console.at(3, 0)); + assert_eq!(console.tiles[1].idx, console.at(4, 0)); + assert_eq!(console.tiles[2].idx, console.at(5, 0)); + assert_eq!(console.tiles[3].idx, console.at(6, 0)); + } + + #[test] + fn print_right_ends_before_given_x() { + let mut console = SparseConsole::init(10, 2); + + console.print_right(8, 0, "ABC"); + + assert_eq!(console.tiles.len(), 3); + assert_eq!(console.tiles[0].idx, console.at(5, 0)); + assert_eq!(console.tiles[1].idx, console.at(6, 0)); + assert_eq!(console.tiles[2].idx, console.at(7, 0)); + } + + #[test] + fn printer_supports_inline_colored_text() { + let mut console = SparseConsole::init(20, 2); + + console.printer(0, 0, "#[red]A#[]B", TextAlign::Left, None); + + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].glyph, to_cp437('A')); + assert_eq!(console.tiles[1].glyph, to_cp437('B')); + } + + #[test] + fn set_offset_scales_offsets_by_console_dimensions() { + let mut console = SparseConsole::init(10, 20); + + console.set_offset(1.0, -0.5); + + assert_eq!(console.offset_x, 0.2); + assert_eq!(console.offset_y, -0.05); + assert!(console.is_dirty); + } + + #[test] + fn set_scale_updates_scale_and_center() { + let mut console = SparseConsole::init(10, 20); + + console.set_scale(2.5, 3, 4); + + assert_eq!(console.get_scale(), (2.5, 3, 4)); + assert!(console.is_dirty); + } + + #[test] + fn clipping_round_trip() { + let mut console = SparseConsole::init(10, 20); + let clipping = Rect::with_size(1, 2, 3, 4); + + assert_eq!(console.get_clipping(), None); + + console.set_clipping(Some(clipping)); + assert_eq!(console.get_clipping(), Some(clipping)); + } + + #[test] + fn clipping_limits_set() { + let mut console = SparseConsole::init(10, 5); + + console.set_clipping(Some(Rect::with_size(2, 1, 3, 2))); + console.set(1, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 65); + console.set(2, 1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 66); + console.set(4, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 67); + console.set(5, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 68); + + assert_eq!(console.tiles.len(), 2); + assert_eq!(console.tiles[0].glyph, 66); + assert_eq!(console.tiles[1].glyph, 67); + } + + #[test] + fn alpha_methods_update_existing_tiles_only() { + let mut console = SparseConsole::init(10, 4); + + console.print(0, 0, "AB"); + + console.set_all_fg_alpha(0.25); + assert!(console.tiles.iter().all(|tile| tile.fg.a == 0.25)); + + console.set_all_bg_alpha(0.5); + assert!(console.tiles.iter().all(|tile| tile.bg.a == 0.5)); + + console.set_all_alpha(0.75, 1.0); + assert!( + console + .tiles + .iter() + .all(|tile| tile.fg.a == 0.75 && tile.bg.a == 1.0) + ); + } + + #[test] + fn set_translation_mode_changes_unicode_print_behavior() { + let mut console = SparseConsole::init(3, 1); + + console.set_translation_mode(CharacterTranslationMode::Unicode); + console.print(0, 0, "가"); + + assert_eq!(console.tiles[0].glyph, '가' as FontCharType); + } + + #[test] + fn set_char_size_updates_dimensions_without_rebuilding_tiles() { + let mut console = SparseConsole::init(3, 2); + + console.print(0, 0, "A"); + console.set_char_size(5, 4); + + assert_eq!(console.get_char_size(), (5, 4)); + assert_eq!(console.tiles.len(), 1); + assert!(console.needs_resize_internal); + } + + #[test] + fn resize_pixels_marks_dirty_without_changing_dimensions() { + let mut console = SparseConsole::init(80, 50); + + console.clear_dirty(); + console.resize_pixels(1024, 768); + + assert!(console.is_dirty); + assert_eq!(console.get_char_size(), (80, 50)); + } + + #[test] + fn clear_dirty_resets_dirty_flag() { + let mut console = SparseConsole::init(80, 50); + assert!(console.is_dirty); + + console.clear_dirty(); + assert!(!console.is_dirty); + } + + #[test] + fn as_any_allows_downcasting_to_sparse_console() { + let console = SparseConsole::init(80, 50); + assert!(console.as_any().downcast_ref::().is_some()); + } + + #[test] + fn as_any_mut_allows_mutable_downcasting_to_sparse_console() { + let mut console = SparseConsole::init(80, 50); + assert!( + console + .as_any_mut() + .downcast_mut::() + .is_some() + ); + } +} diff --git a/bracket-terminal/src/consoles/sprite_console.rs b/bracket-terminal/src/consoles/sprite_console.rs index 422855a..0900144 100755 --- a/bracket-terminal/src/consoles/sprite_console.rs +++ b/bracket-terminal/src/consoles/sprite_console.rs @@ -278,3 +278,252 @@ impl Console for SpriteConsole { self.is_dirty = false; } } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn rgba(r: u8, g: u8, b: u8, a: u8) -> RGBA { + RGBA::from_u8(r, g, b, a) + } + + fn test_sprite(index: usize) -> RenderSprite { + RenderSprite { + destination: Rect::with_size(1, 2, 3, 4), + z_order: 5, + tint: rgba(10, 20, 30, 40), + index, + } + } + + #[test] + fn init_creates_empty_sprite_console() { + let console = SpriteConsole::init(80, 50, 7); + + assert_eq!(console.get_char_size(), (80, 50)); + assert_eq!(console.sprite_sheet, 7); + assert!(console.sprites.is_empty()); + assert!(console.is_dirty); + assert!(!console.needs_resize_internal); + } + + #[rstest] + #[case(0, 0, 30)] + #[case(1, 0, 31)] + #[case(9, 0, 39)] + #[case(0, 1, 20)] + #[case(0, 2, 10)] + #[case(0, 3, 0)] + #[case(9, 3, 9)] + fn at_uses_bottom_origin_storage(#[case] x: i32, #[case] y: i32, #[case] expected: usize) { + let console = SpriteConsole::init(10, 4, 0); + assert_eq!(console.at(x, y), expected); + } + + #[test] + fn sprite_console_at_mapping_differs_from_test_console_row_major_mapping() { + let sprite_console = SpriteConsole::init(10, 4, 0); + + assert_eq!(sprite_console.at(0, 0), 30); + assert_eq!(sprite_console.at(0, 3), 0); + } + + #[test] + fn render_sprite_appends_sprite_and_marks_dirty() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.clear_dirty(); + console.render_sprite(test_sprite(3)); + + assert!(console.is_dirty); + assert_eq!(console.sprites.len(), 1); + assert_eq!(console.sprites[0].destination, Rect::with_size(1, 2, 3, 4)); + assert_eq!(console.sprites[0].z_order, 5); + assert_eq!(console.sprites[0].tint, rgba(10, 20, 30, 40)); + assert_eq!(console.sprites[0].index, 3); + } + + #[test] + fn cls_clears_sprites_and_marks_dirty() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + console.render_sprite(test_sprite(2)); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls(); + assert!(console.is_dirty); + assert!(console.sprites.is_empty()); + } + + #[test] + fn cls_bg_clears_sprites_and_ignores_background() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + console.clear_dirty(); + assert!(!console.is_dirty); + + console.cls_bg(rgba(1, 2, 3, 4)); + assert!(console.is_dirty); + assert!(console.sprites.is_empty()); + } + + #[test] + fn text_and_tile_methods_do_nothing() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.print(1, 2, "hello"); + console.print_color(1, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), "hello"); + console.set(1, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), 65); + console.set_bg(1, 2, rgba(1, 2, 3, 4)); + console.fill_region( + Rect::with_size(0, 0, 2, 2), + 65, + rgba(1, 2, 3, 4), + rgba(5, 6, 7, 8), + ); + console.print_centered(1, "hello"); + console.print_color_centered(1, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), "hello"); + console.print_centered_at(1, 2, "hello"); + console.print_color_centered_at(1, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), "hello"); + console.print_right(10, 2, "hello"); + console.print_color_right(10, 2, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8), "hello"); + console.printer(1, 2, "#[red]hello", TextAlign::Left, None); + + assert!(console.sprites.is_empty()); + } + + #[test] + fn draw_methods_do_nothing() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.draw_box(1, 2, 3, 4, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + console.draw_box_double(1, 2, 3, 4, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + console.draw_hollow_box(1, 2, 3, 4, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + console.draw_hollow_box_double(1, 2, 3, 4, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + console.draw_bar_horizontal(1, 2, 3, 1, 10, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + console.draw_bar_vertical(1, 2, 3, 1, 10, rgba(1, 2, 3, 4), rgba(5, 6, 7, 8)); + + assert!(console.sprites.is_empty()); + } + + #[test] + fn to_xp_layer_returns_layer_with_console_dimensions() { + let console = SpriteConsole::init(80, 50, 7); + let layer = console.to_xp_layer(); + + assert_eq!(layer.width, 80); + assert_eq!(layer.height, 50); + } + + #[test] + fn scale_methods_are_no_ops_with_fixed_default_result() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.set_offset(1.0, 2.0); + console.set_scale(3.0, 4, 5); + + assert_eq!(console.get_scale(), (1.0, 0, 0)); + } + + #[test] + fn clipping_methods_are_no_ops() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.set_clipping(Some(Rect::with_size(1, 2, 3, 4))); + assert_eq!(console.get_clipping(), None); + } + + #[test] + fn set_all_fg_alpha_updates_sprite_tint_alpha() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + console.render_sprite(test_sprite(2)); + console.set_all_fg_alpha(0.25); + + assert!(console.sprites.iter().all(|sprite| sprite.tint.a == 0.25)); + } + + #[test] + fn set_all_bg_alpha_does_nothing() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + let original_alpha = console.sprites[0].tint.a; + + console.set_all_bg_alpha(0.25); + assert_eq!(console.sprites[0].tint.a, original_alpha); + } + + #[test] + fn set_all_alpha_updates_sprite_tint_alpha_from_fg_only() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + console.render_sprite(test_sprite(2)); + console.set_all_alpha(0.75, 0.25); + + assert!(console.sprites.iter().all(|sprite| sprite.tint.a == 0.75)); + } + + #[test] + fn set_translation_mode_does_nothing() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.set_translation_mode(CharacterTranslationMode::Unicode); + assert!(console.sprites.is_empty()); + } + + #[test] + fn set_char_size_updates_dimensions_without_changing_sprites() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.render_sprite(test_sprite(1)); + console.set_char_size(100, 60); + + assert_eq!(console.get_char_size(), (100, 60)); + assert_eq!(console.sprites.len(), 1); + assert!(console.needs_resize_internal); + } + + #[test] + fn resize_pixels_marks_dirty_without_changing_dimensions() { + let mut console = SpriteConsole::init(80, 50, 7); + + console.clear_dirty(); + console.resize_pixels(1024, 768); + + assert!(console.is_dirty); + assert_eq!(console.get_char_size(), (80, 50)); + } + + #[test] + fn clear_dirty_resets_dirty_flag() { + let mut console = SpriteConsole::init(80, 50, 7); + assert!(console.is_dirty); + + console.clear_dirty(); + assert!(!console.is_dirty); + } + + #[test] + fn as_any_allows_downcasting_to_sprite_console() { + let console = SpriteConsole::init(80, 50, 7); + assert!(console.as_any().downcast_ref::().is_some()); + } + + #[test] + fn as_any_mut_allows_mutable_downcasting_to_sprite_console() { + let mut console = SpriteConsole::init(80, 50, 7); + assert!( + console + .as_any_mut() + .downcast_mut::() + .is_some() + ); + } +} diff --git a/bracket-terminal/src/consoles/virtual_console.rs b/bracket-terminal/src/consoles/virtual_console.rs index b953b8a..6580788 100755 --- a/bracket-terminal/src/consoles/virtual_console.rs +++ b/bracket-terminal/src/consoles/virtual_console.rs @@ -46,17 +46,22 @@ impl VirtualConsole { pub fn from_text(text: &str, width: usize) -> Self { let raw_lines = text.split('\n'); let mut lines: Vec = Vec::new(); + for line in raw_lines { let mut newline: String = String::from(""); - line.chars().for_each(|c| { + for c in line.chars() { newline.push(c); - if newline.len() > width { - lines.push(newline.clone()); - newline.clear(); + + if newline.len() >= width { + lines.push(newline); + newline = String::new(); } - }); - lines.push(newline.clone()); + } + + if !newline.is_empty() { + lines.push(newline); + } } let num_tiles: usize = width * lines.len(); @@ -437,3 +442,343 @@ impl Console for VirtualConsole { // Clears the dirty bit fn clear_dirty(&mut self) {} } + +#[cfg(test)] +mod tests { + use super::*; + use rstest::rstest; + + fn rgba(r: f32, g: f32, b: f32, a: f32) -> RGBA { + RGBA::from_f32(r, g, b, a) + } + + #[test] + fn new_creates_dense_console_with_expected_dimensions() { + let console = VirtualConsole::new(Point::new(80, 50)); + + assert_eq!(console.get_char_size(), (80, 50)); + assert_eq!(console.tiles.len(), 80 * 50); + assert_eq!(console.get_clipping(), None); + } + + #[rstest] + #[case(0, 0, 30)] + #[case(1, 0, 31)] + #[case(9, 0, 39)] + #[case(0, 1, 20)] + #[case(0, 2, 10)] + #[case(0, 3, 0)] + #[case(9, 3, 9)] + fn at_uses_bottom_origin_storage(#[case] x: i32, #[case] y: i32, #[case] expected: usize) { + let console = VirtualConsole::new(Point::new(10, 4)); + assert_eq!(console.at(x, y), expected); + } + + #[test] + fn virtual_console_at_mapping_differs_from_test_console_row_major_mapping() { + let virtual_console = VirtualConsole::new(Point::new(10, 4)); + + assert_eq!(virtual_console.at(0, 0), 30); + assert_eq!(virtual_console.at(0, 3), 0); + } + + #[test] + fn cls_resets_all_tiles_to_space_white_on_black() { + let mut console = VirtualConsole::new(Point::new(3, 2)); + + console.set(1, 1, rgba(0.1, 0.2, 0.3, 0.4), rgba(0.5, 0.6, 0.7, 0.8), 99); + console.cls(); + + assert!(console.tiles.iter().all(|tile| { + tile.glyph == 32 + && tile.fg == rgba(1.0, 1.0, 1.0, 1.0) + && tile.bg == rgba(0.0, 0.0, 0.0, 1.0) + })); + } + + #[test] + fn cls_bg_resets_all_tiles_with_given_background() { + let mut console = VirtualConsole::new(Point::new(3, 2)); + let bg = rgba(0.1, 0.2, 0.3, 0.4); + + console.cls_bg(bg); + + assert!(console.tiles.iter().all(|tile| { + tile.glyph == 32 && tile.fg == rgba(1.0, 1.0, 1.0, 1.0) && tile.bg == bg + })); + } + + #[test] + fn print_writes_glyphs_but_keeps_existing_colors() { + let mut console = VirtualConsole::new(Point::new(5, 2)); + let idx = console.at(1, 0); + let original_fg = console.tiles[idx].fg; + let original_bg = console.tiles[idx].bg; + + console.print(1, 0, "ABC"); + + assert_eq!(console.tiles[console.at(1, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(2, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(3, 0)].glyph, 67); + assert_eq!(console.tiles[idx].fg, original_fg); + assert_eq!(console.tiles[idx].bg, original_bg); + } + + #[test] + fn print_clips_out_of_bounds_characters() { + let mut console = VirtualConsole::new(Point::new(3, 1)); + + console.print(1, 0, "ABCD"); + + assert_eq!(console.tiles[console.at(0, 0)].glyph, 0); + assert_eq!(console.tiles[console.at(1, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(2, 0)].glyph, 66); + } + + #[test] + fn print_color_writes_glyphs_and_colors() { + let mut console = VirtualConsole::new(Point::new(5, 2)); + let fg = rgba(0.1, 0.2, 0.3, 0.4); + let bg = rgba(0.5, 0.6, 0.7, 0.8); + + console.print_color(1, 0, fg, bg, "XY"); + + let x = console.tiles[console.at(1, 0)]; + let y = console.tiles[console.at(2, 0)]; + + assert_eq!(x.glyph, 88); + assert_eq!(x.fg, fg); + assert_eq!(x.bg, bg); + + assert_eq!(y.glyph, 89); + assert_eq!(y.fg, fg); + assert_eq!(y.bg, bg); + } + + #[test] + fn set_writes_single_tile() { + let mut console = VirtualConsole::new(Point::new(3, 2)); + let fg = rgba(0.1, 0.2, 0.3, 0.4); + let bg = rgba(0.5, 0.6, 0.7, 0.8); + + console.set(2, 1, fg, bg, 123); + + let tile = console.tiles[console.at(2, 1)]; + assert_eq!(tile.glyph, 123); + assert_eq!(tile.fg, fg); + assert_eq!(tile.bg, bg); + } + + #[test] + fn set_ignores_out_of_bounds_coordinates() { + let mut console = VirtualConsole::new(Point::new(3, 1)); + + console.set(3, 0, rgba(0.1, 0.2, 0.3, 0.4), rgba(0.5, 0.6, 0.7, 0.8), 65); + console.set( + -1, + 0, + rgba(0.1, 0.2, 0.3, 0.4), + rgba(0.5, 0.6, 0.7, 0.8), + 66, + ); + console.set(0, 1, rgba(0.1, 0.2, 0.3, 0.4), rgba(0.5, 0.6, 0.7, 0.8), 67); + + assert!(console.tiles.iter().all(|tile| tile.glyph == 0)); + } + + #[test] + fn set_bg_changes_only_background() { + let mut console = VirtualConsole::new(Point::new(3, 2)); + let idx = console.at(1, 1); + let original_glyph = console.tiles[idx].glyph; + let original_fg = console.tiles[idx].fg; + let bg = rgba(0.9, 0.8, 0.7, 0.6); + + console.set_bg(1, 1, bg); + + assert_eq!(console.tiles[idx].glyph, original_glyph); + assert_eq!(console.tiles[idx].fg, original_fg); + assert_eq!(console.tiles[idx].bg, bg); + } + + #[test] + fn fill_region_updates_each_tile_in_region() { + let mut console = VirtualConsole::new(Point::new(5, 5)); + let fg = rgba(0.1, 0.1, 0.1, 1.0); + let bg = rgba(0.2, 0.2, 0.2, 1.0); + + console.fill_region(Rect::with_size(1, 1, 2, 3), 88, fg, bg); + + for y in 1..4 { + for x in 1..3 { + let tile = console.tiles[console.at(x, y)]; + assert_eq!(tile.glyph, 88); + assert_eq!(tile.fg, fg); + assert_eq!(tile.bg, bg); + } + } + + assert_eq!(console.tiles[console.at(0, 0)].glyph, 0); + } + + #[test] + fn print_centered_uses_console_width() { + let mut console = VirtualConsole::new(Point::new(10, 2)); + + console.print_centered(0, "ABCD"); + + assert_eq!(console.tiles[console.at(3, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(4, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(5, 0)].glyph, 67); + assert_eq!(console.tiles[console.at(6, 0)].glyph, 68); + } + + #[test] + fn print_right_ends_before_given_x() { + let mut console = VirtualConsole::new(Point::new(10, 2)); + + console.print_right(8, 0, "ABC"); + + assert_eq!(console.tiles[console.at(5, 0)].glyph, 65); + assert_eq!(console.tiles[console.at(6, 0)].glyph, 66); + assert_eq!(console.tiles[console.at(7, 0)].glyph, 67); + assert_eq!(console.tiles[console.at(8, 0)].glyph, 0); + } + + #[test] + fn clipping_round_trip() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + let clipping = Rect::with_size(1, 2, 3, 4); + + assert_eq!(console.get_clipping(), None); + + console.set_clipping(Some(clipping)); + assert_eq!(console.get_clipping(), Some(clipping)); + } + + #[test] + fn clipping_limits_set_and_print() { + let mut console = VirtualConsole::new(Point::new(10, 5)); + + console.set_clipping(Some(Rect::with_size(2, 1, 3, 2))); + console.print(0, 1, "ABCDE"); + console.set(4, 2, rgba(0.1, 0.2, 0.3, 0.4), rgba(0.5, 0.6, 0.7, 0.8), 90); + console.set(5, 2, rgba(0.1, 0.2, 0.3, 0.4), rgba(0.5, 0.6, 0.7, 0.8), 91); + + assert_eq!(console.tiles[console.at(0, 1)].glyph, 0); + assert_eq!(console.tiles[console.at(1, 1)].glyph, 0); + assert_eq!(console.tiles[console.at(2, 1)].glyph, 67); + assert_eq!(console.tiles[console.at(3, 1)].glyph, 68); + assert_eq!(console.tiles[console.at(4, 1)].glyph, 69); + assert_eq!(console.tiles[console.at(4, 2)].glyph, 90); + assert_eq!(console.tiles[console.at(5, 2)].glyph, 0); + } + + #[test] + fn alpha_methods_update_all_tiles() { + let mut console = VirtualConsole::new(Point::new(3, 2)); + + console.set_all_fg_alpha(0.25); + assert!(console.tiles.iter().all(|tile| tile.fg.a == 0.25)); + + console.set_all_bg_alpha(0.5); + assert!(console.tiles.iter().all(|tile| tile.bg.a == 0.5)); + + console.set_all_alpha(0.75, 1.0); + assert!( + console + .tiles + .iter() + .all(|tile| tile.fg.a == 0.75 && tile.bg.a == 1.0) + ); + } + + #[test] + fn set_translation_mode_changes_unicode_print_behavior() { + let mut console = VirtualConsole::new(Point::new(3, 1)); + + console.set_translation_mode(CharacterTranslationMode::Unicode); + console.print(0, 0, "가"); + + assert_eq!(console.tiles[console.at(0, 0)].glyph, '가' as FontCharType); + } + + #[test] + fn from_text_wraps_lines_and_prints_content() { + let console = VirtualConsole::from_text("abcdef\ngh", 3); + + assert_eq!(console.width, 3); + assert_eq!(console.height, 3); + assert_eq!(console.tiles[console.at(0, 0)].glyph, 97); + assert_eq!(console.tiles[console.at(1, 0)].glyph, 98); + assert_eq!(console.tiles[console.at(2, 0)].glyph, 99); + assert_eq!(console.tiles[console.at(0, 1)].glyph, 100); + assert_eq!(console.tiles[console.at(1, 1)].glyph, 101); + assert_eq!(console.tiles[console.at(2, 1)].glyph, 102); + assert_eq!(console.tiles[console.at(0, 2)].glyph, 103); + assert_eq!(console.tiles[console.at(1, 2)].glyph, 104); + } + + #[test] + fn resize_pixels_is_ignored() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + + console.resize_pixels(100, 200); + + assert_eq!(console.get_char_size(), (10, 20)); + assert_eq!(console.tiles.len(), 200); + } + + #[test] + fn get_scale_returns_fixed_default() { + let console = VirtualConsole::new(Point::new(10, 20)); + assert_eq!(console.get_scale(), (1.0, 0, 0)); + } + + #[test] + #[should_panic(expected = "Unsupported on virtual consoles.")] + fn set_offset_panics() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + console.set_offset(1.0, 2.0); + } + + #[test] + #[should_panic(expected = "Unsupported on virtual consoles.")] + fn set_scale_panics() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + console.set_scale(2.0, 3, 4); + } + + #[test] + #[should_panic(expected = "Not implemented.")] + fn set_char_size_panics() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + console.set_char_size(30, 40); + } + + #[test] + fn clear_dirty_is_no_op() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + + console.clear_dirty(); + assert_eq!(console.get_char_size(), (10, 20)); + } + + #[test] + fn as_any_allows_downcasting_to_virtual_console() { + let console = VirtualConsole::new(Point::new(10, 20)); + assert!(console.as_any().downcast_ref::().is_some()); + } + + #[test] + fn as_any_mut_allows_mutable_downcasting_to_virtual_console() { + let mut console = VirtualConsole::new(Point::new(10, 20)); + + assert!( + console + .as_any_mut() + .downcast_mut::() + .is_some() + ); + } +} diff --git a/bracket-terminal/src/lib.rs b/bracket-terminal/src/lib.rs index f44418c..b37227b 100755 --- a/bracket-terminal/src/lib.rs +++ b/bracket-terminal/src/lib.rs @@ -9,6 +9,10 @@ mod hal; mod initializer; mod input; pub mod rex; + +#[cfg(test)] +mod test_utils; + pub use bracket_embedding::prelude::{EMBED, embedded_resource, link_resource}; pub type BResult = anyhow::Result>; diff --git a/bracket-terminal/src/test_utils.rs b/bracket-terminal/src/test_utils.rs new file mode 100644 index 0000000..e12464a --- /dev/null +++ b/bracket-terminal/src/test_utils.rs @@ -0,0 +1,144 @@ +use super::console::{CharacterTranslationMode, Console, TextAlign}; +use crate::prelude::FontCharType; +use bracket_color::prelude::RGBA; +use bracket_geometry::prelude::Rect; +use bracket_rex::prelude::XpLayer; +use std::any::Any; + +pub(crate) struct TestConsole { + pub size: (u32, u32), + pub clipping: Option, +} + +impl TestConsole { + pub(crate) fn new(width: u32, height: u32) -> Self { + Self { + size: (width, height), + clipping: None, + } + } + + pub(crate) fn with_clipping(mut self, clipping: Rect) -> Self { + self.clipping = Some(clipping); + self + } +} + +impl Console for TestConsole { + fn get_char_size(&self) -> (u32, u32) { + self.size + } + + fn at(&self, x: i32, y: i32) -> usize { + y as usize * self.size.0 as usize + x as usize + } + + fn get_clipping(&self) -> Option { + self.clipping + } + + fn set_clipping(&mut self, clipping: Option) { + self.clipping = clipping; + } + + fn resize_pixels(&mut self, _width: u32, _height: u32) {} + fn cls(&mut self) {} + fn cls_bg(&mut self, _background: RGBA) {} + fn print(&mut self, _x: i32, _y: i32, _output: &str) {} + fn print_color(&mut self, _x: i32, _y: i32, _fg: RGBA, _bg: RGBA, _output: &str) {} + fn printer( + &mut self, + _x: i32, + _y: i32, + _output: &str, + _align: TextAlign, + _background: Option, + ) { + } + fn set(&mut self, _x: i32, _y: i32, _fg: RGBA, _bg: RGBA, _glyph: FontCharType) {} + fn set_bg(&mut self, _x: i32, _y: i32, _bg: RGBA) {} + fn draw_box(&mut self, _x: i32, _y: i32, _width: i32, _height: i32, _fg: RGBA, _bg: RGBA) {} + fn draw_hollow_box( + &mut self, + _x: i32, + _y: i32, + _width: i32, + _height: i32, + _fg: RGBA, + _bg: RGBA, + ) { + } + fn draw_box_double( + &mut self, + _x: i32, + _y: i32, + _width: i32, + _height: i32, + _fg: RGBA, + _bg: RGBA, + ) { + } + fn draw_hollow_box_double( + &mut self, + _x: i32, + _y: i32, + _width: i32, + _height: i32, + _fg: RGBA, + _bg: RGBA, + ) { + } + fn fill_region(&mut self, _target: Rect, _glyph: FontCharType, _fg: RGBA, _bg: RGBA) {} + fn draw_bar_horizontal( + &mut self, + _x: i32, + _y: i32, + _width: i32, + _n: i32, + _max: i32, + _fg: RGBA, + _bg: RGBA, + ) { + } + fn draw_bar_vertical( + &mut self, + _x: i32, + _y: i32, + _height: i32, + _n: i32, + _max: i32, + _fg: RGBA, + _bg: RGBA, + ) { + } + fn print_centered(&mut self, _y: i32, _text: &str) {} + fn print_color_centered(&mut self, _y: i32, _fg: RGBA, _bg: RGBA, _text: &str) {} + fn print_centered_at(&mut self, _x: i32, _y: i32, _text: &str) {} + fn print_color_centered_at(&mut self, _x: i32, _y: i32, _fg: RGBA, _bg: RGBA, _text: &str) {} + fn print_right(&mut self, _x: i32, _y: i32, _text: &str) {} + fn print_color_right(&mut self, _x: i32, _y: i32, _fg: RGBA, _bg: RGBA, _text: &str) {} + + fn to_xp_layer(&self) -> XpLayer { + XpLayer::new(self.size.0 as usize, self.size.1 as usize) + } + + fn set_offset(&mut self, _x: f32, _y: f32) {} + fn set_scale(&mut self, _scale: f32, _center_x: i32, _center_y: i32) {} + fn get_scale(&self) -> (f32, i32, i32) { + (1.0, 0, 0) + } + + fn as_any(&self) -> &dyn Any { + self + } + fn as_any_mut(&mut self) -> &mut dyn Any { + self + } + + fn set_all_fg_alpha(&mut self, _alpha: f32) {} + fn set_all_bg_alpha(&mut self, _alpha: f32) {} + fn set_all_alpha(&mut self, _fg: f32, _bg: f32) {} + fn set_translation_mode(&mut self, _mode: CharacterTranslationMode) {} + fn set_char_size(&mut self, _width: u32, _height: u32) {} + fn clear_dirty(&mut self) {} +}