|
| 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 | +} |
0 commit comments