Skip to content

Commit 8d81ec4

Browse files
dmealingclaude
andcommitted
test(java render): verify-conformance corpus runner — 31/31 green
Adds the Java verify-conformance runner under metaobjects-render. Closes the last cross-port runner gap: TS / C# / Python already exercise the corpus, Java did not. VerifyConformanceTest is a 1:1 port of server/csharp/MetaObjects.Render.Tests/VerifyConformanceTests.cs. For each fixture dir under fixtures/verify-conformance/ it reads: - payload.json (declared field shape, parsed to PayloadField[]) - template.mustache (the template text under drift check) - partials/*.mustache (optional partial bodies) - options.json (optional requiredSlots / requiredTags / provider toggle) - expected-drift.json (compared as a (code,path) sorted multiset) Provider mode defaults to "with" iff a partials/ dir exists; "without" returns no provider; an explicit "with" + missing partials/ models the unresolved-partial case. Determinism check at end (running Verify a second time must return the same list). Lints expected codes against the known Verify.ERR_* set so a typo in a fixture's expected-drift.json fails the lint before the assertion runs. Java cross-port runner coverage: fixtures/conformance → 85/85 ✓ (metadata module) fixtures/yaml-conformance → 6/6 ✓ (metadata module) fixtures/persistence-conformance → 12/12 ✓ (integration-tests module) fixtures/render-conformance → 4/4 ✓ (render module — already shipped) fixtures/verify-conformance → 31/31 ✓ (render module — this commit) Java now runs every cross-port conformance corpus. Full reactor green. Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
1 parent 5f17bd5 commit 8d81ec4

1 file changed

Lines changed: 199 additions & 0 deletions

File tree

