Initial commit

This commit is contained in:
George
2026-06-07 12:18:45 +01:00
commit 9f9b85e1f2
305 changed files with 23050 additions and 0 deletions
@@ -0,0 +1,281 @@
package com.livingworld.config;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link SimulationConfig}.
*/
class SimulationConfigTest {
// ------------------------------------------------------------------
// Default values
// ------------------------------------------------------------------
@Nested
@DisplayName("Default values")
class DefaultValues {
@Test
void defaultRegionSizeChunks() {
final SimulationConfig config = new SimulationConfig();
assertEquals(8, config.getRegionSizeChunks());
}
@Test
void defaultSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig();
assertEquals(100, config.getSimulationIntervalTicks());
}
@Test
void defaultMaxRegionsPerCycle() {
final SimulationConfig config = new SimulationConfig();
assertEquals(50, config.getMaxRegionsPerCycle());
}
@Test
void defaultMaxMillisecondsPerCycle() {
final SimulationConfig config = new SimulationConfig();
assertEquals(25, config.getMaxMillisecondsPerCycle());
}
@Test
void defaultEmergencyStopMilliseconds() {
final SimulationConfig config = new SimulationConfig();
assertEquals(40, config.getEmergencyStopMilliseconds());
}
@Test
void defaultEnableDebugCommands() {
final SimulationConfig config = new SimulationConfig();
assertTrue(config.isEnableDebugCommands());
}
@Test
void defaultEnableProfiler() {
final SimulationConfig config = new SimulationConfig();
assertTrue(config.isEnableProfiler());
}
}
// ------------------------------------------------------------------
// Validation valid values should pass
// ------------------------------------------------------------------
@Nested
@DisplayName("Validation with valid values")
class ValidValues {
@Test
void defaultsAreValid() {
final SimulationConfig config = new SimulationConfig();
config.validate(); // Should not throw
}
@Test
void minimumRegionSizeChunks() {
final SimulationConfig config = new SimulationConfig();
config.setRegionSizeChunks(1);
config.validate(); // Should not throw
}
@Test
void minimumSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig();
config.setSimulationIntervalTicks(1);
config.validate(); // Should not throw
}
@Test
void minimumMaxRegionsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxRegionsPerCycle(1);
config.validate(); // Should not throw
}
@Test
void minimumMaxMillisecondsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxMillisecondsPerCycle(1);
config.validate(); // Should not throw
}
@Test
void emergencyStopEqualsMaxMilliseconds() {
final SimulationConfig config = new SimulationConfig();
config.setEmergencyStopMilliseconds(25);
config.setMaxMillisecondsPerCycle(25);
config.validate(); // Should not throw (equal is allowed)
}
@Test
void emergencyStopGreaterThanMaxMilliseconds() {
final SimulationConfig config = new SimulationConfig();
config.setEmergencyStopMilliseconds(50);
config.setMaxMillisecondsPerCycle(25);
config.validate(); // Should not throw
}
}
// ------------------------------------------------------------------
// Validation invalid values should fail
// ------------------------------------------------------------------
@Nested
@DisplayName("Validation with invalid values")
class InvalidValues {
@Test
void zeroRegionSizeChunks() {
final SimulationConfig config = new SimulationConfig();
config.setRegionSizeChunks(0);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void negativeRegionSizeChunks() {
final SimulationConfig config = new SimulationConfig();
config.setRegionSizeChunks(-5);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void zeroSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig();
config.setSimulationIntervalTicks(0);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void negativeSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig();
config.setSimulationIntervalTicks(-10);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void zeroMaxRegionsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxRegionsPerCycle(0);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void negativeMaxRegionsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxRegionsPerCycle(-1);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void zeroMaxMillisecondsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxMillisecondsPerCycle(0);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void negativeMaxMillisecondsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxMillisecondsPerCycle(-10);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void emergencyStopLessThanMaxMilliseconds() {
final SimulationConfig config = new SimulationConfig();
config.setEmergencyStopMilliseconds(20);
config.setMaxMillisecondsPerCycle(25);
assertThrows(IllegalArgumentException.class, () -> config.validate());
}
@Test
void emergencyStopMessageContainsValues() {
final SimulationConfig config = new SimulationConfig();
config.setEmergencyStopMilliseconds(10);
config.setMaxMillisecondsPerCycle(30);
final IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> config.validate());
assertTrue(exception.getMessage().contains("emergencyStopMilliseconds"));
assertTrue(exception.getMessage().contains("maxMillisecondsPerCycle"));
}
}
// ------------------------------------------------------------------
// Setters
// ------------------------------------------------------------------
@Nested
@DisplayName("Setters")
class Setters {
@Test
void setRegionSizeChunks() {
final SimulationConfig config = new SimulationConfig();
config.setRegionSizeChunks(16);
assertEquals(16, config.getRegionSizeChunks());
}
@Test
void setSimulationIntervalTicks() {
final SimulationConfig config = new SimulationConfig();
config.setSimulationIntervalTicks(50);
assertEquals(50, config.getSimulationIntervalTicks());
}
@Test
void setMaxRegionsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxRegionsPerCycle(100);
assertEquals(100, config.getMaxRegionsPerCycle());
}
@Test
void setMaxMillisecondsPerCycle() {
final SimulationConfig config = new SimulationConfig();
config.setMaxMillisecondsPerCycle(50);
assertEquals(50, config.getMaxMillisecondsPerCycle());
}
@Test
void setEmergencyStopMilliseconds() {
final SimulationConfig config = new SimulationConfig();
config.setEmergencyStopMilliseconds(60);
assertEquals(60, config.getEmergencyStopMilliseconds());
}
@Test
void setEnableDebugCommandsFalse() {
final SimulationConfig config = new SimulationConfig();
config.setEnableDebugCommands(false);
assertFalse(config.isEnableDebugCommands());
}
@Test
void setEnableProfilerFalse() {
final SimulationConfig config = new SimulationConfig();
config.setEnableProfiler(false);
assertFalse(config.isEnableProfiler());
}
}
// ------------------------------------------------------------------
// toString
// ------------------------------------------------------------------
@Test
@DisplayName("toString should not throw")
void toStringShouldNotThrow() {
final SimulationConfig config = new SimulationConfig();
final String result = config.toString();
assertTrue(result.contains("SimulationConfig"));
assertTrue(result.contains("regionSizeChunks=8"));
assertTrue(result.contains("simulationIntervalTicks=100"));
}
}
@@ -0,0 +1,221 @@
package com.livingworld.core.services;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link ServiceRegistry}.
*/
class ServiceRegistryTest {
// ------------------------------------------------------------------
// Registration and retrieval
// ------------------------------------------------------------------
@Nested
@DisplayName("Registration and retrieval")
class RegistrationAndRetrieval {
@Test
void registerAndGet() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("test-service", String.class);
registry.register(key, "hello");
String result = registry.get(key);
assertEquals("hello", result);
}
@Test
void findReturnsPresentService() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("test-service-2", Integer.class);
registry.register(key, 42);
var found = registry.find(key);
assertTrue(found.isPresent());
assertEquals(42, found.get());
}
@Test
void findReturnsEmptyForUnregistered() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("unregistered", Boolean.class);
var found = registry.find(key);
assertFalse(found.isPresent());
}
@Test
void isRegisteredReturnsTrueAfterRegistration() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("test-service-3", Object.class);
registry.register(key, new Object());
assertTrue(registry.isRegistered(key));
}
@Test
void isRegisteredReturnsFalseForUnregistered() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("unregistered-2", Number.class);
assertFalse(registry.isRegistered(key));
}
}
// ------------------------------------------------------------------
// Null argument validation
// ------------------------------------------------------------------
@Nested
@DisplayName("Null argument validation")
class NullArgumentValidation {
@Test
void registerWithNullKeyThrows() {
var registry = new ServiceRegistry();
assertThrows(IllegalArgumentException.class, () -> registry.register(null, "service"));
}
@Test
void registerWithNullServiceThrows() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("null-service", String.class);
assertThrows(IllegalArgumentException.class, () -> registry.register(key, null));
}
@Test
void getWithNullKeyThrows() {
var registry = new ServiceRegistry();
assertThrows(IllegalArgumentException.class, () -> registry.get(null));
}
@Test
void findWithNullKeyThrows() {
var registry = new ServiceRegistry();
assertThrows(IllegalArgumentException.class, () -> registry.find(null));
}
@Test
void isRegisteredWithNullKeyThrows() {
var registry = new ServiceRegistry();
assertThrows(IllegalArgumentException.class, () -> registry.isRegistered(null));
}
}
// ------------------------------------------------------------------
// Duplicate registration rejection
// ------------------------------------------------------------------
@Nested
@DisplayName("Duplicate registration rejection")
class DuplicateRegistrationRejection {
@Test
void duplicateKeyThrows() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("duplicate", String.class);
registry.register(key, "first");
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> registry.register(key, "second"));
assertTrue(exception.getMessage().contains("already registered"));
}
@Test
void differentKeysDoNotConflict() {
var registry = new ServiceRegistry();
var key1 = new ServiceKey<>("key-1", String.class);
var key2 = new ServiceKey<>("key-2", Integer.class);
registry.register(key1, "hello");
registry.register(key2, 42);
assertEquals("hello", registry.get(key1));
assertEquals(42, registry.get(key2));
}
}
// ------------------------------------------------------------------
// Lock mechanism
// ------------------------------------------------------------------
@Nested
@DisplayName("Lock mechanism")
class LockMechanism {
@Test
void isLockedReturnsFalseByDefault() {
var registry = new ServiceRegistry();
assertFalse(registry.isLocked());
}
@Test
void lockSetsLockedState() {
var registry = new ServiceRegistry();
registry.lock();
assertTrue(registry.isLocked());
}
@Test
void registerAfterLockThrows() {
var registry = new ServiceRegistry();
registry.lock();
var key = new ServiceKey<>("after-lock", String.class);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> registry.register(key, "service"));
assertTrue(exception.getMessage().contains("locked"));
}
@Test
void registerBeforeLockThenRegisterAfterThrows() {
var registry = new ServiceRegistry();
var key1 = new ServiceKey<>("before-lock", String.class);
registry.register(key1, "before");
registry.lock();
var key2 = new ServiceKey<>("after-lock-2", Integer.class);
assertThrows(IllegalStateException.class, () -> registry.register(key2, 42));
}
@Test
void getAndFindWorkAfterLock() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("locked-service", String.class);
registry.register(key, "locked");
registry.lock();
assertEquals("locked", registry.get(key));
assertTrue(registry.find(key).isPresent());
}
}
// ------------------------------------------------------------------
// get() throws IllegalStateException for missing service
// ------------------------------------------------------------------
@Nested
@DisplayName("get() throws IllegalStateException for missing service")
class GetMissingThrows {
@Test
void getUnregisteredServiceThrows() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("missing", String.class);
IllegalStateException exception = assertThrows(IllegalStateException.class, () -> registry.get(key));
assertTrue(exception.getMessage().contains("No service registered"));
}
}
// ------------------------------------------------------------------
// No Minecraft dependencies
// ------------------------------------------------------------------
@Nested
@DisplayName("Plain Java - no Minecraft imports")
class PlainJava {
@Test
void registryIsPureJava() {
var registry = new ServiceRegistry();
var key = new ServiceKey<>("plain", String.class);
registry.register(key, "test");
assertEquals("test", registry.get(key));
}
}
}
@@ -0,0 +1,181 @@
package com.livingworld.events;
import static org.junit.jupiter.api.Assertions.*;
import java.util.concurrent.atomic.AtomicBoolean;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link LivingWorldEventBus}.
*/
public class LivingWorldEventBusTest {
// ------------------------------------------------------------------
// Registration tests
// ------------------------------------------------------------------
@Test
void registerAddsListenerForEventType() {
LivingWorldEventBus bus = new LivingWorldEventBus();
LivingWorldEventListener listener = event -> {};
assertDoesNotThrow(() -> bus.register("weather_change", listener));
}
@Test
void registerPreservesRegistrationOrder() {
LivingWorldEventBus bus = new LivingWorldEventBus();
AtomicBoolean firstCalled = new AtomicBoolean(false);
AtomicBoolean secondCalled = new AtomicBoolean(false);
LivingWorldEventListener first = event -> firstCalled.set(true);
LivingWorldEventListener second = event -> secondCalled.set(true);
bus.register("weather_change", first);
bus.register("weather_change", second);
BaseLivingWorldEvent event = new BaseLivingWorldEvent("weather_change", 0L, "core");
publishEvent(bus, "weather_change", event);
assertTrue(firstCalled.get());
assertTrue(secondCalled.get());
}
@Test
void registerBlankEventTypeThrows() {
LivingWorldEventBus bus = new LivingWorldEventBus();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
bus.register(" ", null);
});
assertEquals("eventType must not be null or blank", exception.getMessage());
}
@Test
void registerNullListenerThrows() {
LivingWorldEventBus bus = new LivingWorldEventBus();
IllegalArgumentException exception = assertThrows(IllegalArgumentException.class, () -> {
bus.register("weather_change", null);
});
assertEquals("listener must not be null", exception.getMessage());
}
// ------------------------------------------------------------------
// Publishing tests
// ------------------------------------------------------------------
@Test
void publishDispatchesToRegisteredListeners() {
LivingWorldEventBus bus = new LivingWorldEventBus();
AtomicBoolean listenerCalled = new AtomicBoolean(false);
bus.register("weather_change", event -> listenerCalled.set(true));
BaseLivingWorldEvent event = new BaseLivingWorldEvent(
"weather_change", 100L, "core"
);
publishEvent(bus, "weather_change", event);
assertTrue(listenerCalled.get());
}
@Test
void publishUnknownEventTypeDoesNotCrash() {
LivingWorldEventBus bus = new LivingWorldEventBus();
BaseLivingWorldEvent event = new BaseLivingWorldEvent("unknown_type_123", 100L, "core");
assertDoesNotThrow(() -> publishEvent(bus, "unknown_type_123", event));
}
@Test
void publishNullEventThrows() {
LivingWorldEventBus bus = new LivingWorldEventBus();
assertThrowsWithMessage(() -> { try { publishEvent(bus, "weather_change", null); } catch (RuntimeException e) { throw e; } },
IllegalArgumentException.class, "event must not be null");
}
// ------------------------------------------------------------------
// Metrics tests
// ------------------------------------------------------------------
@Test
void getPublishedEventCountIncrementsOnPublish() {
LivingWorldEventBus bus = new LivingWorldEventBus();
BaseLivingWorldEvent event1 = new BaseLivingWorldEvent("weather_change", 100L, "core");
bus.publish(event1);
assertEquals(1, bus.getPublishedEventCount());
// Clear suppression to allow publishing events with different ticks
bus.clearSuppressionForCurrentTick();
BaseLivingWorldEvent event2 = new BaseLivingWorldEvent("soil_moisture", 200L, "core");
bus.publish(event2);
assertEquals(2, bus.getPublishedEventCount());
}
@Test
void getListenerCountReturnsCorrectNumberOfListeners() {
LivingWorldEventBus bus = new LivingWorldEventBus();
bus.register("weather_change", event -> {});
bus.register("weather_change", event -> {});
bus.register("pollution_level", event -> {});
assertEquals(2, bus.getListenerCount("weather_change"));
assertEquals(1, bus.getListenerCount("pollution_level"));
assertEquals(0, bus.getListenerCount("unknown_event"));
}
// ------------------------------------------------------------------
// Helper methods (kept in test file for simplicity)
// ------------------------------------------------------------------
private void publishEvent(LivingWorldEventBus bus, String eventType, LivingWorldEvent event) {
// Use reflection to call the package-private publish method
try {
java.lang.reflect.Method method = LivingWorldEventBus.class.getDeclaredMethod(
"publish", LivingWorldEvent.class
);
method.setAccessible(true);
if (event == null) {
method.invoke(bus, new Object[]{null});
} else {
method.invoke(bus, event);
}
} catch (java.lang.reflect.InvocationTargetException e) {
// Unwrap the target exception for proper test assertions
Throwable cause = e.getCause();
if (cause instanceof RuntimeException) {
throw (RuntimeException) cause;
} else if (cause != null) {
throw new RuntimeException(cause);
}
} catch (Exception e) {
throw new RuntimeException(e);
}
}
private void assertThrowsWithMessage(Runnable runnable, Class<? extends Throwable> expectedType, String expectedMessage) {
try {
runnable.run();
fail("Expected " + expectedType.getSimpleName() + " but no exception was thrown");
} catch (AssertionError e) {
throw e;
} catch (Throwable t) {
if (expectedType.isInstance(t)) {
assertEquals(expectedMessage, t.getMessage());
} else {
throw new AssertionError("Expected " + expectedType.getSimpleName() + " but got " + t.getClass().getSimpleName() + ": " + t.getMessage(), t);
}
}
}
}
@@ -0,0 +1,378 @@
package com.livingworld.regions;
import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import java.util.HashMap;
import java.util.Map;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link RegionCoordinate}.
*/
class RegionCoordinateTest {
// ------------------------------------------------------------------
// Validation dimensionId
// ------------------------------------------------------------------
@Nested
@DisplayName("dimensionId validation")
class DimensionIdValidation {
@Test
void nullDimensionIdThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> new RegionCoordinate(null, 0, 0));
assertTrue(ex.getMessage().contains("dimensionId"));
}
@Test
void blankDimensionIdThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> new RegionCoordinate("", 0, 0));
assertTrue(ex.getMessage().contains("dimensionId"));
}
@Test
void whitespaceOnlyDimensionIdThrows() {
IllegalArgumentException ex = assertThrows(IllegalArgumentException.class,
() -> new RegionCoordinate(" ", 0, 0));
assertTrue(ex.getMessage().contains("dimensionId"));
}
@Test
void validDimensionIdAccepted() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertEquals("overworld", rc.dimensionId());
}
}
// ------------------------------------------------------------------
// Validation regionSizeChunks
// ------------------------------------------------------------------
@Nested
@DisplayName("regionSizeChunks validation")
class RegionSizeValidation {
@Test
void zeroRegionSizeThrows() {
assertThrows(IllegalArgumentException.class,
() -> RegionCoordinate.fromChunk("overworld", 0, 0, 0));
}
@Test
void negativeRegionSizeThrows() {
assertThrows(IllegalArgumentException.class,
() -> RegionCoordinate.fromChunk("overworld", 0, 0, -1));
}
@Test
void oneRegionSizeAccepted() {
RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 5, 5, 1);
assertEquals(5, rc.x());
assertEquals(5, rc.z());
}
}
// ------------------------------------------------------------------
// fromBlock specified test cases
// ------------------------------------------------------------------
@Nested
@DisplayName("fromBlock coordinate mapping")
class FromBlockMapping {
/** Block 0,0 maps to region 0,0 */
@Test
void blockZeroZeroMapsToRegionZeroZero() {
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 0, 0, 8);
assertEquals(0, rc.x());
assertEquals(0, rc.z());
}
/** Block 127,127 maps to region 0,0 with region size 8 chunks */
@Test
void block127_127MapsToRegionZeroZero() {
// chunk = floorDiv(block, 16) → floorDiv(127,16) = 7
// region = floorDiv(chunk, 8) → floorDiv(7,8) = 0
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 127, 127, 8);
assertEquals(0, rc.x());
assertEquals(0, rc.z());
}
/** Block 128,0 maps to region 1,0 */
@Test
void block128ZeroMapsToRegionOneZero() {
// chunk = floorDiv(128,16) = 8
// region = floorDiv(8,8) = 1
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", 128, 0, 8);
assertEquals(1, rc.x());
assertEquals(0, rc.z());
}
/** Block -1,0 maps to region -1,0 */
@Test
void blockMinusOneZeroMapsToRegionMinusOneZero() {
// chunk = floorDiv(-1,16) = -1
// region = floorDiv(-1,8) = -1
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -1, 0, 8);
assertEquals(-1, rc.x());
assertEquals(0, rc.z());
}
/** Block -128,0 maps to region -1,0 */
@Test
void blockMinus128ZeroMapsToRegionMinusOneZero() {
// chunk = floorDiv(-128,16) = -8
// region = floorDiv(-8,8) = -1
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -128, 0, 8);
assertEquals(-1, rc.x());
assertEquals(0, rc.z());
}
/** Block -129,0 maps to region -2,0 */
@Test
void blockMinus129ZeroMapsToRegionMinusTwoZero() {
// chunk = floorDiv(-129,16) = -9
// region = floorDiv(-9,8) = -2
RegionCoordinate rc = RegionCoordinate.fromBlock("overworld", -129, 0, 8);
assertEquals(-2, rc.x());
assertEquals(0, rc.z());
}
}
// ------------------------------------------------------------------
// fromChunk basic checks
// ------------------------------------------------------------------
@Nested
@DisplayName("fromChunk coordinate mapping")
class FromChunkMapping {
@Test
void chunkZeroZeroMapsToRegionZeroZero() {
RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 0, 0, 8);
assertEquals(0, rc.x());
assertEquals(0, rc.z());
}
@Test
void chunkSevenSevenMapsToRegionZeroZero() {
RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 7, 7, 8);
assertEquals(0, rc.x());
assertEquals(0, rc.z());
}
@Test
void chunkEightEightMapsToRegionOneOne() {
RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", 8, 8, 8);
assertEquals(1, rc.x());
assertEquals(1, rc.z());
}
@Test
void chunkMinusOneMinusOneMapsToRegionMinusOneMinusOne() {
RegionCoordinate rc = RegionCoordinate.fromChunk("overworld", -1, -1, 8);
assertEquals(-1, rc.x());
assertEquals(-1, rc.z());
}
}
// ------------------------------------------------------------------
// Boundary methods min/max chunk coordinates
// ------------------------------------------------------------------
@Nested
@DisplayName("minChunkX / minChunkZ / maxChunkX / maxChunkZ")
class ChunkBoundaryMethods {
@Test
void regionZeroBoundariesWithSizeEight() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertEquals(0, rc.minChunkX(8));
assertEquals(0, rc.minChunkZ(8));
assertEquals(7, rc.maxChunkX(8));
assertEquals(7, rc.maxChunkZ(8));
}
@Test
void regionOneBoundariesWithSizeEight() {
RegionCoordinate rc = new RegionCoordinate("overworld", 1, 1);
assertEquals(8, rc.minChunkX(8));
assertEquals(8, rc.minChunkZ(8));
assertEquals(15, rc.maxChunkX(8));
assertEquals(15, rc.maxChunkZ(8));
}
@Test
void regionMinusOneBoundariesWithSizeEight() {
RegionCoordinate rc = new RegionCoordinate("overworld", -1, -1);
assertEquals(-8, rc.minChunkX(8));
assertEquals(-8, rc.minChunkZ(8));
assertEquals(-1, rc.maxChunkX(8));
assertEquals(-1, rc.maxChunkZ(8));
}
@Test
void invalidRegionSizeThrowsInBoundaryMethods() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertThrows(IllegalArgumentException.class, () -> rc.minChunkX(0));
assertThrows(IllegalArgumentException.class, () -> rc.minChunkZ(-1));
assertThrows(IllegalArgumentException.class, () -> rc.maxChunkX(0));
assertThrows(IllegalArgumentException.class, () -> rc.maxChunkZ(-5));
}
}
// ------------------------------------------------------------------
// centerBlock methods
// ------------------------------------------------------------------
@Nested
@DisplayName("centerBlockX / centerBlockZ")
class CenterBlockMethods {
/** Region 0 with size 8: chunks 0-7, blocks 0-127 → centre = floorDiv(0+127,2) = 63 */
@Test
void centerOfRegionZero() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertEquals(63, rc.centerBlockX(8));
assertEquals(63, rc.centerBlockZ(8));
}
/** Region 1 with size 8: chunks 8-15, blocks 128-255 → centre = floorDiv(128+255,2) = 191 */
@Test
void centerOfRegionOne() {
RegionCoordinate rc = new RegionCoordinate("overworld", 1, 1);
assertEquals(191, rc.centerBlockX(8));
assertEquals(191, rc.centerBlockZ(8));
}
/** Region -1 with size 8: chunks -8 to -1, blocks -128 to -1 → centre = floorDiv(-128+-1,2) = -65 */
@Test
void centerOfRegionMinusOne() {
RegionCoordinate rc = new RegionCoordinate("overworld", -1, -1);
assertEquals(-65, rc.centerBlockX(8));
assertEquals(-65, rc.centerBlockZ(8));
}
@Test
void invalidRegionSizeThrowsInCenterMethods() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertThrows(IllegalArgumentException.class, () -> rc.centerBlockX(0));
assertThrows(IllegalArgumentException.class, () -> rc.centerBlockZ(-1));
}
}
// ------------------------------------------------------------------
// stableId
// ------------------------------------------------------------------
@Nested
@DisplayName("stableId")
class StableIdTests {
@Test
void stableIdIsStable() {
RegionCoordinate rc = new RegionCoordinate("overworld", 1, -2);
String id1 = rc.stableId();
String id2 = rc.stableId();
assertEquals(id1, id2);
}
@Test
void stableIdFormat() {
RegionCoordinate rc = new RegionCoordinate("the_nether", 3, 5);
assertEquals("the_nether:3:5", rc.stableId());
}
@Test
void negativeCoordinatesInStableId() {
RegionCoordinate rc = new RegionCoordinate("overworld", -1, -2);
assertEquals("overworld:-1:-2", rc.stableId());
}
}
// ------------------------------------------------------------------
// HashMap key usage
// ------------------------------------------------------------------
@Nested
@DisplayName("HashMap key compatibility")
class HashMapKeyTests {
@Test
void worksAsHashMapKey() {
Map<RegionCoordinate, String> map = new HashMap<>();
RegionCoordinate key1 = new RegionCoordinate("overworld", 0, 0);
RegionCoordinate key2 = new RegionCoordinate("overworld", 0, 0);
RegionCoordinate key3 = new RegionCoordinate("overworld", 1, 0);
map.put(key1, "value1");
assertEquals("value1", map.get(key2)); // same logical key
assertNotEquals("value1", map.get(key3)); // different key
}
@Test
void equalsAndHashCodeContract() {
RegionCoordinate rc1 = new RegionCoordinate("overworld", 0, 0);
RegionCoordinate rc2 = new RegionCoordinate("overworld", 0, 0);
assertEquals(rc1, rc2);
assertEquals(rc1.hashCode(), rc2.hashCode());
}
@Test
void notEqualToDifferentDimension() {
RegionCoordinate rc1 = new RegionCoordinate("overworld", 0, 0);
RegionCoordinate rc2 = new RegionCoordinate("nether", 0, 0);
assertFalse(rc1.equals(rc2));
}
@Test
void notEqualToDifferentType() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertFalse(rc.equals("not a region coordinate"));
}
@Test
void nullSafeEquals() {
RegionCoordinate rc = new RegionCoordinate("overworld", 0, 0);
assertFalse(rc.equals(null));
}
}
// ------------------------------------------------------------------
// toString
// ------------------------------------------------------------------
@Nested
@DisplayName("toString")
class ToStringTests {
@Test
void toStringContainsAllFields() {
RegionCoordinate rc = new RegionCoordinate("overworld", 1, -3);
String s = rc.toString();
assertTrue(s.contains("RegionCoordinate"));
assertTrue(s.contains("overworld"));
assertTrue(s.contains("x=1"));
assertTrue(s.contains("z=-3"));
}
@Test
void toStringShouldNotThrow() {
RegionCoordinate rc = new RegionCoordinate("the_end", 0, 0);
assertDoesNotThrow(() -> rc.toString());
}
}
}
@@ -0,0 +1,200 @@
package com.livingworld.regions;
import org.junit.jupiter.api.Test;
import java.util.UUID;
import static org.junit.jupiter.api.Assertions.*;
/**
* Tests for {@link RegionFactory}.
*/
class RegionFactoryTest {
private final RegionFactory factory = new RegionFactory();
// ------------------------------------------------------------------
// Null handling
// ------------------------------------------------------------------
@Test
void createNewRegionWithNullCoordinateThrows() {
IllegalArgumentException exception = assertThrows(
IllegalArgumentException.class,
() -> factory.createNewRegion(null, 0L)
);
assertEquals("coordinate must not be null", exception.getMessage());
}
// ------------------------------------------------------------------
// Basic creation
// ------------------------------------------------------------------
@Test
void createNewRegionReturnsValidRegion() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
long tick = 42L;
Region region = factory.createNewRegion(coordinate, tick);
assertNotNull(region);
assertDoesNotThrow(() -> region.validate());
}
// ------------------------------------------------------------------
// UUID assignment
// ------------------------------------------------------------------
@Test
void createNewRegionAssignsRandomUUID() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region1 = factory.createNewRegion(coordinate, 0L);
Region region2 = factory.createNewRegion(coordinate, 0L);
assertNotEquals(region1.getId(), region2.getId());
}
@Test
void createNewRegionUUIDIsNotNull() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
assertNotNull(region.getId());
assertInstanceOf(UUID.class, region.getId());
}
// ------------------------------------------------------------------
// Coordinate assignment
// ------------------------------------------------------------------
@Test
void createNewRegionUsesArgumentCoordinate() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 5, -3);
Region region = factory.createNewRegion(coordinate, 0L);
assertEquals(coordinate, region.getCoordinate());
}
// ------------------------------------------------------------------
// Lifecycle state
// ------------------------------------------------------------------
@Test
void createNewRegionStartsActive() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
assertEquals(RegionLifecycleState.ACTIVE, region.getLifecycleState());
}
// ------------------------------------------------------------------
// Simulation tick values
// ------------------------------------------------------------------
@Test
void createNewRegionSetsCreatedAtSimulationTick() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
long tick = 12345L;
Region region = factory.createNewRegion(coordinate, tick);
assertEquals(tick, region.getCreatedAtSimulationTick());
}
@Test
void createNewRegionSetsLastUpdatedSimulationTick() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
long tick = 12345L;
Region region = factory.createNewRegion(coordinate, tick);
assertEquals(tick, region.getLastUpdatedSimulationTick());
}
// ------------------------------------------------------------------
// Dirty flag
// ------------------------------------------------------------------
@Test
void createNewRegionIsDirty() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
assertTrue(region.isDirty());
}
// ------------------------------------------------------------------
// Default flags
// ------------------------------------------------------------------
@Test
void createNewRegionHasDefaultFlags() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
RegionFlags flags = region.getFlags();
assertNotNull(flags);
assertFalse(flags.isHasPlayerActivity());
assertFalse(flags.isHasHighPollution());
assertFalse(flags.isHasLowSoilQuality());
assertFalse(flags.isHasActiveEcosystemEvent());
assertFalse(flags.isForceLoadedBySimulation());
assertFalse(flags.isCorrupted());
}
// ------------------------------------------------------------------
// Default metrics
// ------------------------------------------------------------------
@Test
void createNewRegionHasDefaultMetrics() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
RegionMetrics metrics = region.getMetrics();
assertNotNull(metrics);
assertEquals(60.0, metrics.getEcosystemHealth());
assertEquals(0.0, metrics.getPollutionScore());
assertEquals(60.0, metrics.getSoilQuality());
assertEquals(60.0, metrics.getWaterQuality());
assertEquals(50.0, metrics.getVegetationPressure());
assertEquals(0.0, metrics.getResourceDepletion());
assertEquals(50.0, metrics.getRecoveryPressure());
}
// ------------------------------------------------------------------
// Empty module data
// ------------------------------------------------------------------
@Test
void createNewRegionHasEmptyModuleData() {
RegionCoordinate coordinate = new RegionCoordinate("overworld", 0, 0);
Region region = factory.createNewRegion(coordinate, 0L);
RegionModuleData moduleData = region.getModuleData();
assertNotNull(moduleData);
assertTrue(moduleData.moduleIds().isEmpty());
}
// ------------------------------------------------------------------
// Different dimensions
// ------------------------------------------------------------------
@Test
void createNewRegionWorksForDifferentDimensions() {
RegionCoordinate coordOverworld = new RegionCoordinate("overworld", 0, 0);
RegionCoordinate coordNether = new RegionCoordinate("the_nether", 1, 2);
RegionCoordinate coordEnd = new RegionCoordinate("end", -3, -4);
Region regionOverworld = factory.createNewRegion(coordOverworld, 0L);
Region regionNether = factory.createNewRegion(coordNether, 0L);
Region regionEnd = factory.createNewRegion(coordEnd, 0L);
assertEquals(coordOverworld, regionOverworld.getCoordinate());
assertEquals(coordNether, regionNether.getCoordinate());
assertEquals(coordEnd, regionEnd.getCoordinate());
assertNotEquals(regionOverworld.getId(), regionNether.getId());
assertNotEquals(regionNether.getId(), regionEnd.getId());
}
}
@@ -0,0 +1,217 @@
package com.livingworld.regions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertFalse;
import static org.junit.jupiter.api.Assertions.assertNotEquals;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Unit tests for {@link RegionFlags}.
*/
class RegionFlagsTest {
// ------------------------------------------------------------------
// Default values
// ------------------------------------------------------------------
@Nested
@DisplayName("Default values")
class DefaultValues {
@Test
void allFlagsDefaultToFalse() {
RegionFlags flags = new RegionFlags();
assertFalse(flags.isHasPlayerActivity());
assertFalse(flags.isHasHighPollution());
assertFalse(flags.isHasLowSoilQuality());
assertFalse(flags.isHasActiveEcosystemEvent());
assertFalse(flags.isForceLoadedBySimulation());
assertFalse(flags.isCorrupted());
}
@Test
void instanceIsNotNull() {
assertNotNull(new RegionFlags());
}
}
// ------------------------------------------------------------------
// Getters and Setters
// ------------------------------------------------------------------
@Nested
@DisplayName("Getters and Setters")
class GetterSetterTests {
@Test
void hasPlayerActivity() {
RegionFlags flags = new RegionFlags();
flags.setHasPlayerActivity(true);
assertTrue(flags.isHasPlayerActivity());
flags.setHasPlayerActivity(false);
assertFalse(flags.isHasPlayerActivity());
}
@Test
void hasHighPollution() {
RegionFlags flags = new RegionFlags();
flags.setHasHighPollution(true);
assertTrue(flags.isHasHighPollution());
flags.setHasHighPollution(false);
assertFalse(flags.isHasHighPollution());
}
@Test
void hasLowSoilQuality() {
RegionFlags flags = new RegionFlags();
flags.setHasLowSoilQuality(true);
assertTrue(flags.isHasLowSoilQuality());
flags.setHasLowSoilQuality(false);
assertFalse(flags.isHasLowSoilQuality());
}
@Test
void hasActiveEcosystemEvent() {
RegionFlags flags = new RegionFlags();
flags.setHasActiveEcosystemEvent(true);
assertTrue(flags.isHasActiveEcosystemEvent());
flags.setHasActiveEcosystemEvent(false);
assertFalse(flags.isHasActiveEcosystemEvent());
}
@Test
void forceLoadedBySimulation() {
RegionFlags flags = new RegionFlags();
flags.setForceLoadedBySimulation(true);
assertTrue(flags.isForceLoadedBySimulation());
flags.setForceLoadedBySimulation(false);
assertFalse(flags.isForceLoadedBySimulation());
}
@Test
void corrupted() {
RegionFlags flags = new RegionFlags();
flags.setCorrupted(true);
assertTrue(flags.isCorrupted());
flags.setCorrupted(false);
assertFalse(flags.isCorrupted());
}
}
// ------------------------------------------------------------------
// copy()
// ------------------------------------------------------------------
@Nested
@DisplayName("copy()")
class CopyTests {
@Test
void copyReturnsIndependentInstance() {
RegionFlags original = new RegionFlags();
original.setHasPlayerActivity(true);
original.setCorrupted(true);
RegionFlags copy = original.copy();
assertNotNull(copy);
assertNotSame(original, copy);
}
@Test
void copyPreservesAllFlagValues() {
RegionFlags original = new RegionFlags();
original.setHasPlayerActivity(true);
original.setHasHighPollution(true);
original.setHasLowSoilQuality(true);
original.setHasActiveEcosystemEvent(true);
original.setForceLoadedBySimulation(true);
original.setCorrupted(true);
RegionFlags copy = original.copy();
assertTrue(copy.isHasPlayerActivity());
assertTrue(copy.isHasHighPollution());
assertTrue(copy.isHasLowSoilQuality());
assertTrue(copy.isHasActiveEcosystemEvent());
assertTrue(copy.isForceLoadedBySimulation());
assertTrue(copy.isCorrupted());
}
@Test
void modifyingCopyDoesNotAffectOriginal() {
RegionFlags original = new RegionFlags();
original.setHasPlayerActivity(true);
original.setCorrupted(false);
RegionFlags copy = original.copy();
copy.setHasPlayerActivity(false);
copy.setCorrupted(true);
assertTrue(original.isHasPlayerActivity());
assertFalse(original.isCorrupted());
}
}
// ------------------------------------------------------------------
// clearTransientFlags()
// ------------------------------------------------------------------
@Nested
@DisplayName("clearTransientFlags()")
class ClearTransientFlagTests {
@Test
void clearsTransientFlagsOnly() {
RegionFlags flags = new RegionFlags();
flags.setHasPlayerActivity(true);
flags.setHasActiveEcosystemEvent(true);
flags.setHasHighPollution(true);
flags.setHasLowSoilQuality(true);
flags.setForceLoadedBySimulation(true);
flags.setCorrupted(true);
flags.clearTransientFlags();
// Transient flags cleared
assertFalse(flags.isHasPlayerActivity());
assertFalse(flags.isHasActiveEcosystemEvent());
// Persistent flags preserved
assertTrue(flags.isHasHighPollution());
assertTrue(flags.isHasLowSoilQuality());
assertTrue(flags.isForceLoadedBySimulation());
assertTrue(flags.isCorrupted());
}
@Test
void safeToCallWhenAllFlagsAlreadyFalse() {
RegionFlags flags = new RegionFlags();
flags.clearTransientFlags(); // should not throw
assertFalse(flags.isHasPlayerActivity());
assertFalse(flags.isHasActiveEcosystemEvent());
}
}
// ------------------------------------------------------------------
// toString
// ------------------------------------------------------------------
@Nested
@DisplayName("toString")
class ToStringTests {
@Test
void toStringContainsFlagNames() {
RegionFlags flags = new RegionFlags();
flags.setHasPlayerActivity(true);
String s = flags.toString();
assertTrue(s.contains("hasPlayerActivity=true"));
assertTrue(s.contains("corrupted=false"));
}
}
}
@@ -0,0 +1,308 @@
package com.livingworld.regions;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Tests for {@link RegionLifecycleController}.
*/
class RegionLifecycleControllerTest {
// ------------------------------------------------------------------
// Allowed transitions
// ------------------------------------------------------------------
@Nested
@DisplayName("Allowed transitions")
class AllowedTransitions {
@Test
@DisplayName("UNLOADED -> LOADING is allowed")
void unloadedToLoading() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.UNLOADED, RegionLifecycleState.LOADING));
}
@Test
@DisplayName("LOADING -> ACTIVE is allowed")
void loadingToActive() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.LOADING, RegionLifecycleState.ACTIVE));
}
@Test
@DisplayName("LOADING -> FAILED is allowed")
void loadingToFailed() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.LOADING, RegionLifecycleState.FAILED));
}
@Test
@DisplayName("ACTIVE -> DIRTY is allowed")
void activeToDirty() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.ACTIVE, RegionLifecycleState.DIRTY));
}
@Test
@DisplayName("DIRTY -> SAVING is allowed")
void dirtyToSaving() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.DIRTY, RegionLifecycleState.SAVING));
}
@Test
@DisplayName("SAVING -> ACTIVE is allowed")
void savingToActive() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.SAVING, RegionLifecycleState.ACTIVE));
}
@Test
@DisplayName("SAVING -> FAILED is allowed")
void savingToFailed() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.SAVING, RegionLifecycleState.FAILED));
}
@Test
@DisplayName("ACTIVE -> UNLOADING is allowed")
void activeToUnloading() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.ACTIVE, RegionLifecycleState.UNLOADING));
}
@Test
@DisplayName("UNLOADING -> UNLOADED is allowed")
void unloadingToUnloaded() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.UNLOADING, RegionLifecycleState.UNLOADED));
}
@Test
@DisplayName("FAILED -> LOADING is allowed")
void failedToLoading() {
assertTrue(RegionLifecycleController.canTransition(
RegionLifecycleState.FAILED, RegionLifecycleState.LOADING));
}
}
// ------------------------------------------------------------------
// Disallowed transitions
// ------------------------------------------------------------------
@Nested
@DisplayName("Disallowed transitions")
class DisallowedTransitions {
private static final String[] STATES = {
"UNLOADED", "LOADING", "ACTIVE", "DIRTY", "SAVING", "FAILED", "UNLOADING"
};
@Test
@DisplayName("Same state is not allowed (no self-transitions)")
void sameStateNotAllowed() {
for (String state : STATES) {
assertFalse(RegionLifecycleController.canTransition(
RegionLifecycleState.valueOf(state), RegionLifecycleState.valueOf(state)));
}
}
@Test
@DisplayName("UNLOADED -> ACTIVE is not allowed")
void unloadedToActive() {
assertFalse(RegionLifecycleController.canTransition(
RegionLifecycleState.UNLOADED, RegionLifecycleState.ACTIVE));
}
@Test
@DisplayName("ACTIVE -> LOADING is not allowed")
void activeToLoading() {
assertFalse(RegionLifecycleController.canTransition(
RegionLifecycleState.ACTIVE, RegionLifecycleState.LOADING));
}
@Test
@DisplayName("FAILED -> UNLOADED is not allowed")
void failedToUnloaded() {
assertFalse(RegionLifecycleController.canTransition(
RegionLifecycleState.FAILED, RegionLifecycleState.UNLOADED));
}
@Test
@DisplayName("ACTIVE -> SAVING is not allowed")
void activeToSaving() {
assertFalse(RegionLifecycleController.canTransition(
RegionLifecycleState.ACTIVE, RegionLifecycleState.SAVING));
}
@Test
@DisplayName("All invalid transitions throw IllegalStateException on transition()")
void invalidTransitionsThrowException() {
Region region1 = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.ACTIVE, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
// ACTIVE -> LOADING (invalid)
assertThrows(IllegalStateException.class, () ->
RegionLifecycleController.transition(region1, RegionLifecycleState.LOADING));
// ACTIVE -> SAVING (invalid)
Region region2 = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.ACTIVE, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
assertThrows(IllegalStateException.class, () ->
RegionLifecycleController.transition(region2, RegionLifecycleState.SAVING));
}
}
// ------------------------------------------------------------------
// Null handling
// ------------------------------------------------------------------
@Nested
@DisplayName("Null handling")
class NullHandling {
@Test
@DisplayName("canTransition throws IllegalArgumentException for null from")
void canTransitionNullFrom() {
assertThrows(IllegalArgumentException.class, () ->
RegionLifecycleController.canTransition(null, RegionLifecycleState.ACTIVE));
}
@Test
@DisplayName("canTransition throws IllegalArgumentException for null to")
void canTransitionNullTo() {
assertThrows(IllegalArgumentException.class, () ->
RegionLifecycleController.canTransition(RegionLifecycleState.UNLOADED, null));
}
@Test
@DisplayName("transition throws IllegalArgumentException for null region")
void transitionNullRegion() {
assertThrows(IllegalArgumentException.class, () ->
RegionLifecycleController.transition(null, RegionLifecycleState.LOADING));
}
@Test
@DisplayName("transition throws IllegalArgumentException for null target")
void transitionNullTarget() {
Region region = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.UNLOADED, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
assertThrows(IllegalArgumentException.class, () ->
RegionLifecycleController.transition(region, null));
}
}
// ------------------------------------------------------------------
// transition() side effects
// ------------------------------------------------------------------
@Nested
@DisplayName("transition() side effects")
class TransitionSideEffects {
@Test
@DisplayName("transition() updates lifecycle state and marks dirty")
void transitionUpdatesStateAndMarksDirty() {
Region region = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.UNLOADED, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
assertFalse(region.isDirty());
assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());
RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);
assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());
assertTrue(region.isDirty());
}
@Test
@DisplayName("transition() updates state through full load-save-unload cycle")
void fullCycleTransition() {
Region region = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.UNLOADED, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
// UNLOADED -> LOADING
RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);
assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());
// LOADING -> ACTIVE
RegionLifecycleController.transition(region, RegionLifecycleState.ACTIVE);
assertEquals(RegionLifecycleState.ACTIVE, region.getLifecycleState());
// ACTIVE -> DIRTY
RegionLifecycleController.transition(region, RegionLifecycleState.DIRTY);
assertEquals(RegionLifecycleState.DIRTY, region.getLifecycleState());
// DIRTY -> SAVING
RegionLifecycleController.transition(region, RegionLifecycleState.SAVING);
assertEquals(RegionLifecycleState.SAVING, region.getLifecycleState());
// SAVING -> ACTIVE (successful save)
RegionLifecycleController.transition(region, RegionLifecycleState.ACTIVE);
assertEquals(RegionLifecycleState.ACTIVE, region.getLifecycleState());
// ACTIVE -> UNLOADING
RegionLifecycleController.transition(region, RegionLifecycleState.UNLOADING);
assertEquals(RegionLifecycleState.UNLOADING, region.getLifecycleState());
// UNLOADING -> UNLOADED
RegionLifecycleController.transition(region, RegionLifecycleState.UNLOADED);
assertEquals(RegionLifecycleState.UNLOADED, region.getLifecycleState());
}
@Test
@DisplayName("transition() allows FAILED -> LOADING recovery")
void failedToLoadingRecovery() {
Region region = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.FAILED, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
RegionLifecycleController.transition(region, RegionLifecycleState.LOADING);
assertEquals(RegionLifecycleState.LOADING, region.getLifecycleState());
}
@Test
@DisplayName("transition() allows SAVING -> FAILED error path")
void savingToFailedErrorPath() {
Region region = new Region(
java.util.UUID.randomUUID(),
new RegionCoordinate("minecraft:overworld", 0, 0),
RegionLifecycleState.ACTIVE, 0, 0, false,
new RegionFlags(), new RegionMetrics(), new RegionModuleData());
// ACTIVE -> DIRTY
RegionLifecycleController.transition(region, RegionLifecycleState.DIRTY);
assertEquals(RegionLifecycleState.DIRTY, region.getLifecycleState());
// DIRTY -> SAVING
RegionLifecycleController.transition(region, RegionLifecycleState.SAVING);
assertEquals(RegionLifecycleState.SAVING, region.getLifecycleState());
// SAVING -> FAILED (save error)
RegionLifecycleController.transition(region, RegionLifecycleState.FAILED);
assertEquals(RegionLifecycleState.FAILED, region.getLifecycleState());
}
}
}
@@ -0,0 +1,435 @@
package com.livingworld.regions;
import static org.junit.jupiter.api.Assertions.assertEquals;
import static org.junit.jupiter.api.Assertions.assertNotSame;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Nested;
import org.junit.jupiter.api.Test;
/**
* Comprehensive tests for {@link RegionMetrics}.
*/
class RegionMetricsTest {
// ------------------------------------------------------------------
// Default Values Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Default Values")
class DefaultValues {
@Test
@DisplayName("defaults() returns correct ecosystem health")
void defaultsEcosystemHealth() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(60, metrics.getEcosystemHealth(), 0.001);
}
@Test
@DisplayName("defaults() returns correct pollution score")
void defaultsPollutionScore() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(0, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("defaults() returns correct soil quality")
void defaultsSoilQuality() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(60, metrics.getSoilQuality(), 0.001);
}
@Test
@DisplayName("defaults() returns correct water quality")
void defaultsWaterQuality() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(60, metrics.getWaterQuality(), 0.001);
}
@Test
@DisplayName("defaults() returns correct vegetation pressure")
void defaultsVegetationPressure() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(50, metrics.getVegetationPressure(), 0.001);
}
@Test
@DisplayName("defaults() returns correct resource depletion")
void defaultsResourceDepletion() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(0, metrics.getResourceDepletion(), 0.001);
}
@Test
@DisplayName("defaults() returns correct recovery pressure")
void defaultsRecoveryPressure() {
RegionMetrics metrics = RegionMetrics.defaults();
assertEquals(50, metrics.getRecoveryPressure(), 0.001);
}
@Test
@DisplayName("default constructor initializes all fields to 0")
void defaultConstructorAllZeros() {
RegionMetrics metrics = new RegionMetrics();
assertEquals(0, metrics.getEcosystemHealth(), 0.001);
assertEquals(0, metrics.getPollutionScore(), 0.001);
assertEquals(0, metrics.getSoilQuality(), 0.001);
assertEquals(0, metrics.getWaterQuality(), 0.001);
assertEquals(0, metrics.getVegetationPressure(), 0.001);
assertEquals(0, metrics.getResourceDepletion(), 0.001);
assertEquals(0, metrics.getRecoveryPressure(), 0.001);
}
}
// ------------------------------------------------------------------
// Getter/Setter Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Getter and Setter Clamping")
class GetterSetterClamping {
@Test
@DisplayName("setter clamps values above 100 to 100")
void setterClampsAboveMax() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(150);
assertEquals(100, metrics.getEcosystemHealth(), 0.001);
}
@Test
@DisplayName("setter clamps values below 0 to 0")
void setterClampsBelowMin() {
RegionMetrics metrics = new RegionMetrics();
metrics.setPollutionScore(-50);
assertEquals(0, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("setter accepts exact boundary values")
void setterAcceptsBoundaries() {
RegionMetrics metrics = new RegionMetrics();
metrics.setSoilQuality(0);
assertEquals(0, metrics.getSoilQuality(), 0.001);
metrics.setSoilQuality(100);
assertEquals(100, metrics.getSoilQuality(), 0.001);
}
@Test
@DisplayName("setter accepts normal values")
void setterAcceptsNormalValues() {
RegionMetrics metrics = new RegionMetrics();
metrics.setWaterQuality(42.5);
assertEquals(42.5, metrics.getWaterQuality(), 0.001);
}
@Test
@DisplayName("all setters clamp correctly")
void allSettersClamp() {
RegionMetrics metrics = new RegionMetrics();
metrics.setVegetationPressure(-10);
assertEquals(0, metrics.getVegetationPressure(), 0.001);
metrics.setResourceDepletion(200);
assertEquals(100, metrics.getResourceDepletion(), 0.001);
metrics.setRecoveryPressure(-5);
assertEquals(0, metrics.getRecoveryPressure(), 0.001);
}
}
// ------------------------------------------------------------------
// Normalize Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Normalize")
class NormalizeTests {
@Test
@DisplayName("normalize() clamps values above 100")
void normalizeClampsAboveMax() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(150);
metrics.setPollutionScore(-20);
metrics.normalize();
assertEquals(100, metrics.getEcosystemHealth(), 0.001);
assertEquals(0, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("normalize() clamps values below 0")
void normalizeClampsBelowMin() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(-50);
metrics.setPollutionScore(200);
metrics.normalize();
assertEquals(0, metrics.getEcosystemHealth(), 0.001);
assertEquals(100, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("normalize() leaves valid values unchanged")
void normalizeLeavesValidUnchanged() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(50);
metrics.setPollutionScore(30);
metrics.normalize();
assertEquals(50, metrics.getEcosystemHealth(), 0.001);
assertEquals(30, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("normalize() returns this for chaining")
void normalizeReturnsThis() {
RegionMetrics metrics = new RegionMetrics();
RegionMetrics result = metrics.normalize();
assertTrue(result == metrics, "normalize() should return the same instance");
}
}
// ------------------------------------------------------------------
// Copy Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Copy")
class CopyTests {
@Test
@DisplayName("copy() returns independent instance")
void copyReturnsIndependentInstance() {
RegionMetrics original = new RegionMetrics();
original.setEcosystemHealth(80);
original.setPollutionScore(30);
RegionMetrics copy = original.copy();
assertEquals(80, copy.getEcosystemHealth(), 0.001);
assertEquals(30, copy.getPollutionScore(), 0.001);
// Modify the copy
copy.setEcosystemHealth(90);
// Original should be unchanged
assertEquals(80, original.getEcosystemHealth(), 0.001);
}
@Test
@DisplayName("copy() returns a different object")
void copyReturnsDifferentObject() {
RegionMetrics metrics = new RegionMetrics();
metrics.setSoilQuality(75);
RegionMetrics copy = metrics.copy();
assertNotSame(metrics, copy);
}
@Test
@DisplayName("copy() copies all fields")
void copyAllFields() {
RegionMetrics original = new RegionMetrics();
original.setEcosystemHealth(10);
original.setPollutionScore(20);
original.setSoilQuality(30);
original.setWaterQuality(40);
original.setVegetationPressure(50);
original.setResourceDepletion(60);
original.setRecoveryPressure(70);
RegionMetrics copy = original.copy();
assertEquals(original.getEcosystemHealth(), copy.getEcosystemHealth(), 0.001);
assertEquals(original.getPollutionScore(), copy.getPollutionScore(), 0.001);
assertEquals(original.getSoilQuality(), copy.getSoilQuality(), 0.001);
assertEquals(original.getWaterQuality(), copy.getWaterQuality(), 0.001);
assertEquals(original.getVegetationPressure(), copy.getVegetationPressure(), 0.001);
assertEquals(original.getResourceDepletion(), copy.getResourceDepletion(), 0.001);
assertEquals(original.getRecoveryPressure(), copy.getRecoveryPressure(), 0.001);
}
}
// ------------------------------------------------------------------
// ApplyDelta Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Apply Delta")
class ApplyDeltaTests {
@Test
@DisplayName("applyDelta() modifies all fields correctly")
void applyDeltaAllFields() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(50);
metrics.setPollutionScore(20);
metrics.setSoilQuality(80);
metrics.applyDelta(10, -5, 15, 0, 0, 0, 0);
assertEquals(60, metrics.getEcosystemHealth(), 0.001);
assertEquals(15, metrics.getPollutionScore(), 0.001);
assertEquals(95, metrics.getSoilQuality(), 0.001);
}
@Test
@DisplayName("applyDelta() clamps results above max")
void applyDeltaClampsAboveMax() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(95);
metrics.applyDelta(20, 0, 0, 0, 0, 0, 0);
assertEquals(100, metrics.getEcosystemHealth(), 0.001);
}
@Test
@DisplayName("applyDelta() clamps results below min")
void applyDeltaClampsBelowMin() {
RegionMetrics metrics = new RegionMetrics();
metrics.setPollutionScore(5);
metrics.applyDelta(-10, 0, 0, 0, 0, 0, 0);
assertEquals(0, metrics.getEcosystemHealth(), 0.001);
}
@Test
@DisplayName("applyDelta() returns this for chaining")
void applyDeltaReturnsThis() {
RegionMetrics metrics = new RegionMetrics();
RegionMetrics result = metrics.applyDelta(0, 0, 0, 0, 0, 0, 0);
assertTrue(result == metrics);
}
@Test
@DisplayName("applyEcosystemHealthDelta() modifies only ecosystem health")
void applySingleDelta() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(50);
metrics.setPollutionScore(30);
metrics.applyEcosystemHealthDelta(10);
assertEquals(60, metrics.getEcosystemHealth(), 0.001);
assertEquals(30, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("applyPollutionScoreDelta() clamps correctly")
void applySingleDeltaClamps() {
RegionMetrics metrics = new RegionMetrics();
metrics.setPollutionScore(95);
metrics.applyPollutionScoreDelta(20);
assertEquals(100, metrics.getPollutionScore(), 0.001);
}
@Test
@DisplayName("applySoilQualityDelta() clamps below min")
void applySingleDeltaClampsBelow() {
RegionMetrics metrics = new RegionMetrics();
metrics.setSoilQuality(5);
metrics.applySoilQualityDelta(-10);
assertEquals(0, metrics.getSoilQuality(), 0.001);
}
@Test
@DisplayName("applyWaterQualityDelta() works correctly")
void applyWaterQualityDelta() {
RegionMetrics metrics = new RegionMetrics();
metrics.setWaterQuality(50);
metrics.applyWaterQualityDelta(25);
assertEquals(75, metrics.getWaterQuality(), 0.001);
}
@Test
@DisplayName("applyVegetationPressureDelta() works correctly")
void applyVegetationPressureDelta() {
RegionMetrics metrics = new RegionMetrics();
metrics.setVegetationPressure(30);
metrics.applyVegetationPressureDelta(-10);
assertEquals(20, metrics.getVegetationPressure(), 0.001);
}
@Test
@DisplayName("applyResourceDepletionDelta() works correctly")
void applyResourceDepletionDelta() {
RegionMetrics metrics = new RegionMetrics();
metrics.setResourceDepletion(40);
metrics.applyResourceDepletionDelta(60);
assertEquals(100, metrics.getResourceDepletion(), 0.001);
}
@Test
@DisplayName("applyRecoveryPressureDelta() works correctly")
void applyRecoveryPressureDelta() {
RegionMetrics metrics = new RegionMetrics();
metrics.setRecoveryPressure(25);
metrics.applyRecoveryPressureDelta(-15);
assertEquals(10, metrics.getRecoveryPressure(), 0.001);
}
@Test
@DisplayName("individual apply methods return this for chaining")
void individualApplyMethodsReturnThis() {
RegionMetrics metrics = new RegionMetrics();
assertTrue(metrics.applyEcosystemHealthDelta(5) == metrics);
assertTrue(metrics.applyPollutionScoreDelta(5) == metrics);
assertTrue(metrics.applySoilQualityDelta(5) == metrics);
assertTrue(metrics.applyWaterQualityDelta(5) == metrics);
assertTrue(metrics.applyVegetationPressureDelta(5) == metrics);
assertTrue(metrics.applyResourceDepletionDelta(5) == metrics);
assertTrue(metrics.applyRecoveryPressureDelta(5) == metrics);
}
}
// ------------------------------------------------------------------
// Constants Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("Constants")
class ConstantsTests {
@Test
@DisplayName("MIN_VALUE is 0")
void minValue() {
assertEquals(0, RegionMetrics.MIN_VALUE);
}
@Test
@DisplayName("MAX_VALUE is 100")
void maxValue() {
assertEquals(100, RegionMetrics.MAX_VALUE);
}
}
// ------------------------------------------------------------------
// toString Tests
// ------------------------------------------------------------------
@Nested
@DisplayName("toString")
class ToStringTests {
@Test
@DisplayName("toString() includes all field names and values")
void toStringIncludesAllFields() {
RegionMetrics metrics = new RegionMetrics();
metrics.setEcosystemHealth(60);
metrics.setPollutionScore(10);
String str = metrics.toString();
assertTrue(str.contains("ecosystemHealth"));
assertTrue(str.contains("pollutionScore"));
assertTrue(str.contains("soilQuality"));
assertTrue(str.contains("waterQuality"));
assertTrue(str.contains("vegetationPressure"));
assertTrue(str.contains("resourceDepletion"));
assertTrue(str.contains("recoveryPressure"));
}
@Test
@DisplayName("toString() starts with class name")
void toStringStartsWithClassName() {
RegionMetrics metrics = new RegionMetrics();
assertTrue(metrics.toString().startsWith("RegionMetrics{"));
}
}
}