package net.jangaroo.jooc.mvnplugin.test;
import org.apache.maven.artifact.DependencyResolutionRequiredException;
import org.apache.maven.plugin.AbstractMojo;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.project.MavenProject;
import org.eclipse.jetty.server.Handler;
import org.eclipse.jetty.server.Server;
import org.eclipse.jetty.servlet.DefaultServlet;
import org.eclipse.jetty.servlet.ServletHolder;
import org.eclipse.jetty.util.resource.Resource;
import org.eclipse.jetty.util.resource.ResourceCollection;
import org.mortbay.jetty.plugin.JettyWebAppContext;
import java.io.File;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Base class for running tests either automatically (JooTestMojo) or start test Jetty and keep it running (JettyRunTestsMojo).
*/
public abstract class JooTestMojoBase extends AbstractMojo {
/**
* The maven project.
*
* @parameter expression="${project}"
* @required
* @readonly
*/
@SuppressWarnings({"UnusedDeclaration"})
protected MavenProject project;
/**
* Directory whose META-INF/RESOURCES/joo/classes sub-directory contains compiled classes.
*
* @parameter expression="${project.build.outputDirectory}"
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private File outputDirectory;
/**
* Directory whose joo/classes sub-directory contains compiled test classes.
*
* @parameter expression="${project.build.testOutputDirectory}" default-value="${project.build.testOutputDirectory}"
*/
protected File testOutputDirectory;
/**
* the tests.html file relative to the test resources folder
*
* @parameter default-value="tests.html"
*/
@SuppressWarnings({"UnusedDeclaration"})
protected String testsHtml;
/**
* Whether to load the test application in debug mode (#joo.debug).
*
* @parameter default-value=false
*/
@SuppressWarnings({"UnusedDeclaration"})
protected boolean debugTests;
/**
* the project's test resources
*
* @parameter expression="${project.testResources}"
*/
protected List<org.apache.maven.model.Resource> testResources;
/**
* To avoid port clashes when multiple tests are running at the same
* time on the same machine, the jetty port is selected randomly within
* an range of <code>[jooUnitJettyPortLowerBound:jooUnitJettyPortUpperBound]</code>.
* Every port is tried until a free one is found or all ports in the range
* are occupied (which results in the build to fail).
*
* @parameter expression="${jooUnitJettyPortUpperBound}" default-value=10200
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private int jooUnitJettyPortUpperBound;
/**
* To avoid port clashes when multiple tests are running at the same
* time on the same machine, the jetty port is selected randomly within
* an range of <code>[jooUnitJettyPortLowerBound:jooUnitJettyPortUpperBound]</code>.
* Every port is tried until a free one is found or all ports in the range
* are occupied (which results in the build to fail).
* When using goal <code>jetty-run-tests</code>, this lower bound is
* always used.
*
* @parameter expression="${jooUnitJettyPortLowerBound}" default-value=10100
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private int jooUnitJettyPortLowerBound;
/**
* The host name to use to reach the locally started Jetty listenes, usually the default, "localhost".
*
* @parameter expression="${jooUnitJettyHost}" default-value="localhost"
*/
@SuppressWarnings({"UnusedDeclaration"})
private String jooUnitJettyHost;
protected String getJettyUrl(Server server) {
return "http://" + jooUnitJettyHost + ":" + server.getConnectors()[0].getPort();
}
protected boolean isTestAvailable() {
for (org.apache.maven.model.Resource r : testResources) {
File testFile = new File(r.getDirectory(), testsHtml);
if (testFile.exists()) {
return true;
}
}
getLog().info("The tests.html file '" + testsHtml + "' could not be found. Skipping.");
return false;
}
protected Server jettyRunTest(boolean tryPortRange) throws MojoExecutionException {
JettyWebAppContext handler;
try {
handler = new JettyWebAppContext();
handler.setWebInfLib(findJars());
handler.setInitParameter("org.eclipse.jetty.servlet.Default.useFileMappedBuffer", "false");
List<Resource> baseResources = new ArrayList<Resource>();
baseResources.add(toResource(new File(outputDirectory, "META-INF/resources")));
baseResources.add(toResource(testOutputDirectory));
for (org.apache.maven.model.Resource r : testResources) {
File testResourceDirectory = new File(r.getDirectory());
if (testResourceDirectory.exists()) {
baseResources.add(toResource(testResourceDirectory));
}
}
handler.setBaseResource(new ResourceCollection(baseResources.toArray(new Resource[baseResources.size()])));
getLog().info("Using base resources " + baseResources);
ServletHolder servletHolder = new ServletHolder("default", DefaultServlet.class);
servletHolder.setInitParameter("cacheControl", "no-store, no-cache, must-revalidate, max-age=0");
handler.addServlet(servletHolder, "/");
getLog().info("Set servlet cache control to 'do not cache'.");
} catch (Exception e) {
throw wrap(e);
}
return startJetty(handler, tryPortRange);
}
protected List<File> findJars() throws DependencyResolutionRequiredException {
List<File> jars = new ArrayList<File>();
for (Object jarUrl : project.getTestClasspathElements()) {
File file = new File((String)jarUrl);
if (file.isFile()) { // should be a jar--don't add folders!
jars.add(file);
getLog().info("Test classpath: " + jarUrl);
} else {
getLog().info("Ignoring test classpath: " + jarUrl);
}
}
return jars;
}
private Resource toResource(File file) throws MojoExecutionException {
try {
return Resource.newResource(file);
} catch (IOException e) {
throw wrap(e);
}
}
private Server startJetty(Handler handler, boolean tryPortRange) throws MojoExecutionException {
if (tryPortRange && jooUnitJettyPortUpperBound != jooUnitJettyPortLowerBound) {
return startJettyOnRandomPort(handler);
} else {
try {
return startJettyOnPort(handler, jooUnitJettyPortLowerBound);
} catch (Exception e) {
throw wrapJettyException(e, jooUnitJettyPortLowerBound);
}
}
}
private Server startJettyOnRandomPort(Handler handler) throws MojoExecutionException {
List<Integer> ports = new ArrayList<Integer>(jooUnitJettyPortUpperBound - jooUnitJettyPortLowerBound + 1);
for (int i = jooUnitJettyPortLowerBound; i <= jooUnitJettyPortUpperBound; i++) {
ports.add(i);
}
Collections.shuffle(ports);
int lastPort = ports.get(ports.size() - 1);
Exception finalException = null;
for (int jooUnitJettyPort : ports) {
try {
return startJettyOnPort(handler, jooUnitJettyPort);
} catch (Exception e) {
if (jooUnitJettyPort != lastPort) {
getLog().info(String.format("Starting Jetty on port %d failed. Retrying ...", jooUnitJettyPort));
} else {
finalException = e;
}
}
}
throw wrapJettyException(finalException, lastPort);
}
private Server startJettyOnPort(Handler handler, int jettyPort) throws Exception {
Server server = new Server(jettyPort);
try {
server.setHandler(handler);
server.start();
getLog().info(String.format("Started Jetty for unit tests on port %d.", jettyPort));
} catch (Exception e) {
stopServerIgnoreException(server);
throw e;
}
return server;
}
protected MojoExecutionException wrap(Exception e) {
return new MojoExecutionException(e.toString(), e);
}
private MojoExecutionException wrapJettyException(Exception e, int jettyPort) {
getLog().error(String.format("Starting Jetty on port %d failed.", jettyPort));
return new MojoExecutionException(String.format("Cannot start jetty server on port %d.", jettyPort), e);
}
protected void stopServerIgnoreException(Server server) {
try {
server.stop();
} catch (Exception e1) {
getLog().warn("Stopping Jetty failed. Never mind.");
}
}
protected String getTestUrl(Server server) throws MojoExecutionException {
StringBuilder builder = new StringBuilder(getJettyUrl(server))
.append("/").append(testsHtml.replace(File.separatorChar, '/'));
if (debugTests) {
builder.append("#joo.debug");
}
return builder.toString();
}
static {
Resource.setDefaultUseCaches(false);
}
}