Lines changed: 199 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,199 @@
1+
package com.metaobjects.render;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import org.junit.Test;
6+
import org.junit.runner.RunWith;
7+
import org.junit.runners.Parameterized;
8+
9+
import java.io.IOException;
10+
import java.nio.charset.StandardCharsets;
11+
import java.nio.file.Files;
12+
import java.nio.file.Path;
13+
import java.nio.file.Paths;
14+
import java.util.ArrayList;
15+
import java.util.Comparator;
16+
import java.util.HashMap;
17+
import java.util.HashSet;
18+
import java.util.LinkedHashMap;
19+
import java.util.List;
20+
import java.util.Map;
21+
import java.util.Set;
22+
import java.util.stream.Collectors;
23+
import java.util.stream.Stream;
24+
25+
import static org.junit.Assert.assertEquals;
26+
import static org.junit.Assert.assertTrue;
27+
28+
/**
29+
* Verify-conformance corpus runner — the cross-language template-drift
30+
* guarantee. Each fixture dir under {@code fixtures/verify-conformance/} holds:
31+
*
32+
* <ul>
33+
* <li>{@code payload.json} the payload FIELD-TREE
34+
* ({@code PayloadField[]} — declared shape, not render data)</li>
35+
* <li>{@code template.mustache} the template text whose vars are drift-checked</li>
36+
* <li>{@code partials/*.mustache} optional partial bodies, referenced as
37+
* {@code partials/<name>}</li>
38+
* <li>{@code options.json} optional
39+
* {@code { "requiredSlots"?, "requiredTags"?, "provider"?: "with" | "without" }}</li>
40+
* <li>{@code expected-drift.json} array of {@code { "code", "path" }} findings
41+
* (compared as a sorted multiset)</li>
42+
* </ul>
43+
*
44+
* <p>The same (payload-tree + template + options) MUST yield the same drift set
45+
* in TS, C#, and Java. 1:1 port of
46+
* {@code server/csharp/MetaObjects.Render.Tests/VerifyConformanceTests.cs} and
47+
* {@code server/typescript/packages/render/test/verify-conformance.test.ts}.
48+
*/
49+
@RunWith(Parameterized.class)
50+
public class VerifyConformanceTest {
51+
52+
private static final Path CORPUS_ROOT;
53+
private static final ObjectMapper JSON = new ObjectMapper();
54+
55+
/**
56+
* Stable verify codes a fixture may legitimately expect (typo guard).
57+
* Cross-port: documented in {@code fixtures/conformance/ERROR-CODES.json}.
58+
*/
59+
private static final Set<String> VERIFY_CODES = Set.of(
60+
Verify.ERR_VAR_NOT_ON_PAYLOAD,
61+
Verify.ERR_PARTIAL_UNRESOLVED,
62+
Verify.ERR_REQUIRED_SLOT_UNUSED,
63+
Verify.ERR_OUTPUT_TAG_MISSING
64+
);
65+
66+
static {
67+
Path p = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
68+
while (p != null && !Files.exists(p.resolve("fixtures/verify-conformance"))) {
69+
p = p.getParent();
70+
}
71+
CORPUS_ROOT = p == null ? null : p.resolve("fixtures/verify-conformance");
72+
}
73+
74+
@Parameterized.Parameters(name = "{0}")
75+
public static List<Object[]> fixtures() throws IOException {
76+
if (CORPUS_ROOT == null || !Files.isDirectory(CORPUS_ROOT)) return List.of();
77+
try (Stream<Path> s = Files.list(CORPUS_ROOT)) {
78+
return s.filter(Files::isDirectory)
79+
.sorted()
80+
.map(p -> new Object[]{p.getFileName().toString(), p})
81+
.collect(Collectors.toList());
82+
}
83+
}
84+
85+
private final String name;
86+
private final Path fixtureDir;
87+
88+
public VerifyConformanceTest(String name, Path fixtureDir) {
89+
this.name = name;
90+
this.fixtureDir = fixtureDir;
91+
}
92+
93+
@Test
94+
public void verifyMatchesExpected() throws IOException {
95+
Path templatePath = fixtureDir.resolve("template.mustache");
96+
Path payloadPath = fixtureDir.resolve("payload.json");
97+
Path expectedPath = fixtureDir.resolve("expected-drift.json");
98+
Path optionsPath = fixtureDir.resolve("options.json");
99+
Path partialsDir = fixtureDir.resolve("partials");
100+
101+
assertTrue("missing template: " + templatePath, Files.isRegularFile(templatePath));
102+
assertTrue("missing payload: " + payloadPath, Files.isRegularFile(payloadPath));
103+
assertTrue("missing expected-drift: " + expectedPath, Files.isRegularFile(expectedPath));
104+
105+
String template = Files.readString(templatePath, StandardCharsets.UTF_8);
106+
List<PayloadField> fields = parseFields(JSON.readTree(payloadPath.toFile()));
107+
108+
// options.json (optional): requiredSlots + requiredTags + provider toggle.
109+
List<String> requiredSlots = null;
110+
List<String> requiredTags = null;
111+
String providerMode = null;
112+
if (Files.isRegularFile(optionsPath)) {
113+
JsonNode opts = JSON.readTree(optionsPath.toFile());
114+
if (opts.has("requiredSlots") && opts.get("requiredSlots").isArray()) {
115+
requiredSlots = stringArray(opts.get("requiredSlots"));
116+
}
117+
if (opts.has("requiredTags") && opts.get("requiredTags").isArray()) {
118+
requiredTags = stringArray(opts.get("requiredTags"));
119+
}
120+
if (opts.has("provider") && opts.get("provider").isTextual()) {
121+
providerMode = opts.get("provider").asText();
122+
}
123+
}
124+
125+
// Provider: "with" → build from partials/ (empty map if none); "without" → none.
126+
// Default: "with" iff a partials/ dir exists. The unresolved-partial case
127+
// sets provider:"with" with no partials/ dir (an empty provider resolves nothing).
128+
String mode = providerMode != null
129+
? providerMode
130+
: (Files.isDirectory(partialsDir) ? "with" : "without");
131+
Provider provider = "with".equals(mode) ? providerFromPartials(partialsDir) : null;
132+
133+
List<VerifyError> expected = parseExpected(JSON.readTree(expectedPath.toFile()));
134+
// Lint: every expected code is a known verify code (typo guard).
135+
for (VerifyError e : expected) {
136+
assertTrue("unknown verify code in expected-drift.json: " + e.code(),
137+
VERIFY_CODES.contains(e.code()));
138+
}
139+
140+
VerifyOptions options = new VerifyOptions(provider, requiredSlots, requiredTags);
141+
List<VerifyError> actual = Verify.check(template, fields, options);
142+
143+
assertEquals(name + ": drift mismatch", sorted(expected), sorted(actual));
144+
// Determinism: identical across runs.
145+
assertEquals(name + ": non-deterministic",
146+
actual, Verify.check(template, fields, options));
147+
}
148+
149+
// ----- helpers -----
150+
151+
private static List<PayloadField> parseFields(JsonNode array) {
152+
List<PayloadField> out = new ArrayList<>();
153+
for (JsonNode el : array) out.add(parseField(el));
154+
return out;
155+
}
156+
157+
private static PayloadField parseField(JsonNode el) {
158+
String name = el.get("name").asText();
159+
if (el.has("fields") && el.get("fields").isArray()) {
160+
return new PayloadField(name, parseFields(el.get("fields")));
161+
}
162+
return PayloadField.scalar(name);
163+
}
164+
165+
private static List<String> stringArray(JsonNode array) {
166+
List<String> out = new ArrayList<>();
167+
for (JsonNode el : array) out.add(el.asText());
168+
return out;
169+
}
170+
171+
private static Provider providerFromPartials(Path partialsDir) throws IOException {
172+
Map<String, String> map = new LinkedHashMap<>();
173+
if (Files.isDirectory(partialsDir)) {
174+
try (Stream<Path> s = Files.list(partialsDir)) {
175+
for (Path f : (Iterable<Path>) s.filter(Files::isRegularFile)
176+
.filter(p -> p.getFileName().toString().endsWith(".mustache"))::iterator) {
177+
String name = f.getFileName().toString().replaceFirst("\\.mustache$", "");
178+
map.put("partials/" + name, Files.readString(f, StandardCharsets.UTF_8));
179+
}
180+
}
181+
}
182+
return new InMemoryProvider(map);
183+
}
184+
185+
private static List<VerifyError> parseExpected(JsonNode array) {
186+
List<VerifyError> out = new ArrayList<>();
187+
for (JsonNode el : array) {
188+
out.add(new VerifyError(el.get("code").asText(), el.get("path").asText()));
189+
}
190+
return out;
191+
}
192+
193+
/** (code, path) ordinal sort for multiset equality. */
194+
private static List<VerifyError> sorted(List<VerifyError> errs) {
195+
List<VerifyError> copy = new ArrayList<>(errs);
196+
copy.sort(Comparator.comparing(VerifyError::code).thenComparing(VerifyError::path));
197+
return copy;
198+
}
199+
}

0 commit comments

Comments
 (0)