From 53cba762b49b0fe57b79db9126014a7861fc6ffd Mon Sep 17 00:00:00 2001 From: Octoturge Date: Sat, 8 Aug 2026 16:39:56 +0200 Subject: [PATCH] Add standalone test coverage for common/'s MiniJson MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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. --- common/README.md | 17 +++ common/run-tests.sh | 21 +++ .../mcmapper/common/json/MiniJsonTest.java | 142 ++++++++++++++++++ 3 files changed, 180 insertions(+) create mode 100644 common/run-tests.sh create mode 100644 common/src/test/java/com/octoturge/mcmapper/common/json/MiniJsonTest.java diff --git a/common/README.md b/common/README.md index 93b4194..937b5e0 100644 --- a/common/README.md +++ b/common/README.md @@ -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. - `config/` — the common config model (backend URL, server token, tracking/reconciliation 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 + +``` +./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). diff --git a/common/run-tests.sh b/common/run-tests.sh new file mode 100644 index 0000000..61c0608 --- /dev/null +++ b/common/run-tests.sh @@ -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 diff --git a/common/src/test/java/com/octoturge/mcmapper/common/json/MiniJsonTest.java b/common/src/test/java/com/octoturge/mcmapper/common/json/MiniJsonTest.java new file mode 100644 index 0000000..cddd7ab --- /dev/null +++ b/common/src/test/java/com/octoturge/mcmapper/common/json/MiniJsonTest.java @@ -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 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 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 column = new LinkedHashMap<>(); + column.put("x", 1.0); + column.put("z", 2.0); + List columns = new ArrayList<>(); + columns.add(column); + Map 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 obj = (Map) 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 obj = (Map) MiniJson.parse(json); + List columns = (List) obj.get("columns"); + assertEquals(2, columns.size()); + Map first = (Map) columns.get(0); + assertEquals(1.0, first.get("x")); + assertEquals(64.0, first.get("height")); + } + + @SuppressWarnings("unchecked") + private static void parseUnescapesStrings() { + Map obj = (Map) MiniJson.parse("{\"error\":\"bad \\\"token\\\"\\nvalue\"}"); + assertEquals("bad \"token\"\nvalue", obj.get("error")); + } + + @SuppressWarnings("unchecked") + private static void parsesNumbers() { + Map obj = (Map) 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 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 columns = new ArrayList<>(); + columns.add(col); + Map msg = new LinkedHashMap<>(); + msg.put("type", "columns"); + msg.put("dimension", 0.0); + msg.put("columns", columns); + + String json = MiniJson.writeObject(msg); + Map reparsed = (Map) MiniJson.parse(json); + assertEquals("columns", reparsed.get("type")); + assertEquals(0.0, reparsed.get("dimension")); + List reparsedColumns = (List) reparsed.get("columns"); + Map reparsedCol = (Map) 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 + ">"); + } + } +}