/* * 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); } }