chore: run CI test in docker
This commit is contained in:
+22
-18
@@ -80,36 +80,40 @@ jobs:
|
|||||||
continue-on-error: false
|
continue-on-error: false
|
||||||
|
|
||||||
test:
|
test:
|
||||||
needs: build
|
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
|
env:
|
||||||
|
GRADLE_CACHE_DIR: ${{ github.workspace }}/.gradle-docker-cache
|
||||||
steps:
|
steps:
|
||||||
- name: Checkout code
|
- name: Checkout code
|
||||||
uses: actions/checkout@v6
|
uses: actions/checkout@v6
|
||||||
|
|
||||||
- name: Set up JDK 21
|
- name: Prepare docker secrets
|
||||||
uses: actions/setup-java@v5
|
run: |
|
||||||
with:
|
[ -f docker/postgresql.secret ] || echo -n "changeit" > docker/postgresql.secret
|
||||||
distribution: 'temurin'
|
|
||||||
java-version: '21'
|
|
||||||
|
|
||||||
- name: Restore Gradle cache
|
- name: Generate cache key
|
||||||
|
id: cache-key-generator
|
||||||
|
run: echo "key=gradle-docker-${{ runner.os }}-${{ hashFiles('**/*.gradle*', '**/gradle-wrapper.properties') }}" >> $GITHUB_OUTPUT
|
||||||
|
|
||||||
|
- name: Restore Gradle cache (Docker)
|
||||||
uses: actions/cache@v6
|
uses: actions/cache@v6
|
||||||
with:
|
with:
|
||||||
path: |
|
path: ${{ env.GRADLE_CACHE_DIR }}
|
||||||
~/.gradle/caches
|
key: ${{ steps.cache-key-generator.outputs.key }}
|
||||||
~/.gradle/wrapper
|
|
||||||
key: ${{ needs.build.outputs.cache-key }}
|
|
||||||
restore-keys: |
|
restore-keys: |
|
||||||
gradle-${{ runner.os }}-
|
gradle-docker-${{ runner.os }}-
|
||||||
|
|
||||||
- name: Grant execute permission to Gradle wrapper
|
- name: Prepare cache directory permissions
|
||||||
run: chmod +x gradlew
|
run: |
|
||||||
|
mkdir -p "$GRADLE_CACHE_DIR"
|
||||||
|
chmod -R 777 "$GRADLE_CACHE_DIR"
|
||||||
|
|
||||||
- name: Start CI Docker Compose services
|
- name: Run tests in Docker
|
||||||
run: ./gradlew ciComposeUp -Pci
|
run: docker compose -f docker/docker-compose-test.yaml run --rm tests
|
||||||
|
|
||||||
- name: Run tests
|
- name: Shut down Docker services
|
||||||
run: ./gradlew test -x ciComposeUp -Pci --no-daemon
|
if: always()
|
||||||
|
run: docker compose -f docker/docker-compose-test.yaml down -v
|
||||||
|
|
||||||
- name: Upload test reports
|
- name: Upload test reports
|
||||||
if: always()
|
if: always()
|
||||||
|
|||||||
@@ -37,3 +37,4 @@ out/
|
|||||||
/docker/.env
|
/docker/.env
|
||||||
/docker/*.secret
|
/docker/*.secret
|
||||||
*.hprof
|
*.hprof
|
||||||
|
/.gradle-docker-cache/
|
||||||
|
|||||||
+11
-40
@@ -1,4 +1,3 @@
|
|||||||
import com.avast.gradle.dockercompose.ComposeExtension
|
|
||||||
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
import org.jlleitschuh.gradle.ktlint.KtlintExtension
|
||||||
|
|
||||||
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
|
val ktorVersion: Provider<String> = providers.gradleProperty("ktor_version")
|
||||||
@@ -15,7 +14,6 @@ plugins {
|
|||||||
id("io.ktor.plugin") version "3.5.1"
|
id("io.ktor.plugin") version "3.5.1"
|
||||||
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
|
id("org.jetbrains.kotlin.plugin.serialization") version "2.4.10"
|
||||||
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
|
id("org.jlleitschuh.gradle.ktlint") version "14.2.0"
|
||||||
id("com.avast.gradle.docker-compose") version "0.17.12"
|
|
||||||
}
|
}
|
||||||
|
|
||||||
group = "io.github.flecomte"
|
group = "io.github.flecomte"
|
||||||
@@ -48,44 +46,17 @@ java {
|
|||||||
|
|
||||||
tasks.withType<Test>().configureEach {
|
tasks.withType<Test>().configureEach {
|
||||||
useJUnitPlatform()
|
useJUnitPlatform()
|
||||||
}
|
jvmArgs("-Djdk.attach.allowAttachSelf=true", "-XX:+EnableDynamicAgentLoading")
|
||||||
|
// Dynamic self-attach (used by MockK/ByteBuddy) times out in Docker containers because the
|
||||||
configure<ComposeExtension> {
|
// SIGQUIT-triggered AttachListener handshake never completes there. Loading the byte-buddy
|
||||||
createNested("test").apply {
|
// agent jar statically via -javaagent avoids the attach handshake entirely: MockK detects the
|
||||||
useComposeFiles.set(listOf("docker/docker-compose-test.yaml"))
|
// pre-installed Instrumentation instance and skips dynamic attach.
|
||||||
setProjectName("event-demo-test")
|
doFirst {
|
||||||
|
val agentJar =
|
||||||
|
classpath.files.firstOrNull { it.name.startsWith("byte-buddy-agent") }
|
||||||
|
?: error("byte-buddy-agent jar not found on test classpath")
|
||||||
|
jvmArgs("-javaagent:$agentJar")
|
||||||
}
|
}
|
||||||
createNested("dev").apply {
|
|
||||||
useComposeFiles.set(listOf("docker/docker-compose-dev.yaml"))
|
|
||||||
setProjectName("event-demo-dev")
|
|
||||||
}
|
|
||||||
createNested("ci").apply {
|
|
||||||
useComposeFiles.set(listOf("docker/docker-compose-ci.yaml"))
|
|
||||||
setProjectName("event-demo-ci")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
|
|
||||||
tasks.test {
|
|
||||||
if (project.hasProperty("ci")) {
|
|
||||||
dependsOn("ciComposeUp")
|
|
||||||
} else {
|
|
||||||
dependsOn("testComposeUp")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
tasks.named("run") {
|
|
||||||
dependsOn("devComposeUp")
|
|
||||||
}
|
|
||||||
tasks.composeUp {
|
|
||||||
dependsOn("copyEnv")
|
|
||||||
}
|
|
||||||
tasks.named("devComposeUp") {
|
|
||||||
dependsOn("copyEnv")
|
|
||||||
}
|
|
||||||
tasks.named("testComposeUp") {
|
|
||||||
dependsOn("copyEnv")
|
|
||||||
}
|
|
||||||
tasks.named("ciComposeUp") {
|
|
||||||
dependsOn("copyEnv")
|
|
||||||
}
|
}
|
||||||
|
|
||||||
tasks.register<Copy>("copyEnv") {
|
tasks.register<Copy>("copyEnv") {
|
||||||
@@ -154,6 +125,6 @@ dependencies {
|
|||||||
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
|
testImplementation("org.jetbrains.kotlin:kotlin-test-junit:${kotlinVersion.get()}")
|
||||||
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
|
testImplementation("io.ktor:ktor-server-test-host-jvm:${ktorVersion.get()}")
|
||||||
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
|
testImplementation("io.kotest:kotest-runner-junit5:${kotestVersion.get()}")
|
||||||
testImplementation("io.mockk:mockk:1.13.17")
|
testImplementation("io.mockk:mockk:1.14.11")
|
||||||
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
|
testImplementation("com.tngtech.archunit:archunit-junit5:1.3.0")
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -0,0 +1,10 @@
|
|||||||
|
# Image officielle Gradle avec JDK 21 déjà installé
|
||||||
|
FROM gradle:9.6.1-jdk21
|
||||||
|
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copie du wrapper et des fichiers de config en premier pour profiter du cache Docker
|
||||||
|
COPY build.gradle.kts settings.gradle.kts ./
|
||||||
|
|
||||||
|
# Lance les tests Kotlin
|
||||||
|
CMD ["gradle", "test", "--no-daemon"]
|
||||||
@@ -1,4 +1,8 @@
|
|||||||
name: event-demo-test
|
name: event-demo-test
|
||||||
include:
|
include:
|
||||||
- path:
|
- path:
|
||||||
- docker-compose-test.yaml
|
- parts/docker-compose-databases.yaml
|
||||||
|
- parts/docker-compose-databases-expose.yaml
|
||||||
|
- parts/docker-compose-tools.yaml
|
||||||
|
- parts/docker-compose-tools-local.yaml
|
||||||
|
- parts/docker-compose-traefik.yaml
|
||||||
@@ -2,7 +2,5 @@ name: event-demo-test
|
|||||||
include:
|
include:
|
||||||
- path:
|
- path:
|
||||||
- parts/docker-compose-databases.yaml
|
- parts/docker-compose-databases.yaml
|
||||||
- parts/docker-compose-databases-expose.yaml
|
- parts/docker-compose-test.yaml
|
||||||
- parts/docker-compose-tools.yaml
|
|
||||||
- parts/docker-compose-tools-local.yaml
|
|
||||||
- parts/docker-compose-traefik.yaml
|
- parts/docker-compose-traefik.yaml
|
||||||
@@ -0,0 +1,20 @@
|
|||||||
|
services:
|
||||||
|
tests:
|
||||||
|
build:
|
||||||
|
context: ../..
|
||||||
|
dockerfile: docker/DockerfileTest
|
||||||
|
volumes:
|
||||||
|
- ${GRADLE_CACHE_DIR:-gradle-cache}:/home/gradle/.gradle
|
||||||
|
- ../..:/app
|
||||||
|
depends_on:
|
||||||
|
flyway:
|
||||||
|
condition: service_completed_successfully
|
||||||
|
postgresql:
|
||||||
|
condition: service_healthy
|
||||||
|
rabbitmq:
|
||||||
|
condition: service_healthy
|
||||||
|
redis:
|
||||||
|
condition: service_healthy
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
gradle-cache:
|
||||||
@@ -13,12 +13,12 @@ jwt {
|
|||||||
}
|
}
|
||||||
|
|
||||||
redis {
|
redis {
|
||||||
url = "redis://localhost:6379"
|
url = "redis://redis:6379"
|
||||||
url = ${?REDIS_URL}
|
url = ${?REDIS_URL}
|
||||||
}
|
}
|
||||||
|
|
||||||
postgresql {
|
postgresql {
|
||||||
url = "jdbc:postgresql://localhost:5432/event-demo"
|
url = "jdbc:postgresql://postgresql/event-demo"
|
||||||
url = ${?POSTGRESQL_URL}
|
url = ${?POSTGRESQL_URL}
|
||||||
|
|
||||||
username = "event-demo"
|
username = "event-demo"
|
||||||
@@ -29,7 +29,7 @@ postgresql {
|
|||||||
}
|
}
|
||||||
|
|
||||||
rabbitmq {
|
rabbitmq {
|
||||||
url = "localhost"
|
url = "rabbitmq"
|
||||||
url = ${?RABBITMQ_URL}
|
url = ${?RABBITMQ_URL}
|
||||||
|
|
||||||
port = "5672"
|
port = "5672"
|
||||||
|
|||||||
@@ -2,6 +2,7 @@ package eventDemo.libs.bus
|
|||||||
|
|
||||||
import com.rabbitmq.client.ConnectionFactory
|
import com.rabbitmq.client.ConnectionFactory
|
||||||
import eventDemo.testHelpers.spyPing
|
import eventDemo.testHelpers.spyPing
|
||||||
|
import eventDemo.testHelpers.testKoinApplicationWithConfig
|
||||||
import io.kotest.core.spec.style.FunSpec
|
import io.kotest.core.spec.style.FunSpec
|
||||||
import io.kotest.datatest.withData
|
import io.kotest.datatest.withData
|
||||||
import io.kotest.matchers.string.shouldStartWith
|
import io.kotest.matchers.string.shouldStartWith
|
||||||
@@ -15,33 +16,28 @@ private data class ObjTest(
|
|||||||
class BusTest :
|
class BusTest :
|
||||||
FunSpec({
|
FunSpec({
|
||||||
context("Pub/sub") {
|
context("Pub/sub") {
|
||||||
val factory =
|
testKoinApplicationWithConfig {
|
||||||
ConnectionFactory().apply {
|
val list: Map<String, Bus<ObjTest>> =
|
||||||
host = "localhost"
|
mapOf(
|
||||||
port = 5672
|
BusInMemory::class.java.simpleName to BusInMemory(),
|
||||||
username = "event-demo"
|
BusInRabbitMQ::class.java.simpleName to
|
||||||
password = "changeit"
|
BusInRabbitMQ(
|
||||||
}
|
get<ConnectionFactory>(),
|
||||||
val list: Map<String, Bus<ObjTest>> =
|
"testExchange",
|
||||||
mapOf(
|
{ it.value },
|
||||||
BusInMemory::class.java.simpleName to BusInMemory(),
|
{ ObjTest(it) },
|
||||||
BusInRabbitMQ::class.java.simpleName to
|
),
|
||||||
BusInRabbitMQ(
|
)
|
||||||
factory,
|
|
||||||
"testExchange",
|
|
||||||
{ it.value },
|
|
||||||
{ ObjTest(it) },
|
|
||||||
),
|
|
||||||
)
|
|
||||||
|
|
||||||
withData(list) { bus ->
|
withData(list) { bus ->
|
||||||
spyPing(exactly = 2, duration = 1.seconds) { ping ->
|
spyPing(exactly = 2, duration = 1.seconds) { ping ->
|
||||||
bus.subscribe { obj ->
|
bus.subscribe { obj ->
|
||||||
ping()
|
ping()
|
||||||
obj.value shouldStartWith "testMessage"
|
obj.value shouldStartWith "testMessage"
|
||||||
|
}
|
||||||
|
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||||
|
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
||||||
}
|
}
|
||||||
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
|
||||||
bus.publish(ObjTest("testMessage${Random.nextInt()}"))
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -15,10 +15,13 @@ import org.koin.core.module.KoinApplicationDslMarker
|
|||||||
import org.koin.dsl.koinApplication
|
import org.koin.dsl.koinApplication
|
||||||
import org.koin.ktor.ext.getKoin
|
import org.koin.ktor.ext.getKoin
|
||||||
|
|
||||||
|
const val CONFIG_FILE_NAME = "application.conf"
|
||||||
|
|
||||||
@KoinApplicationDslMarker
|
@KoinApplicationDslMarker
|
||||||
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
|
suspend fun <T> testKoinApplicationWithConfig(block: suspend Koin.() -> T): T =
|
||||||
koinApplication { modules(appKoinModule(ApplicationConfig("application.conf").configuration)) }
|
koinApplication {
|
||||||
.koin
|
modules(appKoinModule(ApplicationConfig(CONFIG_FILE_NAME).configuration))
|
||||||
|
}.koin
|
||||||
.run {
|
.run {
|
||||||
cleanDataTest()
|
cleanDataTest()
|
||||||
configureProjectionListener()
|
configureProjectionListener()
|
||||||
@@ -34,7 +37,7 @@ fun testApplicationWithConfig(
|
|||||||
) {
|
) {
|
||||||
val logger = KotlinLogging.logger {}
|
val logger = KotlinLogging.logger {}
|
||||||
testApplication {
|
testApplication {
|
||||||
val conf = ApplicationConfig("application.conf")
|
val conf = ApplicationConfig(CONFIG_FILE_NAME)
|
||||||
environment {
|
environment {
|
||||||
config = conf
|
config = conf
|
||||||
}
|
}
|
||||||
|
|||||||
Reference in New Issue
Block a user