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.
This commit is contained in:
@@ -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
|
||||||
|
|
||||||
|
```
|
||||||
|
./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).
|
||||||
|
|||||||
@@ -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 + ">");
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user