Compare commits

..

2 Commits

Author SHA1 Message Date
octoturge ab94cca1b2 Document common test runner as 'bash run-tests.sh' (executable bit doesn't survive this filesystem's git add) 2026-08-08 16:40:33 +02:00
octoturge 53cba762b4 Add standalone test coverage for common/'s MiniJson
Requested after MCMapper-Backend's Phase 2: development follows TDD
(test-first) from here on — this retrofits the piece already built
(MiniJson, the hand-rolled JSON codec) before that request landed.

No JUnit/Gradle involved, matching common/'s "must compile standalone
under plain javac" constraint (see README.md) — a hand-rolled assertion
runner in src/test/java, compiled and run via the new run-tests.sh.
Covers write escaping, nested object/array encoding, parsing (including
the exact columns-message shape DefaultBackendConnection actually
builds), and a write-then-parse round-trip.

Deliberately not wired into either leaf's build.gradle sourceSets (which
only pull in src/main/java) — verified both forge-1_12_2 and forge-1_7_10
still compile with src/test/ present, confirming test code doesn't leak
into the shipped mod jar.
2026-08-08 16:39:56 +02:00
3 changed files with 180 additions and 0 deletions
+17
View File
@@ -17,3 +17,20 @@ Minecraft/Forge/NeoForge API usage — it must compile standalone under plain `j
request/response). Mirrors the WS protocol described in the root plan. request/response). Mirrors the WS protocol described in the root plan.
- `config/` — the common config model (backend URL, server token, tracking/reconciliation - `config/` — the common config model (backend URL, server token, tracking/reconciliation
intervals) each leaf loads via its own loader-specific config system. intervals) each leaf loads via its own loader-specific config system.
- `json/`, `ws/` — the hand-rolled JSON codec and RFC 6455 WS client `DefaultBackendConnection`
is built on (no third-party dependency, for the same "no shading through legacy ForgeGradle"
reason this module stays dependency-free generally).
## Testing
```
bash run-tests.sh
```
Compiles `src/main/java` + `src/test/java` and runs every `*Test.java` class's `main()`. No
JUnit/Gradle — same "must compile standalone under plain `javac`" constraint as the module
itself, and test code never ships in the mod jar so it doesn't need to match either leaf's JDK 8
target. Anything needing a live Forge/Minecraft world (event hook wiring, world reads) isn't
unit-testable this way — those stay integration-tested against a real running
MCMapper-Backend instance instead (see the Phase 1/2 commit messages for how that's been done
so far).
+21
View File
@@ -0,0 +1,21 @@
#!/usr/bin/env bash
# Compiles and runs common/'s standalone test classes (no JUnit, no Gradle — see README.md for
# why: common/ must compile under plain javac, independent of either leaf's ForgeGradle
# toolchain, and test code never ships in the mod jar so it doesn't need to match the leaves'
# JDK 8 target either). Any JDK on PATH works.
set -euo pipefail
cd "$(dirname "$0")"
OUT=$(mktemp -d)
trap 'rm -rf "$OUT"' EXIT
javac -d "$OUT" $(find src/main/java src/test/java -name '*.java')
status=0
for class in $(find src/test/java -name '*Test.java' | sed 's#src/test/java/##; s#\.java$##; s#/#.#g'); do
echo "== $class =="
java -cp "$OUT" "$class" || status=1
echo
done
exit $status
@@ -0,0 +1,142 @@
package com.octoturge.mcmapper.common.json;
import java.util.ArrayList;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
/**
* Hand-rolled test runner (no JUnit) for {@link MiniJson}, following the same
* "common/ must compile and run standalone under plain javac" constraint as the class under
* test — see common/README.md for why, and common/run-tests.sh to actually run this.
*/
public class MiniJsonTest {
private static int passed = 0;
private static int failed = 0;
public static void main(String[] args) {
test("writeObject encodes strings, numbers, booleans, and null", MiniJsonTest::writesScalars);
test("writeObject escapes special characters in strings", MiniJsonTest::escapesStrings);
test("writeObject encodes nested objects and arrays", MiniJsonTest::writesNestedStructures);
test("parse reads a hello_ack-shaped object", MiniJsonTest::parsesHelloAck);
test("parse reads nested arrays of objects (a columns message)", MiniJsonTest::parsesColumnsMessage);
test("parse unescapes strings", MiniJsonTest::parseUnescapesStrings);
test("parse reads numbers as Double, including negatives", MiniJsonTest::parsesNumbers);
test("write-then-parse round-trips a columns-shaped message", MiniJsonTest::roundTripsColumnsMessage);
System.out.println();
System.out.println(passed + " passed, " + failed + " failed");
if (failed > 0) {
System.exit(1);
}
}
private static void writesScalars() {
Map<String, Object> obj = new LinkedHashMap<>();
obj.put("type", "hello");
obj.put("ok", true);
obj.put("count", 3.0);
obj.put("missing", null);
assertEquals("{\"type\":\"hello\",\"ok\":true,\"count\":3.0,\"missing\":null}", MiniJson.writeObject(obj));
}
private static void escapesStrings() {
Map<String, Object> obj = new LinkedHashMap<>();
obj.put("text", "line1\nline2\t\"quoted\"\\backslash");
String json = MiniJson.writeObject(obj);
assertEquals("{\"text\":\"line1\\nline2\\t\\\"quoted\\\"\\\\backslash\"}", json);
}
private static void writesNestedStructures() {
Map<String, Object> column = new LinkedHashMap<>();
column.put("x", 1.0);
column.put("z", 2.0);
List<Object> columns = new ArrayList<>();
columns.add(column);
Map<String, Object> msg = new LinkedHashMap<>();
msg.put("type", "columns");
msg.put("columns", columns);
assertEquals("{\"type\":\"columns\",\"columns\":[{\"x\":1.0,\"z\":2.0}]}", MiniJson.writeObject(msg));
}
@SuppressWarnings("unchecked")
private static void parsesHelloAck() {
Object parsed = MiniJson.parse("{\"type\":\"hello_ack\",\"ok\":true,\"serverId\":\"abc-123\"}");
Map<String, Object> obj = (Map<String, Object>) parsed;
assertEquals("hello_ack", obj.get("type"));
assertEquals(Boolean.TRUE, obj.get("ok"));
assertEquals("abc-123", obj.get("serverId"));
}
@SuppressWarnings("unchecked")
private static void parsesColumnsMessage() {
String json = "{\"type\":\"columns\",\"dimension\":0,\"columns\":["
+ "{\"x\":1,\"z\":2,\"height\":64,\"blockId\":2,\"blockMeta\":0},"
+ "{\"x\":3,\"z\":4,\"height\":65,\"blockId\":3,\"blockMeta\":0}]}";
Map<String, Object> obj = (Map<String, Object>) MiniJson.parse(json);
List<Object> columns = (List<Object>) obj.get("columns");
assertEquals(2, columns.size());
Map<String, Object> first = (Map<String, Object>) columns.get(0);
assertEquals(1.0, first.get("x"));
assertEquals(64.0, first.get("height"));
}
@SuppressWarnings("unchecked")
private static void parseUnescapesStrings() {
Map<String, Object> obj = (Map<String, Object>) MiniJson.parse("{\"error\":\"bad \\\"token\\\"\\nvalue\"}");
assertEquals("bad \"token\"\nvalue", obj.get("error"));
}
@SuppressWarnings("unchecked")
private static void parsesNumbers() {
Map<String, Object> obj = (Map<String, Object>) MiniJson.parse("{\"a\":-5,\"b\":3.5,\"c\":0}");
assertEquals(-5.0, obj.get("a"));
assertEquals(3.5, obj.get("b"));
assertEquals(0.0, obj.get("c"));
}
@SuppressWarnings("unchecked")
private static void roundTripsColumnsMessage() {
// Mirrors exactly what DefaultBackendConnection.sendDeltas() builds — the real
// regression risk isn't MiniJson in isolation, it's this shape drifting silently.
Map<String, Object> col = new LinkedHashMap<>();
col.put("x", 7.0);
col.put("z", -3.0);
col.put("height", 70.0);
col.put("blockId", 2.0);
col.put("blockMeta", 0.0);
List<Object> columns = new ArrayList<>();
columns.add(col);
Map<String, Object> msg = new LinkedHashMap<>();
msg.put("type", "columns");
msg.put("dimension", 0.0);
msg.put("columns", columns);
String json = MiniJson.writeObject(msg);
Map<String, Object> reparsed = (Map<String, Object>) MiniJson.parse(json);
assertEquals("columns", reparsed.get("type"));
assertEquals(0.0, reparsed.get("dimension"));
List<Object> reparsedColumns = (List<Object>) reparsed.get("columns");
Map<String, Object> reparsedCol = (Map<String, Object>) reparsedColumns.get(0);
assertEquals(7.0, reparsedCol.get("x"));
assertEquals(-3.0, reparsedCol.get("z"));
assertEquals(2.0, reparsedCol.get("blockId"));
}
private static void test(String name, Runnable body) {
try {
body.run();
passed++;
System.out.println("PASS " + name);
} catch (AssertionError e) {
failed++;
System.out.println("FAIL " + name + "" + e.getMessage());
}
}
private static void assertEquals(Object expected, Object actual) {
if (expected == null ? actual != null : !expected.equals(actual)) {
throw new AssertionError("expected <" + expected + "> but got <" + actual + ">");
}
}
}