You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@geode.apache.org by je...@apache.org on 2018/11/07 23:26:12 UTC
[geode] branch develop updated: GEODE-5995: Initial import of
gradle docker plugin (#2790)
This is an automated email from the ASF dual-hosted git repository.
jensdeppe pushed a commit to branch develop
in repository https://gitbox.apache.org/repos/asf/geode.git
The following commit(s) were added to refs/heads/develop by this push:
new 23af84c GEODE-5995: Initial import of gradle docker plugin (#2790)
23af84c is described below
commit 23af84c7d715316355c23757b4f0ee34f7f81776
Author: Jens Deppe <jd...@pivotal.io>
AuthorDate: Wed Nov 7 15:26:04 2018 -0800
GEODE-5995: Initial import of gradle docker plugin (#2790)
Also includes:
- Revert java version detection in original code (done for Java 9+ work).
- Handle case where network interface disappears during docker teardown
---
build.gradle | 1 -
buildSrc/build.gradle | 15 +
.../dockerizedtest/DefaultWorkerSemaphore.groovy | 71 +++
.../DockerizedJavaExecHandleBuilder.groovy | 100 +++
.../dockerizedtest/DockerizedTestExtension.groovy | 58 ++
.../dockerizedtest/DockerizedTestPlugin.groovy | 184 ++++++
.../ExitCodeTolerantExecHandle.groovy | 92 +++
.../plugins/dockerizedtest/WorkerSemaphore.groovy | 28 +
.../dockerizedtest/DockerizedExecHandle.java | 673 +++++++++++++++++++++
.../dockerizedtest/DockerizedExecHandleRunner.java | 101 ++++
.../ForciblyStoppableTestWorker.java | 45 ++
.../dockerizedtest/ForkingTestClassProcessor.java | 153 +++++
.../plugins/dockerizedtest/NoMemoryManager.java | 59 ++
.../plugins/dockerizedtest/TestExecuter.java | 116 ++++
.../com.github.pedjak.dockerized-test.properties | 1 +
15 files changed, 1696 insertions(+), 1 deletion(-)
diff --git a/build.gradle b/build.gradle
index 9b9a661..7db35d8 100755
--- a/build.gradle
+++ b/build.gradle
@@ -29,7 +29,6 @@ buildscript {
classpath 'org.sonarsource.scanner.gradle:sonarqube-gradle-plugin:2.6.2'
classpath "com.diffplug.spotless:spotless-plugin-gradle:3.10.0"
classpath "me.champeau.gradle:jmh-gradle-plugin:0.4.7"
- classpath "com.pedjak.gradle.plugins:dockerized-test:0.5.6.35-SNAPSHOT"
classpath 'com.github.ben-manes:gradle-versions-plugin:0.17.0'
classpath "com.netflix.nebula:nebula-project-plugin:4.0.1"
}
diff --git a/buildSrc/build.gradle b/buildSrc/build.gradle
index 00c9903..819fc8a 100644
--- a/buildSrc/build.gradle
+++ b/buildSrc/build.gradle
@@ -18,15 +18,30 @@
repositories {
mavenCentral()
+ maven { url "http://geode-maven.s3-website-us-west-2.amazonaws.com" }
}
dependencies {
compile (group: 'org.apache.geode', name: 'geode-junit', version: '1.3.0') {
exclude group: 'org.apache.logging.log4j'
}
+ compile gradleApi()
+ compile 'org.apache.commons:commons-lang3:3.3.2'
+ compile 'org.apache.maven:maven-artifact:3.3.3'
+ compile 'com.github.docker-java:docker-java:3.1.2-GEODE'
compile group: 'junit', name: 'junit', version: '4.12'
testAnnotationProcessor this.project
+}
+sourceSets {
+ main {
+ java {
+ srcDirs = []
+ }
+ groovy {
+ srcDirs += ['src/main/java']
+ }
+ }
}
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DefaultWorkerSemaphore.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DefaultWorkerSemaphore.groovy
new file mode 100644
index 0000000..a42c4a1
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DefaultWorkerSemaphore.groovy
@@ -0,0 +1,71 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import org.gradle.api.Project
+import org.gradle.api.tasks.testing.Test
+
+import java.util.concurrent.Semaphore
+
+class DefaultWorkerSemaphore implements WorkerSemaphore {
+ private int maxWorkers = Integer.MAX_VALUE
+ private Semaphore semaphore
+ private logger
+
+ @Override
+ void acquire() {
+ semaphore().acquire()
+ logger.debug("Semaphore acquired, available: {}/{}", semaphore().availablePermits(), maxWorkers)
+ }
+
+ @Override
+ void release() {
+ semaphore().release()
+ logger.debug("Semaphore released, available: {}/{}", semaphore().availablePermits(), maxWorkers)
+ }
+
+ @Override
+ synchronized void applyTo(Project project) {
+ if (semaphore) return
+ if (!logger) {
+ logger = project.logger
+ }
+
+ maxWorkers = project.tasks.withType(Test).findAll {
+ it.extensions.docker?.image != null
+ }.collect {
+ def v = it.maxParallelForks
+ it.maxParallelForks = 10000
+ v
+ }.min() ?: 1
+ semaphore()
+ }
+
+ private synchronized setMaxWorkers(int num) {
+ if (this.@maxWorkers > num) {
+ this.@maxWorkers = num
+ }
+ }
+
+ private synchronized Semaphore semaphore() {
+ if (semaphore == null) {
+ semaphore = new Semaphore(maxWorkers)
+ logger.lifecycle("Do not allow more than {} test workers", maxWorkers)
+ }
+ semaphore
+ }
+}
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedJavaExecHandleBuilder.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedJavaExecHandleBuilder.groovy
new file mode 100644
index 0000000..70ac705
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedJavaExecHandleBuilder.groovy
@@ -0,0 +1,100 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import com.pedjak.gradle.plugins.dockerizedtest.DockerizedTestExtension
+import com.pedjak.gradle.plugins.dockerizedtest.ExitCodeTolerantExecHandle
+import com.pedjak.gradle.plugins.dockerizedtest.WorkerSemaphore
+import org.gradle.api.internal.file.FileResolver
+import org.gradle.initialization.BuildCancellationToken
+import org.gradle.process.internal.*
+import org.gradle.process.internal.streams.OutputStreamsForwarder
+
+import java.util.concurrent.Executor
+
+class DockerizedJavaExecHandleBuilder extends JavaExecHandleBuilder {
+
+ def streamsHandler
+ def executor
+ def buildCancellationToken
+ private final DockerizedTestExtension extension
+
+ private final WorkerSemaphore workersSemaphore
+
+ DockerizedJavaExecHandleBuilder(DockerizedTestExtension extension, FileResolver fileResolver, Executor executor, BuildCancellationToken buildCancellationToken, WorkerSemaphore workersSemaphore) {
+ super(fileResolver, executor, buildCancellationToken)
+ this.extension = extension
+ this.executor = executor
+ this.buildCancellationToken = buildCancellationToken
+ this.workersSemaphore = workersSemaphore
+ }
+
+ def StreamsHandler getStreamsHandler() {
+ StreamsHandler effectiveHandler;
+ if (this.streamsHandler != null) {
+ effectiveHandler = this.streamsHandler;
+ } else {
+ boolean shouldReadErrorStream = !redirectErrorStream;
+ effectiveHandler = new OutputStreamsForwarder(standardOutput, errorOutput, shouldReadErrorStream);
+ }
+ return effectiveHandler;
+ }
+
+ ExecHandle build() {
+
+ return new ExitCodeTolerantExecHandle(new DockerizedExecHandle(extension, getDisplayName(),
+ getWorkingDir(),
+ 'java',
+ allArguments,
+ getActualEnvironment(),
+ getStreamsHandler(),
+ inputHandler,
+ listeners,
+ redirectErrorStream,
+ timeoutMillis,
+ daemon,
+ executor,
+ buildCancellationToken),
+ workersSemaphore)
+
+ }
+
+ def timeoutMillis = Integer.MAX_VALUE
+
+ @Override
+ AbstractExecHandleBuilder setTimeout(int timeoutMillis) {
+ this.timeoutMillis = timeoutMillis
+ return super.setTimeout(timeoutMillis)
+ }
+
+ boolean redirectErrorStream
+
+ @Override
+ AbstractExecHandleBuilder redirectErrorStream() {
+ redirectErrorStream = true
+ return super.redirectErrorStream()
+ }
+
+ def listeners = []
+
+ @Override
+ AbstractExecHandleBuilder listener(ExecHandleListener listener) {
+ listeners << listener
+ return super.listener(listener)
+ }
+
+}
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestExtension.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestExtension.groovy
new file mode 100644
index 0000000..8a007f1
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestExtension.groovy
@@ -0,0 +1,58 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import com.github.dockerjava.api.DockerClient
+
+class DockerizedTestExtension {
+
+ String image
+ Map volumes
+ String user
+
+ Closure beforeContainerCreate
+
+ Closure afterContainerCreate
+
+ Closure beforeContainerStart
+
+ Closure afterContainerStart
+
+ Closure afterContainerStop = { containerId, client ->
+ try {
+ client.removeContainerCmd(containerId).exec();
+ } catch (Exception e) {
+ // ignore any error
+ }
+ }
+
+ // could be a DockerClient instance or a closure that returns a DockerClient instance
+ private def clientOrClosure
+
+ void setClient(clientOrClosure) {
+ this.clientOrClosure = clientOrClosure
+ }
+
+ DockerClient getClient() {
+ if (clientOrClosure == null) return null
+ if (DockerClient.class.isAssignableFrom(clientOrClosure.getClass())) {
+ return (DockerClient) clientOrClosure;
+ } else {
+ return (DockerClient) ((Closure) clientOrClosure).call();
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestPlugin.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestPlugin.groovy
new file mode 100644
index 0000000..84d5afc
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/DockerizedTestPlugin.groovy
@@ -0,0 +1,184 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import com.github.dockerjava.api.DockerClient
+import com.github.dockerjava.core.DefaultDockerClientConfig
+import com.github.dockerjava.core.DockerClientBuilder
+import com.github.dockerjava.netty.NettyDockerCmdExecFactory
+import org.apache.commons.lang3.SystemUtils
+import org.apache.maven.artifact.versioning.ComparableVersion
+import org.gradle.api.Action
+import org.gradle.api.GradleException
+import org.gradle.api.Plugin
+import org.gradle.api.Project
+import org.gradle.api.tasks.testing.Test
+import org.gradle.initialization.DefaultBuildCancellationToken
+import org.gradle.internal.concurrent.DefaultExecutorFactory
+import org.gradle.internal.concurrent.ExecutorFactory
+import org.gradle.internal.operations.BuildOperationExecutor
+import org.gradle.internal.remote.Address
+import org.gradle.internal.remote.ConnectionAcceptor
+import org.gradle.internal.remote.MessagingServer
+import org.gradle.internal.remote.ObjectConnection
+import org.gradle.internal.remote.internal.ConnectCompletion
+import org.gradle.internal.remote.internal.IncomingConnector
+import org.gradle.internal.remote.internal.hub.MessageHubBackedObjectConnection
+import org.gradle.internal.remote.internal.inet.MultiChoiceAddress
+import org.gradle.internal.time.Clock
+import org.gradle.process.internal.JavaExecHandleFactory
+import org.gradle.process.internal.worker.DefaultWorkerProcessFactory
+
+import javax.inject.Inject
+
+class DockerizedTestPlugin implements Plugin<Project> {
+
+ def supportedVersion = '4.8'
+ def currentUser
+ def messagingServer
+ def static workerSemaphore = new DefaultWorkerSemaphore()
+ def memoryManager = new com.pedjak.gradle.plugins.dockerizedtest.NoMemoryManager()
+
+ @Inject
+ DockerizedTestPlugin(MessagingServer messagingServer) {
+ this.currentUser = SystemUtils.IS_OS_WINDOWS ? "0" : "id -u".execute().text.trim()
+ this.messagingServer = new MessageServer(messagingServer.connector, messagingServer.executorFactory)
+ }
+
+ void configureTest(project, test) {
+ def ext = test.extensions.create("docker", DockerizedTestExtension, [] as Object[])
+ def startParameter = project.gradle.startParameter
+ ext.volumes = ["$startParameter.gradleUserHomeDir": "$startParameter.gradleUserHomeDir",
+ "$project.projectDir" : "$project.projectDir"]
+ ext.user = currentUser
+ test.doFirst {
+ def extension = test.extensions.docker
+
+ if (extension?.image) {
+
+ workerSemaphore.applyTo(test.project)
+ test.testExecuter = new com.pedjak.gradle.plugins.dockerizedtest.TestExecuter(newProcessBuilderFactory(project, extension, test.processBuilderFactory), actorFactory, moduleRegistry, services.get(BuildOperationExecutor), services.get(Clock));
+
+ if (!extension.client) {
+ extension.client = createDefaultClient()
+ }
+ }
+
+ }
+ }
+
+ DockerClient createDefaultClient() {
+ DockerClientBuilder.getInstance(DefaultDockerClientConfig.createDefaultConfigBuilder())
+ .withDockerCmdExecFactory(new NettyDockerCmdExecFactory())
+ .build()
+ }
+
+ void apply(Project project) {
+
+ boolean unsupportedVersion = new ComparableVersion(project.gradle.gradleVersion).compareTo(new ComparableVersion(supportedVersion)) < 0
+ if (unsupportedVersion) throw new GradleException("dockerized-test plugin requires Gradle ${supportedVersion}+")
+
+ project.tasks.withType(Test).each { test -> configureTest(project, test) }
+ project.tasks.whenTaskAdded { task ->
+ if (task instanceof Test) configureTest(project, task)
+ }
+ }
+
+ def newProcessBuilderFactory(project, extension, defaultProcessBuilderFactory) {
+
+ def executorFactory = new DefaultExecutorFactory()
+ def executor = executorFactory.create("Docker container link")
+ def buildCancellationToken = new DefaultBuildCancellationToken()
+
+ def execHandleFactory = [newJavaExec: { ->
+ new com.pedjak.gradle.plugins.dockerizedtest.DockerizedJavaExecHandleBuilder(extension, project.fileResolver, executor, buildCancellationToken, workerSemaphore)
+ }] as JavaExecHandleFactory
+ new DefaultWorkerProcessFactory(defaultProcessBuilderFactory.loggingManager,
+ messagingServer,
+ defaultProcessBuilderFactory.workerImplementationFactory.classPathRegistry,
+ defaultProcessBuilderFactory.idGenerator,
+ defaultProcessBuilderFactory.gradleUserHomeDir,
+ defaultProcessBuilderFactory.workerImplementationFactory.temporaryFileProvider,
+ execHandleFactory,
+ defaultProcessBuilderFactory.workerImplementationFactory.jvmVersionDetector,
+ defaultProcessBuilderFactory.outputEventListener,
+ memoryManager
+ )
+ }
+
+ class MessageServer implements MessagingServer {
+ def IncomingConnector connector;
+ def ExecutorFactory executorFactory;
+
+ public MessageServer(IncomingConnector connector, ExecutorFactory executorFactory) {
+ this.connector = connector;
+ this.executorFactory = executorFactory;
+ }
+
+ public ConnectionAcceptor accept(Action<ObjectConnection> action) {
+ return new ConnectionAcceptorDelegate(connector.accept(new ConnectEventAction(action, executorFactory), true))
+ }
+
+
+ }
+
+ class ConnectEventAction implements Action<ConnectCompletion> {
+ def action;
+ def executorFactory;
+
+ public ConnectEventAction(Action<ObjectConnection> action, executorFactory) {
+ this.executorFactory = executorFactory
+ this.action = action
+ }
+
+ public void execute(ConnectCompletion completion) {
+ action.execute(new MessageHubBackedObjectConnection(executorFactory, completion));
+ }
+ }
+
+ class ConnectionAcceptorDelegate implements ConnectionAcceptor {
+
+ MultiChoiceAddress address
+
+ @Delegate
+ ConnectionAcceptor delegate
+
+ ConnectionAcceptorDelegate(ConnectionAcceptor delegate) {
+ this.delegate = delegate
+ }
+
+ Address getAddress() {
+ synchronized (delegate)
+ {
+ if (address == null) {
+ def remoteAddresses = NetworkInterface.networkInterfaces.findAll {
+ try {
+ return it.up && !it.loopback
+ } catch (SocketException ex) {
+ logger.warn("Unable to inspect interface " + it)
+ return false
+ }
+ }*.inetAddresses*.collect { it }.flatten()
+ def original = delegate.address
+ address = new MultiChoiceAddress(original.canonicalAddress, original.port, remoteAddresses)
+ }
+ }
+ address
+ }
+ }
+
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/ExitCodeTolerantExecHandle.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/ExitCodeTolerantExecHandle.groovy
new file mode 100644
index 0000000..ab1c6f1
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/ExitCodeTolerantExecHandle.groovy
@@ -0,0 +1,92 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import com.pedjak.gradle.plugins.dockerizedtest.WorkerSemaphore
+import org.gradle.process.ExecResult
+import org.gradle.process.internal.ExecException
+import org.gradle.process.internal.ExecHandle
+import org.gradle.process.internal.ExecHandleListener
+
+/**
+ * All exit codes are normal
+ */
+class ExitCodeTolerantExecHandle implements ExecHandle {
+
+ private final WorkerSemaphore testWorkerSemaphore
+
+ @Delegate
+ private final ExecHandle delegate
+
+ ExitCodeTolerantExecHandle(ExecHandle delegate, WorkerSemaphore testWorkerSemaphore) {
+ this.delegate = delegate
+ this.testWorkerSemaphore = testWorkerSemaphore
+ delegate.addListener(new ExecHandleListener() {
+
+ @Override
+ void executionStarted(ExecHandle execHandle) {
+ // do nothing
+ }
+
+ @Override
+ void executionFinished(ExecHandle execHandle, ExecResult execResult) {
+ testWorkerSemaphore.release()
+ }
+ })
+ }
+
+ ExecHandle start() {
+ testWorkerSemaphore.acquire()
+ try {
+ delegate.start()
+ } catch (Exception e) {
+ testWorkerSemaphore.release()
+ throw e
+ }
+ }
+
+ private static class ExitCodeTolerantExecResult implements ExecResult {
+
+ @Delegate
+ private final ExecResult delegate
+
+ ExitCodeTolerantExecResult(ExecResult delegate) {
+ this.delegate = delegate
+ }
+
+ ExecResult assertNormalExitValue() throws ExecException {
+ // no op because we are perfectly ok if the exit code is anything
+ // because Docker can complain about not being able to remove the used image
+ // although the tests completed fine
+ this
+ }
+ }
+
+ private static class ExecHandleListenerFacade implements ExecHandleListener {
+
+ @Delegate
+ private final ExecHandleListener delegate
+
+ ExecHandleListenerFacade(ExecHandleListener delegate) {
+ this.delegate = delegate
+ }
+
+ void executionFinished(ExecHandle execHandle, ExecResult execResult) {
+ delegate.executionFinished(execHandle, new ExitCodeTolerantExecResult(execResult))
+ }
+ }
+}
diff --git a/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/WorkerSemaphore.groovy b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/WorkerSemaphore.groovy
new file mode 100644
index 0000000..0268088
--- /dev/null
+++ b/buildSrc/src/main/groovy/com/pedjak/gradle/plugins/dockerizedtest/WorkerSemaphore.groovy
@@ -0,0 +1,28 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest
+
+import org.gradle.api.Project
+
+interface WorkerSemaphore {
+
+ void acquire()
+
+ void release()
+
+ void applyTo(Project project)
+}
\ No newline at end of file
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandle.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandle.java
new file mode 100755
index 0000000..11e3ac6
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandle.java
@@ -0,0 +1,673 @@
+/*
+ * Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import static java.lang.String.format;
+
+import java.io.File;
+import java.io.InputStream;
+import java.io.OutputStream;
+import java.io.PipedInputStream;
+import java.io.PipedOutputStream;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.Iterator;
+import java.util.List;
+import java.util.Map;
+import java.util.concurrent.CountDownLatch;
+import java.util.concurrent.Executor;
+import java.util.concurrent.TimeUnit;
+import java.util.concurrent.atomic.AtomicInteger;
+import java.util.concurrent.locks.Condition;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import javax.annotation.Nullable;
+
+import com.github.dockerjava.api.DockerClient;
+import com.github.dockerjava.api.command.CreateContainerCmd;
+import com.github.dockerjava.api.model.Frame;
+import com.github.dockerjava.api.model.StreamType;
+import com.github.dockerjava.api.model.WaitResponse;
+import com.github.dockerjava.core.command.AttachContainerResultCallback;
+import com.github.dockerjava.core.command.WaitContainerResultCallback;
+import com.github.dockerjava.api.model.Bind;
+import com.github.dockerjava.api.model.Volume;
+import com.google.common.base.Joiner;
+import groovy.lang.Closure;
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.initialization.BuildCancellationToken;
+import org.gradle.internal.UncheckedException;
+import org.gradle.internal.event.ListenerBroadcast;
+import org.gradle.internal.operations.CurrentBuildOperationPreservingRunnable;
+import org.gradle.process.ExecResult;
+import org.gradle.process.internal.ExecException;
+import org.gradle.process.internal.ExecHandle;
+import org.gradle.process.internal.ExecHandleListener;
+import org.gradle.process.internal.ExecHandleShutdownHookAction;
+import org.gradle.process.internal.ExecHandleState;
+import org.gradle.process.internal.ProcessSettings;
+import org.gradle.process.internal.StreamsHandler;
+import org.gradle.process.internal.shutdown.ShutdownHookActionRegister;
+
+/**
+ * Default implementation for the ExecHandle interface.
+ *
+ * <h3>State flows</h3>
+ *
+ * <ul>
+ * <li>INIT -> STARTED -> [SUCCEEDED|FAILED|ABORTED|DETACHED]</li>
+ * <li>INIT -> FAILED</li>
+ * <li>INIT -> STARTED -> DETACHED -> ABORTED</li>
+ * </ul>
+ *
+ * State is controlled on all control methods:
+ * <ul>
+ * <li>{@link #start()} allowed when state is INIT</li>
+ * <li>{@link #abort()} allowed when state is STARTED or DETACHED</li>
+ * </ul>
+ */
+public class DockerizedExecHandle implements ExecHandle, ProcessSettings {
+
+ private static final Logger LOGGER = Logging.getLogger(DockerizedExecHandle.class);
+
+ private final String displayName;
+
+ /**
+ * The working directory of the process.
+ */
+ private final File directory;
+
+ /**
+ * The executable to run.
+ */
+ private final String command;
+
+ /**
+ * Arguments to pass to the executable.
+ */
+ private final List<String> arguments;
+
+ /**
+ * The variables to set in the environment the executable is run in.
+ */
+ private final Map<String, String> environment;
+ private final StreamsHandler outputHandler;
+ private final StreamsHandler inputHandler;
+ private final boolean redirectErrorStream;
+ private int timeoutMillis;
+ private boolean daemon;
+
+ /**
+ * Lock to guard all mutable state
+ */
+ private final Lock lock;
+ private final Condition stateChanged;
+
+ private final Executor executor;
+
+ /**
+ * State of this ExecHandle.
+ */
+ private ExecHandleState state;
+
+ /**
+ * When not null, the runnable that is waiting
+ */
+ private DockerizedExecHandleRunner execHandleRunner;
+
+ private ExecResultImpl execResult;
+
+ private final ListenerBroadcast<ExecHandleListener> broadcast;
+
+ private final ExecHandleShutdownHookAction shutdownHookAction;
+
+ private final BuildCancellationToken buildCancellationToken;
+
+ private final DockerizedTestExtension testExtension;
+
+ public DockerizedExecHandle(DockerizedTestExtension testExtension, String displayName,
+ File directory, String command, List<String> arguments,
+ Map<String, String> environment, StreamsHandler outputHandler,
+ StreamsHandler inputHandler,
+ List<ExecHandleListener> listeners, boolean redirectErrorStream,
+ int timeoutMillis, boolean daemon,
+ Executor executor, BuildCancellationToken buildCancellationToken) {
+ this.displayName = displayName;
+ this.directory = directory;
+ this.command = command;
+ this.arguments = arguments;
+ this.environment = environment;
+ this.outputHandler = outputHandler;
+ this.inputHandler = inputHandler;
+ this.redirectErrorStream = redirectErrorStream;
+ this.timeoutMillis = timeoutMillis;
+ this.daemon = daemon;
+ this.executor = executor;
+ this.lock = new ReentrantLock();
+ this.stateChanged = lock.newCondition();
+ this.state = ExecHandleState.INIT;
+ this.buildCancellationToken = buildCancellationToken;
+ this.testExtension = testExtension;
+ shutdownHookAction = new ExecHandleShutdownHookAction(this);
+ broadcast = new ListenerBroadcast<ExecHandleListener>(ExecHandleListener.class);
+ broadcast.addAll(listeners);
+ }
+
+ public File getDirectory() {
+ return directory;
+ }
+
+ public String getCommand() {
+ return command;
+ }
+
+ public boolean isDaemon() {
+ return daemon;
+ }
+
+ @Override
+ public String toString() {
+ return displayName;
+ }
+
+ public List<String> getArguments() {
+ return Collections.unmodifiableList(arguments);
+ }
+
+ public Map<String, String> getEnvironment() {
+ return Collections.unmodifiableMap(environment);
+ }
+
+ public ExecHandleState getState() {
+ lock.lock();
+ try {
+ return state;
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void setState(ExecHandleState state) {
+ lock.lock();
+ try {
+ LOGGER.debug("Changing state to: {}", state);
+ this.state = state;
+ this.stateChanged.signalAll();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private boolean stateIn(ExecHandleState... states) {
+ lock.lock();
+ try {
+ return Arrays.asList(states).contains(this.state);
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ private void setEndStateInfo(ExecHandleState newState, int exitValue, Throwable failureCause) {
+ ShutdownHookActionRegister.removeAction(shutdownHookAction);
+ buildCancellationToken.removeCallback(shutdownHookAction);
+ ExecHandleState currentState;
+ lock.lock();
+ try {
+ currentState = this.state;
+ } finally {
+ lock.unlock();
+ }
+
+ ExecResultImpl
+ newResult =
+ new ExecResultImpl(exitValue, execExceptionFor(failureCause, currentState), displayName);
+ if (!currentState.isTerminal() && newState != ExecHandleState.DETACHED) {
+ try {
+ broadcast.getSource().executionFinished(this, newResult);
+ } catch (Exception e) {
+ newResult = new ExecResultImpl(exitValue, execExceptionFor(e, currentState), displayName);
+ }
+ }
+
+ lock.lock();
+ try {
+ setState(newState);
+ this.execResult = newResult;
+ } finally {
+ lock.unlock();
+ }
+
+ LOGGER.debug("Process '{}' finished with exit value {} (state: {})", displayName, exitValue,
+ newState);
+ }
+
+ @Nullable
+ private ExecException execExceptionFor(Throwable failureCause, ExecHandleState currentState) {
+ return failureCause != null
+ ? new ExecException(failureMessageFor(currentState), failureCause)
+ : null;
+ }
+
+ private String failureMessageFor(ExecHandleState currentState) {
+ return currentState == ExecHandleState.STARTING
+ ? format("A problem occurred starting process '%s'", displayName)
+ : format("A problem occurred waiting for process '%s' to complete.", displayName);
+ }
+
+ public ExecHandle start() {
+ LOGGER.info("Starting process '{}'. Working directory: {} Command: {}",
+ displayName, directory, command + ' ' + Joiner.on(' ').useForNull("null").join(arguments));
+ if (LOGGER.isDebugEnabled()) {
+ LOGGER.debug("Environment for process '{}': {}", displayName, environment);
+ }
+ lock.lock();
+ try {
+ if (!stateIn(ExecHandleState.INIT)) {
+ throw new IllegalStateException(
+ format("Cannot start process '%s' because it has already been started", displayName));
+ }
+ setState(ExecHandleState.STARTING);
+
+ execHandleRunner =
+ new DockerizedExecHandleRunner(this, new CompositeStreamsHandler(), executor);
+ executor.execute(new CurrentBuildOperationPreservingRunnable(execHandleRunner));
+
+ while (stateIn(ExecHandleState.STARTING)) {
+ LOGGER.debug("Waiting until process started: {}.", displayName);
+ try {
+ if (!stateChanged.await(30, TimeUnit.SECONDS)) {
+ execHandleRunner.abortProcess();
+ throw new RuntimeException("Giving up on " + execHandleRunner);
+ }
+ } catch (InterruptedException e) {
+ //ok, wrapping up
+ }
+ }
+
+ if (execResult != null) {
+ execResult.rethrowFailure();
+ }
+
+ LOGGER.info("Successfully started process '{}'", displayName);
+ } finally {
+ lock.unlock();
+ }
+ return this;
+ }
+
+ public void abort() {
+ lock.lock();
+ try {
+ if (stateIn(ExecHandleState.SUCCEEDED, ExecHandleState.FAILED, ExecHandleState.ABORTED)) {
+ return;
+ }
+ if (!stateIn(ExecHandleState.STARTED, ExecHandleState.DETACHED)) {
+ throw new IllegalStateException(
+ format("Cannot abort process '%s' because it is not in started or detached state",
+ displayName));
+ }
+ this.execHandleRunner.abortProcess();
+ this.waitForFinish();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public ExecResult waitForFinish() {
+ lock.lock();
+ try {
+ while (!state.isTerminal()) {
+ try {
+ stateChanged.await();
+ } catch (InterruptedException e) {
+ //ok, wrapping up...
+ throw UncheckedException.throwAsUncheckedException(e);
+ }
+ }
+ } finally {
+ lock.unlock();
+ }
+
+ // At this point:
+ // If in daemon mode, the process has started successfully and all streams to the process have been closed
+ // If in fork mode, the process has completed and all cleanup has been done
+ // In both cases, all asynchronous work for the process has completed and we're done
+
+ return result();
+ }
+
+ private ExecResult result() {
+ lock.lock();
+ try {
+ return execResult.rethrowFailure();
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ void detached() {
+ setEndStateInfo(ExecHandleState.DETACHED, 0, null);
+ }
+
+ void started() {
+ ShutdownHookActionRegister.addAction(shutdownHookAction);
+ buildCancellationToken.addCallback(shutdownHookAction);
+ setState(ExecHandleState.STARTED);
+ broadcast.getSource().executionStarted(this);
+ }
+
+ void finished(int exitCode) {
+ if (exitCode != 0) {
+ setEndStateInfo(ExecHandleState.FAILED, exitCode, null);
+ } else {
+ setEndStateInfo(ExecHandleState.SUCCEEDED, 0, null);
+ }
+ }
+
+ void aborted(int exitCode) {
+ if (exitCode == 0) {
+ // This can happen on Windows
+ exitCode = -1;
+ }
+ setEndStateInfo(ExecHandleState.ABORTED, exitCode, null);
+ }
+
+ void failed(Throwable failureCause) {
+ setEndStateInfo(ExecHandleState.FAILED, -1, failureCause);
+ }
+
+ public void addListener(ExecHandleListener listener) {
+ broadcast.add(listener);
+ }
+
+ public void removeListener(ExecHandleListener listener) {
+ broadcast.remove(listener);
+ }
+
+ public String getDisplayName() {
+ return displayName;
+ }
+
+ public boolean getRedirectErrorStream() {
+ return redirectErrorStream;
+ }
+
+ public int getTimeout() {
+ return timeoutMillis;
+ }
+
+ public Process runContainer() {
+ try {
+ DockerClient client = testExtension.getClient();
+ CreateContainerCmd createCmd = client.createContainerCmd(testExtension.getImage().toString())
+ .withTty(false)
+ .withStdinOpen(true)
+ .withWorkingDir(directory.getAbsolutePath());
+
+ createCmd.withEnv(getEnv());
+
+ String user = testExtension.getUser();
+ if (user != null) {
+ createCmd.withUser(user);
+ }
+ bindVolumes(createCmd);
+ List<String> cmdLine = new ArrayList<String>();
+ cmdLine.add(command);
+ cmdLine.addAll(arguments);
+ createCmd.withCmd(cmdLine);
+
+ invokeIfNotNull(testExtension.getBeforeContainerCreate(), createCmd, client);
+ String containerId = createCmd.exec().getId();
+ invokeIfNotNull(testExtension.getAfterContainerCreate(), containerId, client);
+
+ invokeIfNotNull(testExtension.getBeforeContainerStart(), containerId, client);
+ client.startContainerCmd(containerId).exec();
+ invokeIfNotNull(testExtension.getAfterContainerStart(), containerId, client);
+
+ if (!client.inspectContainerCmd(containerId).exec().getState().getRunning()) {
+ throw new RuntimeException("Container " + containerId + " not running!");
+ }
+
+ Process
+ proc =
+ new DockerizedProcess(client, containerId, testExtension.getAfterContainerStop());
+
+ return proc;
+ } catch (Exception e) {
+ e.printStackTrace();
+ throw new RuntimeException(e);
+ }
+ }
+
+ private void invokeIfNotNull(Closure closure, Object... args) {
+ if (closure != null) {
+ int l = closure.getParameterTypes().length;
+ Object[] nargs;
+ if (l < args.length) {
+ nargs = new Object[l];
+ System.arraycopy(args, 0, nargs, 0, l);
+ } else {
+ nargs = args;
+ }
+ closure.call(nargs);
+ }
+ }
+
+ private List<String> getEnv() {
+ List<String> env = new ArrayList<String>();
+ for (Map.Entry<String, String> e : environment.entrySet()) {
+ env.add(e.getKey() + "=" + e.getValue());
+ }
+ return env;
+ }
+
+ private void bindVolumes(CreateContainerCmd cmd) {
+ List<Volume> volumes = new ArrayList<Volume>();
+ List<Bind> binds = new ArrayList<Bind>();
+ for (Iterator it = testExtension.getVolumes().entrySet().iterator(); it.hasNext(); ) {
+ Map.Entry<Object, Object> e = (Map.Entry<Object, Object>) it.next();
+ Volume volume = new Volume(e.getValue().toString());
+ Bind bind = new Bind(e.getKey().toString(), volume);
+ binds.add(bind);
+ volumes.add(volume);
+ }
+ cmd.withVolumes(volumes).withBinds(binds);
+ }
+
+ private static class ExecResultImpl implements ExecResult {
+ private final int exitValue;
+ private final ExecException failure;
+ private final String displayName;
+
+ ExecResultImpl(int exitValue, ExecException failure, String displayName) {
+ this.exitValue = exitValue;
+ this.failure = failure;
+ this.displayName = displayName;
+ }
+
+ public int getExitValue() {
+ return exitValue;
+ }
+
+ public ExecResult assertNormalExitValue() throws ExecException {
+ // all exit values are ok
+// if (exitValue != 0) {
+// throw new ExecException(format("Process '%s' finished with non-zero exit value %d", displayName, exitValue));
+// }
+ return this;
+ }
+
+ public ExecResult rethrowFailure() throws ExecException {
+ if (failure != null) {
+ throw failure;
+ }
+ return this;
+ }
+
+ @Override
+ public String toString() {
+ return "{exitValue=" + exitValue + ", failure=" + failure + "}";
+ }
+ }
+
+ private class CompositeStreamsHandler implements StreamsHandler {
+ @Override
+ public void connectStreams(Process process, String processName, Executor executor) {
+ inputHandler.connectStreams(process, processName, executor);
+ outputHandler.connectStreams(process, processName, executor);
+ }
+
+ @Override
+ public void start() {
+ inputHandler.start();
+ outputHandler.start();
+ }
+
+ @Override
+ public void stop() {
+ inputHandler.stop();
+ outputHandler.stop();
+ }
+ }
+
+ private class DockerizedProcess extends Process {
+
+ private final DockerClient dockerClient;
+ private final String containerId;
+ private final Closure afterContainerStop;
+
+ private final PipedOutputStream stdInWriteStream = new PipedOutputStream();
+ private final PipedInputStream stdOutReadStream = new PipedInputStream();
+ private final PipedInputStream stdErrReadStream = new PipedInputStream();
+ private final PipedInputStream stdInReadStream = new PipedInputStream(stdInWriteStream);
+ private final PipedOutputStream stdOutWriteStream = new PipedOutputStream(stdOutReadStream);
+ private final PipedOutputStream stdErrWriteStream = new PipedOutputStream(stdErrReadStream);
+
+ private final CountDownLatch finished = new CountDownLatch(1);
+ private AtomicInteger exitCode = new AtomicInteger();
+ private final AttachContainerResultCallback
+ attachContainerResultCallback =
+ new AttachContainerResultCallback() {
+ @Override
+ public void onNext(Frame frame) {
+ try {
+ if (frame.getStreamType().equals(StreamType.STDOUT)) {
+ stdOutWriteStream.write(frame.getPayload());
+ } else if (frame.getStreamType().equals(StreamType.STDERR)) {
+ stdErrWriteStream.write(frame.getPayload());
+ }
+ } catch (Exception e) {
+ LOGGER.error("Error while writing to stream:", e);
+ }
+ super.onNext(frame);
+ }
+ };
+
+ private final WaitContainerResultCallback
+ waitContainerResultCallback =
+ new WaitContainerResultCallback() {
+ @Override
+ public void onNext(WaitResponse waitResponse) {
+ exitCode.set(waitResponse.getStatusCode());
+ try {
+ attachContainerResultCallback.close();
+ attachContainerResultCallback.awaitCompletion();
+ stdOutWriteStream.close();
+ stdErrWriteStream.close();
+ } catch (Exception e) {
+ LOGGER.debug("Error by detaching streams", e);
+ } finally {
+ try {
+ invokeIfNotNull(afterContainerStop, containerId, dockerClient);
+ } catch (Exception e) {
+ LOGGER.debug("Exception thrown at invoking afterContainerStop", e);
+ } finally {
+ finished.countDown();
+ }
+
+ }
+
+
+ }
+ };
+
+ public DockerizedProcess(final DockerClient dockerClient, final String containerId,
+ final Closure afterContainerStop) throws Exception {
+ this.dockerClient = dockerClient;
+ this.containerId = containerId;
+ this.afterContainerStop = afterContainerStop;
+ attachStreams();
+ dockerClient.waitContainerCmd(containerId).exec(waitContainerResultCallback);
+ }
+
+ private void attachStreams() throws Exception {
+ dockerClient.attachContainerCmd(containerId)
+ .withFollowStream(true)
+ .withStdOut(true)
+ .withStdErr(true)
+ .withStdIn(stdInReadStream)
+ .exec(attachContainerResultCallback);
+ if (!attachContainerResultCallback.awaitStarted(10, TimeUnit.SECONDS)) {
+ LOGGER.warn("Not attached to container " + containerId + " within 10secs");
+ throw new RuntimeException("Not attached to container " + containerId + " within 10secs");
+ }
+ }
+
+ @Override
+ public OutputStream getOutputStream() {
+ return stdInWriteStream;
+ }
+
+ @Override
+ public InputStream getInputStream() {
+ return stdOutReadStream;
+ }
+
+ @Override
+ public InputStream getErrorStream() {
+ return stdErrReadStream;
+ }
+
+ @Override
+ public int waitFor() throws InterruptedException {
+ finished.await();
+ return exitCode.get();
+ }
+
+ @Override
+ public int exitValue() {
+ if (finished.getCount() > 0) {
+ throw new IllegalThreadStateException("docker process still running");
+ }
+ return exitCode.get();
+ }
+
+ @Override
+ public void destroy() {
+ dockerClient.killContainerCmd(containerId).exec();
+ }
+
+ @Override
+ public String toString() {
+ return "Container " + containerId + " on " + dockerClient.toString();
+ }
+ }
+
+}
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandleRunner.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandleRunner.java
new file mode 100644
index 0000000..c567830
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/DockerizedExecHandleRunner.java
@@ -0,0 +1,101 @@
+/*
+ * Copyright 2010 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import java.util.concurrent.Executor;
+import java.util.concurrent.locks.Lock;
+import java.util.concurrent.locks.ReentrantLock;
+
+import org.gradle.api.logging.Logger;
+import org.gradle.api.logging.Logging;
+import org.gradle.process.internal.StreamsHandler;
+
+public class DockerizedExecHandleRunner implements Runnable {
+ private static final Logger
+ LOGGER =
+ Logging.getLogger(org.gradle.process.internal.ExecHandleRunner.class);
+
+ private final DockerizedExecHandle execHandle;
+ private final Lock lock = new ReentrantLock();
+ private final Executor executor;
+
+ private Process process;
+ private boolean aborted;
+ private final StreamsHandler streamsHandler;
+
+ public DockerizedExecHandleRunner(DockerizedExecHandle execHandle, StreamsHandler streamsHandler,
+ Executor executor) {
+ this.executor = executor;
+ if (execHandle == null) {
+ throw new IllegalArgumentException("execHandle == null!");
+ }
+ this.streamsHandler = streamsHandler;
+ this.execHandle = execHandle;
+ }
+
+ public void abortProcess() {
+ lock.lock();
+ try {
+ aborted = true;
+ if (process != null) {
+ LOGGER.debug("Abort requested. Destroying process: {}.", execHandle.getDisplayName());
+ process.destroy();
+ }
+ } finally {
+ lock.unlock();
+ }
+ }
+
+ public void run() {
+ try {
+ process = execHandle.runContainer();
+ streamsHandler.connectStreams(process, execHandle.getDisplayName(), executor);
+
+ execHandle.started();
+
+ LOGGER.debug("waiting until streams are handled...");
+ streamsHandler.start();
+ if (execHandle.isDaemon()) {
+ streamsHandler.stop();
+ detached();
+ } else {
+ int exitValue = process.waitFor();
+ streamsHandler.stop();
+ completed(exitValue);
+ }
+ } catch (Throwable t) {
+ execHandle.failed(t);
+ }
+ }
+
+ private void completed(int exitValue) {
+ if (aborted) {
+ execHandle.aborted(exitValue);
+ } else {
+ execHandle.finished(exitValue);
+ }
+ }
+
+ private void detached() {
+ execHandle.detached();
+ }
+
+ public String toString() {
+ return "Handler for " + process.toString();
+ }
+}
+
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForciblyStoppableTestWorker.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForciblyStoppableTestWorker.java
new file mode 100644
index 0000000..fbbe48e
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForciblyStoppableTestWorker.java
@@ -0,0 +1,45 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import java.util.Timer;
+import java.util.TimerTask;
+import java.util.concurrent.TimeUnit;
+
+import org.gradle.api.internal.tasks.testing.WorkerTestClassProcessorFactory;
+import org.gradle.api.internal.tasks.testing.worker.TestWorker;
+
+public class ForciblyStoppableTestWorker extends TestWorker {
+ private static final int SHUTDOWN_TIMEOUT = 60; // secs
+
+ public ForciblyStoppableTestWorker(WorkerTestClassProcessorFactory factory) {
+ super(factory);
+ }
+
+ @Override
+ public void stop() {
+ new Timer(true).schedule(new TimerTask() {
+ @Override
+ public void run() {
+ System.err.println("Worker process did not shutdown gracefully within " + SHUTDOWN_TIMEOUT
+ + "s, forcing it now");
+ Runtime.getRuntime().halt(-100);
+ }
+ }, TimeUnit.SECONDS.toMillis(SHUTDOWN_TIMEOUT));
+ super.stop();
+ }
+}
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForkingTestClassProcessor.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForkingTestClassProcessor.java
new file mode 100644
index 0000000..d6f409b
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/ForkingTestClassProcessor.java
@@ -0,0 +1,153 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import java.io.File;
+import java.net.URL;
+import java.util.List;
+
+import org.gradle.api.Action;
+import org.gradle.api.internal.classpath.ModuleRegistry;
+import org.gradle.api.internal.tasks.testing.TestClassProcessor;
+import org.gradle.api.internal.tasks.testing.TestClassRunInfo;
+import org.gradle.api.internal.tasks.testing.TestResultProcessor;
+import org.gradle.api.internal.tasks.testing.WorkerTestClassProcessorFactory;
+import org.gradle.api.internal.tasks.testing.worker.RemoteTestClassProcessor;
+import org.gradle.api.internal.tasks.testing.worker.TestEventSerializer;
+import org.gradle.internal.remote.ObjectConnection;
+import org.gradle.process.JavaForkOptions;
+import org.gradle.process.internal.worker.WorkerProcess;
+import org.gradle.process.internal.worker.WorkerProcessBuilder;
+import org.gradle.process.internal.worker.WorkerProcessFactory;
+import org.gradle.util.CollectionUtils;
+
+public class ForkingTestClassProcessor implements TestClassProcessor {
+ private final WorkerProcessFactory workerFactory;
+ private final WorkerTestClassProcessorFactory processorFactory;
+ private final JavaForkOptions options;
+ private final Iterable<File> classPath;
+ private final Action<WorkerProcessBuilder> buildConfigAction;
+ private final ModuleRegistry moduleRegistry;
+ private RemoteTestClassProcessor remoteProcessor;
+ private WorkerProcess workerProcess;
+ private TestResultProcessor resultProcessor;
+
+ public ForkingTestClassProcessor(WorkerProcessFactory workerFactory,
+ WorkerTestClassProcessorFactory processorFactory,
+ JavaForkOptions options, Iterable<File> classPath,
+ Action<WorkerProcessBuilder> buildConfigAction,
+ ModuleRegistry moduleRegistry) {
+ this.workerFactory = workerFactory;
+ this.processorFactory = processorFactory;
+ this.options = options;
+ this.classPath = classPath;
+ this.buildConfigAction = buildConfigAction;
+ this.moduleRegistry = moduleRegistry;
+ }
+
+ @Override
+ public void startProcessing(TestResultProcessor resultProcessor) {
+ this.resultProcessor = resultProcessor;
+ }
+
+ @Override
+ public void processTestClass(TestClassRunInfo testClass) {
+ int i = 0;
+ RuntimeException exception = null;
+ while (remoteProcessor == null && i < 10) {
+ try {
+ remoteProcessor = forkProcess();
+ exception = null;
+ break;
+ } catch (RuntimeException e) {
+ exception = e;
+ i++;
+ }
+ }
+
+ if (exception != null) {
+ throw exception;
+ }
+ remoteProcessor.processTestClass(testClass);
+ }
+
+ RemoteTestClassProcessor forkProcess() {
+ WorkerProcessBuilder
+ builder =
+ workerFactory.create(new ForciblyStoppableTestWorker(processorFactory));
+ builder.setBaseName("Gradle Test Executor");
+ builder.setImplementationClasspath(getTestWorkerImplementationClasspath());
+ builder.applicationClasspath(classPath);
+ options.copyTo(builder.getJavaCommand());
+ buildConfigAction.execute(builder);
+
+ workerProcess = builder.build();
+ workerProcess.start();
+
+ ObjectConnection connection = workerProcess.getConnection();
+ connection.useParameterSerializers(TestEventSerializer.create());
+ connection.addIncoming(TestResultProcessor.class, resultProcessor);
+ RemoteTestClassProcessor
+ remoteProcessor =
+ connection.addOutgoing(RemoteTestClassProcessor.class);
+ connection.connect();
+ remoteProcessor.startProcessing();
+ return remoteProcessor;
+ }
+
+ List<URL> getTestWorkerImplementationClasspath() {
+ return CollectionUtils.flattenCollections(URL.class,
+ moduleRegistry.getModule("gradle-core-api").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-core").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-logging").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-messaging").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-base-services").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-cli").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-native").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-testing-base").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-testing-jvm").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getModule("gradle-process-services").getImplementationClasspath()
+ .getAsURLs(),
+ moduleRegistry.getExternalModule("slf4j-api").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getExternalModule("jul-to-slf4j").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getExternalModule("native-platform").getImplementationClasspath()
+ .getAsURLs(),
+ moduleRegistry.getExternalModule("kryo").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getExternalModule("commons-lang").getImplementationClasspath().getAsURLs(),
+ moduleRegistry.getExternalModule("junit").getImplementationClasspath().getAsURLs(),
+ ForkingTestClassProcessor.class.getProtectionDomain().getCodeSource().getLocation()
+ );
+ }
+
+ @Override
+ public void stop() {
+ if (remoteProcessor != null) {
+ try {
+ remoteProcessor.stop();
+ workerProcess.waitForStop();
+ } finally {
+ // do nothing
+ }
+ }
+ }
+
+ @Override
+ public void stopNow() {
+ stop(); // TODO need anything else ??
+ }
+
+}
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/NoMemoryManager.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/NoMemoryManager.java
new file mode 100644
index 0000000..3a8be60
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/NoMemoryManager.java
@@ -0,0 +1,59 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import org.gradle.process.internal.health.memory.JvmMemoryStatusListener;
+import org.gradle.process.internal.health.memory.MemoryHolder;
+import org.gradle.process.internal.health.memory.MemoryManager;
+import org.gradle.process.internal.health.memory.OsMemoryStatusListener;
+
+public class NoMemoryManager implements MemoryManager {
+ @Override
+ public void addListener(JvmMemoryStatusListener jvmMemoryStatusListener) {
+
+ }
+
+ @Override
+ public void addListener(OsMemoryStatusListener osMemoryStatusListener) {
+
+ }
+
+ @Override
+ public void removeListener(JvmMemoryStatusListener jvmMemoryStatusListener) {
+
+ }
+
+ @Override
+ public void removeListener(OsMemoryStatusListener osMemoryStatusListener) {
+
+ }
+
+ @Override
+ public void addMemoryHolder(MemoryHolder memoryHolder) {
+
+ }
+
+ @Override
+ public void removeMemoryHolder(MemoryHolder memoryHolder) {
+
+ }
+
+ @Override
+ public void requestFreeMemory(long l) {
+
+ }
+}
diff --git a/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/TestExecuter.java b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/TestExecuter.java
new file mode 100644
index 0000000..21a07ce
--- /dev/null
+++ b/buildSrc/src/main/java/com/pedjak/gradle/plugins/dockerizedtest/TestExecuter.java
@@ -0,0 +1,116 @@
+/*
+ * Copyright 2015 the original author or authors.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package com.pedjak.gradle.plugins.dockerizedtest;
+
+import java.io.File;
+import java.util.Set;
+import java.util.UUID;
+
+import com.google.common.collect.ImmutableSet;
+import org.gradle.api.file.FileTree;
+import org.gradle.api.internal.classpath.ModuleRegistry;
+import org.gradle.api.internal.tasks.testing.JvmTestExecutionSpec;
+import org.gradle.api.internal.tasks.testing.TestClassProcessor;
+import org.gradle.api.internal.tasks.testing.TestFramework;
+import org.gradle.api.internal.tasks.testing.TestResultProcessor;
+import org.gradle.api.internal.tasks.testing.WorkerTestClassProcessorFactory;
+import org.gradle.api.internal.tasks.testing.detection.DefaultTestClassScanner;
+import org.gradle.api.internal.tasks.testing.detection.TestFrameworkDetector;
+import org.gradle.api.internal.tasks.testing.processors.MaxNParallelTestClassProcessor;
+import org.gradle.api.internal.tasks.testing.processors.RestartEveryNTestClassProcessor;
+import org.gradle.api.internal.tasks.testing.processors.TestMainAction;
+import org.gradle.internal.Factory;
+import org.gradle.internal.actor.ActorFactory;
+import org.gradle.internal.operations.BuildOperationExecutor;
+import org.gradle.internal.time.Clock;
+import org.gradle.process.internal.worker.WorkerProcessFactory;
+
+public class TestExecuter
+ implements org.gradle.api.internal.tasks.testing.TestExecuter<JvmTestExecutionSpec> {
+ private final WorkerProcessFactory workerFactory;
+ private final ActorFactory actorFactory;
+ private final ModuleRegistry moduleRegistry;
+ private final BuildOperationExecutor buildOperationExecutor;
+ private final Clock clock;
+ private TestClassProcessor processor;
+
+ public TestExecuter(WorkerProcessFactory workerFactory, ActorFactory actorFactory,
+ ModuleRegistry moduleRegistry, BuildOperationExecutor buildOperationExecutor,
+ Clock clock) {
+ this.workerFactory = workerFactory;
+ this.actorFactory = actorFactory;
+ this.moduleRegistry = moduleRegistry;
+ this.buildOperationExecutor = buildOperationExecutor;
+ this.clock = clock;
+ }
+
+ @Override
+ public void execute(final JvmTestExecutionSpec testExecutionSpec,
+ TestResultProcessor testResultProcessor) {
+ final TestFramework testFramework = testExecutionSpec.getTestFramework();
+ final WorkerTestClassProcessorFactory testInstanceFactory = testFramework.getProcessorFactory();
+ final Set<File> classpath = ImmutableSet.copyOf(testExecutionSpec.getClasspath());
+ final Factory<TestClassProcessor> forkingProcessorFactory = new Factory<TestClassProcessor>() {
+ public TestClassProcessor create() {
+ return new ForkingTestClassProcessor(workerFactory, testInstanceFactory,
+ testExecutionSpec.getJavaForkOptions(),
+ classpath, testFramework.getWorkerConfigurationAction(), moduleRegistry);
+ }
+ };
+ Factory<TestClassProcessor> reforkingProcessorFactory = new Factory<TestClassProcessor>() {
+ public TestClassProcessor create() {
+ return new RestartEveryNTestClassProcessor(forkingProcessorFactory,
+ testExecutionSpec.getForkEvery());
+ }
+ };
+
+ processor = new MaxNParallelTestClassProcessor(testExecutionSpec.getMaxParallelForks(),
+ reforkingProcessorFactory, actorFactory);
+
+ final FileTree testClassFiles = testExecutionSpec.getCandidateClassFiles();
+
+ Runnable detector;
+ if (testExecutionSpec.isScanForTestClasses()) {
+ TestFrameworkDetector
+ testFrameworkDetector =
+ testExecutionSpec.getTestFramework().getDetector();
+ testFrameworkDetector.setTestClasses(testExecutionSpec.getTestClassesDirs().getFiles());
+ testFrameworkDetector.setTestClasspath(classpath);
+ detector = new DefaultTestClassScanner(testClassFiles, testFrameworkDetector, processor);
+ } else {
+ detector = new DefaultTestClassScanner(testClassFiles, null, processor);
+ }
+
+ Object testTaskOperationId;
+
+ try {
+ testTaskOperationId = buildOperationExecutor.getCurrentOperation().getParentId();
+ } catch (Exception e) {
+ testTaskOperationId = UUID.randomUUID();
+ }
+
+ new TestMainAction(detector, processor, testResultProcessor, clock, testTaskOperationId,
+ testExecutionSpec.getPath(), "Gradle Test Run " + testExecutionSpec.getIdentityPath())
+ .run();
+ }
+
+ public void stopNow() {
+ if (processor != null) {
+ processor.stopNow();
+ }
+ }
+}
diff --git a/buildSrc/src/main/resources/META-INF/gradle-plugins/com.github.pedjak.dockerized-test.properties b/buildSrc/src/main/resources/META-INF/gradle-plugins/com.github.pedjak.dockerized-test.properties
new file mode 100644
index 0000000..1cfe2cb
--- /dev/null
+++ b/buildSrc/src/main/resources/META-INF/gradle-plugins/com.github.pedjak.dockerized-test.properties
@@ -0,0 +1 @@
+implementation-class = com.pedjak.gradle.plugins.dockerizedtest.DockerizedTestPlugin
\ No newline at end of file