McRogueFace/tests/unit/test_heightmap_basic.py
John McCardle 87444c2fd0 HeightMap: add GRID_MAX limit and input validation
Fixes potential integer overflow and invalid input issues:

- Add GRID_MAX constant (8192) to Common.h for global use
- Validate HeightMap dimensions against GRID_MAX to prevent
  integer overflow in w*h calculations (65536*65536 = 0)
- Add min > max validation for clamp() and normalize()
- Add unit tests for all new validation cases

Co-Authored-By: Claude Opus 4.5 <noreply@anthropic.com>
2026-01-11 20:26:04 -05:00

268 lines
7.7 KiB
Python

#!/usr/bin/env python3
"""Unit tests for mcrfpy.HeightMap core functionality (#193)
Tests the HeightMap class constructor, size property, and scalar operations.
"""
import sys
import mcrfpy
def test_constructor_basic():
"""HeightMap can be created with a size tuple"""
hmap = mcrfpy.HeightMap((100, 50))
assert hmap is not None
print("PASS: test_constructor_basic")
def test_constructor_with_fill():
"""HeightMap can be created with a fill value"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
assert hmap is not None
print("PASS: test_constructor_with_fill")
def test_size_property():
"""size property returns correct dimensions"""
hmap = mcrfpy.HeightMap((100, 50))
size = hmap.size
assert size == (100, 50), f"Expected (100, 50), got {size}"
print("PASS: test_size_property")
def test_size_immutable():
"""size property is read-only"""
hmap = mcrfpy.HeightMap((100, 50))
try:
hmap.size = (200, 100)
print("FAIL: test_size_immutable - should have raised AttributeError")
sys.exit(1)
except AttributeError:
pass
print("PASS: test_size_immutable")
def test_fill_method():
"""fill() sets all cells and returns self"""
hmap = mcrfpy.HeightMap((10, 10))
result = hmap.fill(0.5)
assert result is hmap, "fill() should return self"
print("PASS: test_fill_method")
def test_clear_method():
"""clear() sets all cells to 0.0 and returns self"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
result = hmap.clear()
assert result is hmap, "clear() should return self"
print("PASS: test_clear_method")
def test_add_constant_method():
"""add_constant() adds to all cells and returns self"""
hmap = mcrfpy.HeightMap((10, 10))
result = hmap.add_constant(0.25)
assert result is hmap, "add_constant() should return self"
print("PASS: test_add_constant_method")
def test_scale_method():
"""scale() multiplies all cells and returns self"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
result = hmap.scale(2.0)
assert result is hmap, "scale() should return self"
print("PASS: test_scale_method")
def test_clamp_method():
"""clamp() clamps values and returns self"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
result = hmap.clamp(0.0, 1.0)
assert result is hmap, "clamp() should return self"
print("PASS: test_clamp_method")
def test_clamp_with_defaults():
"""clamp() works with default parameters"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
result = hmap.clamp() # Uses defaults 0.0, 1.0
assert result is hmap
print("PASS: test_clamp_with_defaults")
def test_normalize_method():
"""normalize() rescales values and returns self"""
hmap = mcrfpy.HeightMap((10, 10))
hmap.fill(0.25).add_constant(0.1) # Some values
result = hmap.normalize(0.0, 1.0)
assert result is hmap, "normalize() should return self"
print("PASS: test_normalize_method")
def test_normalize_with_defaults():
"""normalize() works with default parameters"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
result = hmap.normalize() # Uses defaults 0.0, 1.0
assert result is hmap
print("PASS: test_normalize_with_defaults")
def test_method_chaining():
"""Methods can be chained"""
hmap = mcrfpy.HeightMap((10, 10))
result = hmap.fill(0.5).scale(2.0).clamp(0.0, 1.0)
assert result is hmap, "Chained methods should return self"
print("PASS: test_method_chaining")
def test_complex_chaining():
"""Complex chains work correctly"""
hmap = mcrfpy.HeightMap((100, 100))
result = (hmap
.fill(0.0)
.add_constant(0.5)
.scale(1.5)
.clamp(0.0, 1.0)
.normalize(0.2, 0.8))
assert result is hmap
print("PASS: test_complex_chaining")
def test_repr():
"""repr() returns a readable string"""
hmap = mcrfpy.HeightMap((100, 50))
r = repr(hmap)
assert "HeightMap" in r
assert "100" in r and "50" in r
print(f"PASS: test_repr - {r}")
def test_invalid_size():
"""Negative or zero size raises ValueError"""
try:
mcrfpy.HeightMap((0, 10))
print("FAIL: test_invalid_size - should have raised ValueError for width=0")
sys.exit(1)
except ValueError:
pass
try:
mcrfpy.HeightMap((10, -5))
print("FAIL: test_invalid_size - should have raised ValueError for height=-5")
sys.exit(1)
except ValueError:
pass
print("PASS: test_invalid_size")
def test_invalid_size_type():
"""Non-tuple size raises TypeError"""
try:
mcrfpy.HeightMap([100, 50]) # list instead of tuple
print("FAIL: test_invalid_size_type - should have raised TypeError")
sys.exit(1)
except TypeError:
pass
print("PASS: test_invalid_size_type")
def test_size_exceeds_grid_max():
"""Size exceeding GRID_MAX (8192) raises ValueError"""
# Test width exceeds limit
try:
mcrfpy.HeightMap((10000, 100))
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for width=10000")
sys.exit(1)
except ValueError as e:
assert "8192" in str(e) or "cannot exceed" in str(e).lower()
# Test height exceeds limit
try:
mcrfpy.HeightMap((100, 10000))
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for height=10000")
sys.exit(1)
except ValueError as e:
assert "8192" in str(e) or "cannot exceed" in str(e).lower()
# Test both exceed limit (would cause integer overflow without validation)
try:
mcrfpy.HeightMap((65536, 65536))
print("FAIL: test_size_exceeds_grid_max - should have raised ValueError for 65536x65536")
sys.exit(1)
except ValueError:
pass
print("PASS: test_size_exceeds_grid_max")
def test_clamp_min_greater_than_max():
"""clamp() with min > max raises ValueError"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
try:
hmap.clamp(min=1.0, max=0.0)
print("FAIL: test_clamp_min_greater_than_max - should have raised ValueError")
sys.exit(1)
except ValueError as e:
assert "min" in str(e).lower() and "max" in str(e).lower()
print("PASS: test_clamp_min_greater_than_max")
def test_normalize_min_greater_than_max():
"""normalize() with min > max raises ValueError"""
hmap = mcrfpy.HeightMap((10, 10), fill=0.5)
try:
hmap.normalize(min=1.0, max=0.0)
print("FAIL: test_normalize_min_greater_than_max - should have raised ValueError")
sys.exit(1)
except ValueError as e:
assert "min" in str(e).lower() and "max" in str(e).lower()
print("PASS: test_normalize_min_greater_than_max")
def test_max_valid_size():
"""Size at GRID_MAX boundary works"""
# Test at the exact limit - this should work
hmap = mcrfpy.HeightMap((8192, 1))
assert hmap.size == (8192, 1)
hmap2 = mcrfpy.HeightMap((1, 8192))
assert hmap2.size == (1, 8192)
print("PASS: test_max_valid_size")
def run_all_tests():
"""Run all tests"""
print("Running HeightMap basic tests...")
print()
test_constructor_basic()
test_constructor_with_fill()
test_size_property()
test_size_immutable()
test_fill_method()
test_clear_method()
test_add_constant_method()
test_scale_method()
test_clamp_method()
test_clamp_with_defaults()
test_normalize_method()
test_normalize_with_defaults()
test_method_chaining()
test_complex_chaining()
test_repr()
test_invalid_size()
test_invalid_size_type()
test_size_exceeds_grid_max()
test_clamp_min_greater_than_max()
test_normalize_min_greater_than_max()
test_max_valid_size()
print()
print("All HeightMap basic tests PASSED!")
# Run tests directly
run_all_tests()
sys.exit(0)