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