From 5a30ab0d3f30392bd4959e3f7c6df7f94b37deb6 Mon Sep 17 00:00:00 2001 From: Mike Primm Date: Fri, 6 Feb 2026 08:37:31 -0500 Subject: [PATCH] Claude Code tire-kicking: generate some unit tests --- CLAUDE.md | 84 ++++++ DynmapCore/build.gradle | 12 + .../dynmap/utils/BufferInputStreamTest.java | 189 +++++++++++++ .../org/dynmap/utils/DynIntHashMapTest.java | 249 ++++++++++++++++++ .../dynmap/utils/IpAddressMatcherTest.java | 122 +++++++++ .../java/org/dynmap/utils/Matrix3DTest.java | 196 ++++++++++++++ .../java/org/dynmap/utils/Vector3DTest.java | 173 ++++++++++++ 7 files changed, 1025 insertions(+) create mode 100644 CLAUDE.md create mode 100644 DynmapCore/src/test/java/org/dynmap/utils/BufferInputStreamTest.java create mode 100644 DynmapCore/src/test/java/org/dynmap/utils/DynIntHashMapTest.java create mode 100644 DynmapCore/src/test/java/org/dynmap/utils/IpAddressMatcherTest.java create mode 100644 DynmapCore/src/test/java/org/dynmap/utils/Matrix3DTest.java create mode 100644 DynmapCore/src/test/java/org/dynmap/utils/Vector3DTest.java diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 00000000..0aae4eeb --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,84 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## Project Overview + +Dynmap is a dynamic web mapping plugin/mod for Minecraft servers. It's a multi-platform project supporting Spigot/PaperMC, Forge, and Fabric across multiple Minecraft versions (1.12.2 - 1.21.x). + +## Build Commands + +```bash +# Build all platforms (requires JDK 21 as default) +./gradlew setup build + +# Build outputs go to /target directory + +# Build specific module (for faster iteration, but NOT for PR submissions) +./gradlew :fabric-1.18:build + +# Forge 1.12.2 (requires JDK 8 - set JAVA_HOME accordingly) +cd oldgradle +./gradlew setup build +``` + +**JDK Requirements:** +- Default: JDK 21 +- Forge 1.12.2 (oldgradle): JDK 8 strictly required +- Runtime targets: JDK 8 (1.16-), JDK 16 (1.17.x), JDK 17 (1.18-1.20.4), JDK 21 (1.20.5+) + +## Architecture + +### Module Structure (71 modules total) + +**Core Shared Modules:** +- `DynmapCoreAPI/` - Stable public API for external plugins/mods (markers, mod support, rendering) +- `DynmapCore/` - Internal shared implementation (NOT stable - subject to breaking changes) +- `dynmap-api/` - Bukkit-specific public API + +**Platform Implementations:** +- `spigot/` - Bukkit/PaperMC implementation +- `bukkit-helper-*` - Version-specific NMS code (25 versions: 1.13-1.21) +- `fabric-*` - Fabric mod implementations (14 versions: 1.14.4-1.21.11) +- `forge-*` - Forge mod implementations (14 versions: 1.14.4-1.21.11) + +### Dependency Flow +``` +External Plugins/Mods + ↓ +DynmapCoreAPI (stable, published to repo.mikeprimm.com) + ↓ +DynmapCore (internal, unstable) + ↓ +Platform-specific modules (Spigot, Fabric, Forge) +``` + +### Key Components in DynmapCore +- `MapManager` - Tile/map rendering orchestration +- `DynmapCore.java` - Main coordination hub (~3,100 lines) +- `storage/` - Storage backends (FileTree, MySQL, PostgreSQL, SQLite, S3) +- `hdmap/` - HD map rendering (block models, shaders, textures) +- `web/` - Embedded Jetty server with servlets +- `markers/` - Marker system implementation + +## Critical Contribution Rules + +**PRs must build and test on ALL platforms including oldgradle. Changes to DynmapCore/DynmapCoreAPI require testing on all platforms.** + +- **Java 8 compatibility required** - Code must compile and run on Java 8 +- **Java only** - No Kotlin, Scala, or other JVM languages +- **No dependency updates** - Library versions are tied to platform compatibility +- **No platform-specific code** - Must work on Windows, Linux (x86/ARM), macOS, Docker +- **Small PRs only** - One feature per PR, no style/formatting changes +- **No mod-specific code** - Use Dynmap APIs instead; external mods should depend on DynmapCoreAPI +- **Apache License v2** - All code must be compatible + +## Testing + +No automated tests exist. Verification is done by: +1. Building all platforms successfully (`./gradlew setup build` AND `cd oldgradle && ./gradlew setup build`) +2. Manual testing on target Minecraft server platforms + +## Storage Backends + +Dynmap supports: FileTree (default), MySQL/MariaDB, PostgreSQL, SQLite, MS SQL Server, AWS S3 diff --git a/DynmapCore/build.gradle b/DynmapCore/build.gradle index 9fa00771..d5955eeb 100644 --- a/DynmapCore/build.gradle +++ b/DynmapCore/build.gradle @@ -26,6 +26,18 @@ dependencies { implementation 'io.github.linktosriram.s3lite:util:0.0.2-SNAPSHOT' implementation 'jakarta.xml.bind:jakarta.xml.bind-api:3.0.1' implementation 'com.sun.xml.bind:jaxb-impl:3.0.0' + // Test dependencies (Java 8 compatible versions) + testImplementation 'junit:junit:4.13.2' + testImplementation 'org.mockito:mockito-core:4.11.0' + testImplementation 'org.assertj:assertj-core:3.24.2' +} + +test { + useJUnit() + testLogging { + events "passed", "skipped", "failed" + exceptionFormat "full" + } } processResources { diff --git a/DynmapCore/src/test/java/org/dynmap/utils/BufferInputStreamTest.java b/DynmapCore/src/test/java/org/dynmap/utils/BufferInputStreamTest.java new file mode 100644 index 00000000..d0ebf21c --- /dev/null +++ b/DynmapCore/src/test/java/org/dynmap/utils/BufferInputStreamTest.java @@ -0,0 +1,189 @@ +package org.dynmap.utils; + +import org.junit.Test; +import java.io.IOException; +import static org.junit.Assert.*; + +public class BufferInputStreamTest { + + @Test + public void testConstructorWithBuffer() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data); + assertEquals(5, stream.length()); + assertSame(data, stream.buffer()); + } + + @Test + public void testConstructorWithBufferAndLength() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data, 3); + assertEquals(3, stream.length()); + assertSame(data, stream.buffer()); + } + + @Test + public void testReadSingleByte() { + byte[] data = {10, 20, 30}; + BufferInputStream stream = new BufferInputStream(data); + assertEquals(10, stream.read()); + assertEquals(20, stream.read()); + assertEquals(30, stream.read()); + assertEquals(-1, stream.read()); // EOF + } + + @Test + public void testReadSingleByteUnsigned() { + // Test that bytes are returned as unsigned (0-255) + byte[] data = {(byte) 255, (byte) 128}; + BufferInputStream stream = new BufferInputStream(data); + assertEquals(255, stream.read()); + assertEquals(128, stream.read()); + } + + @Test + public void testReadArray() throws IOException { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data); + byte[] buffer = new byte[3]; + int bytesRead = stream.read(buffer, 0, 3); + assertEquals(3, bytesRead); + assertEquals(1, buffer[0]); + assertEquals(2, buffer[1]); + assertEquals(3, buffer[2]); + } + + @Test + public void testReadArrayPartial() throws IOException { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + byte[] buffer = new byte[5]; + int bytesRead = stream.read(buffer, 0, 5); + assertEquals(3, bytesRead); + assertEquals(1, buffer[0]); + assertEquals(2, buffer[1]); + assertEquals(3, buffer[2]); + } + + @Test + public void testReadArrayAtEOF() throws IOException { + byte[] data = {1, 2}; + BufferInputStream stream = new BufferInputStream(data); + stream.read(); + stream.read(); + byte[] buffer = new byte[5]; + int bytesRead = stream.read(buffer, 0, 5); + assertEquals(-1, bytesRead); + } + + @Test(expected = IOException.class) + public void testReadArrayNullBuffer() throws IOException { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + stream.read(null, 0, 3); + } + + @Test(expected = IOException.class) + public void testReadArrayNegativeOffset() throws IOException { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + byte[] buffer = new byte[5]; + stream.read(buffer, -1, 3); + } + + @Test + public void testAvailable() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data); + assertEquals(5, stream.available()); + stream.read(); + assertEquals(4, stream.available()); + stream.read(); + stream.read(); + assertEquals(2, stream.available()); + } + + @Test + public void testSkip() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data); + long skipped = stream.skip(2); + assertEquals(2, skipped); + assertEquals(3, stream.read()); + } + + @Test + public void testSkipPastEnd() { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + long skipped = stream.skip(10); + assertEquals(3, skipped); + assertEquals(-1, stream.read()); + } + + @Test + public void testSkipNegative() { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + stream.read(); + long skipped = stream.skip(-5); + assertEquals(0, skipped); + } + + @Test + public void testMarkSupported() { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + assertTrue(stream.markSupported()); + } + + @Test + public void testMarkAndReset() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data); + stream.read(); + stream.read(); + stream.mark(0); // Mark at position 2 + assertEquals(3, stream.read()); + assertEquals(4, stream.read()); + stream.reset(); + assertEquals(3, stream.read()); // Back to position 2 + } + + @Test + public void testResetWithoutMark() { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + stream.read(); + stream.read(); + stream.reset(); // Should reset to 0 (default mark) + assertEquals(1, stream.read()); + } + + @Test + public void testClose() { + byte[] data = {1, 2, 3}; + BufferInputStream stream = new BufferInputStream(data); + stream.close(); // Should not throw + // Stream should still be usable after close (no-op) + assertEquals(1, stream.read()); + } + + @Test + public void testEmptyBuffer() { + byte[] data = {}; + BufferInputStream stream = new BufferInputStream(data); + assertEquals(0, stream.available()); + assertEquals(-1, stream.read()); + } + + @Test + public void testLengthVsBufferLength() { + byte[] data = {1, 2, 3, 4, 5}; + BufferInputStream stream = new BufferInputStream(data, 2); + assertEquals(2, stream.available()); + assertEquals(1, stream.read()); + assertEquals(2, stream.read()); + assertEquals(-1, stream.read()); // EOF at specified length + } +} diff --git a/DynmapCore/src/test/java/org/dynmap/utils/DynIntHashMapTest.java b/DynmapCore/src/test/java/org/dynmap/utils/DynIntHashMapTest.java new file mode 100644 index 00000000..e844d638 --- /dev/null +++ b/DynmapCore/src/test/java/org/dynmap/utils/DynIntHashMapTest.java @@ -0,0 +1,249 @@ +package org.dynmap.utils; + +import org.junit.Test; +import java.util.List; +import static org.junit.Assert.*; + +public class DynIntHashMapTest { + + @Test + public void testDefaultConstructor() { + DynIntHashMap map = new DynIntHashMap(); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + } + + @Test + public void testConstructorWithCapacity() { + DynIntHashMap map = new DynIntHashMap(100); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + } + + @Test + public void testConstructorWithCapacityAndLoadFactor() { + DynIntHashMap map = new DynIntHashMap(100, 0.5f); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorNegativeCapacity() { + new DynIntHashMap(-1); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorZeroLoadFactor() { + new DynIntHashMap(10, 0.0f); + } + + @Test(expected = IllegalArgumentException.class) + public void testConstructorNegativeLoadFactor() { + new DynIntHashMap(10, -0.5f); + } + + @Test + public void testCopyConstructor() { + DynIntHashMap original = new DynIntHashMap(); + original.put(1, "one"); + original.put(2, "two"); + + DynIntHashMap copy = new DynIntHashMap(original); + assertEquals(2, copy.size()); + assertEquals("one", copy.get(1)); + assertEquals("two", copy.get(2)); + + // Verify it's a deep copy + original.put(1, "modified"); + assertEquals("one", copy.get(1)); + } + + @Test + public void testPutAndGet() { + DynIntHashMap map = new DynIntHashMap(); + assertNull(map.put(1, "one")); + assertEquals("one", map.get(1)); + assertEquals(1, map.size()); + } + + @Test + public void testPutReplace() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + Object oldValue = map.put(1, "ONE"); + assertEquals("one", oldValue); + assertEquals("ONE", map.get(1)); + assertEquals(1, map.size()); + } + + @Test + public void testPutNullValue() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, null); + assertNull(map.get(1)); + assertTrue(map.containsKey(1)); + } + + @Test + public void testGetNonExistent() { + DynIntHashMap map = new DynIntHashMap(); + assertNull(map.get(999)); + } + + @Test + public void testContainsKey() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + assertTrue(map.containsKey(1)); + assertFalse(map.containsKey(2)); + } + + @Test + public void testContainsValue() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + map.put(2, "two"); + assertTrue(map.containsValue("one")); + assertTrue(map.containsValue("two")); + assertFalse(map.containsValue("three")); + } + + @Test + public void testContainsValueNull() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, null); + assertTrue(map.containsValue(null)); + } + + @Test + public void testRemove() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + map.put(2, "two"); + + Object removed = map.remove(1); + assertEquals("one", removed); + assertEquals(1, map.size()); + assertFalse(map.containsKey(1)); + assertTrue(map.containsKey(2)); + } + + @Test + public void testRemoveNonExistent() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + + Object removed = map.remove(999); + assertNull(removed); + assertEquals(1, map.size()); + } + + @Test + public void testClear() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + map.put(2, "two"); + map.put(3, "three"); + + map.clear(); + assertTrue(map.isEmpty()); + assertEquals(0, map.size()); + assertNull(map.get(1)); + } + + @Test + public void testIsEmpty() { + DynIntHashMap map = new DynIntHashMap(); + assertTrue(map.isEmpty()); + + map.put(1, "one"); + assertFalse(map.isEmpty()); + + map.remove(1); + assertTrue(map.isEmpty()); + } + + @Test + public void testKeysWithValue() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "value"); + map.put(2, "value"); + map.put(3, "other"); + + List keys = map.keysWithValue("value"); + assertEquals(2, keys.size()); + assertTrue(keys.contains(1)); + assertTrue(keys.contains(2)); + } + + @Test + public void testKeysWithValueNull() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, null); + map.put(2, null); + map.put(3, "value"); + + List keys = map.keysWithValue(null); + assertEquals(2, keys.size()); + assertTrue(keys.contains(1)); + assertTrue(keys.contains(2)); + } + + @Test + public void testKeysWithValueNoMatch() { + DynIntHashMap map = new DynIntHashMap(); + map.put(1, "one"); + map.put(2, "two"); + + List keys = map.keysWithValue("nonexistent"); + assertTrue(keys.isEmpty()); + } + + @Test + public void testRehashing() { + // Use small initial capacity to trigger rehash + DynIntHashMap map = new DynIntHashMap(2, 0.75f); + + // Add enough entries to trigger rehash + for (int i = 0; i < 100; i++) { + map.put(i, "value" + i); + } + + assertEquals(100, map.size()); + + // Verify all entries are still accessible + for (int i = 0; i < 100; i++) { + assertEquals("value" + i, map.get(i)); + } + } + + @Test + public void testNegativeKey() { + DynIntHashMap map = new DynIntHashMap(); + map.put(-1, "negative"); + assertEquals("negative", map.get(-1)); + assertTrue(map.containsKey(-1)); + } + + @Test + public void testZeroKey() { + DynIntHashMap map = new DynIntHashMap(); + map.put(0, "zero"); + assertEquals("zero", map.get(0)); + assertTrue(map.containsKey(0)); + } + + @Test + public void testMaxIntKey() { + DynIntHashMap map = new DynIntHashMap(); + map.put(Integer.MAX_VALUE, "max"); + assertEquals("max", map.get(Integer.MAX_VALUE)); + } + + @Test + public void testMinIntKey() { + DynIntHashMap map = new DynIntHashMap(); + map.put(Integer.MIN_VALUE, "min"); + assertEquals("min", map.get(Integer.MIN_VALUE)); + } +} diff --git a/DynmapCore/src/test/java/org/dynmap/utils/IpAddressMatcherTest.java b/DynmapCore/src/test/java/org/dynmap/utils/IpAddressMatcherTest.java new file mode 100644 index 00000000..ac30f728 --- /dev/null +++ b/DynmapCore/src/test/java/org/dynmap/utils/IpAddressMatcherTest.java @@ -0,0 +1,122 @@ +package org.dynmap.utils; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class IpAddressMatcherTest { + + // IPv4 exact match tests + @Test + public void testIPv4ExactMatch() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.100"); + assertTrue(matcher.matches("192.168.1.100")); + } + + @Test + public void testIPv4ExactMatchNoMatch() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.100"); + assertFalse(matcher.matches("192.168.1.101")); + } + + @Test + public void testIPv4Localhost() { + IpAddressMatcher matcher = new IpAddressMatcher("127.0.0.1"); + assertTrue(matcher.matches("127.0.0.1")); + assertFalse(matcher.matches("127.0.0.2")); + } + + // IPv4 CIDR tests + @Test + public void testIPv4Cidr24() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.0/24"); + assertTrue(matcher.matches("192.168.1.0")); + assertTrue(matcher.matches("192.168.1.1")); + assertTrue(matcher.matches("192.168.1.255")); + assertFalse(matcher.matches("192.168.2.1")); + } + + @Test + public void testIPv4Cidr16() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.0.0/16"); + assertTrue(matcher.matches("192.168.0.1")); + assertTrue(matcher.matches("192.168.255.255")); + assertFalse(matcher.matches("192.169.0.1")); + } + + @Test + public void testIPv4Cidr8() { + IpAddressMatcher matcher = new IpAddressMatcher("10.0.0.0/8"); + assertTrue(matcher.matches("10.0.0.1")); + assertTrue(matcher.matches("10.255.255.255")); + assertFalse(matcher.matches("11.0.0.1")); + } + + @Test + public void testIPv4Cidr32() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.100/32"); + assertTrue(matcher.matches("192.168.1.100")); + assertFalse(matcher.matches("192.168.1.101")); + } + + @Test + public void testIPv4CidrZero() { + IpAddressMatcher matcher = new IpAddressMatcher("0.0.0.0/0"); + assertTrue(matcher.matches("192.168.1.1")); + assertTrue(matcher.matches("10.0.0.1")); + assertTrue(matcher.matches("255.255.255.255")); + } + + // IPv6 tests + @Test + public void testIPv6ExactMatch() { + IpAddressMatcher matcher = new IpAddressMatcher("::1"); + assertTrue(matcher.matches("::1")); + assertTrue(matcher.matches("0:0:0:0:0:0:0:1")); + } + + @Test + public void testIPv6Cidr() { + IpAddressMatcher matcher = new IpAddressMatcher("2001:db8::/32"); + assertTrue(matcher.matches("2001:db8::1")); + assertTrue(matcher.matches("2001:db8:ffff:ffff:ffff:ffff:ffff:ffff")); + assertFalse(matcher.matches("2001:db9::1")); + } + + // Cross-family rejection tests + @Test + public void testIPv4DoesNotMatchIPv6() { + IpAddressMatcher matcher = new IpAddressMatcher("192.168.1.0/24"); + assertFalse(matcher.matches("::1")); + } + + @Test + public void testIPv6DoesNotMatchIPv4() { + IpAddressMatcher matcher = new IpAddressMatcher("::1"); + assertFalse(matcher.matches("127.0.0.1")); + } + + // Edge cases + @Test(expected = IllegalArgumentException.class) + public void testInvalidIPAddress() { + new IpAddressMatcher("invalid.ip.address"); + } + + @Test + public void testPrivateNetworkRanges() { + // Class A private + IpAddressMatcher classA = new IpAddressMatcher("10.0.0.0/8"); + assertTrue(classA.matches("10.0.0.1")); + assertTrue(classA.matches("10.255.255.254")); + + // Class B private + IpAddressMatcher classB = new IpAddressMatcher("172.16.0.0/12"); + assertTrue(classB.matches("172.16.0.1")); + assertTrue(classB.matches("172.31.255.254")); + assertFalse(classB.matches("172.32.0.1")); + + // Class C private + IpAddressMatcher classC = new IpAddressMatcher("192.168.0.0/16"); + assertTrue(classC.matches("192.168.0.1")); + assertTrue(classC.matches("192.168.255.254")); + } +} diff --git a/DynmapCore/src/test/java/org/dynmap/utils/Matrix3DTest.java b/DynmapCore/src/test/java/org/dynmap/utils/Matrix3DTest.java new file mode 100644 index 00000000..a61d3e2a --- /dev/null +++ b/DynmapCore/src/test/java/org/dynmap/utils/Matrix3DTest.java @@ -0,0 +1,196 @@ +package org.dynmap.utils; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class Matrix3DTest { + + private static final double DELTA = 1e-10; + + @Test + public void testIdentityConstructor() { + Matrix3D m = new Matrix3D(); + double[] v = {1.0, 2.0, 3.0}; + m.transform(v); + // Identity matrix should not change the vector + assertEquals(1.0, v[0], DELTA); + assertEquals(2.0, v[1], DELTA); + assertEquals(3.0, v[2], DELTA); + } + + @Test + public void testParameterizedConstructor() { + // Create a matrix that doubles each component + Matrix3D m = new Matrix3D(2, 0, 0, 0, 2, 0, 0, 0, 2); + double[] v = {1.0, 2.0, 3.0}; + m.transform(v); + assertEquals(2.0, v[0], DELTA); + assertEquals(4.0, v[1], DELTA); + assertEquals(6.0, v[2], DELTA); + } + + @Test + public void testTransformArray() { + Matrix3D m = new Matrix3D(1, 2, 3, 4, 5, 6, 7, 8, 9); + double[] v = {1.0, 1.0, 1.0}; + m.transform(v); + // Row 1: 1*1 + 2*1 + 3*1 = 6 + // Row 2: 4*1 + 5*1 + 6*1 = 15 + // Row 3: 7*1 + 8*1 + 9*1 = 24 + assertEquals(6.0, v[0], DELTA); + assertEquals(15.0, v[1], DELTA); + assertEquals(24.0, v[2], DELTA); + } + + @Test + public void testTransformVector3D() { + Matrix3D m = new Matrix3D(1, 2, 3, 4, 5, 6, 7, 8, 9); + Vector3D v = new Vector3D(1.0, 1.0, 1.0); + m.transform(v); + assertEquals(6.0, v.x, DELTA); + assertEquals(15.0, v.y, DELTA); + assertEquals(24.0, v.z, DELTA); + } + + @Test + public void testTransformVector3DWithOutput() { + Matrix3D m = new Matrix3D(1, 2, 3, 4, 5, 6, 7, 8, 9); + Vector3D v = new Vector3D(1.0, 1.0, 1.0); + Vector3D out = new Vector3D(); + m.transform(v, out); + // Input should be unchanged + assertEquals(1.0, v.x, DELTA); + assertEquals(1.0, v.y, DELTA); + assertEquals(1.0, v.z, DELTA); + // Output should have transformed values + assertEquals(6.0, out.x, DELTA); + assertEquals(15.0, out.y, DELTA); + assertEquals(24.0, out.z, DELTA); + } + + @Test + public void testScale() { + Matrix3D m = new Matrix3D(); + m.scale(2.0, 3.0, 4.0); + double[] v = {1.0, 1.0, 1.0}; + m.transform(v); + assertEquals(2.0, v[0], DELTA); + assertEquals(3.0, v[1], DELTA); + assertEquals(4.0, v[2], DELTA); + } + + @Test + public void testRotateXY90Degrees() { + Matrix3D m = new Matrix3D(); + m.rotateXY(90.0); + double[] v = {1.0, 0.0, 0.0}; + m.transform(v); + // X axis rotated 90 degrees clockwise around Z should point to Y + assertEquals(0.0, v[0], DELTA); + assertEquals(-1.0, v[1], DELTA); + assertEquals(0.0, v[2], DELTA); + } + + @Test + public void testRotateXZ90Degrees() { + Matrix3D m = new Matrix3D(); + m.rotateXZ(90.0); + double[] v = {1.0, 0.0, 0.0}; + m.transform(v); + // X axis rotated 90 degrees clockwise around Y should point to +Z + assertEquals(0.0, v[0], DELTA); + assertEquals(0.0, v[1], DELTA); + assertEquals(1.0, v[2], DELTA); + } + + @Test + public void testRotateYZ90Degrees() { + Matrix3D m = new Matrix3D(); + m.rotateYZ(90.0); + double[] v = {0.0, 1.0, 0.0}; + m.transform(v); + // Y axis rotated 90 degrees clockwise around X should point to Z + assertEquals(0.0, v[0], DELTA); + assertEquals(0.0, v[1], DELTA); + assertEquals(-1.0, v[2], DELTA); + } + + @Test + public void testRotate360DegreesReturnsToOriginal() { + Matrix3D m = new Matrix3D(); + m.rotateXY(360.0); + double[] v = {1.0, 2.0, 3.0}; + m.transform(v); + assertEquals(1.0, v[0], DELTA); + assertEquals(2.0, v[1], DELTA); + assertEquals(3.0, v[2], DELTA); + } + + @Test + public void testMultiply() { + // Create two scaling matrices + Matrix3D m1 = new Matrix3D(2, 0, 0, 0, 2, 0, 0, 0, 2); + Matrix3D m2 = new Matrix3D(3, 0, 0, 0, 3, 0, 0, 0, 3); + m1.multiply(m2); + double[] v = {1.0, 1.0, 1.0}; + m1.transform(v); + // Should scale by 6 (2*3) + assertEquals(6.0, v[0], DELTA); + assertEquals(6.0, v[1], DELTA); + assertEquals(6.0, v[2], DELTA); + } + + @Test + public void testShearZ() { + Matrix3D m = new Matrix3D(); + m.shearZ(1.0, 0.0); + // With shearZ matrix [1,0,0; 0,1,0; x_fact,y_fact,1] multiplied as mat*this + // The result matrix has x_fact in position m31 + // transform: v1 = m11*x + m12*y + m13*z = x + // v2 = m21*x + m22*y + m23*z = y + // v3 = m31*x + m32*y + m33*z = x_fact*x + y_fact*y + z + double[] v = {1.0, 0.0, 0.0}; + m.transform(v); + // X contributes to Z via shear + assertEquals(1.0, v[0], DELTA); + assertEquals(0.0, v[1], DELTA); + assertEquals(1.0, v[2], DELTA); + } + + @Test + public void testChainedTransformations() { + Matrix3D m = new Matrix3D(); + m.scale(2.0, 2.0, 2.0); + m.rotateXY(90.0); + double[] v = {1.0, 0.0, 0.0}; + m.transform(v); + // First scale by 2, then rotate - result should be scaled and rotated + assertEquals(0.0, v[0], DELTA); + assertEquals(-2.0, v[1], DELTA); + assertEquals(0.0, v[2], DELTA); + } + + @Test + public void testToString() { + Matrix3D m = new Matrix3D(); + String str = m.toString(); + assertNotNull(str); + assertTrue(str.contains("1.0")); + } + + @Test + public void testToJSON() { + Matrix3D m = new Matrix3D(1, 2, 3, 4, 5, 6, 7, 8, 9); + org.json.simple.JSONArray json = m.toJSON(); + assertEquals(9, json.size()); + assertEquals(1.0, json.get(0)); + assertEquals(2.0, json.get(1)); + assertEquals(3.0, json.get(2)); + assertEquals(4.0, json.get(3)); + assertEquals(5.0, json.get(4)); + assertEquals(6.0, json.get(5)); + assertEquals(7.0, json.get(6)); + assertEquals(8.0, json.get(7)); + assertEquals(9.0, json.get(8)); + } +} diff --git a/DynmapCore/src/test/java/org/dynmap/utils/Vector3DTest.java b/DynmapCore/src/test/java/org/dynmap/utils/Vector3DTest.java new file mode 100644 index 00000000..961ca38a --- /dev/null +++ b/DynmapCore/src/test/java/org/dynmap/utils/Vector3DTest.java @@ -0,0 +1,173 @@ +package org.dynmap.utils; + +import org.junit.Test; +import static org.junit.Assert.*; + +public class Vector3DTest { + + private static final double DELTA = 1e-10; + + @Test + public void testDefaultConstructor() { + Vector3D v = new Vector3D(); + assertEquals(0.0, v.x, DELTA); + assertEquals(0.0, v.y, DELTA); + assertEquals(0.0, v.z, DELTA); + } + + @Test + public void testParameterizedConstructor() { + Vector3D v = new Vector3D(1.0, 2.0, 3.0); + assertEquals(1.0, v.x, DELTA); + assertEquals(2.0, v.y, DELTA); + assertEquals(3.0, v.z, DELTA); + } + + @Test + public void testCopyConstructor() { + Vector3D original = new Vector3D(5.0, 6.0, 7.0); + Vector3D copy = new Vector3D(original); + assertEquals(original.x, copy.x, DELTA); + assertEquals(original.y, copy.y, DELTA); + assertEquals(original.z, copy.z, DELTA); + // Ensure it's a copy, not a reference + original.x = 100.0; + assertEquals(5.0, copy.x, DELTA); + } + + @Test + public void testSet() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(); + Vector3D result = v2.set(v1); + assertEquals(1.0, v2.x, DELTA); + assertEquals(2.0, v2.y, DELTA); + assertEquals(3.0, v2.z, DELTA); + assertSame(v2, result); + } + + @Test + public void testAdd() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(4.0, 5.0, 6.0); + Vector3D result = v1.add(v2); + assertEquals(5.0, v1.x, DELTA); + assertEquals(7.0, v1.y, DELTA); + assertEquals(9.0, v1.z, DELTA); + assertSame(v1, result); + } + + @Test + public void testSubtract() { + Vector3D v1 = new Vector3D(5.0, 7.0, 9.0); + Vector3D v2 = new Vector3D(1.0, 2.0, 3.0); + Vector3D result = v1.subtract(v2); + assertEquals(4.0, v1.x, DELTA); + assertEquals(5.0, v1.y, DELTA); + assertEquals(6.0, v1.z, DELTA); + assertSame(v1, result); + } + + @Test + public void testScale() { + Vector3D v = new Vector3D(2.0, 3.0, 4.0); + Vector3D result = v.scale(2.0); + assertEquals(4.0, v.x, DELTA); + assertEquals(6.0, v.y, DELTA); + assertEquals(8.0, v.z, DELTA); + assertSame(v, result); + } + + @Test + public void testInnerProduct() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(4.0, 5.0, 6.0); + double result = v1.innerProduct(v2); + // 1*4 + 2*5 + 3*6 = 4 + 10 + 18 = 32 + assertEquals(32.0, result, DELTA); + } + + @Test + public void testCrossProduct() { + Vector3D v1 = new Vector3D(1.0, 0.0, 0.0); + Vector3D v2 = new Vector3D(0.0, 1.0, 0.0); + Vector3D result = v1.crossProduct(v2); + // i x j = k + assertEquals(0.0, v1.x, DELTA); + assertEquals(0.0, v1.y, DELTA); + assertEquals(1.0, v1.z, DELTA); + assertSame(v1, result); + } + + @Test + public void testCrossProductGeneral() { + Vector3D v1 = new Vector3D(2.0, 3.0, 4.0); + Vector3D v2 = new Vector3D(5.0, 6.0, 7.0); + v1.crossProduct(v2); + // (3*7 - 4*6, 4*5 - 2*7, 2*6 - 3*5) = (21-24, 20-14, 12-15) = (-3, 6, -3) + assertEquals(-3.0, v1.x, DELTA); + assertEquals(6.0, v1.y, DELTA); + assertEquals(-3.0, v1.z, DELTA); + } + + @Test + public void testLength() { + Vector3D v = new Vector3D(3.0, 4.0, 0.0); + assertEquals(5.0, v.length(), DELTA); + } + + @Test + public void testLengthUnit() { + Vector3D v = new Vector3D(1.0, 0.0, 0.0); + assertEquals(1.0, v.length(), DELTA); + } + + @Test + public void testLengthZero() { + Vector3D v = new Vector3D(); + assertEquals(0.0, v.length(), DELTA); + } + + @Test + public void testEquals() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(1.0, 2.0, 3.0); + assertTrue(v1.equals(v2)); + assertTrue(v2.equals(v1)); + } + + @Test + public void testEqualsNotEqual() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(1.0, 2.0, 4.0); + assertFalse(v1.equals(v2)); + } + + @Test + public void testEqualsNull() { + Vector3D v = new Vector3D(1.0, 2.0, 3.0); + assertFalse(v.equals(null)); + } + + @Test + public void testEqualsWrongType() { + Vector3D v = new Vector3D(1.0, 2.0, 3.0); + assertFalse(v.equals("not a vector")); + } + + @Test + public void testHashCodeConsistency() { + Vector3D v1 = new Vector3D(1.0, 2.0, 3.0); + Vector3D v2 = new Vector3D(1.0, 2.0, 3.0); + assertEquals(v1.hashCode(), v2.hashCode()); + } + + @Test + public void testToString() { + Vector3D v = new Vector3D(1.0, 2.0, 3.0); + String str = v.toString(); + assertTrue(str.contains("1.0")); + assertTrue(str.contains("2.0")); + assertTrue(str.contains("3.0")); + } +}