/*
* Copyright 2008 Alin Dreghiciu.
* Copyright 2008-2011 Toni Menzel.
*
* Licensed 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.ops4j.pax.exam.spi;
import static org.ops4j.pax.exam.Constants.START_LEVEL_SYSTEM_BUNDLES;
import static org.ops4j.pax.exam.CoreOptions.bootDelegationPackage;
import static org.ops4j.pax.exam.CoreOptions.frameworkStartLevel;
import static org.ops4j.pax.exam.CoreOptions.serverMode;
import static org.ops4j.pax.exam.CoreOptions.url;
import static org.ops4j.pax.exam.CoreOptions.when;
import java.io.BufferedReader;
import java.io.IOException;
import java.io.InputStreamReader;
import java.io.OutputStream;
import java.io.OutputStreamWriter;
import java.io.PrintWriter;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
import java.net.ServerSocket;
import java.net.Socket;
import java.util.ArrayList;
import java.util.Iterator;
import java.util.List;
import java.util.ServiceLoader;
import org.ops4j.pax.exam.Configuration;
import org.ops4j.pax.exam.ConfigurationManager;
import org.ops4j.pax.exam.Constants;
import org.ops4j.pax.exam.ExamSystem;
import org.ops4j.pax.exam.Option;
import org.ops4j.pax.exam.OptionUtils;
import org.ops4j.pax.exam.TestContainer;
import org.ops4j.pax.exam.TestContainerException;
import org.ops4j.pax.exam.TestContainerFactory;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
/**
* Pax Exam runtime.
*
* @author Alin Dreghiciu (adreghiciu@gmail.com)
* @author Toni Menzel (toni@okidokiteam.com)
* @since 0.3.0, December 09, 2008
*
*/
public class PaxExamRuntime {
private static final Logger LOG = LoggerFactory.getLogger(PaxExamRuntime.class);
/** Hidden utility class constructor. */
private PaxExamRuntime() {
}
/**
* Discovers the regression container. Discovery is performed via ServiceLoader discovery
* mechanism.
*
* @return discovered test container
*/
public static TestContainerFactory getTestContainerFactory() {
sanityCheck();
TestContainerFactory factory = ServiceLoader.load(TestContainerFactory.class).iterator()
.next();
LOG.debug("Found TestContainerFactory: "
+ ((factory != null) ? factory.getClass().getName() : "<NONE>"));
return factory;
}
/**
* Convenience factory when just dealing with one container (intentionally). Note, this will
* break if there is not exaclty one container available and parsed from options. If there are
* more containers, just the first (whatever comes first) will be picked.
*
* @param system
* to be used.
* @return exactly one Test Container.
*/
public static TestContainer createContainer(ExamSystem system) {
return getTestContainerFactory().create(system)[0];
}
/**
* Creates and starts a test container using options from a configuration class.
*
* @param configurationClassName
* fully qualified class name of a configuration class.
* @return started test container
* @throws Exception when options cannot be parsed
*/
public static TestContainer createContainer(String configurationClassName) throws Exception {
Option[] options = getConfigurationOptions(configurationClassName);
ExamSystem system = DefaultExamSystem.create(options);
TestContainer testContainer = PaxExamRuntime.createContainer(system);
testContainer.start();
return testContainer;
}
/**
* Opens a server socket listening for text commands on the given port.
* Each command is terminated by a newline. The server expects a "stop" command
* followed by a "quit" command.
*
* @param testContainer
* @param localPort
*/
private static void waitForStop(TestContainer testContainer, int localPort) {
try {
ServerSocket serverSocket = new ServerSocket(localPort);
Socket socket = serverSocket.accept();
InputStreamReader isr = new InputStreamReader(socket.getInputStream(), "UTF-8");
BufferedReader reader = new BufferedReader(isr);
OutputStream os = socket.getOutputStream();
OutputStreamWriter writer = new OutputStreamWriter(os, "UTF-8");
PrintWriter pw = new PrintWriter(writer, true);
boolean running = true;
while (running) {
String command = reader.readLine();
LOG.debug("command = {}", command);
if (command.equals("stop")) {
testContainer.stop();
pw.println("stopped");
writer.flush();
LOG.info("test container stopped");
}
else if (command.equals("quit")) {
LOG.debug("quitting PaxExamRuntime");
pw.close();
socket.close();
serverSocket.close();
running = false;
}
}
}
catch (IOException exc) {
LOG.debug("socket error", exc);
}
}
private static Option[] getConfigurationOptions(String configurationClassName)
throws ClassNotFoundException, InstantiationException, IllegalAccessException,
InvocationTargetException {
Class<?> klass = Class.forName(configurationClassName, true,
PaxExamRuntime.class.getClassLoader());
Method m = getConfigurationMethod(klass);
Object configClassInstance = klass.newInstance();
Option[] options = (Option[]) m.invoke(configClassInstance);
return options;
}
private static Method getConfigurationMethod(Class<?> klass) {
Method[] methods = klass.getMethods();
for (Method m : methods) {
Configuration conf = m.getAnnotation(Configuration.class);
if (conf != null) {
return m;
}
}
throw new IllegalArgumentException(klass.getName() + " has no @Configuration method");
}
public static ExamSystem createTestSystem(Option... options) throws IOException {
return DefaultExamSystem.create(OptionUtils.combine(options, defaultTestSystemOptions()));
}
public static ExamSystem createServerSystem(Option... options) throws IOException {
return DefaultExamSystem.create(OptionUtils.combine(options, defaultServerSystemOptions()));
}
private static Option[] defaultTestSystemOptions() {
ConfigurationManager cm = new ConfigurationManager();
String logging = cm.getProperty(Constants.EXAM_LOGGING_KEY,
Constants.EXAM_LOGGING_PAX_LOGGING);
return new Option[] {
bootDelegationPackage("sun.*"),
frameworkStartLevel(Constants.START_LEVEL_TEST_BUNDLE),
url("link:classpath:META-INF/links/org.ops4j.pax.exam.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.exam.inject.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.extender.service.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.osgi.compendium.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
when(logging.equals(Constants.EXAM_LOGGING_PAX_LOGGING)).useOptions(
url("link:classpath:META-INF/links/org.ops4j.pax.logging.api.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES)),
url("link:classpath:META-INF/links/org.ops4j.base.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.swissbox.core.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.swissbox.extender.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.swissbox.framework.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.swissbox.lifecycle.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.ops4j.pax.swissbox.tracker.link").startLevel(
START_LEVEL_SYSTEM_BUNDLES),
url("link:classpath:META-INF/links/org.apache.geronimo.specs.atinject.link")
.startLevel(START_LEVEL_SYSTEM_BUNDLES) };
}
private static Option[] defaultServerSystemOptions() {
return new Option[] { bootDelegationPackage("sun.*"), serverMode() };
}
/**
* Select yourself
*
* @param select
* the exact implementation if you dont want to rely on commons util discovery or
* change different containers in a single project.
*
* @return discovered regression container
*/
public static TestContainerFactory getTestContainerFactory(
Class<? extends TestContainerFactory> select) {
try {
return select.newInstance();
}
catch (InstantiationException e) {
throw new IllegalArgumentException("Class " + select
+ "is not a valid Test Container Factory.", e);
}
catch (IllegalAccessException e) {
throw new IllegalArgumentException("Class " + select
+ "is not a valid Test Container Factory.", e);
}
}
/**
* Exits with an exception if Classpath not set up properly.
*/
private static void sanityCheck() {
List<TestContainerFactory> factories = new ArrayList<TestContainerFactory>();
Iterator<TestContainerFactory> iter = ServiceLoader.load(TestContainerFactory.class)
.iterator();
while (iter.hasNext()) {
factories.add(iter.next());
}
if (factories.size() == 0) {
throw new TestContainerException("No TestContainer implementation in Classpath");
}
else if (factories.size() > 1) {
for (TestContainerFactory fac : factories) {
LOG.error("Ambiguous TestContainer: " + fac.getClass().getName());
}
throw new TestContainerException("Too many TestContainer implementations in Classpath");
}
else {
// good!
return;
}
}
/**
* Runs a standalone container in server mode which can be terminated gracefully by sending text
* commands over a socket.
* <p>
* This class must be invoked with two arguments:
* <ul>
* <li>The fully qualified name of a {@code @Configuration} class
* <li>A port number.
* </ul>
* After starting the container, this process will listen on the given port for a "stop"
* command, sending a reply of "stopped". When finally receiving a "quit" command, the process
* will exit.
*
* @param args
* command line argument
* @throws Exception when options cannot be parsed
*/
public static void main(String[] args) throws Exception {
if (args.length != 2) {
throw new IllegalArgumentException(
"required arguments: <configuration class name> <shutdown port>");
}
TestContainer testContainer = createContainer(args[0]);
waitForStop(testContainer, Integer.parseInt(args[1]));
}
}