3D / voxel unit tests

This commit is contained in:
John McCardle 2026-02-06 16:15:07 -05:00
commit 71cd2b9b41
22 changed files with 4705 additions and 0 deletions

View file

@ -0,0 +1,345 @@
#!/usr/bin/env python3
"""Unit tests for VoxelGrid (Milestone 9)
Tests the core VoxelGrid data structure:
- Creation with various sizes
- Per-voxel get/set operations
- Bounds checking behavior
- Material palette management
- Bulk operations (fill, clear)
- Transform properties (offset, rotation)
- Statistics (count_non_air, count_material)
"""
import sys
# Track test results
passed = 0
failed = 0
def test(name, condition, detail=""):
"""Record test result"""
global passed, failed
if condition:
print(f"[PASS] {name}")
passed += 1
else:
print(f"[FAIL] {name}" + (f" - {detail}" if detail else ""))
failed += 1
def test_creation():
"""Test VoxelGrid creation with various parameters"""
import mcrfpy
# Basic creation
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
test("Creation: basic", vg is not None)
test("Creation: width", vg.width == 16)
test("Creation: height", vg.height == 8)
test("Creation: depth", vg.depth == 16)
test("Creation: default cell_size", vg.cell_size == 1.0)
# With cell_size
vg2 = mcrfpy.VoxelGrid(size=(10, 5, 10), cell_size=2.0)
test("Creation: custom cell_size", vg2.cell_size == 2.0)
# Size property
test("Creation: size tuple", vg.size == (16, 8, 16))
# Initial state
test("Creation: initially empty", vg.count_non_air() == 0)
test("Creation: no materials", vg.material_count == 0)
def test_invalid_creation():
"""Test that invalid parameters raise errors"""
import mcrfpy
errors_caught = 0
try:
vg = mcrfpy.VoxelGrid(size=(0, 8, 16))
except ValueError:
errors_caught += 1
try:
vg = mcrfpy.VoxelGrid(size=(16, -1, 16))
except ValueError:
errors_caught += 1
try:
vg = mcrfpy.VoxelGrid(size=(16, 8, 16), cell_size=-1.0)
except ValueError:
errors_caught += 1
try:
vg = mcrfpy.VoxelGrid(size=(16, 8)) # Missing dimension
except (ValueError, TypeError):
errors_caught += 1
test("Invalid creation: catches errors", errors_caught == 4, f"caught {errors_caught}/4")
def test_get_set():
"""Test per-voxel get/set operations"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Initially all air
test("Get/Set: initial value is air", vg.get(0, 0, 0) == 0)
# Set and get
vg.set(5, 3, 7, stone)
test("Get/Set: set then get", vg.get(5, 3, 7) == stone)
# Verify adjacent cells unaffected
test("Get/Set: adjacent unaffected", vg.get(5, 3, 6) == 0)
test("Get/Set: adjacent unaffected 2", vg.get(4, 3, 7) == 0)
# Set back to air
vg.set(5, 3, 7, 0)
test("Get/Set: set to air", vg.get(5, 3, 7) == 0)
# Multiple materials
wood = vg.add_material("wood", color=mcrfpy.Color(139, 90, 43))
vg.set(0, 0, 0, stone)
vg.set(1, 0, 0, wood)
vg.set(2, 0, 0, stone)
test("Get/Set: multiple materials",
vg.get(0, 0, 0) == stone and vg.get(1, 0, 0) == wood and vg.get(2, 0, 0) == stone)
def test_bounds():
"""Test bounds checking behavior"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(8, 4, 8))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Out of bounds get returns 0 (air)
test("Bounds: negative x", vg.get(-1, 0, 0) == 0)
test("Bounds: negative y", vg.get(0, -1, 0) == 0)
test("Bounds: negative z", vg.get(0, 0, -1) == 0)
test("Bounds: overflow x", vg.get(8, 0, 0) == 0)
test("Bounds: overflow y", vg.get(0, 4, 0) == 0)
test("Bounds: overflow z", vg.get(0, 0, 8) == 0)
test("Bounds: large overflow", vg.get(100, 100, 100) == 0)
# Out of bounds set is silently ignored (no crash)
vg.set(-1, 0, 0, stone) # Should not crash
vg.set(100, 0, 0, stone) # Should not crash
test("Bounds: OOB set doesn't crash", True)
# Corner cases - max valid indices
vg.set(7, 3, 7, stone)
test("Bounds: max valid index", vg.get(7, 3, 7) == stone)
def test_materials():
"""Test material palette management"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
# Add first material
stone_id = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
test("Materials: first ID is 1", stone_id == 1)
# Add with all properties
glass_id = vg.add_material("glass",
color=mcrfpy.Color(200, 200, 255, 128),
sprite_index=5,
transparent=True,
path_cost=0.5)
test("Materials: second ID is 2", glass_id == 2)
# Verify material count
test("Materials: count", vg.material_count == 2)
# Get material and verify properties
stone = vg.get_material(stone_id)
test("Materials: name", stone["name"] == "stone")
test("Materials: color type", hasattr(stone["color"], 'r'))
test("Materials: default sprite_index", stone["sprite_index"] == -1)
test("Materials: default transparent", stone["transparent"] == False)
test("Materials: default path_cost", stone["path_cost"] == 1.0)
glass = vg.get_material(glass_id)
test("Materials: custom sprite_index", glass["sprite_index"] == 5)
test("Materials: custom transparent", glass["transparent"] == True)
test("Materials: custom path_cost", glass["path_cost"] == 0.5)
# Air material (ID 0)
air = vg.get_material(0)
test("Materials: air name", air["name"] == "air")
test("Materials: air transparent", air["transparent"] == True)
# Invalid material ID returns air
invalid = vg.get_material(255)
test("Materials: invalid returns air", invalid["name"] == "air")
def test_fill_clear():
"""Test bulk fill and clear operations"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(10, 5, 10))
total = 10 * 5 * 10 # 500
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Fill with material
vg.fill(stone)
test("Fill: all cells filled", vg.count_non_air() == total)
test("Fill: specific cell", vg.get(5, 2, 5) == stone)
test("Fill: corner cell", vg.get(0, 0, 0) == stone)
test("Fill: opposite corner", vg.get(9, 4, 9) == stone)
# Clear (fill with air)
vg.clear()
test("Clear: all cells empty", vg.count_non_air() == 0)
test("Clear: specific cell", vg.get(5, 2, 5) == 0)
def test_transform():
"""Test transform properties (offset, rotation)"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
# Default values
test("Transform: default offset", vg.offset == (0.0, 0.0, 0.0))
test("Transform: default rotation", vg.rotation == 0.0)
# Set offset
vg.offset = (10.5, -5.0, 20.0)
offset = vg.offset
test("Transform: set offset x", abs(offset[0] - 10.5) < 0.001)
test("Transform: set offset y", abs(offset[1] - (-5.0)) < 0.001)
test("Transform: set offset z", abs(offset[2] - 20.0) < 0.001)
# Set rotation
vg.rotation = 45.0
test("Transform: set rotation", abs(vg.rotation - 45.0) < 0.001)
# Negative rotation
vg.rotation = -90.0
test("Transform: negative rotation", abs(vg.rotation - (-90.0)) < 0.001)
# Large rotation
vg.rotation = 720.0
test("Transform: large rotation", abs(vg.rotation - 720.0) < 0.001)
def test_statistics():
"""Test statistics methods"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(10, 10, 10))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
wood = vg.add_material("wood", color=mcrfpy.Color(139, 90, 43))
# Initially empty
test("Stats: initial non_air", vg.count_non_air() == 0)
test("Stats: initial stone count", vg.count_material(stone) == 0)
# Add some voxels
for i in range(5):
vg.set(i, 0, 0, stone)
for i in range(3):
vg.set(i, 1, 0, wood)
test("Stats: non_air after setting", vg.count_non_air() == 8)
test("Stats: stone count", vg.count_material(stone) == 5)
test("Stats: wood count", vg.count_material(wood) == 3)
test("Stats: air count", vg.count_material(0) == 10*10*10 - 8)
def test_repr():
"""Test string representation"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(16, 8, 16))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
vg.set(0, 0, 0, stone)
repr_str = repr(vg)
test("Repr: contains VoxelGrid", "VoxelGrid" in repr_str)
test("Repr: contains dimensions", "16x8x16" in repr_str)
test("Repr: contains materials", "materials=1" in repr_str)
test("Repr: contains non_air", "non_air=1" in repr_str)
def test_large_grid():
"""Test with larger grid sizes"""
import mcrfpy
# 64x64x64 = 262144 voxels
vg = mcrfpy.VoxelGrid(size=(64, 64, 64))
test("Large: creation", vg is not None)
test("Large: size", vg.size == (64, 64, 64))
stone = vg.add_material("stone", color=mcrfpy.Color(128, 128, 128))
# Fill entire grid
vg.fill(stone)
expected = 64 * 64 * 64
test("Large: fill count", vg.count_non_air() == expected, f"got {vg.count_non_air()}, expected {expected}")
# Clear
vg.clear()
test("Large: clear", vg.count_non_air() == 0)
def test_material_limit():
"""Test material palette limit (255 max)"""
import mcrfpy
vg = mcrfpy.VoxelGrid(size=(8, 8, 8))
# Add many materials
for i in range(255):
mat_id = vg.add_material(f"mat_{i}", color=mcrfpy.Color(i, i, i))
if mat_id != i + 1:
test("Material limit: IDs sequential", False, f"expected {i+1}, got {mat_id}")
return
test("Material limit: 255 materials added", vg.material_count == 255)
# 256th should fail
try:
vg.add_material("overflow", color=mcrfpy.Color(255, 255, 255))
test("Material limit: overflow error", False, "should have raised exception")
except RuntimeError:
test("Material limit: overflow error", True)
def main():
"""Run all tests"""
print("=" * 60)
print("VoxelGrid Unit Tests (Milestone 9)")
print("=" * 60)
print()
test_creation()
print()
test_invalid_creation()
print()
test_get_set()
print()
test_bounds()
print()
test_materials()
print()
test_fill_clear()
print()
test_transform()
print()
test_statistics()
print()
test_repr()
print()
test_large_grid()
print()
test_material_limit()
print()
print("=" * 60)
print(f"Results: {passed} passed, {failed} failed")
print("=" * 60)
return 0 if failed == 0 else 1
if __name__ == "__main__":
sys.exit(main())