diff --git a/.DS_Store b/.DS_Store new file mode 100644 index 0000000..eea9898 Binary files /dev/null and b/.DS_Store differ diff --git a/CLAUDE.md b/CLAUDE.md new file mode 100644 index 0000000..7c6ec72 --- /dev/null +++ b/CLAUDE.md @@ -0,0 +1,64 @@ +# CLAUDE.md + +This file provides guidance to Claude Code (claude.ai/code) when working with code in this repository. + +## What this is + +`env` is a Kotlin/Java library for emulating a microservice's external environment in tests (DBs, message queues, mocks, etc.). Published to Maven Central under `io.github.adven27`. The core abstraction is deliberately agnostic to *how* a system is emulated — Testcontainers/Docker, an embedded process, or a remote server — so a single `Environment` can mix them. + +## Build & test + +Gradle multi-module build. Use the wrapper. + +```bash +./gradlew build # compile + test + lint (detekt + ktlint) all modules +./gradlew build --parallel # what CI runs +./gradlew :env-db-postgresql:test # test one module +./gradlew :example:test --tests "EnvTest.fixedEnvironment" # single test +./gradlew ktlintFormat # auto-fix Kotlin style across a module's src +./gradlew detekt # static analysis only +``` + +Most modules' tests start real Docker containers via Testcontainers, so **a running Docker daemon is required** to run them. JDK 21 toolchain. + +## Architecture + +Everything hangs off two interfaces in `env-core` (`io.github.adven27.env.core`): + +- **`ExternalSystem`** — one emulated system. Lifecycle: `start(fixedEnv)` / `stop()` / `running()`, plus a `config: ExternalSystemConfig`. Constructing an `ExternalSystemConfig` **propagates its properties to JVM system properties** as a side effect (see `ExternalSystemConfig.init`) — that's how the system under test discovers connection details (e.g. `env.db.postgresql.url`). +- **`Environment`** — an ordered map of `name -> ExternalSystem`. `up()` starts all systems concurrently (cached thread pool, one thread per system) with a timeout, rolling back via `down()` on any failure. Look up a started system with `env()` (by type) or `env("NAME")` (by key). + +Two implementation strategies for `ExternalSystem`, both in their own modules: +- **`GenericExternalSystem`** (`env-core`) — wraps any object with `start`/`stop`/`running`/`afterStart` lambdas. Base for non-container systems. +- **`ContainerExternalSystem`** (`env-container`) — wraps a Testcontainers `GenericContainer`. Most `env-db-*` / `env-mq-*` modules build on this. Note: some systems (e.g. `PostgreSqlContainerSystem`) instead *extend* the Testcontainers class directly and implement `ExternalSystem`. + +### The `fixedEnv` flag (important) + +`start(fixedEnv: Boolean)` is the central concept for **fixed vs. dynamic** ports: +- `fixedEnv = true` → bind well-known fixed ports (e.g. Postgres on 5432). Used for stable local dev where the SUT has hardcoded connection config. +- `fixedEnv = false` → let the system pick random free ports. Used in CI / parallel test runs to avoid collisions. + +The value comes from `Environment.Config.envStrategy` — default `SystemPropertyToggle` reads the `ENV_FIXED` system property (default `false`). See `env/example/.../EnvTest.kt` for both modes asserted. + +### Config via system properties + +`Environment.Config` is resolved from system properties by default (`ConfigResolver.FromSystemProperty`): `ENV_FIXED`, `ENV_DRY_RUN`, `ENV_UP_TIMEOUT_SEC` (300), `ENV_DOWN_TIMEOUT_SEC` (10). + +### Standalone runner + +`EnvStarter` (`env-core`) runs any `Environment` subclass as a standalone process (interactive REPL: `s`tatus / `u`p / `d`own / `e`xit), for local dev outside a test run. Invoked via `JavaExec` with the env class name as arg (see README). + +## Module layout + +- `env-core` — interfaces, `Environment`, `EnvStarter`, port utils. No Testcontainers dependency. +- `env-container` — Testcontainers base (`ContainerExternalSystem`, `String.parseImage()` helpers). +- `env-db-*`, `env-mq-*`, `env-redis`, `env-wiremock`, `env-grpc-mock`, `env-selenium`, `env-localstack`, `env-samba`, `env-jar-application` — one system implementation each. Add a new system by creating a `env-` module (register it in `settings.gradle`) and depending on `env-container` or `env-core`. +- `example` — runnable usage reference (`EnvTest.kt`); not published. + +## Conventions + +- **Adding a system**: also add it to `build.gradle`'s `publishMaven` task (the `dependsOn` list) and to `settings.gradle`. +- Config property keys are namespaced `env...` (e.g. `env.mq.rabbit.port`). Define them as constants on the `Config` companion. +- Public API targets Java interop (`@JvmOverloads`, `@JvmStatic`, `@JvmField`); keep that when touching constructors/companions. +- Version lives in one place: `ext.libVersion` in root `build.gradle`. Release commits are just the bumped version number (see git log); publishing is automated on GitHub Release via the `gradle-publish.yml` workflow. +- Style: ktlint + detekt (config in `config/detekt/detekt.yml`), 4-space indent, no trailing commas (`.editorconfig`). diff --git a/env-mq-kafka-embedded/src/main/java/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystem.kt b/env-mq-kafka-embedded/src/main/java/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystem.kt index 0d3f54e..9c959cc 100644 --- a/env-mq-kafka-embedded/src/main/java/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystem.kt +++ b/env-mq-kafka-embedded/src/main/java/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystem.kt @@ -61,6 +61,18 @@ open class EmbeddedKafkaSystem @JvmOverloads constructor( @Suppress("SpreadOperator") constructor(vararg topics: String) : this(topics = arrayOf(*topics)) + fun addTopics(vararg topics: String) { + embeddedKafka.addTopicsWithResults(*topics) + .filterValues { it != null } + .let { failures -> + require(failures.isEmpty()) { + "Не удалось создать топики: " + failures.entries.joinToString { "${it.key}: ${it.value.message}" } + } + } + } + + fun topics(): Set = embeddedKafka.topics + override fun toString() = "Embedded Kafka Broker" open class Config(val bootstrapServers: String = "PLAINTEXT://localhost:$DEFAULT_KAFKA_PORT") : diff --git a/env-mq-kafka-embedded/src/test/kotlin/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystemTest.kt b/env-mq-kafka-embedded/src/test/kotlin/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystemTest.kt index 99b2941..ae3b68b 100644 --- a/env-mq-kafka-embedded/src/test/kotlin/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystemTest.kt +++ b/env-mq-kafka-embedded/src/test/kotlin/io/github/adven27/env/mq/kafka/embedded/EmbeddedKafkaSystemTest.kt @@ -40,6 +40,23 @@ class EmbeddedKafkaSystemTest { ).use { assertTrue(it.listTopics().names().get().contains("some-topic")) } } + @Test + fun topicsCanBeAddedToARunningEnvironment() { + val env = SomeEnvironment().apply { up() } + sut = env + + env.kafka().addTopics("late-topic") + + assertTrue(env.kafka().topics().contains("late-topic")) + Admin.create( + mapOf( + BOOTSTRAP_SERVERS_CONFIG to env.kafka().config.bootstrapServers, + REQUEST_TIMEOUT_MS_CONFIG to "8000", + DEFAULT_API_TIMEOUT_MS_CONFIG to "12000" + ) + ).use { assertTrue(it.listTopics().names().get().contains("late-topic")) } + } + @Test fun fixedEnvironmentIsNotSupported() { val port = Environment.findAvailableTcpPort()