package net.jangaroo.jooc.mvnplugin.test;
import com.thoughtworks.selenium.DefaultSelenium;
import com.thoughtworks.selenium.Selenium;
import com.thoughtworks.selenium.SeleniumException;
import org.apache.commons.io.FileUtils;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.codehaus.plexus.util.cli.CommandLineException;
import org.eclipse.jetty.server.Server;
import org.w3c.dom.Document;
import org.w3c.dom.NamedNodeMap;
import org.w3c.dom.NodeList;
import org.xml.sax.InputSource;
import org.xml.sax.SAXException;
import javax.xml.parsers.DocumentBuilder;
import javax.xml.parsers.DocumentBuilderFactory;
import javax.xml.parsers.ParserConfigurationException;
import java.io.File;
import java.io.FileReader;
import java.io.IOException;
import java.io.Reader;
import java.io.StringReader;
import java.net.InetAddress;
import java.net.UnknownHostException;
import java.util.ArrayList;
/**
* Executes JooUnit tests.
* Unpacks all dependency to its output directory, generates a tests.html which starts up the class
* <code>testSuiteName</code>. Since a real browser is the best JavaScript execution environment
* the test now fires up a jetty on a random port between <code>jooUnitJettyPortLowerBound</code> and
* <code>jooUnitJettyPortUpperBound</code> contacts a selenium server given by
* <code>jooUnitSeleniumRCHost</code>. The Selenium Remote Control then starts a browser, navigates
* the browser to the Jetty we just started and waits for <code>jooUnitTestExecutionTimeout</code>ms
* for the results to appear on the browser screen.
*
* @goal test
* @phase test
* @requiresDependencyResolution test
* @threadSafe
*/
public class JooTestMojo extends JooTestMojoBase {
/**
* Source directory to scan for files to compile.
*
* @parameter expression="${project.build.testSourceDirectory}"
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private File testSourceDirectory;
/**
* Set this to 'true' to bypass unit tests entirely. Its use is NOT RECOMMENDED, especially if you
* enable it using the "maven.test.skip" property, because maven.test.skip disables both running the
* tests and compiling the tests. Consider using the skipTests parameter instead.
*
* @parameter expression="${maven.test.skip}"
*/
private boolean skip;
/**
* Set this to 'true' to skip running tests, but still compile them. Its use is NOT RECOMMENDED, but quite
* convenient on occasion.
*
* @parameter expression="${skipTests}"
*/
private boolean skipTests;
/**
* Output directory for test results.
*
* @parameter expression="${project.build.directory}/surefire-reports/" default-value="${project.build.directory}/surefire-reports/"
* @required
*/
@SuppressWarnings({"UnusedDeclaration"})
private File testResultOutputDirectory;
/**
* @parameter
*/
@SuppressWarnings({"UnusedDeclaration"})
private String testResultFileName;
/**
* Specifies the time in milliseconds to wait for the test results in the browser. Default is 30000ms.
*
* @parameter
*/
@SuppressWarnings("FieldCanBeLocal")
private int jooUnitTestExecutionTimeout = 30000;
/**
* Specifies the number of retries when receiving unexpected result from phantomjs (crash?).
* Default is 5.
*
* @parameter
*/
@SuppressWarnings("FieldCanBeLocal")
private int jooUnitMaxRetriesOnCrashes = 5;
/**
* Defines the Selenium RC host. Default is localhost.
* If the system property SELENIUM_RC_HOST is set, it is used prior to the
* maven parameter.
*
* @parameter
*/
private String jooUnitSeleniumRCHost = "localhost";
/**
* Defines the Selenium RC port. Default is 4444.
*
* @parameter
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private int jooUnitSeleniumRCPort = 4444;
/**
* Selenium browser start command. Default is *firefox
*
* @parameter
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private String jooUnitSeleniumBrowserStartCommand = "*firefox";
/**
* Set this to true to ignore a failure during testing. Its use is NOT RECOMMENDED, but quite convenient on
* occasion.
*
* @parameter expression="${maven.test.failure.ignore}"
*/
private boolean testFailureIgnore;
/**
* The phantomjs executable. If not specified, it expects the phantomjs binary in the PATH.
* If not phantomjs executable (or an outdated one) is found, falls back to Selenium.
*
* @parameter expression="${phantomjs.bin}" default-value="phantomjs"
*/
@SuppressWarnings({"UnusedDeclaration", "FieldCanBeLocal"})
private String phantomBin;
public void execute() throws MojoExecutionException, MojoFailureException {
if (!skip && !skipTests && isTestAvailable()) {
Server server = jettyRunTest(true);
String url = getTestUrl(server);
try {
File testResultOutputFile = new File(testResultOutputDirectory, getTestResultFileName());
File phantomTestRunner = new File(testResultOutputDirectory, "phantomjs-joounit-page-runner.js");
FileUtils.copyInputStreamToFile(getClass().getResourceAsStream("/net/jangaroo/jooc/mvnplugin/phantomjs-joounit-page-runner.js"), phantomTestRunner);
final PhantomJsTestRunner phantomJsTestRunner = new PhantomJsTestRunner(phantomBin, url, testResultOutputFile.getPath(), phantomTestRunner.getPath(), jooUnitTestExecutionTimeout, jooUnitMaxRetriesOnCrashes, getLog());
if (phantomJsTestRunner.canRun()) {
executePhantomJs(testResultOutputFile, phantomJsTestRunner);
} else {
executeSelenium(url);
}
} catch (IOException e) {
throw new MojoExecutionException("Cannot create local copy of phantomjs-joounit-page-runner.js", e);
} finally {
try {
server.stop();
} catch (Exception e) {
// never mind we just couldn't step the selenium server.
getLog().error("Could not stop test Jetty.", e);
}
}
}
}
private void executePhantomJs(File testResultOutputFile, PhantomJsTestRunner phantomJsTestRunner) throws MojoFailureException, MojoExecutionException {
getLog().info("running phantomjs: " + phantomJsTestRunner.toString());
try {
final boolean exitCode = phantomJsTestRunner.execute();
if (exitCode) {
evalTestOutput(new FileReader(testResultOutputFile));
} else {
signalError();
}
} catch (CommandLineException e) {
throw wrap(e);
} catch (IOException e) {
throw wrap(e);
} catch (ParserConfigurationException e) {
throw wrap(e);
} catch (SAXException e) {
throw wrap(e);
}
}
void executeSelenium(String testsHtmlUrl) throws MojoExecutionException, MojoFailureException {
jooUnitSeleniumRCHost = System.getProperty("SELENIUM_RC_HOST", jooUnitSeleniumRCHost);
try {
//check wether the host is reachable
//noinspection ResultOfMethodCallIgnored
InetAddress.getAllByName(jooUnitSeleniumRCHost);
} catch (UnknownHostException e) {
throw new MojoExecutionException("Cannot resolve host " + jooUnitSeleniumRCHost +
". Please specify a host running the selenium remote control or skip tests" +
" by -DskipTests", e);
}
getLog().info("JooTest report directory: " + testResultOutputDirectory.getAbsolutePath());
Selenium selenium = new DefaultSelenium(jooUnitSeleniumRCHost, jooUnitSeleniumRCPort, jooUnitSeleniumBrowserStartCommand, testsHtmlUrl);
try {
selenium.start();
getLog().debug("Opening " + testsHtmlUrl);
selenium.open(testsHtmlUrl);
getLog().debug("Waiting for test results for " + jooUnitTestExecutionTimeout + "ms ...");
selenium.waitForCondition("selenium.browserbot.getCurrentWindow().result != null || selenium.browserbot.getCurrentWindow().classLoadingError != null", "" + jooUnitTestExecutionTimeout);
String classLoadingError = selenium.getEval("selenium.browserbot.getCurrentWindow().classLoadingError");
if (classLoadingError != null && !classLoadingError.equals("null")) {
throw new MojoExecutionException(classLoadingError);
}
String testResultXml = selenium.getEval("selenium.browserbot.getCurrentWindow().result");
writeResultToFile(testResultXml);
evalTestOutput(new StringReader(testResultXml));
} catch (IOException e) {
throw new MojoExecutionException("Cannot write test results to file", e);
} catch (ParserConfigurationException e) {
throw new MojoExecutionException("Cannot create a simple XML Builder", e);
} catch (SAXException e) {
throw new MojoExecutionException("Cannot parse test result", e);
} catch (SeleniumException e) {
throw new MojoExecutionException("Selenium setup exception", e);
} finally {
selenium.stop();
}
}
File writeResultToFile(java.lang.String testResultXml) throws IOException {
File result = new File(testResultOutputDirectory, getTestResultFileName());
FileUtils.writeStringToFile(result, testResultXml);
if (!result.setLastModified(System.currentTimeMillis())) {
getLog().warn("could not set modification time of file " + result);
}
return result;
}
private String getTestResultFileName() {
return testResultFileName != null ? testResultFileName : "TEST-" + project.getArtifactId() + ".xml";
}
void evalTestOutput(Reader inStream) throws ParserConfigurationException, IOException, SAXException, MojoFailureException {
DocumentBuilderFactory documentBuilderFactory = DocumentBuilderFactory.newInstance();
DocumentBuilder dBuilder = documentBuilderFactory.newDocumentBuilder();
InputSource inSource = new InputSource(inStream);
Document d = dBuilder.parse(inSource);
NodeList nl = d.getChildNodes();
NamedNodeMap namedNodeMap = nl.item(0).getAttributes();
final String failures = namedNodeMap.getNamedItem("failures").getNodeValue();
final String errors = namedNodeMap.getNamedItem("errors").getNodeValue();
final String tests = namedNodeMap.getNamedItem("tests").getNodeValue();
final String time = namedNodeMap.getNamedItem("time").getNodeValue();
final String name = namedNodeMap.getNamedItem("name").getNodeValue();
getLog().info(name + " tests run: " + tests + ", Failures: " + failures + ", Errors: " + errors + ", time: " + time + " ms");
if (Integer.parseInt(errors) > 0 || Integer.parseInt(failures) > 0) {
signalFailure();
}
}
private void signalError() throws MojoExecutionException {
throw new MojoExecutionException("There are errors");
}
private void signalFailure() throws MojoFailureException {
if (!testFailureIgnore) {
throw new MojoFailureException("There are test failures");
}
}
public void setSkip(boolean b) {
this.skip = b;
}
public void setSkipTests(boolean b) {
this.skipTests = b;
}
public void setTestSourceDirectory(File f) {
this.testSourceDirectory = f;
}
public void setTestResources(ArrayList<org.apache.maven.model.Resource> resources) {
this.testResources = resources;
}
public void setTestFailureIgnore(boolean b) {
this.testFailureIgnore = b;
}
public void setTestOutputDirectory(File testOutputDirectory) {
this.testOutputDirectory = testOutputDirectory;
}
}