You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@sling.apache.org by bd...@apache.org on 2016/04/29 16:01:27 UTC

svn commit: r1741631 [1/2] - in /sling/trunk/testing/serversetup: ./ src/ src/main/ src/main/java/ src/main/java/org/ src/main/java/org/apache/ src/main/java/org/apache/sling/ src/main/java/org/apache/sling/testing/ src/main/java/org/apache/sling/testi...

Author: bdelacretaz
Date: Fri Apr 29 14:01:27 2016
New Revision: 1741631

URL: http://svn.apache.org/viewvc?rev=1741631&view=rev
Log:
SLING-5703 - new serversetup module extracted from testing/tools. Contributed by Andrei Dulvac, thanks!

Added:
    sling/trunk/testing/serversetup/   (with props)
    sling/trunk/testing/serversetup/pom.xml
    sling/trunk/testing/serversetup/src/
    sling/trunk/testing/serversetup/src/main/
    sling/trunk/testing/serversetup/src/main/java/
    sling/trunk/testing/serversetup/src/main/java/org/
    sling/trunk/testing/serversetup/src/main/java/org/apache/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java
    sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/package-info.java
    sling/trunk/testing/serversetup/src/test/
    sling/trunk/testing/serversetup/src/test/java/
    sling/trunk/testing/serversetup/src/test/java/org/
    sling/trunk/testing/serversetup/src/test/java/org/apache/
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/TestServerSetup.java
    sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/TestSetupPhase.java

Propchange: sling/trunk/testing/serversetup/
------------------------------------------------------------------------------
--- svn:ignore (added)
+++ svn:ignore Fri Apr 29 14:01:27 2016
@@ -0,0 +1,19 @@
+target
+sling
+bin
+logs
+jackrabbit-repository
+derby.log
+*.iml
+*.ipr
+*.iws
+.settings
+.project
+.classpath
+.externalToolBuilders
+maven-eclipse.xml
+jackrabbit
+
+
+
+

Added: sling/trunk/testing/serversetup/pom.xml
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/pom.xml?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/pom.xml (added)
+++ sling/trunk/testing/serversetup/pom.xml Fri Apr 29 14:01:27 2016
@@ -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>

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetup.java Fri Apr 29 14:01:27 2016
@@ -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);
+    }
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/ServerSetupSingleton.java Fri Apr 29 14:01:27 2016
@@ -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());
+    }
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/SetupPhase.java Fri Apr 29 14:01:27 2016
@@ -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();
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/StartRunnableJarPhase.java Fri Apr 29 14:01:27 2016
@@ -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;
+            }
+        };
+    }
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstance.java Fri Apr 29 14:01:27 2016
@@ -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();
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceManager.java Fri Apr 29 14:01:27 2016
@@ -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

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstanceState.java Fri Apr 29 14:01:27 2016
@@ -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

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingInstancesRule.java Fri Apr 29 14:01:27 2016
@@ -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

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/SlingTestBase.java Fri Apr 29 14:01:27 2016
@@ -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;
+    }
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/instance/package-info.java Fri Apr 29 14:01:27 2016
@@ -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;
+

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/JarExecutor.java Fri Apr 29 14:01:27 2016
@@ -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");
+        }
+    }
+}

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/ShutdownHookSingleProcessDestroyer.java Fri Apr 29 14:01:27 2016
@@ -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

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/jarexec/package-info.java Fri Apr 29 14:01:27 2016
@@ -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;
+

Added: sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/package-info.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/package-info.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/package-info.java (added)
+++ sling/trunk/testing/serversetup/src/main/java/org/apache/sling/testing/serversetup/package-info.java Fri Apr 29 14:01:27 2016
@@ -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;
+

Added: sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java
URL: http://svn.apache.org/viewvc/sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java?rev=1741631&view=auto
==============================================================================
--- sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java (added)
+++ sling/trunk/testing/serversetup/src/test/java/org/apache/sling/testing/serversetup/test/ServerSetupSingletonTest.java Fri Apr 29 14:01:27 2016
@@ -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