You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by ro...@apache.org on 2017/11/07 10:20:48 UTC

[sling-org-apache-sling-testing-serversetup] annotated tag org.apache.sling.testing.serversetup-1.0.0 created (now 2a991fc)

This is an automated email from the ASF dual-hosted git repository.

rombert pushed a change to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git.


      at 2a991fc  (tag)
 tagging 487403ba24b69406db49c4898e253e698ac7f3dc (commit)
      by Andrei Dulvac
      on Mon Sep 19 14:23:47 2016 +0000

- Log -----------------------------------------------------------------
org.apache.sling.testing.serversetup-1.0.0
-----------------------------------------------------------------------

This annotated tag includes the following new commits:

     new ec1beca  SLING-5703 - new serversetup module extracted from testing/tools. Contributed by Andrei Dulvac, thanks!
     new cff68e3  SLING-5727 Remove o.a.s.testing.tools dependency in o.a.s.testing.serversetup and adapt http code
     new 3d94c19  temporarily updated dep to org.apache.sling.testing.clients
     new 33138a9  [maven-release-plugin] prepare release org.apache.sling.testing.serversetup-1.0.0
     new 487403b  [maven-release-plugin] copy for tag org.apache.sling.testing.serversetup-1.0.0

The 5 revisions listed above as "new" are entirely new to this
repository and will be described in separate emails.  The revisions
listed as "add" were already present in the repository and have only
been added to this reference.


-- 
To stop receiving notification emails like this one, please contact
['"commits@sling.apache.org" <co...@sling.apache.org>'].

[sling-org-apache-sling-testing-serversetup] 02/05: SLING-5727 Remove o.a.s.testing.tools dependency in o.a.s.testing.serversetup and adapt http code

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git

commit cff68e3cdc0f7938721233b3ca2a07317c2362e8
Author: Andrei Dulvac <du...@apache.org>
AuthorDate: Wed Jun 15 15:00:53 2016 +0000

    SLING-5727 Remove o.a.s.testing.tools dependency in o.a.s.testing.serversetup and adapt http code
    
    
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup@1748593 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            |  10 +-
 .../serversetup/instance/SlingInstance.java        |  11 +--
 .../serversetup/instance/SlingInstanceManager.java |   7 +-
 .../serversetup/instance/SlingInstanceState.java   |  14 +--
 .../serversetup/instance/SlingInstancesRule.java   |   3 +-
 .../serversetup/instance/SlingTestBase.java        | 103 ++++++++++-----------
 .../testing/serversetup/jarexec/JarExecutor.java   |   1 +
 7 files changed, 70 insertions(+), 79 deletions(-)

diff --git a/pom.xml b/pom.xml
index e4fc44b..6296dfb 100644
--- a/pom.xml
+++ b/pom.xml
@@ -71,11 +71,6 @@
 
     <dependencies>
         <dependency>
-            <groupId>org.apache.sling</groupId>
-            <artifactId>org.apache.sling.testing.tools</artifactId>
-            <version>1.0.12</version>
-        </dependency>
-        <dependency>
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.core</artifactId>
         </dependency>
@@ -106,5 +101,10 @@
             <version>4.11</version>
             <scope>compile</scope>
         </dependency>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.clients</artifactId>
+            <version>0.1.0-SNAPSHOT</version>
+        </dependency>
     </dependencies>
 </project>
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
index dca6020..6b22444 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
@@ -17,18 +17,13 @@
 package org.apache.sling.testing.serversetup.instance;
 
 
