Skip to content

Commit 118886f

Browse files
committed
test(codegen-kotlin): KotlinCodegenSnapshotTest + 2 fixtures
1 parent 81b8028 commit 118886f

5 files changed

Lines changed: 156 additions & 0 deletions

File tree

Lines changed: 117 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,117 @@
1+
package com.metaobjects.generator.kotlin
2+
3+
import com.metaobjects.metadata.ktx.loadString
4+
import org.junit.jupiter.api.DynamicTest
5+
import org.junit.jupiter.api.TestFactory
6+
import java.nio.file.Files
7+
import java.nio.file.Path
8+
import java.nio.file.Paths
9+
import kotlin.io.path.absolutePathString
10+
import kotlin.io.path.isDirectory
11+
import kotlin.io.path.isRegularFile
12+
import kotlin.io.path.readText
13+
import kotlin.test.fail
14+
15+
/**
16+
* Parameterized snapshot test — for each fixture under src/test/resources/fixtures/,
17+
* runs the configured generators and compares each emitted .kt against the snapshot
18+
* at src/test/resources/snapshots/<fixture-name>/<filename>.
19+
*
20+
* <p>First run creates snapshots and fails with "review + commit". Subsequent runs gate.
21+
*/
22+
class KotlinCodegenSnapshotTest {
23+
24+
private val fixturesRoot: Path = Paths.get("src/test/resources/fixtures").toAbsolutePath()
25+
private val snapshotsRoot: Path = Paths.get("src/test/resources/snapshots").toAbsolutePath()
26+
27+
@TestFactory
28+
fun snapshotTests(): List<DynamicTest> {
29+
if (!fixturesRoot.isDirectory()) return emptyList()
30+
return Files.list(fixturesRoot).filter { it.isDirectory() }
31+
.sorted()
32+
.map { fixtureDir ->
33+
DynamicTest.dynamicTest("snapshot:${fixtureDir.fileName}") {
34+
runOne(fixtureDir)
35+
}
36+
}
37+
.toList()
38+
}
39+
40+
private fun runOne(fixtureDir: Path) {
41+
val name = fixtureDir.fileName.toString()
42+
val metaText = fixtureDir.resolve("meta.json").readText()
43+
val configText = fixtureDir.resolve("config.json").readText()
44+
val config = parseConfig(configText)
45+
46+
val outDir = Files.createTempDirectory("snap-$name-")
47+
try {
48+
val loader = loadString(name, metaText)
49+
for (g in config.generators) {
50+
val gen = when (g) {
51+
"entity" -> KotlinEntityGenerator()
52+
"table" -> KotlinExposedTableGenerator()
53+
"payload" -> KotlinPayloadGenerator()
54+
"validator" -> KotlinValidatorGenerator()
55+
else -> fail("unknown generator name in config: $g")
56+
}
57+
val args = mutableMapOf("outputDir" to outDir.toString())
58+
if (g == "validator") {
59+
args["packageName"] = config.validatorPackage
60+
?: fail("validator config needs validatorPackage")
61+
}
62+
gen.setArgs(args)
63+
gen.execute(loader)
64+
}
65+
66+
val snapshotDir = snapshotsRoot.resolve(name)
67+
val emittedFiles = Files.walk(outDir).filter { it.isRegularFile() }.sorted().toList()
68+
69+
if (!snapshotDir.isDirectory() || Files.walk(snapshotDir).filter { it.isRegularFile() }.count() == 0L) {
70+
// First run: create snapshots
71+
Files.createDirectories(snapshotDir)
72+
for (f in emittedFiles) {
73+
val rel = outDir.relativize(f)
74+
val target = snapshotDir.resolve(rel.toString())
75+
Files.createDirectories(target.parent)
76+
Files.copy(f, target)
77+
}
78+
fail("snapshots created for '$name' at ${snapshotDir.absolutePathString()} — review + commit. Re-run to gate.")
79+
}
80+
81+
// Subsequent runs: compare each emitted file to its snapshot
82+
for (f in emittedFiles) {
83+
val rel = outDir.relativize(f)
84+
val snap = snapshotDir.resolve(rel.toString())
85+
if (!snap.isRegularFile()) {
86+
fail("emitted file has no snapshot: $rel (write snapshot to $snap and re-run)")
87+
}
88+
val actual = f.readText()
89+
val expected = snap.readText()
90+
if (actual != expected) {
91+
fail("snapshot drift in $name/$rel\nEXPECTED:\n$expected\nACTUAL:\n$actual")
92+
}
93+
}
94+
// Also verify no snapshot file is missing from emission
95+
val snapFiles = Files.walk(snapshotDir).filter { it.isRegularFile() }.toList()
96+
val emittedRels = emittedFiles.map { outDir.relativize(it).toString() }.toSet()
97+
for (s in snapFiles) {
98+
val rel = snapshotDir.relativize(s).toString()
99+
if (rel !in emittedRels) {
100+
fail("snapshot has no emitted counterpart: $rel (stale snapshot? delete or fix generator)")
101+
}
102+
}
103+
} finally {
104+
outDir.toFile().deleteRecursively()
105+
}
106+
}
107+
108+
private data class FixtureConfig(val generators: List<String>, val validatorPackage: String?)
109+
110+
private fun parseConfig(json: String): FixtureConfig {
111+
// Crude JSON parse — fixtures are tiny + controlled. No Jackson dep added for this.
112+
val gens = Regex("\"generators\"\\s*:\\s*\\[([^\\]]+)]").find(json)?.groupValues?.get(1)
113+
?.split(",")?.map { it.trim().trim('"') } ?: emptyList()
114+
val pkg = Regex("\"validatorPackage\"\\s*:\\s*\"([^\"]+)\"").find(json)?.groupValues?.get(1)
115+
return FixtureConfig(gens, pkg)
116+
}
117+
}
Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
{
2+
"generators": ["entity", "table", "validator"],
3+
"validatorPackage": "acme.demo"
4+
}
Lines changed: 19 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,19 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme::demo",
4+
"children": [
5+
{ "object.entity": { "name": "Author", "children": [
6+
{ "field.long": { "name": "id" } },
7+
{ "field.string": { "name": "name", "@maxLength": 100, "@required": true } },
8+
{ "field.string": { "name": "bio" } },
9+
{ "field.int": { "name": "age" } },
10+
{ "field.boolean": { "name": "active" } },
11+
{ "field.double": { "name": "ratio" } },
12+
{ "field.date": { "name": "birthday" } },
13+
{ "field.timestamp": { "name": "createdAt" } },
14+
{ "source.rdb": { "@table": "authors" } },
15+
{ "identity.primary": { "name": "pk", "@fields": ["id"], "@generation": "increment" } }
16+
] } }
17+
]
18+
}
19+
}
Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,3 @@
1+
{
2+
"generators": ["payload"]
3+
}
Lines changed: 13 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
{
2+
"metadata.root": {
3+
"package": "acme::ai",
4+
"children": [
5+
{ "object.value": { "name": "Author", "children": [
6+
{ "field.long": { "name": "id" } },
7+
{ "field.string": { "name": "name" } }
8+
] } },
9+
{ "template.prompt": { "name": "WelcomePrompt",
10+
"@payloadRef": "Author", "@textRef": "ai/welcome" } }
11+
]
12+
}
13+
}

0 commit comments

Comments
 (0)