Skip to content

Commit

Permalink
fix: add a workaround for testcontainers/testcontainers-java#6441 (#152)
Browse files Browse the repository at this point in the history
Adds a workaround for testcontainers/testcontainers-java#6441 issue.
The issue happens when Docker was unavailable during the first attempt to start a container. All future attempts will fail, since `testcontainers` caches the connection attempt result in a static variable, so the state will persist while the class is loaded.
The workaround is to reset the state through reflection each time before we start the container.
  • Loading branch information
monosoul authored Oct 20, 2023
1 parent 3146995 commit 249ac33
Show file tree
Hide file tree
Showing 6 changed files with 79 additions and 21 deletions.
Original file line number Diff line number Diff line change
@@ -1,30 +1,28 @@
package dev.monosoul.jooq.artifact

import ch.qos.logback.classic.Logger
import ch.qos.logback.classic.spi.ILoggingEvent
import ch.qos.logback.core.read.ListAppender
import org.slf4j.LoggerFactory
import org.testcontainers.containers.BindMode.READ_ONLY
import org.testcontainers.containers.GenericContainer
import org.testcontainers.containers.SelinuxContext.SHARED
import org.testcontainers.containers.output.Slf4jLogConsumer
import org.testcontainers.containers.output.ToStringConsumer
import org.testcontainers.containers.startupcheck.IndefiniteWaitOneShotStartupCheckStrategy

class GradleContainer : GenericContainer<GradleContainer>("gradle:jdk17-alpine") {
class GradleContainer(
dockerSocketPath: String = "/var/run/docker.sock",
) : GenericContainer<GradleContainer>("gradle:jdk17-alpine") {

private val listAppender = ListAppender<ILoggingEvent>().also { it.start() }
private val logger = (LoggerFactory.getLogger("GradleContainer[$dockerImageName]") as Logger).also {
it.addAppender(listAppender)
}
val output: List<String> get() = ArrayList(listAppender.list).map { it.formattedMessage }
private val toStringLogConsumer = ToStringConsumer()
val output: String get() = toStringLogConsumer.toUtf8String()
val projectPath = "/home/gradle/project"

init {
withLogConsumer(
Slf4jLogConsumer(logger)
Slf4jLogConsumer(LoggerFactory.getLogger("GradleContainer[$dockerImageName]"))
)
withLogConsumer(toStringLogConsumer)
addFsBind("build/local-repository", "$projectPath/local-repository")
addFsBind("/var/run/docker.sock", "/var/run/docker.sock")
addFsBind("/var/run/docker.sock", dockerSocketPath)
withWorkingDirectory(projectPath)

withStartupCheckStrategy(
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,8 +31,7 @@ class JooqCodegenLoggingLevelsTest {
gradleContainer.stop()
}.isSuccess()

val output = gradleContainer.output.joinToString("\n")
that(output).apply {
that(gradleContainer.output).apply {
not().contains("Database version is older than what dialect POSTGRES supports")
}
}
Expand All @@ -54,8 +53,7 @@ class JooqCodegenLoggingLevelsTest {
gradleContainer.stop()
}.isSuccess()

val output = gradleContainer.output.joinToString("\n")
that(output).apply {
that(gradleContainer.output).apply {
contains("Database version is older than what dialect POSTGRES supports")
}
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -38,8 +38,7 @@ class PluginWorksArtifactTest {
gradleContainer.stop()
}.isSuccess()

val output = gradleContainer.output.joinToString("\n")
that(output).contains("BUILD SUCCESSFUL in ")
that(gradleContainer.output).contains("BUILD SUCCESSFUL in ")
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package dev.monosoul.jooq.artifact

import org.junit.jupiter.api.Test
import org.testcontainers.utility.MountableFile.forClasspathResource
import strikt.api.expect
import strikt.assertions.contains
import strikt.assertions.isSuccess

class WorksAfterFailingToFindDockerArtifactTest {

@Test
fun `should be possible to generate jooq classes even after it failed on first attempt`() {
// given
val gradleContainer = GradleContainer(dockerSocketPath = "/var/run/docker-alt.sock").apply {
withEnv("TESTCONTAINERS_RYUK_DISABLED", "true")
withCopyToContainer(forClasspathResource("/testproject"), projectPath)
withCopyToContainer(forClasspathResource("/gradle_run.sh"), "/gradle_run.sh")
withCommand("/gradle_run.sh")
}

// when & then
expect {
catching {
gradleContainer.start()
gradleContainer.stop()
}.isSuccess()

that(gradleContainer.output).contains("BUILD SUCCESSFUL in ")
}
}
}
8 changes: 8 additions & 0 deletions artifact-tests/src/test/resources/gradle_run.sh
Original file line number Diff line number Diff line change
@@ -0,0 +1,8 @@
#!/bin/sh

gradle classes --info --stacktrace

export DOCKER_HOST=unix:///var/run/docker-alt.sock
export TESTCONTAINERS_DOCKER_SOCKET_OVERRIDE=/var/run/docker-alt.sock

gradle classes --info --stacktrace
Original file line number Diff line number Diff line change
Expand Up @@ -6,16 +6,26 @@ import org.slf4j.LoggerFactory
import org.testcontainers.containers.JdbcDatabaseContainer
import org.testcontainers.containers.output.Slf4jLogConsumer
import org.testcontainers.containers.wait.strategy.HostPortWaitStrategy
import org.testcontainers.dockerclient.DockerClientProviderStrategy
import org.testcontainers.utility.DockerImageName
import java.sql.Driver
import java.util.concurrent.atomic.AtomicBoolean
import java.util.concurrent.locks.ReentrantLock
import kotlin.concurrent.withLock
import kotlin.reflect.KCallable
import kotlin.reflect.full.declaredMembers
import kotlin.reflect.jvm.isAccessible

class GenericDatabaseContainer(
private val image: Image,
private val database: Database.Internal,
private val jdbcAwareClassLoader: ClassLoader,
) : JdbcDatabaseContainer<GenericDatabaseContainer>(DockerImageName.parse(image.name)) {
private val image: Image,
private val database: Database.Internal,
private val jdbcAwareClassLoader: ClassLoader,
) : JdbcDatabaseContainer<GenericDatabaseContainer>(
image.let {
failFastAlways.set(false)
DockerImageName.parse(it.name)
}
) {

private val driverLoadLock = ReentrantLock()
private var driver: Driver? = null
Expand Down Expand Up @@ -67,4 +77,18 @@ class GenericDatabaseContainer(
else -> throw e
}
}

private companion object {
/**
* Workaround for https://github.com/testcontainers/testcontainers-java/issues/6441
*/
val failFastAlways = DockerClientProviderStrategy::class.declaredMembers
.single { it.name == "FAIL_FAST_ALWAYS" }
.apply { isAccessible = true }
.let {
@Suppress("UNCHECKED_CAST")
it as KCallable<AtomicBoolean>
}
.call()
}
}

0 comments on commit 249ac33

Please sign in to comment.