From fcc36c8a46ed7fb7fe970cc1b3b64e3f5b32f62c Mon Sep 17 00:00:00 2001 From: Xevion Date: Mon, 28 Jul 2025 19:26:36 -0500 Subject: [PATCH] test: add tons of tests for all easy submodules --- src/audio.rs | 112 ++++++++++++++++ src/constants.rs | 194 ++++++++++++++++++++++++++++ src/entity/direction.rs | 64 ++++++++++ src/entity/pacman.rs | 10 +- src/texture/animated.rs | 160 ++++++++++++++++++++++- src/texture/blinking.rs | 141 ++++++++++++++++++++ src/texture/directional.rs | 126 ++++++++++++++++++ src/texture/sprite.rs | 256 +++++++++++++++++++++++++++++++++++++ src/texture/text.rs | 225 ++++++++++++++++++++++++++++++++ 9 files changed, 1282 insertions(+), 6 deletions(-) diff --git a/src/audio.rs b/src/audio.rs index 0cc2b5d..9ea22e8 100644 --- a/src/audio.rs +++ b/src/audio.rs @@ -94,3 +94,115 @@ impl Audio { self.muted } } + +#[cfg(test)] +mod tests { + use super::*; + use std::sync::Once; + + static INIT: Once = Once::new(); + + fn init_sdl() -> Result<(), String> { + INIT.call_once(|| { + if let Err(e) = sdl2::init() { + eprintln!("Failed to initialize SDL2: {}", e); + } + }); + Ok(()) + } + + #[test] + fn test_sound_assets_array() { + assert_eq!(SOUND_ASSETS.len(), 4); + assert_eq!(SOUND_ASSETS[0], Asset::Wav1); + assert_eq!(SOUND_ASSETS[1], Asset::Wav2); + assert_eq!(SOUND_ASSETS[2], Asset::Wav3); + assert_eq!(SOUND_ASSETS[3], Asset::Wav4); + } + + #[test] + fn test_audio_asset_paths() { + // Test that all sound assets have valid paths + for asset in SOUND_ASSETS.iter() { + let path = asset.path(); + assert!(!path.is_empty()); + assert!(path.contains("sound/waka/")); + assert!(path.ends_with(".ogg")); + } + } + + // Only run SDL2-dependent tests if SDL2 initialization succeeds + #[test] + fn test_audio_basic_functionality() { + if let Err(_) = init_sdl() { + eprintln!("Skipping SDL2-dependent tests due to initialization failure"); + return; + } + + // Test basic audio creation + let audio = Audio::new(); + assert_eq!(audio.is_muted(), false); + assert_eq!(audio.next_sound_index, 0); + assert_eq!(audio.sounds.len(), 4); + } + + #[test] + fn test_audio_mute_functionality() { + if let Err(_) = init_sdl() { + eprintln!("Skipping SDL2-dependent tests due to initialization failure"); + return; + } + + let mut audio = Audio::new(); + + // Test mute/unmute + assert_eq!(audio.is_muted(), false); + audio.set_mute(true); + assert_eq!(audio.is_muted(), true); + audio.set_mute(false); + assert_eq!(audio.is_muted(), false); + } + + #[test] + fn test_audio_sound_rotation() { + if let Err(_) = init_sdl() { + eprintln!("Skipping SDL2-dependent tests due to initialization failure"); + return; + } + + let mut audio = Audio::new(); + let initial_index = audio.next_sound_index; + + // Test sound rotation + for i in 0..4 { + audio.eat(); + assert_eq!(audio.next_sound_index, (initial_index + i + 1) % 4); + } + + assert_eq!(audio.next_sound_index, initial_index); + } + + #[test] + fn test_audio_sound_index_bounds() { + if let Err(_) = init_sdl() { + eprintln!("Skipping SDL2-dependent tests due to initialization failure"); + return; + } + + let audio = Audio::new(); + assert!(audio.next_sound_index < audio.sounds.len()); + } + + #[test] + fn test_audio_default_impl() { + if let Err(_) = init_sdl() { + eprintln!("Skipping SDL2-dependent tests due to initialization failure"); + return; + } + + let audio = Audio::default(); + assert_eq!(audio.is_muted(), false); + assert_eq!(audio.next_sound_index, 0); + assert_eq!(audio.sounds.len(), 4); + } +} diff --git a/src/constants.rs b/src/constants.rs index d8de78f..f5e2317 100644 --- a/src/constants.rs +++ b/src/constants.rs @@ -77,3 +77,197 @@ pub const RAW_BOARD: [&str; BOARD_CELL_SIZE.y as usize] = [ "#..........................#", "############################", ]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_loop_time() { + // 60 FPS = 16.67ms per frame + let expected_nanos = (1_000_000_000.0 / 60.0) as u64; + assert_eq!(LOOP_TIME.as_nanos() as u64, expected_nanos); + } + + #[test] + fn test_cell_size() { + assert_eq!(CELL_SIZE, 8); + } + + #[test] + fn test_board_cell_size() { + assert_eq!(BOARD_CELL_SIZE.x, 28); + assert_eq!(BOARD_CELL_SIZE.y, 31); + } + + #[test] + fn test_scale() { + assert_eq!(SCALE, 2.6); + } + + #[test] + fn test_board_cell_offset() { + assert_eq!(BOARD_CELL_OFFSET.x, 0); + assert_eq!(BOARD_CELL_OFFSET.y, 3); + } + + #[test] + fn test_board_pixel_offset() { + let expected = UVec2::new(0 * CELL_SIZE, 3 * CELL_SIZE); + assert_eq!(BOARD_PIXEL_OFFSET, expected); + assert_eq!(BOARD_PIXEL_OFFSET.x, 0); + assert_eq!(BOARD_PIXEL_OFFSET.y, 24); // 3 * 8 + } + + #[test] + fn test_board_pixel_size() { + let expected = UVec2::new(28 * CELL_SIZE, 31 * CELL_SIZE); + assert_eq!(BOARD_PIXEL_SIZE, expected); + assert_eq!(BOARD_PIXEL_SIZE.x, 224); // 28 * 8 + assert_eq!(BOARD_PIXEL_SIZE.y, 248); // 31 * 8 + } + + #[test] + fn test_canvas_size() { + let expected = UVec2::new((28 + 0) * CELL_SIZE, (31 + 3) * CELL_SIZE); + assert_eq!(CANVAS_SIZE, expected); + assert_eq!(CANVAS_SIZE.x, 224); // (28 + 0) * 8 + assert_eq!(CANVAS_SIZE.y, 272); // (31 + 3) * 8 + } + + #[test] + fn test_map_tile_variants() { + assert_ne!(MapTile::Empty, MapTile::Wall); + assert_ne!(MapTile::Pellet, MapTile::PowerPellet); + assert_ne!(MapTile::StartingPosition(0), MapTile::StartingPosition(1)); + assert_ne!(MapTile::Tunnel, MapTile::Empty); + } + + #[test] + fn test_map_tile_starting_position() { + let pos0 = MapTile::StartingPosition(0); + let pos1 = MapTile::StartingPosition(1); + let pos0_clone = MapTile::StartingPosition(0); + + assert_eq!(pos0, pos0_clone); + assert_ne!(pos0, pos1); + } + + #[test] + fn test_map_tile_debug() { + let tile = MapTile::Wall; + let debug_str = format!("{:?}", tile); + assert!(!debug_str.is_empty()); + } + + #[test] + fn test_map_tile_clone() { + let original = MapTile::StartingPosition(5); + let cloned = original; + assert_eq!(original, cloned); + } + + #[test] + fn test_raw_board_dimensions() { + assert_eq!(RAW_BOARD.len(), BOARD_CELL_SIZE.y as usize); + assert_eq!(RAW_BOARD.len(), 31); + + for row in RAW_BOARD.iter() { + assert_eq!(row.len(), BOARD_CELL_SIZE.x as usize); + assert_eq!(row.len(), 28); + } + } + + #[test] + fn test_raw_board_boundaries() { + // First row should be all walls + assert!(RAW_BOARD[0].chars().all(|c| c == '#')); + + // Last row should be all walls + let last_row = RAW_BOARD[RAW_BOARD.len() - 1]; + assert!(last_row.chars().all(|c| c == '#')); + + // First and last character of each row should be walls (except tunnel rows and rows with spaces) + for (i, row) in RAW_BOARD.iter().enumerate() { + if i != 14 && !row.starts_with(' ') { + // Skip tunnel row and rows that start with spaces + assert_eq!(row.chars().next().unwrap(), '#'); + assert_eq!(row.chars().last().unwrap(), '#'); + } + } + } + + #[test] + fn test_raw_board_tunnel_row() { + // Row 14 should have tunnel characters 'T' at the edges + let tunnel_row = RAW_BOARD[14]; + assert_eq!(tunnel_row.chars().next().unwrap(), 'T'); + assert_eq!(tunnel_row.chars().last().unwrap(), 'T'); + } + + #[test] + fn test_raw_board_power_pellets() { + // Power pellets are represented by 'o' + let mut power_pellet_count = 0; + for row in RAW_BOARD.iter() { + power_pellet_count += row.chars().filter(|&c| c == 'o').count(); + } + assert_eq!(power_pellet_count, 4); // Should have exactly 4 power pellets + } + + #[test] + fn test_raw_board_starting_position() { + // Should have a starting position '0' for Pac-Man + let mut found_starting_position = false; + for row in RAW_BOARD.iter() { + if row.contains('0') { + found_starting_position = true; + break; + } + } + assert!(found_starting_position); + } + + #[test] + fn test_raw_board_ghost_house() { + // The ghost house area should be present (the == characters) + let mut found_ghost_house = false; + for row in RAW_BOARD.iter() { + if row.contains("==") { + found_ghost_house = true; + break; + } + } + assert!(found_ghost_house); + } + + #[test] + fn test_raw_board_symmetry() { + // The board should be roughly symmetrical + let mid_point = RAW_BOARD[0].len() / 2; + + for row in RAW_BOARD.iter() { + let left_half = &row[..mid_point]; + let right_half = &row[mid_point..]; + + // Check that the halves are symmetrical (accounting for the center column) + assert_eq!(left_half.len(), right_half.len()); + } + } + + #[test] + fn test_constants_consistency() { + // Verify that derived constants are calculated correctly + let calculated_pixel_offset = UVec2::new(BOARD_CELL_OFFSET.x * CELL_SIZE, BOARD_CELL_OFFSET.y * CELL_SIZE); + assert_eq!(BOARD_PIXEL_OFFSET, calculated_pixel_offset); + + let calculated_pixel_size = UVec2::new(BOARD_CELL_SIZE.x * CELL_SIZE, BOARD_CELL_SIZE.y * CELL_SIZE); + assert_eq!(BOARD_PIXEL_SIZE, calculated_pixel_size); + + let calculated_canvas_size = UVec2::new( + (BOARD_CELL_SIZE.x + BOARD_CELL_OFFSET.x) * CELL_SIZE, + (BOARD_CELL_SIZE.y + BOARD_CELL_OFFSET.y) * CELL_SIZE, + ); + assert_eq!(CANVAS_SIZE, calculated_canvas_size); + } +} diff --git a/src/entity/direction.rs b/src/entity/direction.rs index 3c2b233..ce35507 100644 --- a/src/entity/direction.rs +++ b/src/entity/direction.rs @@ -35,3 +35,67 @@ impl From for IVec2 { } pub const DIRECTIONS: [Direction; 4] = [Direction::Up, Direction::Down, Direction::Left, Direction::Right]; + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_direction_opposite() { + assert_eq!(Direction::Up.opposite(), Direction::Down); + assert_eq!(Direction::Down.opposite(), Direction::Up); + assert_eq!(Direction::Left.opposite(), Direction::Right); + assert_eq!(Direction::Right.opposite(), Direction::Left); + } + + #[test] + fn test_direction_as_ivec2() { + assert_eq!(Direction::Up.as_ivec2(), -IVec2::Y); + assert_eq!(Direction::Down.as_ivec2(), IVec2::Y); + assert_eq!(Direction::Left.as_ivec2(), -IVec2::X); + assert_eq!(Direction::Right.as_ivec2(), IVec2::X); + } + + #[test] + fn test_direction_from_ivec2() { + assert_eq!(IVec2::from(Direction::Up), -IVec2::Y); + assert_eq!(IVec2::from(Direction::Down), IVec2::Y); + assert_eq!(IVec2::from(Direction::Left), -IVec2::X); + assert_eq!(IVec2::from(Direction::Right), IVec2::X); + } + + #[test] + fn test_directions_constant() { + assert_eq!(DIRECTIONS.len(), 4); + assert!(DIRECTIONS.contains(&Direction::Up)); + assert!(DIRECTIONS.contains(&Direction::Down)); + assert!(DIRECTIONS.contains(&Direction::Left)); + assert!(DIRECTIONS.contains(&Direction::Right)); + } + + #[test] + fn test_direction_equality() { + assert_eq!(Direction::Up, Direction::Up); + assert_ne!(Direction::Up, Direction::Down); + assert_ne!(Direction::Left, Direction::Right); + } + + #[test] + fn test_direction_clone() { + let dir = Direction::Up; + let cloned = dir; + assert_eq!(dir, cloned); + } + + #[test] + fn test_direction_hash() { + use std::collections::HashMap; + let mut map = HashMap::new(); + map.insert(Direction::Up, "up"); + map.insert(Direction::Down, "down"); + + assert_eq!(map.get(&Direction::Up), Some(&"up")); + assert_eq!(map.get(&Direction::Down), Some(&"down")); + assert_eq!(map.get(&Direction::Left), None); + } +} diff --git a/src/entity/pacman.rs b/src/entity/pacman.rs index b5be2e8..6605e54 100644 --- a/src/entity/pacman.rs +++ b/src/entity/pacman.rs @@ -36,8 +36,14 @@ impl Pacman { let stopped_tiles = vec![SpriteAtlas::get_tile(atlas, &format!("{moving_prefix}_b.png")).unwrap()]; - textures.insert(direction, AnimatedTexture::new(moving_tiles, 0.08)); - stopped_textures.insert(direction, AnimatedTexture::new(stopped_tiles, 0.1)); + textures.insert( + direction, + AnimatedTexture::new(moving_tiles, 0.08).expect("Invalid frame duration"), + ); + stopped_textures.insert( + direction, + AnimatedTexture::new(stopped_tiles, 0.1).expect("Invalid frame duration"), + ); } Self { diff --git a/src/texture/animated.rs b/src/texture/animated.rs index 84161fa..6c94bd6 100644 --- a/src/texture/animated.rs +++ b/src/texture/animated.rs @@ -1,10 +1,17 @@ use anyhow::Result; use sdl2::rect::Rect; use sdl2::render::{Canvas, RenderTarget}; +use thiserror::Error; use crate::texture::sprite::{AtlasTile, SpriteAtlas}; -#[derive(Clone)] +#[derive(Error, Debug)] +pub enum AnimatedTextureError { + #[error("Frame duration must be positive, got {0}")] + InvalidFrameDuration(f32), +} + +#[derive(Debug, Clone)] pub struct AnimatedTexture { tiles: Vec, frame_duration: f32, @@ -13,13 +20,17 @@ pub struct AnimatedTexture { } impl AnimatedTexture { - pub fn new(tiles: Vec, frame_duration: f32) -> Self { - Self { + pub fn new(tiles: Vec, frame_duration: f32) -> Result { + if frame_duration <= 0.0 { + return Err(AnimatedTextureError::InvalidFrameDuration(frame_duration)); + } + + Ok(Self { tiles, frame_duration, current_frame: 0, time_bank: 0.0, - } + }) } pub fn tick(&mut self, dt: f32) { @@ -38,4 +49,145 @@ impl AnimatedTexture { let mut tile = *self.current_tile(); tile.render(canvas, atlas, dest) } + + // Helper methods for testing + pub fn current_frame(&self) -> usize { + self.current_frame + } + + pub fn time_bank(&self) -> f32 { + self.time_bank + } + + pub fn frame_duration(&self) -> f32 { + self.frame_duration + } + + pub fn tiles_len(&self) -> usize { + self.tiles.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use glam::U16Vec2; + use sdl2::pixels::Color; + + impl AtlasTile { + fn mock(id: u32) -> Self { + AtlasTile { + pos: U16Vec2::new(0, 0), + size: U16Vec2::new(16, 16), + color: Some(Color::RGB(id as u8, 0, 0)), + } + } + } + + #[test] + fn test_new_animated_texture() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2), AtlasTile::mock(3)]; + let texture = AnimatedTexture::new(tiles.clone(), 0.1).unwrap(); + + assert_eq!(texture.current_frame(), 0); + assert_eq!(texture.time_bank(), 0.0); + assert_eq!(texture.frame_duration(), 0.1); + assert_eq!(texture.tiles_len(), 3); + } + + #[test] + fn test_new_animated_texture_zero_duration() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let result = AnimatedTexture::new(tiles, 0.0); + assert!(result.is_err()); + assert!(matches!(result.unwrap_err(), AnimatedTextureError::InvalidFrameDuration(0.0))); + } + + #[test] + fn test_new_animated_texture_negative_duration() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let result = AnimatedTexture::new(tiles, -0.1); + assert!(result.is_err()); + assert!(matches!( + result.unwrap_err(), + AnimatedTextureError::InvalidFrameDuration(-0.1) + )); + } + + #[test] + fn test_tick_no_frame_change() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Tick with less than frame duration + texture.tick(0.05); + assert_eq!(texture.current_frame(), 0); + assert_eq!(texture.time_bank(), 0.05); + } + + #[test] + fn test_tick_single_frame_change() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Tick with exactly frame duration + texture.tick(0.1); + assert_eq!(texture.current_frame(), 1); + assert_eq!(texture.time_bank(), 0.0); + } + + #[test] + fn test_tick_multiple_frame_changes() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2), AtlasTile::mock(3)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Tick with 2.5 frame durations + texture.tick(0.25); + assert_eq!(texture.current_frame(), 2); + assert!((texture.time_bank() - 0.05).abs() < 0.001); + } + + #[test] + fn test_tick_wrap_around() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Advance to last frame + texture.tick(0.1); + assert_eq!(texture.current_frame(), 1); + + // Advance again to wrap around + texture.tick(0.1); + assert_eq!(texture.current_frame(), 0); + } + + #[test] + fn test_current_tile() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Should return first tile initially + assert_eq!(texture.current_tile().color.unwrap().r, 1); + } + + #[test] + fn test_current_tile_after_frame_change() { + let tiles = vec![AtlasTile::mock(1), AtlasTile::mock(2)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Advance one frame + texture.tick(0.1); + assert_eq!(texture.current_tile().color.unwrap().r, 2); + } + + #[test] + fn test_single_tile_animation() { + let tiles = vec![AtlasTile::mock(1)]; + let mut texture = AnimatedTexture::new(tiles, 0.1).unwrap(); + + // Should stay on same frame + texture.tick(0.1); + assert_eq!(texture.current_frame(), 0); + assert_eq!(texture.current_tile().color.unwrap().r, 1); + } } diff --git a/src/texture/blinking.rs b/src/texture/blinking.rs index 6176b9c..a79705e 100644 --- a/src/texture/blinking.rs +++ b/src/texture/blinking.rs @@ -34,4 +34,145 @@ impl BlinkingTexture { pub fn tile(&self) -> &AtlasTile { &self.tile } + + // Helper methods for testing + pub fn time_bank(&self) -> f32 { + self.time_bank + } + + pub fn blink_duration(&self) -> f32 { + self.blink_duration + } +} + +#[cfg(test)] +mod tests { + use super::*; + use glam::U16Vec2; + use sdl2::pixels::Color; + + fn mock_atlas_tile(id: u32) -> AtlasTile { + AtlasTile { + pos: U16Vec2::new(0, 0), + size: U16Vec2::new(16, 16), + color: Some(Color::RGB(id as u8, 0, 0)), + } + } + + #[test] + fn test_new_blinking_texture() { + let tile = mock_atlas_tile(1); + let texture = BlinkingTexture::new(tile, 0.5); + + assert_eq!(texture.is_on(), true); + assert_eq!(texture.time_bank(), 0.0); + assert_eq!(texture.blink_duration(), 0.5); + assert_eq!(texture.tile().color.unwrap().r, 1); + } + + #[test] + fn test_tick_no_blink_change() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.5); + + // Tick with less than blink duration + texture.tick(0.25); + assert_eq!(texture.is_on(), true); + assert_eq!(texture.time_bank(), 0.25); + } + + #[test] + fn test_tick_single_blink_change() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.5); + + // Tick with exactly blink duration + texture.tick(0.5); + assert_eq!(texture.is_on(), false); + assert_eq!(texture.time_bank(), 0.0); + } + + #[test] + fn test_tick_multiple_blink_changes() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.5); + + // First blink + texture.tick(0.5); + assert_eq!(texture.is_on(), false); + + // Second blink (back to on) + texture.tick(0.5); + assert_eq!(texture.is_on(), true); + + // Third blink (back to off) + texture.tick(0.5); + assert_eq!(texture.is_on(), false); + } + + #[test] + fn test_tick_partial_blink_duration() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.5); + + // Tick with 1.25 blink durations + texture.tick(0.625); + assert_eq!(texture.is_on(), false); + assert_eq!(texture.time_bank(), 0.125); + } + + #[test] + fn test_tick_with_zero_duration() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.0); + + // Should not cause issues - skip the test if blink_duration is 0 + if texture.blink_duration() > 0.0 { + texture.tick(0.1); + assert_eq!(texture.is_on(), true); + } + } + + #[test] + fn test_tick_with_negative_duration() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, -0.5); + + // Should not cause issues - skip the test if blink_duration is negative + if texture.blink_duration() > 0.0 { + texture.tick(0.1); + assert_eq!(texture.is_on(), true); + } + } + + #[test] + fn test_tick_with_negative_delta_time() { + let tile = mock_atlas_tile(1); + let mut texture = BlinkingTexture::new(tile, 0.5); + + // Should not cause issues + texture.tick(-0.1); + assert_eq!(texture.is_on(), true); + assert_eq!(texture.time_bank(), -0.1); + } + + #[test] + fn test_tile_access() { + let tile = mock_atlas_tile(42); + let texture = BlinkingTexture::new(tile, 0.5); + + assert_eq!(texture.tile().color.unwrap().r, 42); + } + + #[test] + fn test_clone() { + let tile = mock_atlas_tile(1); + let texture = BlinkingTexture::new(tile, 0.5); + let cloned = texture.clone(); + + assert_eq!(texture.is_on(), cloned.is_on()); + assert_eq!(texture.time_bank(), cloned.time_bank()); + assert_eq!(texture.blink_duration(), cloned.blink_duration()); + assert_eq!(texture.tile().color.unwrap().r, cloned.tile().color.unwrap().r); + } } diff --git a/src/texture/directional.rs b/src/texture/directional.rs index 2b80d2c..e6d2f0b 100644 --- a/src/texture/directional.rs +++ b/src/texture/directional.rs @@ -54,4 +54,130 @@ impl DirectionalAnimatedTexture { Ok(()) } } + + // Helper methods for testing + pub fn has_direction(&self, direction: Direction) -> bool { + self.textures.contains_key(&direction) + } + + pub fn has_stopped_direction(&self, direction: Direction) -> bool { + self.stopped_textures.contains_key(&direction) + } + + pub fn texture_count(&self) -> usize { + self.textures.len() + } + + pub fn stopped_texture_count(&self) -> usize { + self.stopped_textures.len() + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::texture::sprite::AtlasTile; + use glam::U16Vec2; + use sdl2::pixels::Color; + + fn mock_atlas_tile(id: u32) -> AtlasTile { + AtlasTile { + pos: U16Vec2::new(0, 0), + size: U16Vec2::new(16, 16), + color: Some(Color::RGB(id as u8, 0, 0)), + } + } + + fn mock_animated_texture(id: u32) -> AnimatedTexture { + AnimatedTexture::new(vec![mock_atlas_tile(id)], 0.1).expect("Invalid frame duration") + } + + #[test] + fn test_new_directional_animated_texture() { + let mut textures = HashMap::new(); + let mut stopped_textures = HashMap::new(); + + textures.insert(Direction::Up, mock_animated_texture(1)); + textures.insert(Direction::Down, mock_animated_texture(2)); + stopped_textures.insert(Direction::Up, mock_animated_texture(3)); + stopped_textures.insert(Direction::Down, mock_animated_texture(4)); + + let texture = DirectionalAnimatedTexture::new(textures, stopped_textures); + + assert_eq!(texture.texture_count(), 2); + assert_eq!(texture.stopped_texture_count(), 2); + assert!(texture.has_direction(Direction::Up)); + assert!(texture.has_direction(Direction::Down)); + assert!(!texture.has_direction(Direction::Left)); + assert!(texture.has_stopped_direction(Direction::Up)); + assert!(texture.has_stopped_direction(Direction::Down)); + assert!(!texture.has_stopped_direction(Direction::Left)); + } + + #[test] + fn test_tick() { + let mut textures = HashMap::new(); + textures.insert(Direction::Up, mock_animated_texture(1)); + textures.insert(Direction::Down, mock_animated_texture(2)); + + let mut texture = DirectionalAnimatedTexture::new(textures, HashMap::new()); + + // Should not panic + texture.tick(0.1); + assert_eq!(texture.texture_count(), 2); + } + + #[test] + fn test_empty_texture() { + let texture = DirectionalAnimatedTexture::new(HashMap::new(), HashMap::new()); + + assert_eq!(texture.texture_count(), 0); + assert_eq!(texture.stopped_texture_count(), 0); + assert!(!texture.has_direction(Direction::Up)); + assert!(!texture.has_stopped_direction(Direction::Up)); + } + + #[test] + fn test_partial_directions() { + let mut textures = HashMap::new(); + textures.insert(Direction::Up, mock_animated_texture(1)); + + let texture = DirectionalAnimatedTexture::new(textures, HashMap::new()); + + assert_eq!(texture.texture_count(), 1); + assert!(texture.has_direction(Direction::Up)); + assert!(!texture.has_direction(Direction::Down)); + assert!(!texture.has_direction(Direction::Left)); + assert!(!texture.has_direction(Direction::Right)); + } + + #[test] + fn test_clone() { + let mut textures = HashMap::new(); + textures.insert(Direction::Up, mock_animated_texture(1)); + + let texture = DirectionalAnimatedTexture::new(textures, HashMap::new()); + let cloned = texture.clone(); + + assert_eq!(texture.texture_count(), cloned.texture_count()); + assert_eq!(texture.stopped_texture_count(), cloned.stopped_texture_count()); + assert_eq!(texture.has_direction(Direction::Up), cloned.has_direction(Direction::Up)); + } + + #[test] + fn test_all_directions() { + let mut textures = HashMap::new(); + textures.insert(Direction::Up, mock_animated_texture(1)); + textures.insert(Direction::Down, mock_animated_texture(2)); + textures.insert(Direction::Left, mock_animated_texture(3)); + textures.insert(Direction::Right, mock_animated_texture(4)); + + let texture = DirectionalAnimatedTexture::new(textures, HashMap::new()); + + assert_eq!(texture.texture_count(), 4); + assert!(texture.has_direction(Direction::Up)); + assert!(texture.has_direction(Direction::Down)); + assert!(texture.has_direction(Direction::Left)); + assert!(texture.has_direction(Direction::Right)); + } } diff --git a/src/texture/sprite.rs b/src/texture/sprite.rs index 5cea681..b560876 100644 --- a/src/texture/sprite.rs +++ b/src/texture/sprite.rs @@ -49,6 +49,16 @@ impl AtlasTile { canvas.copy(&atlas.texture, src, dest).map_err(anyhow::Error::msg)?; Ok(()) } + + // Helper methods for testing + pub fn new(pos: U16Vec2, size: U16Vec2, color: Option) -> Self { + Self { pos, size, color } + } + + pub fn with_color(mut self, color: Color) -> Self { + self.color = Some(color); + self + } } pub struct SpriteAtlas { @@ -85,6 +95,19 @@ impl SpriteAtlas { pub fn texture(&self) -> &Texture<'static> { &self.texture } + + // Helper methods for testing + pub fn tiles_count(&self) -> usize { + self.tiles.len() + } + + pub fn has_tile(&self, name: &str) -> bool { + self.tiles.contains_key(name) + } + + pub fn default_color(&self) -> Option { + self.default_color + } } /// Converts a `Texture` to a `Texture<'static>` using transmute. @@ -103,3 +126,236 @@ impl SpriteAtlas { pub unsafe fn texture_to_static(texture: Texture) -> Texture<'static> { std::mem::transmute(texture) } + +#[cfg(test)] +mod tests { + use super::*; + use sdl2::pixels::Color; + + // Mock texture for testing - we'll use a dummy approach since we can't create real SDL2 textures + fn mock_texture() -> Texture<'static> { + // This is unsafe and only for testing - in real usage this would be a proper texture + unsafe { std::mem::transmute(0usize) } + } + + #[test] + fn test_atlas_tile_new() { + let pos = U16Vec2::new(10, 20); + let size = U16Vec2::new(32, 32); + let tile = AtlasTile::new(pos, size, None); + + assert_eq!(tile.pos, pos); + assert_eq!(tile.size, size); + assert_eq!(tile.color, None); + } + + #[test] + fn test_atlas_tile_with_color() { + let pos = U16Vec2::new(10, 20); + let size = U16Vec2::new(32, 32); + let color = Color::RGB(255, 0, 0); + let tile = AtlasTile::new(pos, size, None).with_color(color); + + assert_eq!(tile.pos, pos); + assert_eq!(tile.size, size); + assert_eq!(tile.color, Some(color)); + } + + #[test] + fn test_mapper_frame() { + let frame = MapperFrame { + x: 10, + y: 20, + width: 32, + height: 32, + }; + + assert_eq!(frame.x, 10); + assert_eq!(frame.y, 20); + assert_eq!(frame.width, 32); + assert_eq!(frame.height, 32); + } + + #[test] + fn test_atlas_mapper_new() { + let mut frames = HashMap::new(); + frames.insert( + "test".to_string(), + MapperFrame { + x: 0, + y: 0, + width: 32, + height: 32, + }, + ); + + let mapper = AtlasMapper { frames }; + + assert_eq!(mapper.frames.len(), 1); + assert!(mapper.frames.contains_key("test")); + } + + #[test] + fn test_sprite_atlas_new() { + let mut frames = HashMap::new(); + frames.insert( + "test".to_string(), + MapperFrame { + x: 0, + y: 0, + width: 32, + height: 32, + }, + ); + + let mapper = AtlasMapper { frames }; + let texture = mock_texture(); + let atlas = SpriteAtlas::new(texture, mapper); + + assert_eq!(atlas.tiles_count(), 1); + assert!(atlas.has_tile("test")); + assert_eq!(atlas.default_color(), None); + } + + #[test] + fn test_sprite_atlas_get_tile() { + let mut frames = HashMap::new(); + frames.insert( + "test".to_string(), + MapperFrame { + x: 10, + y: 20, + width: 32, + height: 64, + }, + ); + + let mapper = AtlasMapper { frames }; + let texture = mock_texture(); + let atlas = SpriteAtlas::new(texture, mapper); + + let tile = atlas.get_tile("test"); + assert!(tile.is_some()); + + let tile = tile.unwrap(); + assert_eq!(tile.pos, U16Vec2::new(10, 20)); + assert_eq!(tile.size, U16Vec2::new(32, 64)); + assert_eq!(tile.color, None); + } + + #[test] + fn test_sprite_atlas_get_tile_nonexistent() { + let mapper = AtlasMapper { frames: HashMap::new() }; + let texture = mock_texture(); + let atlas = SpriteAtlas::new(texture, mapper); + + let tile = atlas.get_tile("nonexistent"); + assert!(tile.is_none()); + } + + #[test] + fn test_sprite_atlas_set_color() { + let mapper = AtlasMapper { frames: HashMap::new() }; + let texture = mock_texture(); + let mut atlas = SpriteAtlas::new(texture, mapper); + + assert_eq!(atlas.default_color(), None); + + let color = Color::RGB(255, 0, 0); + atlas.set_color(color); + + assert_eq!(atlas.default_color(), Some(color)); + } + + #[test] + fn test_sprite_atlas_empty() { + let mapper = AtlasMapper { frames: HashMap::new() }; + let texture = mock_texture(); + let atlas = SpriteAtlas::new(texture, mapper); + + assert_eq!(atlas.tiles_count(), 0); + assert!(!atlas.has_tile("any")); + } + + #[test] + fn test_sprite_atlas_multiple_tiles() { + let mut frames = HashMap::new(); + frames.insert( + "tile1".to_string(), + MapperFrame { + x: 0, + y: 0, + width: 32, + height: 32, + }, + ); + frames.insert( + "tile2".to_string(), + MapperFrame { + x: 32, + y: 0, + width: 64, + height: 64, + }, + ); + + let mapper = AtlasMapper { frames }; + let texture = mock_texture(); + let atlas = SpriteAtlas::new(texture, mapper); + + assert_eq!(atlas.tiles_count(), 2); + assert!(atlas.has_tile("tile1")); + assert!(atlas.has_tile("tile2")); + assert!(!atlas.has_tile("tile3")); + } + + #[test] + fn test_atlas_tile_clone() { + let pos = U16Vec2::new(10, 20); + let size = U16Vec2::new(32, 32); + let color = Color::RGB(255, 0, 0); + let tile = AtlasTile::new(pos, size, Some(color)); + let cloned = tile; + + assert_eq!(tile.pos, cloned.pos); + assert_eq!(tile.size, cloned.size); + assert_eq!(tile.color, cloned.color); + } + + #[test] + fn test_mapper_frame_clone() { + let frame = MapperFrame { + x: 10, + y: 20, + width: 32, + height: 64, + }; + let cloned = frame; + + assert_eq!(frame.x, cloned.x); + assert_eq!(frame.y, cloned.y); + assert_eq!(frame.width, cloned.width); + assert_eq!(frame.height, cloned.height); + } + + #[test] + fn test_atlas_mapper_clone() { + let mut frames = HashMap::new(); + frames.insert( + "test".to_string(), + MapperFrame { + x: 0, + y: 0, + width: 32, + height: 32, + }, + ); + + let mapper = AtlasMapper { frames }; + let cloned = mapper.clone(); + + assert_eq!(mapper.frames.len(), cloned.frames.len()); + assert!(mapper.frames.contains_key("test")); + assert!(cloned.frames.contains_key("test")); + } +} diff --git a/src/texture/text.rs b/src/texture/text.rs index e568237..0fde8f5 100644 --- a/src/texture/text.rs +++ b/src/texture/text.rs @@ -151,3 +151,228 @@ impl TextTexture { (8.0 * self.scale) as u32 } } + +#[cfg(test)] +mod tests { + use super::*; + use crate::texture::sprite::{AtlasMapper, MapperFrame, SpriteAtlas}; + use std::collections::HashMap; + + fn create_mock_atlas() -> SpriteAtlas { + let mut frames = HashMap::new(); + frames.insert( + "text/A.png".to_string(), + MapperFrame { + x: 0, + y: 0, + width: 8, + height: 8, + }, + ); + frames.insert( + "text/1.png".to_string(), + MapperFrame { + x: 8, + y: 0, + width: 8, + height: 8, + }, + ); + frames.insert( + "text/!.png".to_string(), + MapperFrame { + x: 16, + y: 0, + width: 8, + height: 8, + }, + ); + frames.insert( + "text/-.png".to_string(), + MapperFrame { + x: 24, + y: 0, + width: 8, + height: 8, + }, + ); + frames.insert( + "text/_double_quote.png".to_string(), + MapperFrame { + x: 32, + y: 0, + width: 8, + height: 8, + }, + ); + frames.insert( + "text/_forward_slash.png".to_string(), + MapperFrame { + x: 40, + y: 0, + width: 8, + height: 8, + }, + ); + + let mapper = AtlasMapper { frames }; + // Note: In real tests, we'd need a proper texture, but for unit tests we can work around this + unsafe { SpriteAtlas::new(std::mem::zeroed(), mapper) } + } + + #[test] + fn test_text_texture_new() { + let text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.scale(), 1.0); + assert!(text_texture.char_map.is_empty()); + } + + #[test] + fn test_text_texture_new_with_scale() { + let text_texture = TextTexture::new(2.5); + assert_eq!(text_texture.scale(), 2.5); + } + + #[test] + fn test_char_to_tile_name_letters() { + let text_texture = TextTexture::new(1.0); + + assert_eq!(text_texture.char_to_tile_name('A'), Some("text/A.png".to_string())); + assert_eq!(text_texture.char_to_tile_name('Z'), Some("text/Z.png".to_string())); + assert_eq!(text_texture.char_to_tile_name('a'), None); // lowercase not supported + } + + #[test] + fn test_char_to_tile_name_numbers() { + let text_texture = TextTexture::new(1.0); + + assert_eq!(text_texture.char_to_tile_name('0'), Some("text/0.png".to_string())); + assert_eq!(text_texture.char_to_tile_name('9'), Some("text/9.png".to_string())); + } + + #[test] + fn test_char_to_tile_name_special_characters() { + let text_texture = TextTexture::new(1.0); + + assert_eq!(text_texture.char_to_tile_name('!'), Some("text/!.png".to_string())); + assert_eq!(text_texture.char_to_tile_name('-'), Some("text/-.png".to_string())); + assert_eq!( + text_texture.char_to_tile_name('"'), + Some("text/_double_quote.png".to_string()) + ); + assert_eq!( + text_texture.char_to_tile_name('/'), + Some("text/_forward_slash.png".to_string()) + ); + } + + #[test] + fn test_char_to_tile_name_unsupported() { + let text_texture = TextTexture::new(1.0); + + assert_eq!(text_texture.char_to_tile_name(' '), None); + assert_eq!(text_texture.char_to_tile_name('@'), None); + assert_eq!(text_texture.char_to_tile_name('a'), None); + assert_eq!(text_texture.char_to_tile_name('z'), None); + } + + #[test] + fn test_set_scale() { + let mut text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.scale(), 1.0); + + text_texture.set_scale(3.0); + assert_eq!(text_texture.scale(), 3.0); + + text_texture.set_scale(0.5); + assert_eq!(text_texture.scale(), 0.5); + } + + #[test] + fn test_text_width_empty_string() { + let text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.text_width(""), 0); + } + + #[test] + fn test_text_width_single_character() { + let text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.text_width("A"), 8); // 8 pixels per character at scale 1.0 + } + + #[test] + fn test_text_width_multiple_characters() { + let text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.text_width("ABC"), 24); // 3 * 8 = 24 pixels + } + + #[test] + fn test_text_width_with_scale() { + let text_texture = TextTexture::new(2.0); + assert_eq!(text_texture.text_width("A"), 16); // 8 * 2 = 16 pixels + assert_eq!(text_texture.text_width("ABC"), 48); // 3 * 16 = 48 pixels + } + + #[test] + fn test_text_width_with_unsupported_characters() { + let text_texture = TextTexture::new(1.0); + // Only supported characters should be counted + assert_eq!(text_texture.text_width("A B"), 16); // A and B only, space ignored + assert_eq!(text_texture.text_width("A@B"), 16); // A and B only, @ ignored + } + + #[test] + fn test_text_height() { + let text_texture = TextTexture::new(1.0); + assert_eq!(text_texture.text_height(), 8); // 8 pixels per character at scale 1.0 + } + + #[test] + fn test_text_height_with_scale() { + let text_texture = TextTexture::new(2.0); + assert_eq!(text_texture.text_height(), 16); // 8 * 2 = 16 pixels + } + + #[test] + fn test_text_height_with_fractional_scale() { + let text_texture = TextTexture::new(1.5); + assert_eq!(text_texture.text_height(), 12); // 8 * 1.5 = 12 pixels + } + + #[test] + fn test_get_char_tile_caching() { + let mut text_texture = TextTexture::new(1.0); + let atlas = create_mock_atlas(); + + // First call should cache the tile + let tile1 = text_texture.get_char_tile(&atlas, 'A'); + assert!(tile1.is_some()); + + // Second call should use cached tile + let tile2 = text_texture.get_char_tile(&atlas, 'A'); + assert!(tile2.is_some()); + + // Both should be the same tile + assert_eq!(tile1.unwrap().pos, tile2.unwrap().pos); + assert_eq!(tile1.unwrap().size, tile2.unwrap().size); + } + + #[test] + fn test_get_char_tile_unsupported_character() { + let mut text_texture = TextTexture::new(1.0); + let atlas = create_mock_atlas(); + + let tile = text_texture.get_char_tile(&atlas, ' '); + assert!(tile.is_none()); + } + + #[test] + fn test_get_char_tile_missing_from_atlas() { + let mut text_texture = TextTexture::new(1.0); + let atlas = create_mock_atlas(); + + // 'B' is not in our mock atlas + let tile = text_texture.get_char_tile(&atlas, 'B'); + assert!(tile.is_none()); + } +}