Files
smart-rgb/crates/borders-core/tests/common/assertions.rs
2025-10-25 16:15:50 -05:00

270 lines
12 KiB
Rust

//! Fluent assertion API for testing Bevy ECS World state
//!
//! Provides chainable assertion methods that test behavior through public interfaces.
use assert2::assert;
use extension_traits::extension;
use std::collections::HashSet;
use borders_core::prelude::*;
/// Fluent assertion builder for World state
///
/// Access via `world.assert()` to chain multiple assertions.
///
/// # Example
/// ```
/// world.assert()
/// .player_owns(tile, player_id)
/// .has_territory_changes()
/// .no_invalid_state();
/// ```
pub struct WorldAssertions<'w> {
world: &'w World,
}
/// Extension trait to add assertion capabilities to World
#[extension(pub trait WorldAssertExt)]
impl World {
/// Begin a fluent assertion chain
fn assert(&self) -> WorldAssertions<'_> {
WorldAssertions { world: self }
}
}
impl<'w> WorldAssertions<'w> {
/// Assert that a player owns a specific tile
#[track_caller]
pub fn player_owns(self, tile: U16Vec2, expected: NationId) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
let actual = mgr.get_nation_id(tile);
assert!(actual == Some(expected), "Expected player {} to own tile {:?}, but found {:?}", expected.get(), tile, actual.map(|id| id.get()));
self
}
/// Assert that a tile is unclaimed
#[track_caller]
pub fn tile_unclaimed(self, tile: U16Vec2) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
let ownership = mgr.get_ownership(tile);
assert!(ownership.is_unclaimed(), "Expected tile {:?} to be unclaimed, but it's owned by {:?}", tile, ownership.nation_id());
self
}
/// Assert that an attack exists between attacker and target
#[track_caller]
pub fn attack_exists(self, attacker: NationId, target: Option<NationId>) -> Self {
let attacks = self.world.resource::<ActiveAttacks>();
let player_attacks = attacks.get_attacks_for_player(attacker);
let exists = player_attacks.iter().any(|(att, tgt, _, _, is_outgoing)| *att == attacker && *tgt == target && *is_outgoing);
assert!(exists, "Expected attack from player {} to {:?}, but none found. Active attacks: {:?}", attacker.get(), target.map(|t| t.get()), player_attacks);
self
}
/// Assert that no attack exists between attacker and target
#[track_caller]
pub fn no_attack(self, attacker: NationId, target: Option<NationId>) -> Self {
let attacks = self.world.resource::<ActiveAttacks>();
let player_attacks = attacks.get_attacks_for_player(attacker);
let exists = player_attacks.iter().any(|(att, tgt, _, _, is_outgoing)| *att == attacker && *tgt == target && *is_outgoing);
assert!(!exists, "Expected NO attack from player {} to {:?}, but found one. Active attacks: {:?}", attacker.get(), target.map(|t| t.get()), player_attacks);
self
}
/// Assert that a player has specific border tiles
#[track_caller]
pub fn border_tiles(self, player_id: NationId, expected: &HashSet<U16Vec2>) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
assert!(&border_tiles.0 == expected, "Border tiles mismatch for player {}.\nExpected: {:?}\nActual: {:?}", player_id.get(), expected, border_tiles.0);
self
}
/// Assert that TerritoryManager has recorded changes
#[track_caller]
pub fn has_territory_changes(self) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
assert!(mgr.has_changes(), "Expected territory changes to be tracked, but ChangeBuffer is empty");
self
}
/// Assert that TerritoryManager has NO recorded changes
#[track_caller]
pub fn no_territory_changes(self) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
assert!(!mgr.has_changes(), "Expected no territory changes, but ChangeBuffer contains: {:?}", mgr.iter_changes().collect::<Vec<_>>());
self
}
/// Assert that a resource exists in the world
#[track_caller]
pub fn resource_exists<T: Resource>(self, resource_name: &str) -> Self {
assert!(self.world.contains_resource::<T>(), "Expected resource '{}' to exist, but it was not found", resource_name);
self
}
/// Assert that a resource does NOT exist in the world
#[track_caller]
pub fn resource_missing<T: Resource>(self, resource_name: &str) -> Self {
assert!(!self.world.contains_resource::<T>(), "Expected resource '{}' to be missing, but it exists", resource_name);
self
}
/// Assert that no invalid game state exists
///
/// Checks common invariants:
/// - No self-attacks in ActiveAttacks
/// - All player entities have required components
/// - Territory ownership is consistent
#[track_caller]
pub fn no_invalid_state(self) -> Self {
let attacks = self.world.resource::<ActiveAttacks>();
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
for &player_id in entity_map.0.keys() {
let player_attacks = attacks.get_attacks_for_player(player_id);
for (attacker, target, _, _, _) in player_attacks {
if let Some(target_id) = target {
assert!(attacker != target_id, "INVARIANT VIOLATION: Player {} is attacking itself", attacker.get());
}
}
}
for (&player_id, &entity) in &entity_map.0 {
assert!(self.world.get::<borders_core::game::Troops>(entity).is_some(), "Player {} entity missing Troops component", player_id.get());
assert!(self.world.get::<BorderTiles>(entity).is_some(), "Player {} entity missing BorderTiles component", player_id.get());
}
self
}
/// Assert that a player has a specific troop count
#[track_caller]
pub fn player_troops(self, player_id: NationId, expected: f32) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
let troops = self.world.get::<borders_core::game::Troops>(*entity).expect("Troops component not found");
let difference = (troops.0 - expected).abs();
assert!(difference < 0.01, "Expected player {} to have {} troops, but found {} (difference: {})", player_id.get(), expected, troops.0, difference);
self
}
/// Assert that a tile is a border tile
#[allow(clippy::wrong_self_convention)]
#[track_caller]
pub fn is_border(self, tile: U16Vec2) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
assert!(mgr.is_border(tile), "Expected tile {:?} to be a border tile, but it is not", tile);
self
}
/// Assert that a tile is NOT a border tile
#[track_caller]
pub fn not_border(self, tile: U16Vec2) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
assert!(!mgr.is_border(tile), "Expected tile {:?} to NOT be a border tile, but it is", tile);
self
}
/// Assert that a player owns all tiles in a slice
#[track_caller]
pub fn player_owns_all(self, tiles: &[U16Vec2], expected: NationId) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
for &tile in tiles {
let actual = mgr.get_nation_id(tile);
assert!(actual == Some(expected), "Expected player {} to own tile {:?}, but found {:?}", expected.get(), tile, actual.map(|id| id.get()));
}
self
}
/// Assert that the ChangeBuffer contains exactly N changes
#[track_caller]
pub fn change_count(self, expected: usize) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
let actual = mgr.iter_changes().count();
assert!(actual == expected, "Expected {} changes in ChangeBuffer, but found {}", expected, actual);
self
}
/// Assert that a player entity has all standard game components
///
/// Checks for: PlayerName, PlayerColor, BorderTiles, Troops, TerritorySize
#[track_caller]
pub fn player_has_components(self, player_id: NationId) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).unwrap_or_else(|| panic!("Player entity not found for player {}", player_id.get()));
assert!(self.world.get::<borders_core::game::PlayerName>(*entity).is_some(), "Player {} missing PlayerName component", player_id.get());
assert!(self.world.get::<borders_core::game::PlayerColor>(*entity).is_some(), "Player {} missing PlayerColor component", player_id.get());
assert!(self.world.get::<BorderTiles>(*entity).is_some(), "Player {} missing BorderTiles component", player_id.get());
assert!(self.world.get::<borders_core::game::Troops>(*entity).is_some(), "Player {} missing Troops component", player_id.get());
assert!(self.world.get::<borders_core::game::TerritorySize>(*entity).is_some(), "Player {} missing TerritorySize component", player_id.get());
self
}
/// Assert that the map has specific dimensions
#[track_caller]
pub fn map_dimensions(self, width: u16, height: u16) -> Self {
let mgr = self.world.resource::<TerritoryManager>();
assert!(mgr.width() == width, "Expected map width {}, but found {}", width, mgr.width());
assert!(mgr.height() == height, "Expected map height {}, but found {}", height, mgr.height());
assert!(mgr.len() == (width as usize) * (height as usize), "Expected map size {}, but found {}", (width as usize) * (height as usize), mgr.len());
self
}
/// Assert that BorderCache is synchronized with ECS BorderTiles component
#[track_caller]
pub fn border_cache_synced(self, player_id: NationId) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
let component_borders = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
let cache = self.world.resource::<BorderCache>();
let cache_borders = cache.get(player_id);
match cache_borders {
Some(cached) => {
assert!(&component_borders.0 == cached, "BorderCache out of sync with BorderTiles component for player {}.\nECS component: {:?}\nCache: {:?}", player_id.get(), component_borders.0, cached);
}
None => {
assert!(component_borders.0.is_empty(), "BorderCache missing entry for player {} but component has {} borders", player_id.get(), component_borders.0.len());
}
}
self
}
/// Assert that a player has NO border tiles
#[track_caller]
pub fn no_border_tiles(self, player_id: NationId) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
assert!(border_tiles.0.is_empty(), "Expected player {} to have no border tiles, but found {} tiles: {:?}", player_id.get(), border_tiles.0.len(), border_tiles.0);
self
}
/// Assert that a player has a specific number of border tiles
#[track_caller]
pub fn border_count(self, player_id: NationId, expected: usize) -> Self {
let entity_map = self.world.resource::<borders_core::game::PlayerEntityMap>();
let entity = entity_map.0.get(&player_id).expect("Player entity not found");
let border_tiles = self.world.get::<BorderTiles>(*entity).expect("BorderTiles component not found");
assert!(border_tiles.0.len() == expected, "Expected player {} to have {} border tiles, but found {}", player_id.get(), expected, border_tiles.0.len());
self
}
}