Skip to content

Commit d101e40

Browse files
dmealingclaude
andcommitted
test(java-conformance): TemplateGenerator cross-port byte-equivalence harness
3/3 fixtures pass byte-equivalently against the shared Plan-0 corpus. Cross-port Mustache equivalence now proven across TS/Python/C#/Java reference factories — the same template + same payload renders to the same bytes in every port. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
1 parent c385baf commit d101e40

1 file changed

Lines changed: 141 additions & 0 deletions

File tree

Lines changed: 141 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,141 @@
1+
package com.metaobjects.render.templategen;
2+
3+
import com.fasterxml.jackson.databind.JsonNode;
4+
import com.fasterxml.jackson.databind.ObjectMapper;
5+
import com.metaobjects.render.InMemoryProvider;
6+
import com.metaobjects.render.Provider;
7+
import org.junit.Test;
8+
import org.junit.runner.RunWith;
9+
import org.junit.runners.Parameterized;
10+
11+
import java.io.IOException;
12+
import java.nio.file.Files;
13+
import java.nio.file.Path;
14+
import java.nio.file.Paths;
15+
import java.util.*;
16+
import java.util.stream.Collectors;
17+
import java.util.stream.Stream;
18+
19+
import static org.junit.Assert.assertEquals;
20+
import static org.junit.Assert.assertTrue;
21+
22+
/**
23+
* Cross-port byte-equivalence harness for the Java {@link TemplateGenerator}.
24+
*
25+
* <p>Mirrors the TS reference adapter:
26+
* {@code server/typescript/packages/codegen-ts/test/conformance/template-generator-conformance.test.ts}
27+
*
28+
* <p>Fixture format: {@code fixtures/render-conformance/template-generator/README.md}
29+
*
30+
* <p>The "root" passed to the walk callback here is the entity-name list from
31+
* {@code meta.json} — keeps the render module independent of the metadata
32+
* module. Real adopters typically pass a {@code MetaRoot}; the factory's
33+
* generic root type accommodates both.
34+
*/
35+
@RunWith(Parameterized.class)
36+
public class TemplateGeneratorConformanceTest {
37+
38+
private static final ObjectMapper JSON = new ObjectMapper();
39+
private static final Path FIXTURES_DIR;
40+
41+
static {
42+
Path p = Paths.get(System.getProperty("user.dir")).toAbsolutePath();
43+
while (p != null && !Files.exists(p.resolve("fixtures/render-conformance/template-generator"))) {
44+
p = p.getParent();
45+
}
46+
FIXTURES_DIR = p == null ? null : p.resolve("fixtures/render-conformance/template-generator");
47+
}
48+
49+
@Parameterized.Parameters(name = "{0}")
50+
public static List<Object[]> fixtures() throws IOException {
51+
if (FIXTURES_DIR == null || !Files.isDirectory(FIXTURES_DIR)) return List.of();
52+
try (Stream<Path> s = Files.list(FIXTURES_DIR)) {
53+
return s.filter(Files::isDirectory)
54+
.filter(p -> p.getFileName().toString().startsWith("fixture-"))
55+
.sorted()
56+
.map(p -> new Object[]{p.getFileName().toString(), p})
57+
.collect(Collectors.toList());
58+
}
59+
}
60+
61+
private final String name;
62+
private final Path fixtureDir;
63+
64+
public TemplateGeneratorConformanceTest(String name, Path fixtureDir) {
65+
this.name = name;
66+
this.fixtureDir = fixtureDir;
67+
}
68+
69+
private static Object jsonToPayload(JsonNode n) {
70+
if (n.isObject()) {
71+
Map<String, Object> m = new LinkedHashMap<>();
72+
n.fields().forEachRemaining(e -> m.put(e.getKey(), jsonToPayload(e.getValue())));
73+
return m;
74+
}
75+
if (n.isArray()) {
76+
List<Object> l = new ArrayList<>();
77+
for (JsonNode c : n) l.add(jsonToPayload(c));
78+
return l;
79+
}
80+
if (n.isTextual()) return n.asText();
81+
if (n.isIntegralNumber()) return n.asLong();
82+
if (n.isFloatingPointNumber()) return n.asDouble();
83+
if (n.isBoolean()) return n.asBoolean();
84+
if (n.isNull()) return null;
85+
throw new IllegalArgumentException("Unhandled JsonNode kind: " + n.getNodeType());
86+
}
87+
88+
@Test
89+
public void conformance() throws IOException {
90+
JsonNode meta = JSON.readTree(fixtureDir.resolve("meta.json").toFile());
91+
String templateBody = Files.readString(fixtureDir.resolve("template.mustache"));
92+
JsonNode walkJson = JSON.readTree(fixtureDir.resolve("walk.json").toFile());
93+
94+
List<TemplateWalkResult> walkEntries = new ArrayList<>();
95+
Set<String> referencedEntities = new HashSet<>();
96+
for (JsonNode w : walkJson) {
97+
walkEntries.add(new TemplateWalkResult(
98+
jsonToPayload(w.get("data")),
99+
w.get("outputPath").asText()));
100+
if (w.has("entity") && !w.get("entity").isNull()) {
101+
referencedEntities.add(w.get("entity").asText());
102+
}
103+
}
104+
105+
// "Root" for the walk callback: the set of entity names from meta.json.
106+
// Validates referenced-entity sanity without dragging in MetaRoot.
107+
Set<String> entityNames = new HashSet<>();
108+
for (JsonNode e : meta.get("entities")) {
109+
entityNames.add(e.get("name").asText());
110+
}
111+
for (String ref : referencedEntities) {
112+
assertTrue("walk.json references unknown entity: " + ref, entityNames.contains(ref));
113+
}
114+
115+
Provider provider = new InMemoryProvider(Map.of("conformance/template", templateBody));
116+
List<EmittedFile> files = TemplateGenerator.generate(
117+
name,
118+
"conformance/template",
119+
(Set<String> root) -> walkEntries,
120+
provider,
121+
meta.get("format").asText(),
122+
entityNames);
123+
124+
List<String> emittedPaths = files.stream().map(EmittedFile::path).sorted().toList();
125+
List<String> expectedPaths = walkEntries.stream().map(TemplateWalkResult::outputPath).sorted().toList();
126+
assertEquals(expectedPaths, emittedPaths);
127+
128+
Path expectedDir = fixtureDir.resolve("expected");
129+
for (TemplateWalkResult w : walkEntries) {
130+
Path expectedFile = expectedDir.resolve(w.outputPath());
131+
assertTrue("missing expected/" + w.outputPath(), Files.exists(expectedFile));
132+
String expected = Files.readString(expectedFile);
133+
String actual = files.stream()
134+
.filter(f -> f.path().equals(w.outputPath()))
135+
.findFirst().orElseThrow().content();
136+
assertEquals(
137+
"byte-equivalence failure in " + name + ": " + w.outputPath(),
138+
expected, actual);
139+
}
140+
}
141+
}

0 commit comments

Comments
 (0)