From 66ce2f0d4ca38765e5eb940e4ce65dcf5b6dbf3d Mon Sep 17 00:00:00 2001 From: Marty Pitt Date: Wed, 25 Feb 2026 15:12:41 +0000 Subject: [PATCH 1/9] Add preflight-spec module and upgrade Gradle to 9.3.1 MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Introduces the preflight-spec module — a standalone markdown parsing/writing library for Preflight test spec files. Each spec is a markdown file that serves as both living documentation and a machine-parseable test definition. - New module: preflight-core/preflight-spec with TestSpecReader, TestSpecWriter, data model (TestSpec, Stub, StubMode), and internal parsers for front matter and HTML comment directives - 46 tests covering parsing, writing, round-trip, and error cases - MarkdownSpec base class in preflight-runtime for executing spec files as Kotest tests - Upgraded all Gradle wrappers from 8.13 to 9.3.1 (bundles Kotlin 2.2.21, required for Orbital 0.37.0-SNAPSHOT compatibility) - Fixed KafkaContainerSupport for removed emitConsumerInfoMessages parameter - Removed broken ExamplesSpec that referenced compileOnly types from test source Co-Authored-By: Claude Opus 4.6 --- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- gradle/wrapper/gradle-wrapper.properties | 2 +- .../gradle/wrapper/gradle-wrapper.properties | 2 +- .../preflight-gradle-plugin/build.gradle.kts | 2 +- .../preflight-runtime/build.gradle.kts | 7 +- .../orbitalhq/preflight/dsl/MarkdownSpec.kt | 76 ++ .../containers/kafka/KafkaContainerSupport.kt | 1 - .../com/orbitalhq/preflight/ExamplesSpec.kt | 45 -- preflight-core/preflight-spec/README.md | 213 +++++ .../preflight-spec/build.gradle.kts | 41 + .../preflight/spec/SpecParseException.kt | 24 + .../com/orbitalhq/preflight/spec/TestSpec.kt | 24 + .../preflight/spec/TestSpecReader.kt | 241 ++++++ .../preflight/spec/TestSpecWriter.kt | 90 +++ .../spec/internal/DirectiveParser.kt | 34 + .../spec/internal/FrontMatterParser.kt | 54 ++ .../preflight/spec/DirectiveParserTest.kt | 61 ++ .../orbitalhq/preflight/spec/RoundTripTest.kt | 96 +++ .../preflight/spec/TestSpecReaderTest.kt | 764 ++++++++++++++++++ .../preflight/spec/TestSpecWriterTest.kt | 137 ++++ preflight-core/settings.gradle.kts | 1 + 23 files changed, 1866 insertions(+), 55 deletions(-) create mode 100644 preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/MarkdownSpec.kt delete mode 100644 preflight-core/preflight-runtime/src/test/kotlin/com/orbitalhq/preflight/ExamplesSpec.kt create mode 100644 preflight-core/preflight-spec/README.md create mode 100644 preflight-core/preflight-spec/build.gradle.kts create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/SpecParseException.kt create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/TestSpec.kt create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/TestSpecReader.kt create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/TestSpecWriter.kt create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/internal/DirectiveParser.kt create mode 100644 preflight-core/preflight-spec/src/main/kotlin/com/orbitalhq/preflight/spec/internal/FrontMatterParser.kt create mode 100644 preflight-core/preflight-spec/src/test/kotlin/com/orbitalhq/preflight/spec/DirectiveParserTest.kt create mode 100644 preflight-core/preflight-spec/src/test/kotlin/com/orbitalhq/preflight/spec/RoundTripTest.kt create mode 100644 preflight-core/preflight-spec/src/test/kotlin/com/orbitalhq/preflight/spec/TestSpecReaderTest.kt create mode 100644 preflight-core/preflight-spec/src/test/kotlin/com/orbitalhq/preflight/spec/TestSpecWriterTest.kt diff --git a/example-projects/mixed-sources/gradle/wrapper/gradle-wrapper.properties b/example-projects/mixed-sources/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/example-projects/mixed-sources/gradle/wrapper/gradle-wrapper.properties +++ b/example-projects/mixed-sources/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/example-projects/project-with-orbital-dependency/gradle/wrapper/gradle-wrapper.properties b/example-projects/project-with-orbital-dependency/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/example-projects/project-with-orbital-dependency/gradle/wrapper/gradle-wrapper.properties +++ b/example-projects/project-with-orbital-dependency/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/example-projects/simple-project/gradle/wrapper/gradle-wrapper.properties b/example-projects/simple-project/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/example-projects/simple-project/gradle/wrapper/gradle-wrapper.properties +++ b/example-projects/simple-project/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/gradle/wrapper/gradle-wrapper.properties b/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/gradle/wrapper/gradle-wrapper.properties +++ b/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/preflight-core/gradle/wrapper/gradle-wrapper.properties b/preflight-core/gradle/wrapper/gradle-wrapper.properties index 37f853b..37f78a6 100644 --- a/preflight-core/gradle/wrapper/gradle-wrapper.properties +++ b/preflight-core/gradle/wrapper/gradle-wrapper.properties @@ -1,6 +1,6 @@ distributionBase=GRADLE_USER_HOME distributionPath=wrapper/dists -distributionUrl=https\://services.gradle.org/distributions/gradle-8.13-bin.zip +distributionUrl=https\://services.gradle.org/distributions/gradle-9.3.1-bin.zip networkTimeout=10000 validateDistributionUrl=true zipStoreBase=GRADLE_USER_HOME diff --git a/preflight-core/preflight-gradle-plugin/build.gradle.kts b/preflight-core/preflight-gradle-plugin/build.gradle.kts index cbad436..f810748 100644 --- a/preflight-core/preflight-gradle-plugin/build.gradle.kts +++ b/preflight-core/preflight-gradle-plugin/build.gradle.kts @@ -22,7 +22,7 @@ repositories { } dependencies { - implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:1.9.23") + implementation("org.jetbrains.kotlin:kotlin-gradle-plugin:2.2.21") implementation(project(":preflight-runtime")) testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") diff --git a/preflight-core/preflight-runtime/build.gradle.kts b/preflight-core/preflight-runtime/build.gradle.kts index 62a95f5..3d9fb3c 100644 --- a/preflight-core/preflight-runtime/build.gradle.kts +++ b/preflight-core/preflight-runtime/build.gradle.kts @@ -3,10 +3,11 @@ plugins { `maven-publish` } -val taxiVersion = "1.66.0-SNAPSHOT" -val orbitalVersion = "0.36.0-M9" // Default version, can be overridden in consumer projects +val taxiVersion = "1.69.1" +val orbitalVersion = "0.37.0-SNAPSHOT" // Default version, can be overridden in consumer projects dependencies { + implementation(project(":preflight-spec")) testImplementation(platform("org.junit:junit-bom:5.10.0")) testImplementation("org.junit.jupiter:junit-jupiter") implementation(platform("org.testcontainers:testcontainers-bom:1.19.3")) @@ -132,4 +133,4 @@ configurations.all { because("Use OSS JOOQ instead of commercial version") } } -} \ No newline at end of file +} diff --git a/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/MarkdownSpec.kt b/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/MarkdownSpec.kt new file mode 100644 index 0000000..7d5ab99 --- /dev/null +++ b/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/MarkdownSpec.kt @@ -0,0 +1,76 @@ +package com.orbitalhq.preflight.dsl + +import com.fasterxml.jackson.module.kotlin.jacksonObjectMapper +import com.orbitalhq.preflight.spec.StubMode +import com.orbitalhq.preflight.spec.TestSpec +import com.orbitalhq.preflight.spec.TestSpecReader +import com.orbitalhq.stubbing.StubService +import io.kotest.matchers.shouldBe +import java.nio.file.Files +import java.nio.file.Path +import java.nio.file.Paths +import kotlin.io.path.extension +import kotlin.io.path.isRegularFile + +abstract class MarkdownSpec( + specsPath: String = "test-resources/specs", + sourceConfig: PreflightSourceConfig = FilePathSourceConfig() +) : OrbitalSpec({ + + val objectMapper = jacksonObjectMapper() + val specsDir = Paths.get(specsPath) + + if (Files.exists(specsDir) && Files.isDirectory(specsDir)) { + val specFiles = Files.walk(specsDir) + .filter { it.isRegularFile() && it.extension == "md" } + .sorted() + .toList() + + for (file in specFiles) { + val spec = TestSpecReader.readFile(file) + registerSpec(spec, objectMapper) + } + } + +}, sourceConfig) + +private fun OrbitalSpec.registerSpec( + spec: TestSpec, + objectMapper: com.fasterxml.jackson.databind.ObjectMapper +) { + val stubCustomizer: (StubService) -> Unit = { stubService -> + for (stub in spec.dataSources) { + when (stub.mode) { + StubMode.REQUEST_RESPONSE -> { + val response = stub.response + if (response != null) { + stubService.addResponse(stub.operationName, response) + } + } + StubMode.STREAM -> { + val emitter = stubService.addResponseEmitter(stub.operationName) + stub.messages?.forEach { message -> + emitter.next(message) + } + } + } + } + } + + describe(spec.name) { + it("matches expected result") { + val expectedJson = objectMapper.readTree(spec.expectedResult) + val isArray = expectedJson.isArray + + if (isArray) { + val actual = spec.query.queryForCollectionOfMaps(stubCustomizer) + val actualJson = objectMapper.valueToTree(actual) + actualJson shouldBe expectedJson + } else { + val actual = spec.query.queryForMap(stubCustomizer) + val actualJson = objectMapper.valueToTree(actual) + actualJson shouldBe expectedJson + } + } + } +} diff --git a/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/containers/kafka/KafkaContainerSupport.kt b/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/containers/kafka/KafkaContainerSupport.kt index 7dd0af1..44e683c 100644 --- a/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/containers/kafka/KafkaContainerSupport.kt +++ b/preflight-core/preflight-runtime/src/main/kotlin/com/orbitalhq/preflight/dsl/containers/kafka/KafkaContainerSupport.kt @@ -127,7 +127,6 @@ fun kafkaContainer( schemaStore, formatRegistry = formatRegistry, meterRegistry = SimpleMeterRegistry(), - emitConsumerInfoMessages = false, kafkaConsumerStatsFlowBuilder = KafkaConsumerStatsFlowBuilder(GaugeRegistry.simple()) ) KafkaInvoker(kafkaStreamManager, kafkaStreamPublisher) diff --git a/preflight-core/preflight-runtime/src/test/kotlin/com/orbitalhq/preflight/ExamplesSpec.kt b/preflight-core/preflight-runtime/src/test/kotlin/com/orbitalhq/preflight/ExamplesSpec.kt deleted file mode 100644 index 505b781..0000000 --- a/preflight-core/preflight-runtime/src/test/kotlin/com/orbitalhq/preflight/ExamplesSpec.kt +++ /dev/null @@ -1,45 +0,0 @@ -package com.orbitalhq.preflight - -import app.cash.turbine.test -import com.orbitalhq.preflight.dsl.OrbitalSpec -import com.orbitalhq.preflight.dsl.forSchema -import com.orbitalhq.stubbing.ResponseEmitter -import kotlin.time.Duration.Companion.seconds -import com.orbitalhq.expectMap -import io.kotest.matchers.shouldBe - -class ExamplesSpec : OrbitalSpec( - { - - describe("Examples") { - it("can run a saved streaming query") { - var eventEmitter: ResponseEmitter? = null - val resultStream = runNamedQueryForStream("StreamPerson") { stubService -> - eventEmitter = stubService.addResponseEmitter("clickEvents") - } - resultStream.test(timeout = 10.seconds) { - eventEmitter!!.next("""{ "id" : "hi-1" }""") - val next = expectMap() - next.shouldBe(mapOf("personId" to "hi-1")) - } - } - } - }, forSchema( - """ - model PersonClickedEvent { - id : PersonId inherits String - } - model Person { - personId : PersonId - } - service PersonApi { - stream clickEvents : Stream - write operation upsertPerson(Person) - } - query StreamPerson { - stream { PersonClickedEvent } - call PersonApi::upsertPerson - } -""".trimIndent() - ) -) \ No newline at end of file diff --git a/preflight-core/preflight-spec/README.md b/preflight-core/preflight-spec/README.md new file mode 100644 index 0000000..b5e6a60 --- /dev/null +++ b/preflight-core/preflight-spec/README.md @@ -0,0 +1,213 @@ +# preflight-spec + +A standalone library for reading and writing Preflight test spec files. These are markdown files — one test per file — that serve as both living documentation and machine-parseable test specifications. + +This module has **no dependency on Orbital or preflight-runtime**. It parses markdown to a data model and generates markdown from a data model. The bridge to Preflight's test execution lives in `preflight-runtime`. + +## Spec format + +Every spec file starts with YAML front matter declaring the version, followed by a strict heading structure: + +```markdown +--- +spec-version: 0.1 +--- + +# Customer Order Flow + +Optional description of what this test covers. + +## Query + +` ``taxiql +find { Customer(customerId == "12345") } with { orders: Order[] } +` `` + +## Data Sources + +### Fetch Customer Details + + +Response: +` ``json +{ "id": "12345", "name": "Alice Smith" } +` `` + +### List Customer Orders + + +Response: +` ``json +[{ "orderId": "ORD-99", "status": "confirmed" }] +` `` + +## Expected Result + +` ``json +{ + "customer": { "name": "Alice Smith" }, + "orders": [{ "id": "ORD-99", "status": "confirmed" }] +} +` `` + +## Flow + +` ``mermaid +sequenceDiagram + participant Q as Query Engine + Q->>C: getCustomer + C-->>Q: Customer +` `` +``` + +*(The backticks above are escaped for nesting. Real files use standard triple-backtick fencing.)* + +### Sections + +| Heading | Level | Required | Purpose | +|---------|-------|----------|---------| +| `# ` | H1 | Yes | Test name. Exactly one per file. | +| `## Query` | H2 | Yes | TaxiQL query in a `taxiql` fenced code block. | +| `## Data Sources` | H2 | Yes | Parent for stubbed operations. | +| `###