blob: 610c334beb3af22870679fd5fd2f8bd1edfdd9a2 [file] [log] [blame]
/*
* 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);
}
}