-
Notifications
You must be signed in to change notification settings - Fork 33
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
Add tempest testing #33
Merged
Merged
Changes from all commits
Commits
Show all changes
2 commits
Select commit
Hold shift + click to select a range
File filter
Filter by extension
Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
There are no files selected for viewing
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,14 +1,18 @@ | ||
// Auto-generated from polyrepo's master-dependencies.json. Update via polyrepo dep-add and polyrepo dep-upgrade. | ||
ext.dep = [ | ||
"assertj": "org.assertj:assertj-core:3.18.1", | ||
"awsDynamodbLocal": "com.amazonaws:DynamoDBLocal:1.13.5", | ||
"aws2Dynamodb": "software.amazon.awssdk:dynamodb:2.15.66", | ||
"aws2DynamodbEnhanced": "software.amazon.awssdk:dynamodb-enhanced:2.15.66", | ||
"awsDynamodb": "com.amazonaws:aws-java-sdk-dynamodb:1.11.774", | ||
"clikt": "com.github.ajalt:clikt:2.8.0", | ||
"docker": "com.github.docker-java:docker-java:3.2.1", | ||
"findbugsJsr305": "com.google.code.findbugs:jsr305:3.0.2", | ||
"grpcBom": "io.grpc:grpc-bom:1.21.0", | ||
"guava": "com.google.guava:guava:30.0-jre", | ||
"guice": "com.google.inject:guice:4.2.3", | ||
"jCommander": "com.beust:jcommander:1.72", | ||
"junit4Api": "junit:junit:4.13", | ||
"junitApi": "org.junit.jupiter:junit-jupiter-api:5.7.0", | ||
"junitEngine": "org.junit.jupiter:junit-jupiter-engine:5.7.0", | ||
"junitGradlePlugin": "org.junit.platform:junit-platform-gradle-plugin:1.2.0", | ||
|
@@ -20,9 +24,6 @@ ext.dep = [ | |
"ktlintVersion": "0.40.0", | ||
"loggingApi": "io.github.microutils:kotlin-logging:1.7.9", | ||
"mavenPublishGradlePlugin": "com.vanniktech:gradle-maven-publish-plugin:0.12.0", | ||
"miskAws2DynamodbTesting": "com.squareup.misk:misk-aws2-dynamodb-testing:0.17.0-20210210.0438-e43f047", | ||
"miskAwsDynamodbTesting": "com.squareup.misk:misk-aws-dynamodb-testing:0.17.0-20210210.0438-e43f047", | ||
"miskTesting": "com.squareup.misk:misk-testing:0.17.0-20210210.0438-e43f047", | ||
"moshiCore": "com.squareup.moshi:moshi:1.11.0", | ||
"nettyBom": "io.netty:netty-bom:4.1.34.Final", | ||
"okHttp": "com.squareup.okhttp3:okhttp:4.10.0-RC1", | ||
|
@@ -31,6 +32,7 @@ ext.dep = [ | |
"retrofit": "com.squareup.retrofit2:retrofit:2.9.0", | ||
"retrofitWire": "com.squareup.retrofit2:converter-wire:2.9.0", | ||
"shadowJarPlugin": "com.github.jengelman.gradle.plugins:shadow:6.1.0", | ||
"log4jCore": "org.apache.logging.log4j:log4j-core:2.14.0", | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Strictly necessary? There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. required by |
||
"spotlessPlugin": "com.diffplug.spotless:spotless-plugin-gradle:4.5.1", | ||
"wireGradlePlugin": "com.squareup.wire:wire-gradle-plugin:3.6.0", | ||
"wireGrpcClient": "com.squareup.wire:wire-grpc-client:3.6.0", | ||
|
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
POM_ARTIFACT_ID=musiclibrary-testing | ||
POM_NAME=musiclibrary-testing | ||
POM_DESCRIPTION=musiclibrary-testing | ||
POM_PACKAGING=jar |
File renamed without changes.
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,9 +1,21 @@ | ||
include 'tempest-internal' | ||
include 'tempest' | ||
include 'tempest-testing' | ||
include 'tempest-testing-internal' | ||
include 'tempest-testing-docker' | ||
include 'tempest-testing-jvm' | ||
include 'tempest-testing-junit4' | ||
include 'tempest-testing-junit5' | ||
include 'tempest2' | ||
include 'tempest2-testing' | ||
include 'tempest2-testing-internal' | ||
include 'tempest2-testing-docker' | ||
include 'tempest2-testing-jvm' | ||
include 'tempest2-testing-junit4' | ||
include 'tempest2-testing-junit5' | ||
include ':samples:guides' | ||
include ':samples:musiclibrary' | ||
include ':samples:musiclibrary2' | ||
include ':samples:musiclibrary-testing' | ||
include ':samples:urlshortener' | ||
include ':samples:urlshortener2' | ||
include 'test-utils' |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,22 @@ | ||
apply plugin: 'java-library' | ||
apply plugin: 'kotlin' | ||
|
||
dependencies { | ||
api project(":tempest-testing") | ||
implementation project(":tempest-testing-internal") | ||
implementation dep.kotlinStdLib | ||
// The docker-java we use in tests depends on an old version of junixsocket that depends on | ||
// log4j. We force it up a minor version in packages that use it. | ||
implementation('com.kohlschutter.junixsocket:junixsocket-native-common:2.3.2') { | ||
force = true | ||
} | ||
implementation('com.kohlschutter.junixsocket:junixsocket-common:2.3.2') { | ||
force = true | ||
} | ||
implementation dep.docker | ||
|
||
testImplementation dep.assertj | ||
testImplementation dep.junitEngine | ||
testImplementation project(":samples:urlshortener") | ||
testImplementation project(":tempest-testing-junit5") | ||
} |
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,4 @@ | ||
POM_ARTIFACT_ID=tempest-testing-docker | ||
POM_NAME=tempest-testing-docker | ||
POM_DESCRIPTION=tempest-testing-docker | ||
POM_PACKAGING=jar |
218 changes: 218 additions & 0 deletions
218
tempest-testing-docker/src/main/kotlin/app/cash/tempest/testing/Containers.kt
This file contains bidirectional Unicode text that may be interpreted or compiled differently than what appears below. To review, open the file in an editor that reveals hidden Unicode characters.
Learn more about bidirectional Unicode characters
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,218 @@ | ||
package app.cash.tempest.testing | ||
|
||
import app.cash.tempest.testing.internal.getLogger | ||
import com.github.dockerjava.api.DockerClient | ||
import com.github.dockerjava.api.command.CreateContainerCmd | ||
import com.github.dockerjava.api.exception.NotFoundException | ||
import com.github.dockerjava.api.model.Frame | ||
import com.github.dockerjava.core.DockerClientBuilder | ||
import com.github.dockerjava.core.async.ResultCallbackTemplate | ||
import com.github.dockerjava.core.command.PullImageResultCallback | ||
import com.github.dockerjava.core.command.WaitContainerResultCallback | ||
import com.github.dockerjava.netty.NettyDockerCmdExecFactory | ||
import java.util.concurrent.atomic.AtomicBoolean | ||
|
||
/** | ||
* A [Container] creates a Docker container for testing. | ||
* | ||
* Tests provide a lambda to build a [CreateContainerCmd]. The [createCmd] lambda must set | ||
* [CreateContainerCmd.withName] and [CreateContainerCmd.withImage]. All other fields are | ||
* optional. The [Composer] takes care of setting up the network. | ||
* | ||
* There may be a need to configure your container between the creation and start steps. | ||
* [beforeStartHook] provides you with an id to your container allowing you to | ||
* manipulate as necessary before the command/entrypoint is invoked. | ||
* | ||
* See [Composer] for an example. | ||
*/ | ||
internal data class Container( | ||
val createCmd: CreateContainerCmd.() -> Unit, | ||
val beforeStartHook: (docker: DockerClient, id: String) -> Unit | ||
) { | ||
constructor(createCmd: CreateContainerCmd.() -> Unit) : this(createCmd, { _, _ -> }) | ||
} | ||
|
||
/** | ||
* [Composer] composes many [Container]s together to use in a unit test. | ||
* | ||
* The [Container]s are networked using a dedicated Docker network. Tests need to expose ports | ||
* in order for the test to communicate with the containers over 127.0.0.1. | ||
* | ||
* The following example composes Kafka and Zookeeper containers for testing. Kafka is exposed | ||
* to the jUnit test via 127.0.0.1:9102. In this example, Zookeeper is not exposed to the test. | ||
* | ||
* ``` | ||
* val zkContainer = Container { | ||
* this | ||
* .withImage("confluentinc/cp-zookeeper") | ||
* .withName("zookeeper") | ||
* .withEnv("ZOOKEEPER_CLIENT_PORT=2181") | ||
* } | ||
* val kafka = Container { | ||
* this | ||
* .withImage("confluentinc/cp-kafka" | ||
* .withName("kafka") | ||
* .withExposedPorts(ExposedPort.tcp(port)) | ||
* .withPortBindings(Ports().apply { | ||
* bind(ExposedPort.tcp(9102), Ports.Binding.bindPort(9102)) | ||
* }) | ||
* .withEnv( | ||
* "KAFKA_ZOOKEEPER_CONNECT=zookeeper:2181", | ||
* "KAFKA_ADVERTISED_LISTENERS=PLAINTEXT://localhost:9102") | ||
* } | ||
* val composer = Composer("e-kafka", zkContainer, kafka) | ||
* composer.start() | ||
* ``` | ||
*/ | ||
internal class Composer(private val name: String, private vararg val containers: Container) { | ||
zhxnlai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
|
||
private val network = DockerNetwork( | ||
"$name-net", | ||
docker | ||
) | ||
private val containerIds = mutableMapOf<String, String>() | ||
private val running = AtomicBoolean(false) | ||
|
||
fun start() { | ||
if (!running.compareAndSet(false, true)) return | ||
Runtime.getRuntime().addShutdownHook(Thread { stop() }) | ||
|
||
network.start() | ||
|
||
for (container in containers) { | ||
val name = container.name() | ||
val create = docker.createContainerCmd("todo").apply(container.createCmd) | ||
zhxnlai marked this conversation as resolved.
Show resolved
Hide resolved
|
||
require(create.image != "todo") { | ||
"must provide an image for container ${create.name}" | ||
} | ||
|
||
docker.listContainersCmd() | ||
.withShowAll(true) | ||
.withLabelFilter(mapOf("name" to name)) | ||
.exec() | ||
.forEach { | ||
log.info { "removing previous $name container with id ${it.id}" } | ||
docker.removeContainerCmd(it.id).exec() | ||
} | ||
|
||
log.info { "pulling ${create.image} for $name container" } | ||
|
||
val imageParts = create.image!!.split(":") | ||
docker.pullImageCmd(imageParts[0]) | ||
.withTag(imageParts.getOrElse(1) { "latest" }) | ||
.exec(PullImageResultCallback()).awaitCompletion() | ||
|
||
log.info { "starting $name container" } | ||
|
||
val id = create | ||
.withNetworkMode(network.id()) | ||
.withLabels(mapOf("name" to name)) | ||
.withTty(true) | ||
.exec() | ||
.id | ||
containerIds[name] = id | ||
|
||
container.beforeStartHook(docker, id) | ||
|
||
docker.startContainerCmd(id).exec() | ||
docker.logContainerCmd(id) | ||
.withStdErr(true) | ||
.withStdOut(true) | ||
.withFollowStream(true) | ||
.withSince(0) | ||
.exec(LogContainerResultCallback()) | ||
.awaitStarted() | ||
|
||
log.info { "started $name; container id=$id" } | ||
} | ||
} | ||
|
||
private fun Container.name(): String { | ||
val create = docker.createContainerCmd("todo").apply(createCmd) | ||
require(!create.name.isNullOrBlank()) { | ||
"must provide a name for the container" | ||
} | ||
return "$name/${create.name}" | ||
} | ||
|
||
fun stop() { | ||
if (!running.compareAndSet(true, false)) return | ||
|
||
for (container in containers) { | ||
val name = container.name() | ||
val id = containerIds[name]!! | ||
log.info { "killing $name with container id $id" } | ||
docker.removeContainerCmd(id).withForce(true).exec() | ||
|
||
try { | ||
log.info { "waiting for $name to terminate" } | ||
docker.waitContainerCmd(id).exec( | ||
GracefulWaitContainerResultCallback() | ||
).awaitCompletion() | ||
} catch (th: Throwable) { | ||
log.error(th) { "could not kill $name with container id $id" } | ||
} | ||
|
||
log.info { "killed $name with container id $id" } | ||
} | ||
|
||
network.stop() | ||
} | ||
|
||
private class LogContainerResultCallback : ResultCallbackTemplate<LogContainerResultCallback, Frame>() { | ||
override fun onNext(item: Frame) { | ||
String(item.payload).trim().split('\r', '\n').filter { it.isNotBlank() }.forEach { | ||
log.info(it) | ||
} | ||
} | ||
} | ||
|
||
private class GracefulWaitContainerResultCallback : WaitContainerResultCallback() { | ||
override fun onError(throwable: Throwable?) { | ||
// this is ok, just meant that the container already terminated before we tried to wait | ||
if (throwable is NotFoundException) { | ||
return | ||
} | ||
super.onError(throwable) | ||
} | ||
} | ||
|
||
private companion object { | ||
private val log = getLogger<Composer>() | ||
private val docker: DockerClient = DockerClientBuilder.getInstance() | ||
.withDockerCmdExecFactory(NettyDockerCmdExecFactory()) | ||
.build() | ||
} | ||
} | ||
|
||
private class DockerNetwork(private val name: String, private val docker: DockerClient) { | ||
|
||
private lateinit var networkId: String | ||
|
||
fun id(): String { | ||
return networkId | ||
} | ||
|
||
fun start() { | ||
log.info { "creating $name network" } | ||
|
||
docker.listNetworksCmd().withNameFilter(name).exec().forEach { | ||
log.info { "removing previous $name network with id ${it.id}" } | ||
docker.removeNetworkCmd(it.id).exec() | ||
} | ||
networkId = docker.createNetworkCmd() | ||
.withName(name) | ||
.withCheckDuplicate(true) | ||
.exec() | ||
.id | ||
} | ||
|
||
fun stop() { | ||
log.info { "removing $name network with id $networkId" } | ||
docker.removeNetworkCmd(networkId).exec() | ||
} | ||
|
||
companion object { | ||
private val log = getLogger<DockerNetwork>() | ||
} | ||
} |
Oops, something went wrong.
Add this suggestion to a batch that can be applied as a single commit.
This suggestion is invalid because no changes were made to the code.
Suggestions cannot be applied while the pull request is closed.
Suggestions cannot be applied while viewing a subset of changes.
Only one suggestion per line can be applied in a batch.
Add this suggestion to a batch that can be applied as a single commit.
Applying suggestions on deleted lines is not supported.
You must change the existing code in this line in order to create a valid suggestion.
Outdated suggestions cannot be applied.
This suggestion has been applied or marked resolved.
Suggestions cannot be applied from pending reviews.
Suggestions cannot be applied on multi-line comments.
Suggestions cannot be applied while the pull request is queued to merge.
Suggestion cannot be applied right now. Please check back later.
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
removing misk dependencies
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
Yay!