323 lines
10 KiB
Rust
323 lines
10 KiB
Rust
use hcie_tile::*;
|
||
|
||
/// Integration tests for hcie-tile sparse tile storage.
|
||
///
|
||
/// Covers: tile creation, pixel read/write, tile key mapping,
|
||
/// sparse storage behavior, from_dense / to_dense roundtrip,
|
||
/// update_tiles_in_region, composite_into, edge cases.
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tile (single 256×256 block) tests
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn new_transparent_tile_is_all_zeros() {
|
||
let tile = Tile::new_transparent();
|
||
assert_eq!(tile.pixels.len(), TILE_BYTES);
|
||
// Spot-check a few positions
|
||
assert_eq!(tile.pixels[0], 0);
|
||
assert_eq!(tile.pixels[3], 0);
|
||
assert_eq!(tile.pixels[TILE_BYTES - 1], 0);
|
||
}
|
||
|
||
#[test]
|
||
fn new_blank_tile_is_white_and_opaque() {
|
||
let tile = Tile::new_blank();
|
||
assert_eq!(tile.pixels.len(), TILE_BYTES);
|
||
// Every pixel should be [255, 255, 255, 255]
|
||
for i in 0..10 {
|
||
let base = i * 4;
|
||
assert_eq!(tile.pixels[base], 255, "R should be 255 at pixel {}", i);
|
||
assert_eq!(tile.pixels[base + 1], 255, "G should be 255 at pixel {}", i);
|
||
assert_eq!(tile.pixels[base + 2], 255, "B should be 255 at pixel {}", i);
|
||
assert_eq!(tile.pixels[base + 3], 255, "A should be 255 at pixel {}", i);
|
||
}
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// TiledLayer creation
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn new_tiled_layer_has_correct_dimensions() {
|
||
let tl = TiledLayer::new(512, 256);
|
||
assert_eq!(tl.width(), 512);
|
||
assert_eq!(tl.height(), 256);
|
||
}
|
||
|
||
#[test]
|
||
fn new_tiled_layer_has_zero_tiles() {
|
||
let tl = TiledLayer::new(512, 256);
|
||
assert_eq!(tl.tile_count(), 0, "empty layer should have no allocated tiles");
|
||
}
|
||
|
||
#[test]
|
||
fn from_dense_size_matches() {
|
||
let pixels = vec![255u8; 64 * 64 * 4];
|
||
let tl = TiledLayer::from_dense(&pixels, 64, 64);
|
||
assert_eq!(tl.width(), 64);
|
||
assert_eq!(tl.height(), 64);
|
||
}
|
||
|
||
#[test]
|
||
fn from_dense_creates_tiles_for_non_transparent_data() {
|
||
let pixels = vec![255u8; 256 * 256 * 4]; // fully opaque, exactly one tile
|
||
let tl = TiledLayer::from_dense(&pixels, 256, 256);
|
||
assert_eq!(tl.tile_count(), 1, "fully opaque 256x256 should produce exactly 1 tile");
|
||
}
|
||
|
||
#[test]
|
||
fn from_dense_prunes_transparent_tiles() {
|
||
let pixels = vec![0u8; 256 * 256 * 4]; // fully transparent, exactly one tile
|
||
let tl = TiledLayer::from_dense(&pixels, 256, 256);
|
||
assert_eq!(
|
||
tl.tile_count(),
|
||
0,
|
||
"fully transparent layer should have no allocated tiles"
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Tile key mapping
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn tile_key_maps_pixel_zero_to_zero_zero() {
|
||
let key = TiledLayer::tile_key(0, 0);
|
||
assert_eq!(key, (0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn tile_key_maps_pixel_255_to_0() {
|
||
let key = TiledLayer::tile_key(255, 255);
|
||
assert_eq!(key, (0, 0));
|
||
}
|
||
|
||
#[test]
|
||
fn tile_key_maps_pixel_256_to_1() {
|
||
let key = TiledLayer::tile_key(256, 256);
|
||
assert_eq!(key, (1, 1));
|
||
}
|
||
|
||
#[test]
|
||
fn tile_key_maps_large_coordinates() {
|
||
let key = TiledLayer::tile_key(1000, 2000);
|
||
assert_eq!(key, (1000 / TILE_SIZE, 2000 / TILE_SIZE));
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Pixel read/write
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn get_pixel_outside_allocated_tile_returns_transparent() {
|
||
let tl = TiledLayer::new(512, 512);
|
||
let pixel = tl.get_pixel(100, 100);
|
||
assert_eq!(pixel, [0, 0, 0, 0], "unwritten pixel should be transparent");
|
||
}
|
||
|
||
#[test]
|
||
fn set_pixel_and_get_pixel_roundtrip() {
|
||
let mut tl = TiledLayer::new(64, 64);
|
||
tl.set_pixel(10, 10, [255, 128, 64, 200]);
|
||
let pixel = tl.get_pixel(10, 10);
|
||
assert_eq!(pixel, [255, 128, 64, 200]);
|
||
}
|
||
|
||
#[test]
|
||
fn set_pixel_out_of_bounds_is_noop() {
|
||
let mut tl = TiledLayer::new(64, 64);
|
||
tl.set_pixel(100, 100, [255, 0, 0, 255]); // outside layer bounds
|
||
assert_eq!(tl.tile_count(), 0, "out-of-bounds set should not create tiles");
|
||
}
|
||
|
||
#[test]
|
||
fn set_pixel_auto_creates_tile() {
|
||
let mut tl = TiledLayer::new(256, 256);
|
||
tl.set_pixel(0, 0, [255, 0, 0, 255]);
|
||
assert_eq!(tl.tile_count(), 1, "setting a pixel should create the containing tile");
|
||
}
|
||
|
||
#[test]
|
||
fn get_pixel_written_twice_returns_last_value() {
|
||
let mut tl = TiledLayer::new(64, 64);
|
||
tl.set_pixel(5, 5, [10, 20, 30, 40]);
|
||
tl.set_pixel(5, 5, [50, 60, 70, 80]);
|
||
assert_eq!(tl.get_pixel(5, 5), [50, 60, 70, 80]);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// to_dense roundtrip
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn to_dense_returns_correct_size() {
|
||
let tl = TiledLayer::new(64, 64);
|
||
let dense = tl.to_dense();
|
||
assert_eq!(dense.len(), (64 * 64 * 4) as usize);
|
||
}
|
||
|
||
#[test]
|
||
fn to_dense_after_set_pixel_preserves_value() {
|
||
let mut tl = TiledLayer::new(16, 16);
|
||
tl.set_pixel(7, 8, [100, 150, 200, 250]);
|
||
let dense = tl.to_dense();
|
||
let idx = ((8 * 16 + 7) * 4) as usize;
|
||
assert_eq!(dense[idx], 100);
|
||
assert_eq!(dense[idx + 1], 150);
|
||
assert_eq!(dense[idx + 2], 200);
|
||
assert_eq!(dense[idx + 3], 250);
|
||
}
|
||
|
||
#[test]
|
||
fn from_dense_to_dense_roundtrip() {
|
||
let original = vec![42u8; 128 * 128 * 4];
|
||
let tl = TiledLayer::from_dense(&original, 128, 128);
|
||
let result = tl.to_dense();
|
||
assert_eq!(original, result, "from_dense → to_dense should be identity");
|
||
}
|
||
|
||
#[test]
|
||
fn from_dense_to_dense_roundtrip_transparent() {
|
||
let original = vec![0u8; 128 * 128 * 4];
|
||
let tl = TiledLayer::from_dense(&original, 128, 128);
|
||
let result = tl.to_dense();
|
||
assert_eq!(original, result, "transparent from_dense → to_dense should be identity");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// update_tiles_in_region
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn update_tiles_in_region_writes_pixels() {
|
||
let mut tl = TiledLayer::new(64, 64);
|
||
let mut pixels = vec![0u8; 64 * 64 * 4];
|
||
// Set a block of pixels to red
|
||
for y in 10..20 {
|
||
for x in 10..20 {
|
||
let idx = ((y * 64 + x) * 4) as usize;
|
||
pixels[idx] = 255;
|
||
pixels[idx + 3] = 255;
|
||
}
|
||
}
|
||
tl.update_tiles_in_region(&pixels, 64, 0, 0, 64, 64);
|
||
assert_eq!(tl.get_pixel(15, 15), [255, 0, 0, 255], "updated pixel should be red");
|
||
assert_eq!(tl.get_pixel(0, 0), [0, 0, 0, 0], "pixel outside update region stays unchanged");
|
||
}
|
||
|
||
#[test]
|
||
fn update_tiles_in_region_partial_update() {
|
||
let mut tl = TiledLayer::new(256, 256);
|
||
let mut pixels = vec![0u8; 64 * 64 * 4];
|
||
for i in (0..pixels.len()).step_by(4) {
|
||
pixels[i] = 255;
|
||
pixels[i + 3] = 255;
|
||
}
|
||
// Update only the sub-region (0,0)-(64,64)
|
||
tl.update_tiles_in_region(&pixels, 64, 0, 0, 64, 64);
|
||
assert_eq!(tl.get_pixel(32, 32), [255, 0, 0, 255]);
|
||
assert_eq!(tl.get_pixel(100, 100), [0, 0, 0, 0], "pixels outside region should be zero");
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// composite_into
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn composite_into_copies_pixels() {
|
||
let mut tl = TiledLayer::new(32, 32);
|
||
tl.set_pixel(5, 5, [100, 150, 200, 255]);
|
||
|
||
let mut output = vec![0u8; 32 * 32 * 4];
|
||
tl.composite_into(&mut output, 32, 32, 0, 0, 32, 32);
|
||
|
||
let idx = ((5 * 32 + 5) * 4) as usize;
|
||
assert_eq!(output[idx], 100);
|
||
assert_eq!(output[idx + 1], 150);
|
||
assert_eq!(output[idx + 2], 200);
|
||
assert_eq!(output[idx + 3], 255);
|
||
}
|
||
|
||
#[test]
|
||
fn composite_into_respects_region_bounds() {
|
||
let mut tl = TiledLayer::new(64, 64);
|
||
tl.set_pixel(30, 30, [255, 0, 0, 255]);
|
||
|
||
let mut output = vec![0u8; 64 * 64 * 4];
|
||
// Composite only the top-left 16×16 region
|
||
tl.composite_into(&mut output, 64, 64, 0, 0, 16, 16);
|
||
|
||
// Pixel at (30,30) is outside the region and should not be copied
|
||
let idx = ((30 * 64 + 30) * 4) as usize;
|
||
assert_eq!(
|
||
output[idx..idx + 4],
|
||
[0, 0, 0, 0],
|
||
"pixel outside region should not appear in output"
|
||
);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Edge cases
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn zero_sized_layer_has_no_tiles() {
|
||
let tl = TiledLayer::new(0, 0);
|
||
assert_eq!(tl.tile_count(), 0);
|
||
assert_eq!(tl.width(), 0);
|
||
assert_eq!(tl.height(), 0);
|
||
}
|
||
|
||
#[test]
|
||
fn very_large_layer_does_not_panic() {
|
||
let tl = TiledLayer::new(4096, 4096);
|
||
assert_eq!(tl.width(), 4096);
|
||
assert_eq!(tl.height(), 4096);
|
||
}
|
||
|
||
#[test]
|
||
fn tile_count_increases_with_written_area() {
|
||
let mut tl = TiledLayer::new(512, 512);
|
||
assert_eq!(tl.tile_count(), 0);
|
||
|
||
// Write one pixel in two different tiles
|
||
tl.set_pixel(0, 0, [1, 1, 1, 1]);
|
||
assert_eq!(tl.tile_count(), 1);
|
||
|
||
tl.set_pixel(300, 300, [2, 2, 2, 2]);
|
||
assert_eq!(tl.tile_count(), 2, "pixels in different tiles should create two tiles");
|
||
}
|
||
|
||
#[test]
|
||
fn tiles_are_independent() {
|
||
let mut tl = TiledLayer::new(512, 512);
|
||
tl.set_pixel(0, 0, [10, 20, 30, 40]);
|
||
tl.set_pixel(300, 300, [50, 60, 70, 80]);
|
||
|
||
assert_eq!(tl.get_pixel(0, 0), [10, 20, 30, 40]);
|
||
assert_eq!(tl.get_pixel(300, 300), [50, 60, 70, 80]);
|
||
// neighboring pixel should be untouched
|
||
assert_eq!(tl.get_pixel(1, 0), [0, 0, 0, 0]);
|
||
}
|
||
|
||
// ---------------------------------------------------------------------------
|
||
// Serde roundtrip (if Tile is Serialize/Deserialize)
|
||
// ---------------------------------------------------------------------------
|
||
|
||
#[test]
|
||
fn tile_serde_json_roundtrip() {
|
||
let tile = Tile::new_blank();
|
||
let json = serde_json::to_string(&tile).expect("serialize tile");
|
||
let restored: Tile = serde_json::from_str(&json).expect("deserialize tile");
|
||
assert_eq!(tile.pixels, restored.pixels, "JSON serde roundtrip should preserve pixels");
|
||
}
|
||
|
||
#[test]
|
||
fn tile_bincode_roundtrip() {
|
||
let tile = Tile::new_blank();
|
||
let bytes = bincode::serialize(&tile).expect("serialize tile via bincode");
|
||
let restored: Tile = bincode::deserialize(&bytes).expect("deserialize tile via bincode");
|
||
assert_eq!(tile.pixels, restored.pixels, "bincode serde roundtrip should preserve pixels");
|
||
}
|