Add test coverage retrofit for Phase 1/2 (worker, api, frontend)
Requested after Phase 2: from here on, MCMapper development follows TDD (test-first) — this retrofits the pieces already built before that request landed. worker: unit tests for the tile rasterizer (background fill, exact upscaled-block boundaries, full-grid painting, out-of-bounds columns) and the block-color palette (distinctness checks, including that the "unmapped block" placeholder never accidentally collides with a real block's color). 16 tests total alongside the existing mesher tests. api: wired up `bun test`. Unit tests for chunkOf's coordinate math. Integration tests (real Postgres/Redis/MinIO, see README's new "Running tests" section) for wsGateway.message() — auth accept/reject, upsert + dedup on columns/sections, not-authenticated/invalid-JSON handling — and for the tile/mesh/servers HTTP routes, driven through Elysia's in-process `.handle()` rather than a bound port (sidesteps the stale dev-server port-collision issue hit repeatedly this session). index.ts now exports `app` and only calls `.listen()` when run directly, specifically so tests can drive it this way. frontend: extracted mesh.js's binary-format parser into its own ESM module (mesh-format.js) so it's unit-testable without a browser/Babylon; mesh.js now imports it. Tests build a buffer independently of the parser (mirroring worker's encoder layout) so a mismatch in either direction — Rust producer or JS consumer drifting — would be caught.
This commit is contained in:
@@ -78,3 +78,58 @@ fn wool_color(meta: u8) -> [u8; 3] {
|
||||
_ => UNKNOWN_COLOR,
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
#[test]
|
||||
fn air_is_the_dedicated_air_color() {
|
||||
assert_eq!(color_for(0, 0), AIR_COLOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn known_block_ignores_meta_unless_the_block_uses_it() {
|
||||
// Stone (id 1) has no meta variants — any meta should map to the same color.
|
||||
assert_eq!(color_for(1, 0), color_for(1, 15));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn oak_log_and_planks_differ() {
|
||||
assert_ne!(color_for(5, 0), color_for(17, 0));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn planks_vary_by_meta() {
|
||||
let oak = color_for(5, 0);
|
||||
let spruce = color_for(5, 1);
|
||||
let birch = color_for(5, 2);
|
||||
assert_ne!(oak, spruce);
|
||||
assert_ne!(oak, birch);
|
||||
assert_ne!(spruce, birch);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn wool_all_sixteen_colors_are_distinct() {
|
||||
let colors: Vec<[u8; 3]> = (0..16).map(|meta| color_for(35, meta)).collect();
|
||||
for i in 0..colors.len() {
|
||||
for j in (i + 1)..colors.len() {
|
||||
assert_ne!(colors[i], colors[j], "wool meta {i} and {j} share a color");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unmapped_block_id_falls_back_to_unknown_color() {
|
||||
assert_eq!(color_for(9999, 0), UNKNOWN_COLOR);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn unknown_color_is_distinct_from_every_real_terrain_color() {
|
||||
// The whole point of UNKNOWN_COLOR is to be visually obvious — assert it doesn't
|
||||
// silently collide with a real block's color and blend in.
|
||||
for id in [1u16, 2, 3, 4, 7, 8, 12, 17, 24, 41, 49, 87, 121] {
|
||||
assert_ne!(color_for(id, 0), UNKNOWN_COLOR, "block {id} accidentally matches UNKNOWN_COLOR");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -56,3 +56,64 @@ impl RenderBackend for CpuRenderBackend {
|
||||
Ok(bytes)
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
use crate::palette::color_for;
|
||||
|
||||
fn decode(bytes: &[u8]) -> RgbImage {
|
||||
image::load_from_memory(bytes).expect("worker must always produce a decodable PNG").to_rgb8()
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn empty_columns_produce_a_full_background_tile() {
|
||||
let backend = CpuRenderBackend::new();
|
||||
let bytes = backend.rasterize_tile(&[]).unwrap();
|
||||
let img = decode(&bytes);
|
||||
assert_eq!(img.dimensions(), (256, 256));
|
||||
for pixel in img.pixels() {
|
||||
assert_eq!(*pixel, Rgb([30, 30, 40]));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_single_column_paints_exactly_its_upscaled_16x16_block() {
|
||||
let backend = CpuRenderBackend::new();
|
||||
let stone = color_for(1, 0);
|
||||
let columns = vec![ColumnPixel { local_x: 0, local_z: 0, block_id: 1, block_meta: 0 }];
|
||||
let img = decode(&backend.rasterize_tile(&columns).unwrap());
|
||||
|
||||
// Inside the upscaled region for local (0,0): pixels 0..16 on both axes.
|
||||
assert_eq!(*img.get_pixel(0, 0), Rgb(stone));
|
||||
assert_eq!(*img.get_pixel(15, 15), Rgb(stone));
|
||||
// Just outside that region must still be background — proves UPSCALE didn't bleed.
|
||||
assert_eq!(*img.get_pixel(16, 0), Rgb([30, 30, 40]));
|
||||
assert_eq!(*img.get_pixel(0, 16), Rgb([30, 30, 40]));
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn a_full_grid_paints_every_upscaled_pixel() {
|
||||
let backend = CpuRenderBackend::new();
|
||||
let grass = color_for(2, 0);
|
||||
let mut columns = Vec::with_capacity(256);
|
||||
for x in 0..16u8 {
|
||||
for z in 0..16u8 {
|
||||
columns.push(ColumnPixel { local_x: x, local_z: z, block_id: 2, block_meta: 0 });
|
||||
}
|
||||
}
|
||||
let img = decode(&backend.rasterize_tile(&columns).unwrap());
|
||||
assert_eq!(img.dimensions(), (256, 256));
|
||||
for pixel in img.pixels() {
|
||||
assert_eq!(*pixel, Rgb(grass));
|
||||
}
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn out_of_bounds_columns_are_ignored_rather_than_panicking() {
|
||||
let backend = CpuRenderBackend::new();
|
||||
let columns = vec![ColumnPixel { local_x: 200, local_z: 0, block_id: 1, block_meta: 0 }];
|
||||
let result = backend.rasterize_tile(&columns);
|
||||
assert!(result.is_ok());
|
||||
}
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user