Skip to content
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 2 commits into from
Mar 8, 2021
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
8 changes: 5 additions & 3 deletions dependencies.gradle
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",
Expand All @@ -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",
Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

removing misk dependencies

Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yay!

"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",
Expand All @@ -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",
Copy link
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Strictly necessary?

Copy link
Collaborator Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

required by kotlin-logging

"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",
Expand Down
2 changes: 0 additions & 2 deletions samples/guides/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -7,9 +7,7 @@ dependencies {
implementation dep.kotlinStdLib
implementation dep.kotlinxCoroutines

testImplementation dep.miskAwsDynamodbTesting
testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -7,7 +7,6 @@ dependencies {
implementation dep.kotlinStdLib

testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
4 changes: 4 additions & 0 deletions samples/musiclibrary-testing/gradle.properties
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
2 changes: 0 additions & 2 deletions samples/musiclibrary/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -4,9 +4,7 @@ dependencies {
implementation project(":tempest")
implementation dep.kotlinStdLib

testImplementation dep.miskAwsDynamodbTesting
testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
1 change: 0 additions & 1 deletion samples/musiclibrary2/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,7 +5,6 @@ dependencies {
implementation dep.kotlinStdLib

testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
2 changes: 0 additions & 2 deletions samples/urlshortener/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -5,9 +5,7 @@ dependencies {
implementation dep.kotlinStdLib
implementation dep.clikt

testImplementation dep.miskAwsDynamodbTesting
testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
1 change: 0 additions & 1 deletion samples/urlshortener2/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,6 @@ dependencies {
implementation dep.clikt

testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
14 changes: 13 additions & 1 deletion settings.gradle
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'
1 change: 0 additions & 1 deletion tempest-internal/build.gradle
Original file line number Diff line number Diff line change
Expand Up @@ -8,7 +8,6 @@ dependencies {
implementation dep.kotlinStdLib

testImplementation dep.assertj
testImplementation dep.miskTesting
testImplementation dep.junitApi
testImplementation dep.junitEngine
}
Expand Down
22 changes: 22 additions & 0 deletions tempest-testing-docker/build.gradle
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")
}
4 changes: 4 additions & 0 deletions tempest-testing-docker/gradle.properties
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
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>()
}
}
Loading