-import org.apache.sling.testing.tools.http.RequestBuilder;
-import org.apache.sling.testing.tools.http.RequestExecutor;
+import org.apache.sling.testing.clients.SlingClient;
 
 /**
  * Interface used to communicate with a sling instance
  */
 public interface SlingInstance {
 
-    /** Start server if needed, and return a RequestBuilder that points to it */
-    public RequestBuilder getRequestBuilder();
-
-
     /** Start server if needed, and return its base URL */
     public String getServerBaseUrl();
 
@@ -40,6 +35,6 @@ public interface SlingInstance {
     public String getServerPassword();
 
 
-    /** Returns a RequestExecutor for this server **/
-    public RequestExecutor getRequestExecutor();
+    /** Returns a SlingClient for this server **/
+    public SlingClient getSlingClient();
 }
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
index f16151f..fda6b9f 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
@@ -16,6 +16,8 @@
  */
 package org.apache.sling.testing.serversetup.instance;
 
+import org.apache.sling.testing.clients.ClientException;
+
 import java.util.Iterator;
 import java.util.Map;
 import java.util.Properties;
@@ -25,15 +27,16 @@ import java.util.concurrent.ConcurrentHashMap;
  *  Helper class for running tests against multiple Sling instances,
  *  takes care of starting the Sling instances and waiting for them to be ready.
  */
+@SuppressWarnings("ALL")
 public class SlingInstanceManager implements Iterable<SlingInstance > {
     private final Map<String, SlingInstance> slingTestInstances = new ConcurrentHashMap<String, SlingInstance>();
 
-    public SlingInstanceManager(String... instanceNames) {
+    public SlingInstanceManager(String... instanceNames) throws ClientException {
         this(System.getProperties(), instanceNames);
     }
 
     /** Get configuration but do not start server yet, that's done on demand */
-    public SlingInstanceManager(Properties systemProperties, String... instanceNames) {
+    public SlingInstanceManager(Properties systemProperties, String... instanceNames) throws ClientException {
         if (instanceNames == null || instanceNames.length == 0) {
             instanceNames = new String [] { SlingInstanceState.DEFAULT_INSTANCE_NAME };
         }
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
index b67499e..f230a41 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
@@ -32,13 +32,13 @@ public class SlingInstanceState {
     public static final String DEFAULT_INSTANCE_NAME = "default";
 
     private String serverBaseUrl;
-    private boolean serverStarted;
-    private boolean serverReady;
-    private boolean serverReadyTestFailed;
-    private boolean installBundlesFailed;
-    private boolean extraBundlesInstalled;
-    private boolean startupInfoProvided;
-    private boolean serverInfoLogged;
+    private boolean serverStarted = false;
+    private boolean serverReady = false;
+    private boolean serverReadyTestFailed = false;
+    private boolean installBundlesFailed = false;
+    private boolean extraBundlesInstalled = false;
+    private boolean startupInfoProvided = false;
+    private boolean serverInfoLogged = false;
     private JarExecutor jarExecutor;
 
     /**
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
index 2be2d9c..b0a6059 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
@@ -16,6 +16,7 @@
  */
 package org.apache.sling.testing.serversetup.instance;
 
+import org.apache.sling.testing.clients.ClientException;
 import org.junit.rules.TestRule;
 import org.junit.runner.Description;
 import org.junit.runners.model.Statement;
@@ -28,7 +29,7 @@ public class SlingInstancesRule implements TestRule {
     private SlingInstance currentInstance;
     private final Iterable<SlingInstance> instances;
     
-    public SlingInstancesRule(String ... instanceNames) {
+    public SlingInstancesRule(String ... instanceNames) throws ClientException {
         this(new SlingInstanceManager(instanceNames));
     }
     
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
index b92acaf..3415841 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
@@ -16,20 +16,18 @@
  */
 package org.apache.sling.testing.serversetup.instance;
 
-import org.apache.http.client.HttpClient;
-import org.apache.http.impl.client.DefaultHttpClient;
-import org.apache.sling.testing.tools.http.RequestBuilder;
-import org.apache.sling.testing.tools.http.RequestExecutor;
-import org.apache.sling.testing.tools.junit.TestDescriptionInterceptor;
-import org.apache.sling.testing.tools.osgi.WebconsoleClient;
-import org.apache.sling.testing.tools.sling.BundlesInstaller;
-import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+import org.apache.sling.testing.clients.ClientException;
+import org.apache.sling.testing.clients.SlingClient;
+import org.apache.sling.testing.clients.osgi.BundlesInstaller;
+import org.apache.sling.testing.clients.osgi.OsgiConsoleClient;
+import org.apache.sling.testing.clients.util.TimeoutsProvider;
 import org.apache.sling.testing.serversetup.jarexec.JarExecutor;
 import org.junit.After;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
 
 import java.io.File;
+import java.net.URI;
 import java.util.*;
 
 import static org.junit.Assert.fail;
@@ -37,7 +35,9 @@ import static org.junit.Assert.fail;
 /** Base class for running tests against a Sling instance,
  *  takes care of starting Sling and waiting for it to be ready.
  */
+@SuppressWarnings("ALL")
 public class SlingTestBase implements SlingInstance {
+    // TODO: unify these
     public static final String TEST_SERVER_URL_PROP = "test.server.url";
     public static final String TEST_SERVER_USERNAME = "test.server.username";
     public static final String TEST_SERVER_PASSWORD = "test.server.password";
@@ -58,10 +58,7 @@ public class SlingTestBase implements SlingInstance {
     private final String serverPassword;
     private final SlingInstanceState slingTestState;
     private final Properties systemProperties;
-    private RequestBuilder builder;
-    private DefaultHttpClient httpClient = new DefaultHttpClient();
-    private RequestExecutor executor = new RequestExecutor(httpClient);
-    private WebconsoleClient webconsoleClient;
+    private OsgiConsoleClient osgiConsoleClient;
     private BundlesInstaller bundlesInstaller;
     private boolean serverStartedByThisClass;
 
@@ -70,8 +67,7 @@ public class SlingTestBase implements SlingInstance {
 
 
     public SlingTestBase() {
-        this(SlingInstanceState.getInstance(SlingInstanceState.DEFAULT_INSTANCE_NAME),
-                System.getProperties());
+        this(SlingInstanceState.getInstance(SlingInstanceState.DEFAULT_INSTANCE_NAME), System.getProperties());
     }
 
     /** Get configuration but do not start server yet, that's done on demand */
@@ -79,10 +75,9 @@ public class SlingTestBase implements SlingInstance {
         this.slingTestState = slingTestState;
         this.systemProperties = systemProperties;
         this.keepJarRunning = "true".equals(systemProperties.getProperty(KEEP_JAR_RUNNING_PROP));
-        this.httpClient.addRequestInterceptor(new TestDescriptionInterceptor());
 
-
-        final String configuredUrl = systemProperties.getProperty(TEST_SERVER_URL_PROP, systemProperties.getProperty("launchpad.http.server.url"));
+        final String configuredUrl = systemProperties.getProperty(TEST_SERVER_URL_PROP,
+                systemProperties.getProperty("launchpad.http.server.url"));
         if(configuredUrl != null && configuredUrl.trim().length() > 0) {
             slingTestState.setServerBaseUrl(configuredUrl);
             slingTestState.setServerStarted(true);
@@ -122,10 +117,14 @@ public class SlingTestBase implements SlingInstance {
             serverPassword = ADMIN;
         }
 
-        builder = new RequestBuilder(slingTestState.getServerBaseUrl());
-        webconsoleClient = new WebconsoleClient(slingTestState.getServerBaseUrl(), serverUsername, serverPassword);
-        builder = new RequestBuilder(slingTestState.getServerBaseUrl());
-        bundlesInstaller = new BundlesInstaller(webconsoleClient);
+        // create client
+        try {
+            osgiConsoleClient = new OsgiConsoleClient(URI.create(slingTestState.getServerBaseUrl()), serverUsername, serverPassword);
+        } catch (ClientException e) {
+            throw new RuntimeException("Cannot instantiate client", e);
+        }
+
+        bundlesInstaller = new BundlesInstaller(osgiConsoleClient);
 
         if(!slingTestState.isServerInfoLogged()) {
             log.info("Server base URL={}", slingTestState.getServerBaseUrl());
@@ -172,7 +171,7 @@ public class SlingTestBase implements SlingInstance {
     }
 
     protected void installAdditionalBundles() {
-        if(slingTestState.isInstallBundlesFailed()) {
+        if (slingTestState.isInstallBundlesFailed()) {
             fail("Bundles could not be installed, cannot run tests");
         } else if(!slingTestState.isExtraBundlesInstalled()) {
             final List<File> toInstall = getBundlesToInstall();
@@ -182,7 +181,7 @@ public class SlingTestBase implements SlingInstance {
                     bundlesInstaller.installBundles(toInstall, false);
                     final List<String> symbolicNames = new LinkedList<String>();
                     for (File f : toInstall) {
-                        symbolicNames.add(bundlesInstaller.getBundleSymbolicName(f));
+                        symbolicNames.add(osgiConsoleClient.getBundleSymbolicName(f));
                     }
                     bundlesInstaller.waitForBundlesInstalled(symbolicNames,
                             TimeoutsProvider.getInstance().getTimeout(BUNDLE_INSTALL_TIMEOUT_SECONDS, 10));
@@ -216,12 +215,6 @@ public class SlingTestBase implements SlingInstance {
         }
     }
 
-    /** Start server if needed, and return a RequestBuilder that points to it */
-    public RequestBuilder getRequestBuilder() {
-        startServerIfNeeded();
-        return builder;
-    }
-
     /** Start server if needed, and return its base URL */
     public String getServerBaseUrl() {
         startServerIfNeeded();
@@ -238,6 +231,16 @@ public class SlingTestBase implements SlingInstance {
         return serverPassword;
     }
 
+    @Override
+    public SlingClient getSlingClient() {
+        return osgiConsoleClient;
+    }
+
+    public OsgiConsoleClient getOsgiConsoleClient() {
+        startServerIfNeeded();
+        return osgiConsoleClient;
+    }
+
     /** Optionally block here so that the runnable jar stays up - we can
      *  then run tests against it from another VM.
      */
@@ -286,28 +289,26 @@ public class SlingTestBase implements SlingInstance {
         // that contains the pattern that's optionally supplied with the
         // path, separated by a colon
         log.info("Checking that GET requests return expected content (timeout={} seconds): {}", timeoutSec, testPaths);
-        while(System.currentTimeMillis() < endTime) {
+        while (System.currentTimeMillis() < endTime) {
             boolean errors = false;
-            for(String p : testPaths) {
+            for (String p : testPaths) {
                 final String [] s = p.split(":");
                 final String path = s[0];
                 final String pattern = (s.length > 0 ? s[1] : "");
                 try {
-                    executor.execute(builder.buildGetRequest(path).withCredentials(serverUsername, serverPassword))
-                    .assertStatus(200)
-                    .assertContentContains(pattern);
-                } catch(AssertionError ae) {
+                    osgiConsoleClient.doGet(path, null, 200).checkContentContains(pattern);
+                } catch(ClientException e) {
                     errors = true;
-                    log.debug("Request to {}@{}{} failed, will retry ({})",
-                            new Object[] { serverUsername, slingTestState.getServerBaseUrl(), path, ae});
+                    log.debug("Request to {}@{} failed, will retry ({})",
+                            new Object[] { serverUsername, osgiConsoleClient.getUrl(path), e});
                 } catch(Exception e) {
                     errors = true;
-                    log.debug("Request to {}@{}{} failed, will retry ({})",
-                            new Object[] { serverUsername, slingTestState.getServerBaseUrl(), path, pattern, e });
+                    log.debug("Request to {}@{} failed, will retry ({})",
+                            new Object[] { serverUsername, osgiConsoleClient.getUrl(path), pattern, e });
                 }
             }
 
-            if(!errors) {
+            if (!errors) {
                 slingTestState.setServerReady(true);
                 log.info("All {} paths return expected content, server ready", testPaths.size());
                 break;
@@ -315,7 +316,7 @@ public class SlingTestBase implements SlingInstance {
             Thread.sleep(TimeoutsProvider.getInstance().getTimeout(1000L));
         }
 
-        if(!slingTestState.isServerReady()) {
+        if (!slingTestState.isServerReady()) {
             slingTestState.setServerReadyTestFailed(true);
             final String msg = "Server not ready after " + timeoutSec + " seconds, giving up";
             log.info(msg);
@@ -342,7 +343,9 @@ public class SlingTestBase implements SlingInstance {
         return toInstall;
     }
 
-    /** Get the list of additional bundles to install, as specified by additionalBundlesPath parameter */
+    /**
+     * Get the list of additional bundles to install, as specified by additionalBundlesPath parameter
+     */
     protected List<File> getBundlesToInstall(String additionalBundlesPath) {
         final List<File> result = new LinkedList<File>();
         if(additionalBundlesPath == null) {
@@ -358,7 +361,7 @@ public class SlingTestBase implements SlingInstance {
         // Collect all filenames of candidate bundles
         final List<String> bundleNames = new ArrayList<String>();
         final String [] files = dir.list();
-        if(files != null) {
+        if (files != null) {
             for(String file : files) {
                 if(file.endsWith(".jar")) {
                     bundleNames.add(file);
@@ -375,7 +378,7 @@ public class SlingTestBase implements SlingInstance {
             }
         }
         Collections.sort(sortedPropertyKeys);
-        for(String key : sortedPropertyKeys) {
+        for (String key : sortedPropertyKeys) {
             final String filenamePrefix = systemProperties.getProperty(key);
             for(String bundleFilename : bundleNames) {
                 if(bundleFilename.startsWith(filenamePrefix)) {
@@ -391,16 +394,4 @@ public class SlingTestBase implements SlingInstance {
         return serverStartedByThisClass;
     }
 
-    public HttpClient getHttpClient() {
-        return httpClient;
-    }
-
-    public RequestExecutor getRequestExecutor() {
-        return executor;
-    }
-
-    public WebconsoleClient getWebconsoleClient() {
-        startServerIfNeeded();
-        return webconsoleClient;
-    }
 }
diff --git a/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java b/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
index 401f0be..c8f33fb 100644
--- a/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
+++ b/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
@@ -34,6 +34,7 @@ import org.slf4j.LoggerFactory;
 /** Start a runnable jar by forking a JVM process,
  *  and terminate the process when this VM exits.
  */
+@SuppressWarnings("ALL")
 public class JarExecutor {
     private final File jarToExecute;
     private final String jvmFullPath;

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-testing-serversetup] 05/05: [maven-release-plugin] copy for tag org.apache.sling.testing.serversetup-1.0.0

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git

commit 487403ba24b69406db49c4898e253e698ac7f3dc
Author: Andrei Dulvac <du...@apache.org>
AuthorDate: Mon Sep 19 14:23:47 2016 +0000

    [maven-release-plugin] copy for tag org.apache.sling.testing.serversetup-1.0.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.testing.serversetup-1.0.0@1761450 13f79535-47bb-0310-9956-ffa450edef68

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-testing-serversetup] 03/05: temporarily updated dep to org.apache.sling.testing.clients

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git

commit 3d94c190205d044206174af7ace7f4802bcd3b81
Author: Andrei Dulvac <du...@apache.org>
AuthorDate: Mon Sep 19 14:22:51 2016 +0000

    temporarily updated dep to org.apache.sling.testing.clients
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup@1761448 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 2 +-
 1 file changed, 1 insertion(+), 1 deletion(-)

diff --git a/pom.xml b/pom.xml
index 6296dfb..c9a4105 100644
--- a/pom.xml
+++ b/pom.xml
@@ -104,7 +104,7 @@
         <dependency>
             <groupId>org.apache.sling</groupId>
             <artifactId>org.apache.sling.testing.clients</artifactId>
-            <version>0.1.0-SNAPSHOT</version>
+            <version>1.0.0</version>
         </dependency>
     </dependencies>
 </project>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-testing-serversetup] 04/05: [maven-release-plugin] prepare release org.apache.sling.testing.serversetup-1.0.0

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git

commit 33138a90fa7acd72356fa256baf5a432cf14ff1d
Author: Andrei Dulvac <du...@apache.org>
AuthorDate: Mon Sep 19 14:23:34 2016 +0000

    [maven-release-plugin] prepare release org.apache.sling.testing.serversetup-1.0.0
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup@1761449 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml | 10 +++++-----
 1 file changed, 5 insertions(+), 5 deletions(-)

diff --git a/pom.xml b/pom.xml
index c9a4105..fc04a42 100644
--- a/pom.xml
+++ b/pom.xml
@@ -24,11 +24,11 @@
         <groupId>org.apache.sling</groupId>
         <artifactId>sling</artifactId>
         <version>25</version>
-        <relativePath/>
+        <relativePath />
     </parent>
 
     <artifactId>org.apache.sling.testing.serversetup</artifactId>
-    <version>0.1.0-SNAPSHOT</version>
+    <version>1.0.0</version>
     <packaging>bundle</packaging>
 
     <name>Apache Sling Server Setup Tools</name>
@@ -37,9 +37,9 @@
     </description>
 
     <scm>
-        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/testing/serversetup</connection>
-        <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup</developerConnection>
-        <url>http://svn.apache.org/viewvc/sling/trunk/testing/serversetup</url>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/tags/org.apache.sling.testing.serversetup-1.0.0</connection>
+        <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/tags/org.apache.sling.testing.serversetup-1.0.0</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/tags/org.apache.sling.testing.serversetup-1.0.0</url>
     </scm>
 
     <build>

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.

[sling-org-apache-sling-testing-serversetup] 01/05: SLING-5703 - new serversetup module extracted from testing/tools. Contributed by Andrei Dulvac, thanks!

Posted by ro...@apache.org.
This is an automated email from the ASF dual-hosted git repository.

rombert pushed a commit to annotated tag org.apache.sling.testing.serversetup-1.0.0
in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git

commit ec1becabc85388ecd0e2c3997f780cffade7c388
Author: Bertrand Delacretaz <bd...@apache.org>
AuthorDate: Fri Apr 29 14:01:27 2016 +0000

    SLING-5703 - new serversetup module extracted from testing/tools. Contributed by Andrei Dulvac, thanks!
    
    git-svn-id: https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup@1741631 13f79535-47bb-0310-9956-ffa450edef68
---
 pom.xml                                            | 110 ++++++
 .../sling/testing/serversetup/ServerSetup.java     | 260 +++++++++++++
 .../testing/serversetup/ServerSetupSingleton.java  |  62 ++++
 .../sling/testing/serversetup/SetupPhase.java      |  32 ++
 .../testing/serversetup/StartRunnableJarPhase.java |  97 +++++
 .../serversetup/instance/SlingInstance.java        |  45 +++
 .../serversetup/instance/SlingInstanceManager.java |  87 +++++
 .../serversetup/instance/SlingInstanceState.java   | 143 ++++++++
 .../serversetup/instance/SlingInstancesRule.java   |  59 +++
 .../serversetup/instance/SlingTestBase.java        | 406 +++++++++++++++++++++
 .../testing/serversetup/instance/package-info.java |  24 ++
 .../testing/serversetup/jarexec/JarExecutor.java   | 210 +++++++++++
 .../ShutdownHookSingleProcessDestroyer.java        | 117 ++++++
 .../testing/serversetup/jarexec/package-info.java  |  24 ++
 .../sling/testing/serversetup/package-info.java    |  24 ++
 .../serversetup/test/ServerSetupSingletonTest.java | 185 ++++++++++
 .../testing/serversetup/test/TestServerSetup.java  |  30 ++
 .../testing/serversetup/test/TestSetupPhase.java   |  60 +++
 18 files changed, 1975 insertions(+)

diff --git a/pom.xml b/pom.xml
new file mode 100644
index 0000000..e4fc44b
--- /dev/null
+++ b/pom.xml
@@ -0,0 +1,110 @@
+<?xml version="1.0" encoding="UTF-8"?>
+<!--
+    Licensed to the Apache Software Foundation (ASF) under one
+    or more contributor license agreements.  See the NOTICE file
+    distributed with this work for additional information
+    regarding copyright ownership.  The ASF licenses this file
+    to you 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.
+-->
+<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/maven-v4_0_0.xsd">
+
+    <modelVersion>4.0.0</modelVersion>
+    <parent>
+        <groupId>org.apache.sling</groupId>
+        <artifactId>sling</artifactId>
+        <version>25</version>
+        <relativePath/>
+    </parent>
+
+    <artifactId>org.apache.sling.testing.serversetup</artifactId>
+    <version>0.1.0-SNAPSHOT</version>
+    <packaging>bundle</packaging>
+
+    <name>Apache Sling Server Setup Tools</name>
+    <description>
+        Sling Server Setup utilities.
+    </description>
+
+    <scm>
+        <connection>scm:svn:http://svn.apache.org/repos/asf/sling/trunk/testing/serversetup</connection>
+        <developerConnection> scm:svn:https://svn.apache.org/repos/asf/sling/trunk/testing/serversetup</developerConnection>
+        <url>http://svn.apache.org/viewvc/sling/trunk/testing/serversetup</url>
+    </scm>
+
+    <build>
+        <plugins>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-scr-plugin</artifactId>
+            </plugin>
+            <plugin>
+                <groupId>org.apache.felix</groupId>
+                <artifactId>maven-bundle-plugin</artifactId>
+                <extensions>true</extensions>
+                <configuration>
+                    <instructions>
+                        <Export-Package>
+                            org.apache.sling.testing.serversetup.*,
+                            org.apache.sling.testing.serversetup.jarexec,
+                            org.apache.sling.testing.serversetup.instance
+                        </Export-Package>
+                        <Import-Package>
+                            org.apache.commons.exec.*; resolution:=optional,
+                            *
+                        </Import-Package>
+                    </instructions>
+                </configuration>
+            </plugin>
+        </plugins>
+    </build>
+
+    <dependencies>
+        <dependency>
+            <groupId>org.apache.sling</groupId>
+            <artifactId>org.apache.sling.testing.tools</artifactId>
+            <version>1.0.12</version>
+        </dependency>
+        <dependency>
+            <groupId>org.osgi</groupId>
+            <artifactId>org.osgi.core</artifactId>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.felix</groupId>
+            <artifactId>org.apache.felix.scr.annotations</artifactId>
+            <scope>provided</scope>
+        </dependency>
+        <dependency>
+            <groupId>org.apache.commons</groupId>
+            <artifactId>commons-exec</artifactId>
+            <version>1.1</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-api</artifactId>
+            <version>1.5.11</version>
+        </dependency>
+        <dependency>
+            <groupId>org.slf4j</groupId>
+            <artifactId>slf4j-simple</artifactId>
+            <version>1.5.11</version>
+            <scope>test</scope>
+        </dependency>
+        <dependency>
+            <groupId>junit</groupId>
+            <artifactId>junit</artifactId>
+            <version>4.11</version>
+            <scope>compile</scope>
+        </dependency>
+    </dependencies>
+</project>
diff --git a/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java b/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java
new file mode 100644
index 0000000..610c334
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java
@@ -0,0 +1,260 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup;
+
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Properties;
+import java.util.Set;
+
+import junit.framework.AssertionFailedError;
+
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+
+/** This is an evolution of the SlingTestBase/JarExecutor 
+ *  combination that we had at revision 1201491, used
+ *  to control the server side of integration tests.
+ *  
+ *  This class allows a number of startup and shutdown phases
+ *  to be defined, and executes some or all of them in a specified
+ *  order, according to a property which lists their names.
+ *  
+ *  Flexibility in those startup/shutdown phases allows for 
+ *  creating test scenarios like automated testing of
+ *  system upgrades, where you would for example:
+ *  
+ *  <pre>
+ *  1. Start the old runnable jar
+ *  2. Wait for it to be ready
+ *  3. Install some bundles and wait for them to be ready
+ *  4. Create some content in that version
+ *  5. Stop that jar
+ *  6. Start the new runnable jar
+ *  7. Wait for it to be ready
+ *  8. Run tests against that new jar to verify the upgrade 
+ *  </pre>
+ *
+ *  Running the whole thing might take a long time, so when
+ *  debugging the upgrade or the tests you might want to 
+ *  restart from a state saved at step 5, and only run steps
+ *  6 to 8, for example.
+ *  
+ *  Those steps are SetupPhase objects identified by
+ *  their name, and specifying a partial list of names allows you
+ *  to run only some of them in a given test run, speeding up
+ *  development and troubleshooting as much as possible.
+ *  
+ *  TODO: the companion samples/integration-tests module 
+ *  should be updated to use this class to setup the Sling server
+ *  that it tests, instead of the SlingTestBase class that it
+ *  currently uses.
+ */
+public class ServerSetup {
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    
+    /** Context that our SetupPhase objects can use to exchange data */
+    private final Map<String, Object> context = new HashMap<String, Object>();
+    
+    private final List<String> phasesToRun = new ArrayList<String>();
+    
+    /** Our configuration */
+    private Properties config;
+    
+    /** Prefix used for our property names */
+    public static final String PROP_NAME_PREFIX = "server.setup";
+    
+    /** Config property name: comma-separated list of phases to run */
+    public static final String PHASES_TO_RUN_PROP = PROP_NAME_PREFIX + ".phases";
+    
+    /** Standard suffix for shutdown tasks IDs */
+    public static final String SHUTDOWN_ID_SUFFIX = ".shutdown";
+    
+    /** Our SetupPhases, keyed by their id which must be unique */
+    private final Map<String, SetupPhase> phases = new HashMap<String, SetupPhase>();
+    
+    /** List of phases that already ran */
+    private final Set<String> donePhases = new HashSet<String>();
+    
+    /** List of phases that failed */
+    private final Set<String> failedPhases = new HashSet<String>();
+    
+    /** Context attribute: server access URL */
+    public static final String SERVER_BASE_URL = "server.base.url";
+    
+    /** Shutdown hook thread */
+    private Thread shutdownHook;
+    
+    @SuppressWarnings("serial")
+    public static class SetupException extends Exception {
+        public SetupException(String reason) {
+            super(reason);
+        }
+        
+        public SetupException(String reason, Throwable cause) {
+            super(reason, cause);
+        }
+    };
+    
+    /** Runs all startup phases that have not run yet,
+     *  and throws an Exception or call Junit's fail()
+     *  method if one of them fails or failed in a 
+     *  previous call of this method.
+     *  
+     *  This can be called several times, will only run
+     *  setup phases that have not run yet.
+     */
+    public synchronized void setupTestServer() throws Exception {
+        
+        // On the first call, list our available phases
+        if(donePhases.isEmpty()) {
+            if(log.isInfoEnabled()) {
+                final List<String> ids = new ArrayList<String>();
+                ids.addAll(phases.keySet());
+                Collections.sort(ids);
+                log.info("Will run SetupPhases {} out of {}", phasesToRun, ids);
+            }
+        }
+        
+        // Run all startup phases that didn't run yet
+        runRemainingPhases(true);
+        
+        // And setup our shutdown hook
+        if(shutdownHook == null) {
+            shutdownHook = new Thread(getClass().getSimpleName() + "Shutdown") {
+                public void run() {
+                    try {
+                        shutdown();
+                    } catch(Exception e) {
+                        log.warn("Exception in shutdown hook", e);
+                    }
+                    
+                }
+            };
+            Runtime.getRuntime().addShutdownHook(shutdownHook);
+            log.info("Shutdown hook added to run shutdown phases");
+        }
+    }
+    
+    /** Run phases that haven't run yet */
+    private void runRemainingPhases(boolean isStartup) throws Exception {
+        final String mode = isStartup ? "startup" : "shutdown";
+        
+        // In startup mode, fail if any phases failed previously
+        // (in shutdown mode it's probably safer to try to run cleanup phases)
+        if(isStartup && !failedPhases.isEmpty()) {
+            throw new SetupException("Some SetupPhases previously failed: " + failedPhases);
+        }
+        
+        for(String id : phasesToRun) {
+            final SetupPhase p = phases.get(id);
+            
+            if(donePhases.contains(id)) {
+                log.debug("SetupPhase ({}) with id {} already ran, ignored", mode, id);
+                continue;
+            }
+            
+            if(p == null) {
+                log.info("SetupPhase ({}) with id {} not found, ignored", mode, id);
+                donePhases.add(id);
+                continue;
+            }
+            
+            if(p.isStartupPhase() == isStartup) {
+                log.info("Executing {} phase: {}", mode, p); 
+                try {
+                    p.run(this);
+                } catch(Exception e) {
+                    failedPhases.add(id);
+                    throw e;
+                } catch (AssertionFailedError ae) {
+                    // Some of our tools throw this, might not to avoid it in the future
+                    failedPhases.add(id);
+                    throw new Exception("AssertionFailedError in runRemainingPhases", ae);
+                } finally {
+                    donePhases.add(id);
+                }
+            }
+        }
+    }
+    
+    /** Called by a shutdown hook to run
+     *  all shutdown phases, but can also
+     *  be called explicitly, each shutdown
+     *  phase only runs once anyway.
+     */
+    public void shutdown() throws Exception {
+        runRemainingPhases(false);
+    }
+    
+    /** Return a context that {@SetupPhase} can use to 
+     *  communicate among them and with the outside.
+     */
+    public Map<String, Object> getContext() {
+        return context;
+    }
+    
+    /** Set configuration and reset our lists of phases
+     *  that already ran or failed.
+     */
+    public void setConfig(Properties props) {
+        config = props;
+
+        final String str = props.getProperty(PHASES_TO_RUN_PROP);
+        phasesToRun.clear();
+        final String [] phases = str == null ? new String [] {} : str.split(",");
+        for(int i=0 ; i < phases.length; i++) {
+            phases[i] = phases[i].trim();
+        }
+        phasesToRun.addAll(Arrays.asList(phases));
+        
+        if(phasesToRun.isEmpty()) {
+            log.warn("No setup phases defined, {} is empty, is that on purpose?", PHASES_TO_RUN_PROP);
+        }
+        
+        donePhases.clear();
+        failedPhases.clear();
+    }
+    
+    /** Return the configuration Properties that were set
+     *  by {@link #setConfig}
+     */
+    public Properties getConfig() {
+        return config;
+    }
+    
+    /** Return the IDs of phases that should run */
+    public List<String> getPhasesToRun() {
+        return Collections.unmodifiableList(phasesToRun);
+    }
+
+    /** Add a SetupPhase to our list. Its ID must be
+     *  unique in that list.
+     */
+    public void addSetupPhase(SetupPhase p) throws SetupException {
+        if(phases.containsKey(p.getId())) {
+            throw new SetupException("A SetupPhase with ID=" + p.getId() + " is already in our list:" + phases.keySet());
+        }
+        phases.put(p.getId(), p);
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java b/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java
new file mode 100644
index 0000000..cb86ade
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java
@@ -0,0 +1,62 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup;
+
+import java.util.Properties;
+
+/** In general we just need a singleton ServerSetup, that
+ *  uses System properties for its configuration - this class
+ *  supplies that.
+ */
+public class ServerSetupSingleton {
+    
+    /** Property name of the ServerSetup class that we instantiate */
+    public static final String CLASS_NAME_PROP = ServerSetup.PROP_NAME_PREFIX + ".class.name";
+    
+    private static ServerSetup instance;
+    
+    /** Create an instance based on the {@CLASS_NAME_PROP)
+     *  property if needed and return it.
+     *  
+     *  @param config Ignored unless an instance is created
+     */
+    public static ServerSetup instance(Properties config) throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        if(instance == null) {
+            synchronized (ServerSetupSingleton.class) {
+                if(instance == null) {
+                    final String className = config.getProperty(CLASS_NAME_PROP);
+                    if(className == null) {
+                        throw new IllegalArgumentException("Missing config property: " + CLASS_NAME_PROP);
+                    }
+                    instance = (ServerSetup)
+                        ServerSetupSingleton.class.getClassLoader()
+                        .loadClass(className)
+                        .newInstance();
+                    instance.setConfig(config);
+                }
+            }
+        }
+        return instance;
+    }
+    
+    /** Same as no-parameter instance() method, but uses System properties
+     *  to create its instance.
+     */
+    public static ServerSetup instance() throws InstantiationException, IllegalAccessException, ClassNotFoundException {
+        return instance(System.getProperties());
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java b/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java
new file mode 100644
index 0000000..4be74a7
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup;
+
+/** A single phase of the test server setup */
+public interface SetupPhase {
+    /** Run this phase in the context of supplied ServerSetup */
+    public void run(ServerSetup owner) throws Exception;
+    
+    /** Is this a startup or shutdown phase? */
+    public boolean isStartupPhase();
+    
+    /** Get the phase ID string, a list of those
+     *  is used by {@link ServerSetup} to decide
+     *  which phases to run
+     */
+    public String getId();
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java b/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java
new file mode 100644
index 0000000..d5547df
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java
@@ -0,0 +1,97 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup;
+
+import java.util.Properties;
+
+import org.apache.sling.testing.serversetup.jarexec.JarExecutor;
+import org.apache.sling.testing.serversetup.jarexec.JarExecutor;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** SetupPhase that uses a JarExecutor to start
+ *  a runnable jar, and stop it at system shutdown
+ *  if our SetupServer wants that.
+ */
+public class StartRunnableJarPhase implements SetupPhase {
+
+    public static final String TEST_SERVER_HOSTNAME = "test.server.hostname";
+    
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private final String id;
+    private final String description;
+    private final JarExecutor executor;
+    
+    public StartRunnableJarPhase(final ServerSetup owner, String id, String description, Properties config)
+            throws JarExecutor.ExecutorException {
+        this.id = id;
+        this.description = description;
+        executor = new JarExecutor(config);
+
+        String hostname = config.getProperty(TEST_SERVER_HOSTNAME);
+        if(hostname == null) {
+            hostname = "localhost";
+        }
+        final String url = "http://" + hostname + ":" + executor.getServerPort();
+        log.info("Server base URL={}", url);
+        owner.getContext().put(ServerSetup.SERVER_BASE_URL, url);
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + " (" + id + ") " + description; 
+    }
+    
+    /** @inheritDoc */
+    public void run(ServerSetup owner) throws Exception {
+        executor.start();
+    }
+
+    /** @inheritDoc */
+    public boolean isStartupPhase() {
+        return true;
+    }
+
+    /** @inheritDoc */
+    public String getId() {
+        return id;
+    }
+    
+    /** Return a SetupPhase that kills the process started by this phase */
+    public SetupPhase getKillPhase(final String id) {
+        return new SetupPhase() {
+            public void run(ServerSetup owner) throws Exception {
+                executor.stop();
+            }
+
+            public boolean isStartupPhase() {
+                // This is not a shutdown phase, it's meant to
+                // use during startup to forcibly kill an instance
+                return true;
+            }
+
+            @Override
+            public String toString() {
+                return "Kill the process started by " + StartRunnableJarPhase.this;
+            }
+
+            public String getId() {
+                return id;
+            }
+        };
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
new file mode 100644
index 0000000..dca6020
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
@@ -0,0 +1,45 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.instance;
+
+
+import org.apache.sling.testing.tools.http.RequestBuilder;
+import org.apache.sling.testing.tools.http.RequestExecutor;
+
+/**
+ * Interface used to communicate with a sling instance
+ */
+public interface SlingInstance {
+
+    /** Start server if needed, and return a RequestBuilder that points to it */
+    public RequestBuilder getRequestBuilder();
+
+
+    /** Start server if needed, and return its base URL */
+    public String getServerBaseUrl();
+
+
+    /** Return username configured for execution of HTTP requests */
+    public String getServerUsername();
+
+    /** Return password configured for execution of HTTP requests */
+    public String getServerPassword();
+
+
+    /** Returns a RequestExecutor for this server **/
+    public RequestExecutor getRequestExecutor();
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
new file mode 100644
index 0000000..f16151f
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
@@ -0,0 +1,87 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.instance;
+
+import java.util.Iterator;
+import java.util.Map;
+import java.util.Properties;
+import java.util.concurrent.ConcurrentHashMap;
+
+/**
+ *  Helper class for running tests against multiple Sling instances,
+ *  takes care of starting the Sling instances and waiting for them to be ready.
+ */
+public class SlingInstanceManager implements Iterable<SlingInstance > {
+    private final Map<String, SlingInstance> slingTestInstances = new ConcurrentHashMap<String, SlingInstance>();
+
+    public SlingInstanceManager(String... instanceNames) {
+        this(System.getProperties(), instanceNames);
+    }
+
+    /** Get configuration but do not start server yet, that's done on demand */
+    public SlingInstanceManager(Properties systemProperties, String... instanceNames) {
+        if (instanceNames == null || instanceNames.length == 0) {
+            instanceNames = new String [] { SlingInstanceState.DEFAULT_INSTANCE_NAME };
+        }
+
+        for (String instanceName : instanceNames) {
+            Properties instanceProperties = removeInstancePrefix(systemProperties, instanceName);
+
+            SlingInstanceState state = SlingInstanceState.getInstance(instanceName);
+            SlingInstance instance = new SlingTestBase(state, instanceProperties);
+            slingTestInstances.put(instanceName, instance);
+        }
+    }
+
+
+    private Properties removeInstancePrefix(Properties properties, String instanceName) {
+        Properties result = new Properties();
+        for (Object propertyKey : properties.keySet()) {
+            Object propertyValue = properties.get(propertyKey);
+
+            if (propertyKey instanceof String) {
+                String propertyName = (String) propertyKey;
+                String instancePropertyName = null;
+                if (propertyName.startsWith(instanceName + ".")) {
+                    instancePropertyName = propertyName.substring(instanceName.length()+1);
+                }
+
+                if (instancePropertyName != null) {
+                    result.put(instancePropertyName, propertyValue);
+                }
+                else if (!result.containsKey(propertyName)) {
+                    result.put(propertyName, propertyValue);
+                }
+            }
+            else {
+                result.put(propertyKey, propertyValue);
+
+            }
+        }
+
+        return result;
+    }
+
+
+    public SlingInstance getInstance(String instanceName) {
+        return slingTestInstances.get(instanceName);
+    }
+
+    public Iterator<SlingInstance> iterator() {
+        return slingTestInstances.values().iterator();
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
new file mode 100644
index 0000000..b67499e
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
@@ -0,0 +1,143 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.instance;
+
+import org.apache.sling.testing.serversetup.jarexec.JarExecutor;
+
+import java.util.HashMap;
+import java.util.Map;
+import java.util.Set;
+import java.util.concurrent.CopyOnWriteArraySet;
+
+
+/**
+ * Information about a sling instance that is shared between tests.
+ */
+public class SlingInstanceState {
+
+    public static final String DEFAULT_INSTANCE_NAME = "default";
+
+    private String serverBaseUrl;
+    private boolean serverStarted;
+    private boolean serverReady;
+    private boolean serverReadyTestFailed;
+    private boolean installBundlesFailed;
+    private boolean extraBundlesInstalled;
+    private boolean startupInfoProvided;
+    private boolean serverInfoLogged;
+    private JarExecutor jarExecutor;
+
+    /**
+     * List of the urls of currently started servers
+     */
+    static Set<String> startedServersUrls = new CopyOnWriteArraySet<String>();
+
+    /**
+     * List of the instance names and states
+     */
+    private static final Map<String, SlingInstanceState> slingInstancesState = new HashMap<String, SlingInstanceState>();
+
+
+    public static synchronized SlingInstanceState getInstance(String instanceName) {
+        if (slingInstancesState.containsKey(instanceName)) {
+            return slingInstancesState.get(instanceName);
+        }
+        else {
+            slingInstancesState.put(instanceName, new SlingInstanceState());
+        }
+
+        return slingInstancesState.get(instanceName);
+    }
+
+
+    private SlingInstanceState() {
+
+    }
+
+    public boolean isServerStarted() {
+        return serverStarted;
+    }
+
+    public boolean setServerStarted(boolean serverStarted) {
+        this.serverStarted = serverStarted;
+        return startedServersUrls.add(serverBaseUrl);
+    }
+
+    public boolean isServerReady() {
+        return serverReady;
+    }
+
+    public void setServerReady(boolean serverReady) {
+        this.serverReady = serverReady;
+    }
+
+    public boolean isServerReadyTestFailed() {
+        return serverReadyTestFailed;
+    }
+
+    public void setServerReadyTestFailed(boolean serverReadyTestFailed) {
+        this.serverReadyTestFailed = serverReadyTestFailed;
+    }
+
+    public boolean isInstallBundlesFailed() {
+        return installBundlesFailed;
+    }
+
+    public void setInstallBundlesFailed(boolean installBundlesFailed) {
+        this.installBundlesFailed = installBundlesFailed;
+    }
+
+    public boolean isExtraBundlesInstalled() {
+        return extraBundlesInstalled;
+    }
+
+    public void setExtraBundlesInstalled(boolean extraBundlesInstalled) {
+        this.extraBundlesInstalled = extraBundlesInstalled;
+    }
+
+    public boolean isStartupInfoProvided() {
+        return startupInfoProvided;
+    }
+
+    public void setStartupInfoProvided(boolean startupInfoProvided) {
+        this.startupInfoProvided = startupInfoProvided;
+    }
+
+    public boolean isServerInfoLogged() {
+        return serverInfoLogged;
+    }
+
+    public void setServerInfoLogged(boolean serverInfoLogged) {
+        this.serverInfoLogged = serverInfoLogged;
+    }
+
+    public JarExecutor getJarExecutor() {
+        return jarExecutor;
+    }
+
+    public void setJarExecutor(JarExecutor jarExecutor) {
+        this.jarExecutor = jarExecutor;
+    }
+
+    public String getServerBaseUrl() {
+        return serverBaseUrl;
+    }
+
+    public void setServerBaseUrl(String serverBaseUrl) {
+        this.serverBaseUrl = serverBaseUrl;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
new file mode 100644
index 0000000..2be2d9c
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.instance;
+
+import org.junit.rules.TestRule;
+import org.junit.runner.Description;
+import org.junit.runners.model.Statement;
+
+/**
+ *  JUnit Rule that executes tests for multiple Sling instances.
+ */
+public class SlingInstancesRule implements TestRule {
+
+    private SlingInstance currentInstance;
+    private final Iterable<SlingInstance> instances;
+    
+    public SlingInstancesRule(String ... instanceNames) {
+        this(new SlingInstanceManager(instanceNames));
+    }
+    
+    public SlingInstancesRule(Iterable<SlingInstance> it) {
+        instances = it;
+    }
+
+    /** Evaluate our base statement once for every instance.
+     *  Tests can use our getSlingInstance() method to access the current one.
+     *  See MultipleOsgiConsoleTest example in the samples integration tests module.
+     */
+    public Statement apply(final Statement base, Description dest) {
+        return new Statement() {
+            @Override
+            public void evaluate() throws Throwable {
+                for(SlingInstance instance : instances) {
+                    currentInstance = instance;
+                    base.evaluate();
+               }
+                currentInstance = null;
+            }
+        };
+    }
+    
+    public SlingInstance getSlingInstance() {
+        return currentInstance;
+    }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
new file mode 100644
index 0000000..b92acaf
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
@@ -0,0 +1,406 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.instance;
+
+import org.apache.http.client.HttpClient;
+import org.apache.http.impl.client.DefaultHttpClient;
+import org.apache.sling.testing.tools.http.RequestBuilder;
+import org.apache.sling.testing.tools.http.RequestExecutor;
+import org.apache.sling.testing.tools.junit.TestDescriptionInterceptor;
+import org.apache.sling.testing.tools.osgi.WebconsoleClient;
+import org.apache.sling.testing.tools.sling.BundlesInstaller;
+import org.apache.sling.testing.tools.sling.TimeoutsProvider;
+import org.apache.sling.testing.serversetup.jarexec.JarExecutor;
+import org.junit.After;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+import java.io.File;
+import java.util.*;
+
+import static org.junit.Assert.fail;
+
+/** Base class for running tests against a Sling instance,
+ *  takes care of starting Sling and waiting for it to be ready.
+ */
+public class SlingTestBase implements SlingInstance {
+    public static final String TEST_SERVER_URL_PROP = "test.server.url";
+    public static final String TEST_SERVER_USERNAME = "test.server.username";
+    public static final String TEST_SERVER_PASSWORD = "test.server.password";
+    public static final String SERVER_READY_TIMEOUT_PROP = "server.ready.timeout.seconds";
+    public static final String SERVER_READY_PROP_PREFIX = "server.ready.path";
+    public static final String KEEP_JAR_RUNNING_PROP = "keepJarRunning";
+    public static final String SERVER_HOSTNAME_PROP = "test.server.hostname";
+    public static final String ADDITONAL_BUNDLES_PATH = "additional.bundles.path";
+    public static final String ADDITONAL_BUNDLES_UNINSTALL = "additional.bundles.uninstall";
+    public static final String BUNDLE_TO_INSTALL_PREFIX = "sling.additional.bundle";
+    public static final String START_BUNDLES_TIMEOUT_SECONDS = "start.bundles.timeout.seconds";
+    public static final String BUNDLE_INSTALL_TIMEOUT_SECONDS = "bundle.install.timeout.seconds";
+    public static final String ADMIN = "admin";
+
+    private final boolean keepJarRunning;
+    private final boolean uninstallAdditionalBundles;
+    private final String serverUsername;
+    private final String serverPassword;
+    private final SlingInstanceState slingTestState;
+    private final Properties systemProperties;
+    private RequestBuilder builder;
+    private DefaultHttpClient httpClient = new DefaultHttpClient();
+    private RequestExecutor executor = new RequestExecutor(httpClient);
+    private WebconsoleClient webconsoleClient;
+    private BundlesInstaller bundlesInstaller;
+    private boolean serverStartedByThisClass;
+
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+
+    public SlingTestBase() {
+        this(SlingInstanceState.getInstance(SlingInstanceState.DEFAULT_INSTANCE_NAME),
+                System.getProperties());
+    }
+
+    /** Get configuration but do not start server yet, that's done on demand */
+    public SlingTestBase(SlingInstanceState slingTestState, Properties systemProperties) {
+        this.slingTestState = slingTestState;
+        this.systemProperties = systemProperties;
+        this.keepJarRunning = "true".equals(systemProperties.getProperty(KEEP_JAR_RUNNING_PROP));
+        this.httpClient.addRequestInterceptor(new TestDescriptionInterceptor());
+
+
+        final String configuredUrl = systemProperties.getProperty(TEST_SERVER_URL_PROP, systemProperties.getProperty("launchpad.http.server.url"));
+        if(configuredUrl != null && configuredUrl.trim().length() > 0) {
+            slingTestState.setServerBaseUrl(configuredUrl);
+            slingTestState.setServerStarted(true);
+            uninstallAdditionalBundles = "true".equals(systemProperties.getProperty(ADDITONAL_BUNDLES_UNINSTALL));
+        } else {
+            synchronized(this.slingTestState) {
+                try {
+                    if(slingTestState.getJarExecutor() == null) {
+                        slingTestState.setJarExecutor(new JarExecutor(systemProperties));
+                    }
+                } catch(Exception e) {
+                    log.error("JarExecutor setup failed", e);
+                    fail("JarExecutor setup failed: " + e);
+                }
+            }
+            String serverHost = systemProperties.getProperty(SERVER_HOSTNAME_PROP);
+            if(serverHost == null || serverHost.trim().length() == 0) {
+                serverHost = "localhost";
+            }
+            slingTestState.setServerBaseUrl("http://" + serverHost + ":" + slingTestState.getJarExecutor().getServerPort());
+            uninstallAdditionalBundles = false; // never undeploy additional bundles in case the server is provisioned here!
+        }
+
+        // Set configured username using "admin" as default credential
+        final String configuredUsername = systemProperties.getProperty(TEST_SERVER_USERNAME);
+        if (configuredUsername != null && configuredUsername.trim().length() > 0) {
+            serverUsername = configuredUsername;
+        } else {
+            serverUsername = ADMIN;
+        }
+
+        // Set configured password using "admin" as default credential
+        final String configuredPassword = systemProperties.getProperty(TEST_SERVER_PASSWORD);
+        if (configuredPassword != null && configuredPassword.trim().length() > 0) {
+            serverPassword = configuredPassword;
+        } else {
+            serverPassword = ADMIN;
+        }
+
+        builder = new RequestBuilder(slingTestState.getServerBaseUrl());
+        webconsoleClient = new WebconsoleClient(slingTestState.getServerBaseUrl(), serverUsername, serverPassword);
+        builder = new RequestBuilder(slingTestState.getServerBaseUrl());
+        bundlesInstaller = new BundlesInstaller(webconsoleClient);
+
+        if(!slingTestState.isServerInfoLogged()) {
+            log.info("Server base URL={}", slingTestState.getServerBaseUrl());
+            slingTestState.setServerInfoLogged(true);
+        }
+    }
+
+    /**
+     * Automatically by the SlingRemoteTestRunner since package version 1.1.0.
+     */
+    @After
+    public void uninstallAdditionalBundlesIfNecessary() {
+        if (uninstallAdditionalBundles) {
+            log.info("Uninstalling additional bundles...");
+            uninstallAdditionalBundles();
+        }
+    }
+
+    /** Start the server, if not done yet */
+    private void startServerIfNeeded() {
+        try {
+            if(slingTestState.isServerStarted() && !serverStartedByThisClass && !slingTestState.isStartupInfoProvided()) {
+                log.info(TEST_SERVER_URL_PROP + " was set: not starting server jar (" + slingTestState.getServerBaseUrl() + ")");
+            }
+            if(!slingTestState.isServerStarted()) {
+                synchronized (slingTestState) {
+                    if(!slingTestState.isServerStarted()) {
+                        slingTestState.getJarExecutor().start();
+                        serverStartedByThisClass = true;
+                        if(!slingTestState.setServerStarted(true)) {
+                            fail("A server is already started at " + slingTestState.getServerBaseUrl());
+                        }
+                    }
+                }
+            }
+            slingTestState.setStartupInfoProvided(true);
+            waitForServerReady();
+            installAdditionalBundles();
+            blockIfRequested();
+        } catch(Exception e) {
+            log.error("Exception in maybeStartServer()", e);
+            fail("maybeStartServer() failed: " + e);
+        }
+    }
+
+    protected void installAdditionalBundles() {
+        if(slingTestState.isInstallBundlesFailed()) {
+            fail("Bundles could not be installed, cannot run tests");
+        } else if(!slingTestState.isExtraBundlesInstalled()) {
+            final List<File> toInstall = getBundlesToInstall();
+            if (!toInstall.isEmpty()) {
+                try {
+                    // Install bundles, check that they are installed and start them all
+                    bundlesInstaller.installBundles(toInstall, false);
+                    final List<String> symbolicNames = new LinkedList<String>();
+                    for (File f : toInstall) {
+                        symbolicNames.add(bundlesInstaller.getBundleSymbolicName(f));
+                    }
+                    bundlesInstaller.waitForBundlesInstalled(symbolicNames,
+                            TimeoutsProvider.getInstance().getTimeout(BUNDLE_INSTALL_TIMEOUT_SECONDS, 10));
+                    bundlesInstaller.startAllBundles(symbolicNames,
+                            TimeoutsProvider.getInstance().getTimeout(START_BUNDLES_TIMEOUT_SECONDS, 30));
+                } catch(AssertionError ae) {
+                    log.info("Exception while installing additional bundles", ae);
+                    slingTestState.setInstallBundlesFailed(true);
+                } catch(Exception e) {
+                    log.info("Exception while installing additional bundles", e);
+                    slingTestState.setInstallBundlesFailed(true);
+                }
+                if(slingTestState.isInstallBundlesFailed()) {
+                    fail("Could not start all installed bundles:" + toInstall);
+                }
+            } else {
+                log.info("Not installing additional bundles, probably System property {} not set",
+                        ADDITONAL_BUNDLES_PATH);
+            }
+        }
+
+        slingTestState.setExtraBundlesInstalled(!slingTestState.isInstallBundlesFailed());
+    }
+    
+    protected void uninstallAdditionalBundles() {
+        try {
+            // always uninstall independent of installation status
+            bundlesInstaller.uninstallBundles(getBundlesToInstall());
+        } catch (Exception e) {
+             log.info("Exception while uninstalling additional bundles", e);
+        }
+    }
+
+    /** Start server if needed, and return a RequestBuilder that points to it */
+    public RequestBuilder getRequestBuilder() {
+        startServerIfNeeded();
+        return builder;
+    }
+
+    /** Start server if needed, and return its base URL */
+    public String getServerBaseUrl() {
+        startServerIfNeeded();
+        return slingTestState.getServerBaseUrl();
+    }
+
+    /** Return username configured for execution of HTTP requests */
+    public String getServerUsername() {
+        return serverUsername;
+    }
+
+    /** Return password configured for execution of HTTP requests */
+    public String getServerPassword() {
+        return serverPassword;
+    }
+
+    /** Optionally block here so that the runnable jar stays up - we can
+     *  then run tests against it from another VM.
+     */
+    protected void blockIfRequested() {
+        if (keepJarRunning) {
+            log.info(KEEP_JAR_RUNNING_PROP + " set to true - entering infinite loop"
+                     + " so that runnable jar stays up. Kill this process to exit.");
+            synchronized (slingTestState) {
+                try {
+                    slingTestState.wait();
+                } catch(InterruptedException iex) {
+                    log.info("InterruptedException in blockIfRequested");
+                }
+            }
+        }
+    }
+
+    /** Check a number of server URLs for readyness */
+    protected void waitForServerReady() throws Exception {
+        if(slingTestState.isServerReady()) {
+            return;
+        }
+        if(slingTestState.isServerReadyTestFailed()) {
+            fail("Server is not ready according to previous tests");
+        }
+
+        // Timeout for readiness test
+        final String sec = systemProperties.getProperty(SERVER_READY_TIMEOUT_PROP);
+        final int timeoutSec = TimeoutsProvider.getInstance().getTimeout(sec == null ? 60 : Integer.valueOf(sec));
+        log.info("Will wait up to " + timeoutSec + " seconds for server to become ready");
+        final long endTime = System.currentTimeMillis() + timeoutSec * 1000L;
+
+        // Get the list of paths to test and expected content regexps
+        final List<String> testPaths = new ArrayList<String>();
+        final TreeSet<Object> propertyNames = new TreeSet<Object>();
+        propertyNames.addAll(systemProperties.keySet());
+        for(Object o : propertyNames) {
+            final String key = (String)o;
+            if(key.startsWith(SERVER_READY_PROP_PREFIX)) {
+                testPaths.add(systemProperties.getProperty(key));
+            }
+        }
+
+        // Consider the server ready if it responds to a GET on each of
+        // our configured request paths with a 200 result and content
+        // that contains the pattern that's optionally supplied with the
+        // path, separated by a colon
+        log.info("Checking that GET requests return expected content (timeout={} seconds): {}", timeoutSec, testPaths);
+        while(System.currentTimeMillis() < endTime) {
+            boolean errors = false;
+            for(String p : testPaths) {
+                final String [] s = p.split(":");
+                final String path = s[0];
+                final String pattern = (s.length > 0 ? s[1] : "");
+                try {
+                    executor.execute(builder.buildGetRequest(path).withCredentials(serverUsername, serverPassword))
+                    .assertStatus(200)
+                    .assertContentContains(pattern);
+                } catch(AssertionError ae) {
+                    errors = true;
+                    log.debug("Request to {}@{}{} failed, will retry ({})",
+                            new Object[] { serverUsername, slingTestState.getServerBaseUrl(), path, ae});
+                } catch(Exception e) {
+                    errors = true;
+                    log.debug("Request to {}@{}{} failed, will retry ({})",
+                            new Object[] { serverUsername, slingTestState.getServerBaseUrl(), path, pattern, e });
+                }
+            }
+
+            if(!errors) {
+                slingTestState.setServerReady(true);
+                log.info("All {} paths return expected content, server ready", testPaths.size());
+                break;
+            }
+            Thread.sleep(TimeoutsProvider.getInstance().getTimeout(1000L));
+        }
+
+        if(!slingTestState.isServerReady()) {
+            slingTestState.setServerReadyTestFailed(true);
+            final String msg = "Server not ready after " + timeoutSec + " seconds, giving up";
+            log.info(msg);
+            fail(msg);
+        }
+    }
+
+    /**
+     * Get the list of additional bundles to install, as specified by the system property {@link #ADDITONAL_BUNDLES_PATH} 
+     * @return the list of {@link File}s pointing to the Bundle JARs or the empty list in case no additional bundles should be installed (never {@code null}).
+     */
+    protected List<File> getBundlesToInstall() {
+        final String paths = systemProperties.getProperty(ADDITONAL_BUNDLES_PATH);
+        if(paths == null) {
+            return Collections.emptyList();
+        } 
+        
+        final List<File> toInstall = new ArrayList<File>();
+        // Paths can contain a comma-separated list
+        final String [] allPaths = paths.split(",");
+        for(String path : allPaths) {
+            toInstall.addAll(getBundlesToInstall(path.trim()));
+        }
+        return toInstall;
+    }
+
+    /** Get the list of additional bundles to install, as specified by additionalBundlesPath parameter */
+    protected List<File> getBundlesToInstall(String additionalBundlesPath) {
+        final List<File> result = new LinkedList<File>();
+        if(additionalBundlesPath == null) {
+            return result;
+        }
+
+        final File dir = new File(additionalBundlesPath);
+        if(!dir.isDirectory() || !dir.canRead()) {
+            log.info("Cannot read additional bundles directory {}, ignored", dir.getAbsolutePath());
+            return result;
+        }
+
+        // Collect all filenames of candidate bundles
+        final List<String> bundleNames = new ArrayList<String>();
+        final String [] files = dir.list();
+        if(files != null) {
+            for(String file : files) {
+                if(file.endsWith(".jar")) {
+                    bundleNames.add(file);
+                }
+            }
+        }
+
+        // We'll install those that are specified by system properties, in order
+        final List<String> sortedPropertyKeys = new ArrayList<String>();
+        for(Object key : systemProperties.keySet()) {
+            final String str = key.toString();
+            if(str.startsWith(BUNDLE_TO_INSTALL_PREFIX)) {
+                sortedPropertyKeys.add(str);
+            }
+        }
+        Collections.sort(sortedPropertyKeys);
+        for(String key : sortedPropertyKeys) {
+            final String filenamePrefix = systemProperties.getProperty(key);
+            for(String bundleFilename : bundleNames) {
+                if(bundleFilename.startsWith(filenamePrefix)) {
+                    result.add(new File(dir, bundleFilename));
+                }
+            }
+        }
+
+        return result;
+    }
+
+    public boolean isServerStartedByThisClass() {
+        return serverStartedByThisClass;
+    }
+
+    public HttpClient getHttpClient() {
+        return httpClient;
+    }
+
+    public RequestExecutor getRequestExecutor() {
+        return executor;
+    }
+
+    public WebconsoleClient getWebconsoleClient() {
+        startServerIfNeeded();
+        return webconsoleClient;
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java b/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java
new file mode 100644
index 0000000..62b8f55
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+@Version("1.0.0")
+package org.apache.sling.testing.serversetup.instance;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java b/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
new file mode 100644
index 0000000..401f0be
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
@@ -0,0 +1,210 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup.jarexec;
+
+import java.io.File;
+import java.io.IOException;
+import java.util.Arrays;
+import java.util.Properties;
+import java.util.regex.Pattern;
+
+import org.apache.commons.exec.CommandLine;
+import org.apache.commons.exec.DefaultExecutor;
+import org.apache.commons.exec.ExecuteException;
+import org.apache.commons.exec.ExecuteResultHandler;
+import org.apache.commons.exec.Executor;
+import org.apache.commons.exec.PumpStreamHandler;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Start a runnable jar by forking a JVM process,
+ *  and terminate the process when this VM exits.
+ */
+public class JarExecutor {
+    private final File jarToExecute;
+    private final String jvmFullPath;
+    private final int serverPort;
+    private final Properties config;
+    private Executor executor;
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+
+    public static final int DEFAULT_PORT = 8765;
+    public static final int DEFAULT_EXIT_TIMEOUT = 30;
+
+    public static final String DEFAULT_JAR_FOLDER = "target/dependency";
+    public static final String DEFAULT_JAR_NAME_REGEXP = "org.apache.sling.*jar$";
+    public static final String PROP_PREFIX = "jar.executor.";
+    public static final String PROP_SERVER_PORT = PROP_PREFIX + "server.port";
+    public static final String PROP_JAR_FOLDER = PROP_PREFIX + "jar.folder";
+    public static final String PROP_JAR_NAME_REGEXP = PROP_PREFIX + "jar.name.regexp";
+    public static final String PROP_VM_OPTIONS = PROP_PREFIX + "vm.options";
+    public static final String PROP_WORK_FOLDER = PROP_PREFIX + "work.folder";
+    public static final String PROP_JAR_OPTIONS = PROP_PREFIX + "jar.options";
+    public static final String PROP_EXIT_TIMEOUT_SECONDS = PROP_PREFIX + "exit.timeout.seconds";
+    public static final String PROP_WAIT_ONSHUTDOWN = PROP_PREFIX + "wait.on.shutdown";
+    public static final String PROP_JAVA_PATH = PROP_PREFIX + "java.executable.path";
+    public static final String PROP_SYNC_EXEC = PROP_PREFIX + "synchronous.exec";
+    public static final String PROP_SYNC_EXEC_EXPECTED = PROP_PREFIX + "synchronous.exec.expected.result";
+
+    @SuppressWarnings("serial")
+    public static class ExecutorException extends Exception {
+        ExecutorException(String reason) {
+            super(reason);
+        }
+        ExecutorException(String reason, Throwable cause) {
+            super(reason, cause);
+        }
+    }
+
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + ": " + jarToExecute.getName() + " (port " + serverPort + ")";
+    }
+
+    public int getServerPort() {
+        return serverPort;
+    }
+
+    /** Build a JarExecutor, locate the jar to run, etc */
+    public JarExecutor(Properties config) throws ExecutorException {
+        this.config = config;
+        final boolean isWindows = System.getProperty("os.name").toLowerCase().contains("windows");
+
+        String portStr = config.getProperty(PROP_SERVER_PORT);
+        serverPort = portStr == null ? DEFAULT_PORT : Integer.valueOf(portStr);
+
+        final String configJvmPath = config.getProperty(PROP_JAVA_PATH);
+        if(configJvmPath == null) {
+            final String javaExecutable = isWindows ? "java.exe" : "java";
+            jvmFullPath = System.getProperty( "java.home" ) + File.separator + "bin" + File.separator + javaExecutable;
+        } else {
+            jvmFullPath = configJvmPath;
+        }
+
+        String jarFolderPath = config.getProperty(PROP_JAR_FOLDER);
+        jarFolderPath = jarFolderPath == null ? DEFAULT_JAR_FOLDER : jarFolderPath;
+        final File jarFolder = new File(jarFolderPath);
+
+        String jarNameRegexp = config.getProperty(PROP_JAR_NAME_REGEXP);
+        jarNameRegexp = jarNameRegexp == null ? DEFAULT_JAR_NAME_REGEXP : jarNameRegexp;
+        final Pattern jarPattern = Pattern.compile(jarNameRegexp);
+
+        // Find executable jar
+        final String [] candidates = jarFolder.list();
+        if(candidates == null) {
+            throw new ExecutorException(
+                    "No files found in jar folder specified by "
+                    + PROP_JAR_FOLDER + " property: " + jarFolder.getAbsolutePath());
+        }
+        File f = null;
+        for(String filename : candidates) {
+            if(jarPattern.matcher(filename).matches()) {
+                f = new File(jarFolder, filename);
+                break;
+            }
+        }
+
+        if(f == null) {
+            throw new ExecutorException("Executable jar matching '" + jarPattern
+                    + "' not found in " + jarFolder.getAbsolutePath()
+                    + ", candidates are " + Arrays.asList(candidates));
+        }
+        jarToExecute = f;
+    }
+
+    /** Start the jar if not done yet, and setup runtime hook
+     *  to stop it.
+     */
+    public void start() throws Exception {
+        final ExecuteResultHandler h = new ExecuteResultHandler() {
+            public void onProcessFailed(ExecuteException ex) {
+                log.error("Process execution failed:" + ex, ex);
+            }
+
+            public void onProcessComplete(int result) {
+                log.info("Process execution complete, exit code=" + result);
+            }
+        };
+
+        final String vmOptions = config.getProperty(PROP_VM_OPTIONS);
+        executor = new DefaultExecutor();
+        final CommandLine cl = new CommandLine(jvmFullPath);
+        if (vmOptions != null && vmOptions.length() > 0) {
+            cl.addArguments(vmOptions);
+        }
+        cl.addArgument("-jar");
+        cl.addArgument(jarToExecute.getAbsolutePath());
+
+        // Additional options for the jar that's executed.
+        // $JAREXEC_SERVER_PORT$ is replaced our serverPort value
+        String jarOptions = config.getProperty(PROP_JAR_OPTIONS);
+        if(jarOptions != null && jarOptions.length() > 0) {
+            jarOptions = jarOptions.replaceAll("\\$JAREXEC_SERVER_PORT\\$", String.valueOf(serverPort));
+            log.info("Executable jar options: {}", jarOptions);
+            cl.addArguments(jarOptions);
+        }
+
+        final String workFolderOption = config.getProperty(PROP_WORK_FOLDER);
+        if(workFolderOption != null && workFolderOption.length() > 0) {
+            final File workFolder = new File(workFolderOption);
+            if(!workFolder.isDirectory()) {
+                throw new IOException("Work dir set by " + PROP_WORK_FOLDER + " option does not exist: "
+                        + workFolder.getAbsolutePath());
+            }
+            log.info("Setting working directory for executable jar: {}", workFolder.getAbsolutePath());
+            executor.setWorkingDirectory(workFolder);
+        }
+
+        String tmStr = config.getProperty(PROP_EXIT_TIMEOUT_SECONDS);
+        final int exitTimeoutSeconds = tmStr == null ? DEFAULT_EXIT_TIMEOUT : Integer.valueOf(tmStr);
+
+        if("true".equals(config.getProperty(PROP_SYNC_EXEC, ""))) {
+            final long start = System.currentTimeMillis();
+            log.info("Executing and waiting for result: " + cl);
+            final int result = executor.execute(cl);
+            final int expected = Integer.valueOf(config.getProperty(PROP_SYNC_EXEC_EXPECTED, "0"));
+            log.info("Execution took " + (System.currentTimeMillis() - start) + " msec");
+            if(result != expected) {
+                throw new ExecutorException("Expected result code " + expected + ", got " + result);
+            }
+        } else {
+            log.info("Executing asynchronously: " + cl);
+            executor.setStreamHandler(new PumpStreamHandler());
+            final ShutdownHookSingleProcessDestroyer pd = new ShutdownHookSingleProcessDestroyer("java -jar " + jarToExecute.getName(), exitTimeoutSeconds);
+            final boolean waitOnShutdown = Boolean.valueOf(config.getProperty(PROP_WAIT_ONSHUTDOWN, "false"));
+            log.info("Setting up ProcessDestroyer with waitOnShutdown=" + waitOnShutdown);
+            pd.setWaitOnShutdown(waitOnShutdown);
+            executor.setProcessDestroyer(pd);
+            executor.execute(cl, h);
+        }
+    }
+
+    /** Stop the process that we started, if any, and wait for it to exit before returning */
+    public void stop() {
+        if(executor == null) {
+            throw new IllegalStateException("Process not started, no Executor set");
+        }
+        final Object d = executor.getProcessDestroyer();
+        if(d instanceof ShutdownHookSingleProcessDestroyer) {
+            ((ShutdownHookSingleProcessDestroyer)d).destroyProcess(true);
+            log.info("Process destroyed");
+        } else {
+            throw new IllegalStateException(d + " is not a Runnable, cannot destroy process");
+        }
+    }
+}
diff --git a/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java b/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java
new file mode 100644
index 0000000..c42fb35
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java
@@ -0,0 +1,117 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements. See the NOTICE file distributed with this
+ * work for additional information regarding copyright ownership. The ASF
+ * licenses this file to You 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 org.apache.sling.testing.serversetup.jarexec;
+
+import java.util.Timer;
+import java.util.TimerTask;
+
+import org.apache.commons.exec.ProcessDestroyer;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+/** Simple ProcessDestroyer for a single process, meant to be used
+ *  with our JarExecutor. 
+ */
+class ShutdownHookSingleProcessDestroyer implements ProcessDestroyer, Runnable {
+
+    private final Logger log = LoggerFactory.getLogger(getClass());
+    private Thread shutdownHookThread;
+    private Process process;
+    private final int timeoutSeconds;
+    private final String processInfo;
+    private boolean waitOnShutdown = false;
+    
+    public ShutdownHookSingleProcessDestroyer(String processInfo, int timeoutSeconds) {
+        this.processInfo = processInfo;
+        this.timeoutSeconds = timeoutSeconds;
+    }
+    
+    public boolean getWaitOnShutdown() {
+        return waitOnShutdown;
+    }
+
+    public void setWaitOnShutdown(boolean waitOnShutdown) {
+        this.waitOnShutdown = waitOnShutdown;
+    }
+
+    public synchronized boolean add(Process p) {
+        if(process != null) {
+            throw new IllegalStateException("Process already set: " + process);
+        }
+        
+        if(shutdownHookThread == null) {
+            shutdownHookThread = new Thread(this, getClass().getSimpleName());
+            Runtime.getRuntime().addShutdownHook(shutdownHookThread);
+        }
+        
+        process = p;
+        return true;
+    }
+
+    public synchronized boolean remove(Process p) {
+        p = null;
+        return true;
+    }
+
+    public int size() {
+        return 1;
+    }
+    
+    public void run() {
+        destroyProcess(waitOnShutdown);
+    }
+    
+   public void destroyProcess(boolean waitForIt) {
+       Process toDestroy = null;
+       synchronized (this) {
+           toDestroy = process;
+           process = null;
+       }
+       
+       if(toDestroy == null) {
+           return;
+       }
+       
+       toDestroy.destroy();
+       
+       if(waitForIt) {
+           log.info("Waiting for destroyed process {} to exit (timeout={} seconds)", processInfo, timeoutSeconds);
+           final Thread mainThread = Thread.currentThread();
+           final Timer t = new Timer(true);
+           final TimerTask task = new TimerTask() {
+                @Override
+                public void run() {
+                    mainThread.interrupt();
+                }
+           };
+           t.schedule(task, timeoutSeconds * 1000L);
+           try {
+               toDestroy.waitFor();
+               try {
+                   final int exit = toDestroy.exitValue();
+                   log.info("Process {} ended with exit code {}", processInfo, exit);
+               } catch(IllegalStateException ise) {
+                   log.error("Failed to destroy process " + processInfo);
+               }
+           } catch (InterruptedException e) {
+               log.error("Timeout waiting for process " + processInfo + " to exit");
+            } finally {
+                t.cancel();
+            }
+       }
+   }
+}
\ No newline at end of file
diff --git a/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java b/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java
new file mode 100644
index 0000000..5f07b46
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+@Version("1.0.0")
+package org.apache.sling.testing.serversetup.jarexec;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/main/java/org/apache/sling/testing/serversetup/package-info.java b/src/main/java/org/apache/sling/testing/serversetup/package-info.java
new file mode 100644
index 0000000..21c5aa2
--- /dev/null
+++ b/src/main/java/org/apache/sling/testing/serversetup/package-info.java
@@ -0,0 +1,24 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one
+ * or more contributor license agreements.  See the NOTICE file
+ * distributed with this work for additional information
+ * regarding copyright ownership.  The ASF licenses this file
+ * to you 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.
+ */
+
+@Version("1.0.0")
+package org.apache.sling.testing.serversetup;
+
+import aQute.bnd.annotation.Version;
+
diff --git a/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java b/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java
new file mode 100644
index 0000000..c8dbfc9
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java
@@ -0,0 +1,185 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.test;
+
+import static org.junit.Assert.assertEquals;
+import static org.junit.Assert.assertNull;
+import static org.junit.Assert.assertTrue;
+import static org.junit.Assert.fail;
+
+import java.util.Properties;
+
+import org.apache.sling.testing.serversetup.ServerSetup;
+import org.apache.sling.testing.serversetup.ServerSetupSingleton;
+import org.junit.Before;
+import org.junit.Test;
+
+/** Test the ServerSetupSingleton */
+public class ServerSetupSingletonTest {
+    private ServerSetup serverSetup;
+    private Properties props;
+    
+    @Before
+    public void setup() throws Exception {
+        props = new Properties();
+        props.setProperty(ServerSetupSingleton.CLASS_NAME_PROP, TestServerSetup.class.getName());
+        props.setProperty(ServerSetup.PHASES_TO_RUN_PROP, "one, \t\n two, three, four, five  \t");
+        serverSetup = ServerSetupSingleton.instance(props);
+        serverSetup.setConfig(props);
+        TestSetupPhase.clearExecutionLog();
+        TestSetupPhase.failingPhases = "";
+    }
+
+    @Test
+    public void testProperties() {
+        assertTrue(serverSetup.getConfig() == props);
+    }
+    
+    @Test
+    public void testContext() {
+        final String key = "foo";
+        assertNull(serverSetup.getContext().get(key));
+        serverSetup.getContext().put(key, this);
+        assertEquals(serverSetup.getContext().get(key), this);
+    }
+    
+    @Test
+    public void testStartup() throws Exception {
+        serverSetup.setupTestServer();
+        assertEquals("Expecting all startup phases to have run",
+                "one,two,three", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.setupTestServer();
+        assertEquals("Expecting second setup call to have no effect",
+                "one,two,three", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test
+    public void testShutdown() throws Exception {
+        serverSetup.shutdown();
+        assertEquals("Expecting all shutdown phases to have run",
+                "four,five", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.shutdown();
+        assertEquals("Expecting second shutdown call to be ignored",
+                "four,five", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test
+    public void testStartupAndShutdown() throws Exception {
+        serverSetup.setupTestServer();
+        assertEquals("Expecting all startup phases to have run",
+                "one,two,three", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.shutdown();
+        assertEquals("Expecting all phases to have run",
+                "one,two,three,four,five", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test
+    public void testStartupSomeOnly() throws Exception {
+        props.setProperty(ServerSetup.PHASES_TO_RUN_PROP, "one, three, five");
+        serverSetup.setConfig(props);
+        
+        serverSetup.setupTestServer();
+        assertEquals("Expecting only two startup phases to have run",
+                "one,three", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.setupTestServer();
+        assertEquals("Expecting second setup call to have no effect",
+                "one,three", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test
+    public void testShutdownSomeOnly() throws Exception {
+        props.setProperty(ServerSetup.PHASES_TO_RUN_PROP, "four");
+        serverSetup.setConfig(props);
+        
+        serverSetup.shutdown();
+        assertEquals("Expecting only one shutdown phase to have run",
+                "four", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.shutdown();
+        assertEquals("Expecting second setup call to have no effect",
+                "four", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test
+    public void testFailingStartup() {
+        TestSetupPhase.failingPhases = "two, five";
+        
+        // setupTestServer will fail every time it's called
+        // after a failure, as that means the server is unusable
+        for(int i=0; i < 3; i++) {
+            try {
+                serverSetup.setupTestServer();
+                fail("startup should have failed");
+            } catch(Exception ignored) {
+            }
+            
+            assertEquals("Expecting only one startup phase to have run",
+                    "one", TestSetupPhase.executionLog.toString());
+        }
+    }
+    
+    @Test
+    public void testFailingShutdown() throws Exception {
+        TestSetupPhase.failingPhases = "two, five";
+        
+        try {
+            serverSetup.shutdown();
+            fail("shutdown should have failed");
+        } catch(Exception ignored) {
+        }
+        
+        assertEquals("Expecting only one startup phase to have run",
+                "four", TestSetupPhase.executionLog.toString());
+        
+        // Calling shutdown again does not throw an Exception again,
+        // it's not really useful at shutdown.
+        serverSetup.shutdown();
+        
+        assertEquals("Still expecting only one startup phase to have run",
+                "four", TestSetupPhase.executionLog.toString());
+    }
+    
+    @Test(expected=ServerSetup.SetupException.class)
+    public void testDuplicateStartupPhase() throws ServerSetup.SetupException {
+        serverSetup.addSetupPhase(new TestSetupPhase("two", true));
+    }
+    
+    @Test(expected=ServerSetup.SetupException.class)
+    public void testDuplicateShutdownPhase() throws ServerSetup.SetupException {
+        serverSetup.addSetupPhase(new TestSetupPhase("two", false));
+    }
+    
+    @Test
+    public void testAddPhasesLater() throws Exception {
+        props.setProperty(ServerSetup.PHASES_TO_RUN_PROP, "one, B, five, A, two");
+        serverSetup.setConfig(props);
+        serverSetup.addSetupPhase(new TestSetupPhase("A", true));
+        serverSetup.addSetupPhase(new TestSetupPhase("B", false));
+        serverSetup.setupTestServer();
+        
+        assertEquals("Expecting all startup phases to have run",
+                "one,A,two", TestSetupPhase.executionLog.toString());
+        
+        serverSetup.shutdown();
+        assertEquals("Expecting all phases to have run",
+                "one,A,two,B,five", TestSetupPhase.executionLog.toString());
+    }
+}
\ No newline at end of file
diff --git a/src/test/java/org/apache/sling/testing/serversetup/test/TestServerSetup.java b/src/test/java/org/apache/sling/testing/serversetup/test/TestServerSetup.java
new file mode 100644
index 0000000..8b8ec3b
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/serversetup/test/TestServerSetup.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.test;
+
+
+import org.apache.sling.testing.serversetup.ServerSetup;
+
+public class TestServerSetup extends ServerSetup {
+    public TestServerSetup() throws ServerSetup.SetupException {
+        addSetupPhase(new TestSetupPhase("one", true));
+        addSetupPhase(new TestSetupPhase("three", true));
+        addSetupPhase(new TestSetupPhase("two", true));
+        addSetupPhase(new TestSetupPhase("five", false));
+        addSetupPhase(new TestSetupPhase("four", false));
+    }
+}
diff --git a/src/test/java/org/apache/sling/testing/serversetup/test/TestSetupPhase.java b/src/test/java/org/apache/sling/testing/serversetup/test/TestSetupPhase.java
new file mode 100644
index 0000000..5e65a0c
--- /dev/null
+++ b/src/test/java/org/apache/sling/testing/serversetup/test/TestSetupPhase.java
@@ -0,0 +1,60 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You 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 org.apache.sling.testing.serversetup.test;
+
+
+import org.apache.sling.testing.serversetup.ServerSetup;
+import org.apache.sling.testing.serversetup.SetupPhase;
+
+class TestSetupPhase implements SetupPhase {
+    static StringBuilder executionLog;
+    static String failingPhases = "";
+    private final boolean isStartup;
+    private final String id;
+    
+    TestSetupPhase(String id, boolean isStartup) {
+        this.id = id;
+        this.isStartup = isStartup;
+    }
+    
+    static void clearExecutionLog() {
+        executionLog = new StringBuilder();
+    }
+    
+    public void run(ServerSetup owner) throws Exception {
+        if(failingPhases.contains(id)) {
+            throw new Exception("Failing as failingPhase contains my id");
+        }
+        if(executionLog.length() > 0) {
+            executionLog.append(",");
+        }
+        executionLog.append(getId());
+    }
+
+    public boolean isStartupPhase() {
+        return isStartup;
+    }
+    
+    @Override
+    public String toString() {
+        return getClass().getSimpleName() + "(" + id + ")";
+    }
+
+    public String getId() {
+        return id;
+    }
+}

-- 
To stop receiving notification emails like this one, please contact
"commits@sling.apache.org" <co...@sling.apache.org>.