This is an automated email from the ASF dual-hosted git repository. rombert pushed a commit to branch master in repository https://gitbox.apache.org/repos/asf/sling-org-apache-sling-testing-serversetup.git
commit 1fac0338d3b77a4269e90e242909cbe64f27f863 Author: Bertrand Delacretaz <[email protected]> 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@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 "[email protected]" <[email protected]>.
