mirror of
https://github.com/Xevion/smart-rgb.git
synced 2025-12-06 01:16:24 -06:00
270 lines
12 KiB
Rust
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
|
|
}
|
|
}
|