/*
* 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.tools.sling;
import static org.junit.Assert.fail;
import java.io.File;
import java.util.ArrayList;
import java.util.Collections;
import java.util.LinkedList;
import java.util.List;
import java.util.Properties;
import java.util.TreeSet;
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.jarexec.JarExecutor;
import org.apache.sling.testing.tools.junit.TestDescriptionInterceptor;
import org.apache.sling.testing.tools.osgi.WebconsoleClient;
import org.junit.After;
import org.junit.runners.ParentRunner;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/** 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;
}
}