/* * 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.codehaus.jstestrunner.junit; import java.io.File; import java.lang.annotation.ElementType; import java.lang.annotation.Inherited; import java.lang.annotation.Retention; import java.lang.annotation.RetentionPolicy; import java.lang.annotation.Target; import java.net.URL; import java.util.List; import java.util.logging.Level; import java.util.logging.Logger; import org.codehaus.jstestrunner.JSTestExecutionServer; import org.codehaus.jstestrunner.JSTestSuiteRunnerService; import org.codehaus.jstestrunner.jetty.JSTestResultHandler.JSTestResult; import org.codehaus.jstestrunner.jetty.JSTestResultServer; import org.junit.runner.Description; import org.junit.runner.notification.Failure; import org.junit.runner.notification.RunNotifier; import org.junit.runners.ParentRunner; import org.junit.runners.model.InitializationError; import org.junit.runners.model.Statement; /** * A JavaScript Test Runner Suite specifically for JUnit. It allows a pattern of * URLs to be requested that will cause tests to occur. The test runner will * first ensure that a test results server container is started. By default the * test results server will listen for requests on 0.0.0.0:9080. Theses defaults * can be overridden. * * <p> * Once the test results server started it will then map the context path for * serving tests to the target/classes and target/test-classes folders. The * default includes policy is to load all files matching the pattern * "**\/*Test.html" and "**\/*Test.htm". There is no default excludes. * * <p> * A test execution server is also managed as a separate process. The command * used by the test runner is expected as a system property named * "org.codehaus.jstestrunner.commandPattern". * * <p> * * @see JSTestSuiteRunnerService for more information. * * @author Christopher Hunt */ public class JSTestSuiteRunner extends ParentRunner<URL> { /** * Describes the context path used by the test results server. Defaults to * "/". */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface ContextPath { String value(); } /** * Describes the URL patterns that are to be excluded for test execution. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Exclude { String[] value(); } /** * Describes the host and port of the test results server using the * host:port convention. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Host { String value(); } /** * Describes the URL patterns that are to be included for test execution. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface Include { String[] value(); } /** * A JavaScript execution failure object. */ private static class JSTestFailure extends Failure { private final URL url; public JSTestFailure(Description description, URL url, String message) { super(description, new RuntimeException(message)); this.url = url; } @Override public String getTestHeader() { return JSTestSuiteRunnerService.getFormattedPath(url); } @Override public String getTrace() { // The stack means nothing here. return getMessage(); } @Override public String toString() { return getTestHeader(); } } /** * Describes where in relation to the project home folder the context path * should be mapped to. This can have multiple values. The defaults are * "target/classes" and "target/test-classes" given Maven's convention. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface ResourceBase { String[] value(); } /** * Describes where in relation to the project home folder the test runner * file can be made available from for execution purposes. This file runs on * the JS test execution engine and processes each file for testing. By * default the target/js-testrunner folder is used. */ @Retention(RetentionPolicy.RUNTIME) @Target(ElementType.TYPE) @Inherited public @interface TestRunnerFilePath { String value(); } private final JSTestSuiteRunnerService jSTestSuiteRunnerService; private final List<URL> urls; public JSTestSuiteRunner(Class<?> testClass) throws InitializationError { super(testClass); // We don't care to see what these packages are up to unless there's // some complaining to be done. Logger logger = Logger.getLogger("org.eclipse.jetty"); logger.setLevel(Level.WARNING); // Set up our host. Host hostAnnotation = testClass.getAnnotation(Host.class); String host; int port; if (hostAnnotation == null) { host = "localhost"; port = 9080; } else { String[] hostParts = hostAnnotation.value().split(":"); if (hostParts.length != 2) { throw new InitializationError( "Host must be of the form host:port"); } host = hostParts[0]; port = Integer.valueOf(hostParts[1]); } // Set up our context path. ContextPath contextPathAnnotation = testClass .getAnnotation(ContextPath.class); String contextPath; if (contextPathAnnotation == null) { contextPath = "/"; } else { contextPath = contextPathAnnotation.value(); } // Set up our resource bases. ResourceBase resourceBaseAnnotation = testClass .getAnnotation(ResourceBase.class); String[] resourceBases; if (resourceBaseAnnotation == null) { resourceBases = new String[] { "do_not_checkin" + File.separator + "target" + File.separator + "classes", "do_not_checkin" + File.separator + "target" + File.separator + "test-classes" }; } else { resourceBases = resourceBaseAnnotation.value(); } // Inclusion patterns Include includeAnnotation = testClass.getAnnotation(Include.class); String[] includes; if (includeAnnotation == null) { includes = new String[] { "**/*Test.html", "**/*Test.htm" }; } else { includes = includeAnnotation.value(); } // Inclusion patterns Exclude excludeAnnotation = testClass.getAnnotation(Exclude.class); String[] excludes; if (excludeAnnotation == null) { excludes = new String[0]; } else { excludes = excludeAnnotation.value(); } // Resolve the commandPattern from system properties. String commandPattern = System .getProperty("org.codehaus.jstestrunner.commandPattern"); if (commandPattern == null) { commandPattern = "phantomjs '%1$s' %2$s"; } // Test runner file path. TestRunnerFilePath testRunnerFilePathAnnotation = testClass .getAnnotation(TestRunnerFilePath.class); String testRunnerFilePath; if (testRunnerFilePathAnnotation == null) { testRunnerFilePath = "target" + File.separator + "js-testrunner"; } else { testRunnerFilePath = testRunnerFilePathAnnotation.value(); } /** * Determine the URLs representing the tests. */ urls = JSTestSuiteRunnerService.scanTestFiles(host, port, resourceBases, includes, excludes); jSTestSuiteRunnerService = new JSTestSuiteRunnerService(); JSTestExecutionServer jSTestExecutionServer = new JSTestExecutionServer(); jSTestExecutionServer.setCommandPattern(commandPattern); jSTestExecutionServer.setTestRunnerFilePath(testRunnerFilePath); jSTestExecutionServer.setUrls(urls); JSTestResultServer jSTestResultServer = new JSTestResultServer(); jSTestResultServer.setContextPath(contextPath); jSTestResultServer.setPort(port); jSTestResultServer.setResourceBases(resourceBases); jSTestSuiteRunnerService .setjSTestExecutionServer(jSTestExecutionServer); jSTestSuiteRunnerService.setjSTestResultServer(jSTestResultServer); } /** * Clean up our test environment. * * @param statement * the statement we should append to. * @return the appended statement. */ private Statement afterTests(final Statement statement) { return new Statement() { @Override public void evaluate() throws Throwable { try { // Evaluate all that comes before this point. statement.evaluate(); } finally { jSTestSuiteRunnerService.afterTests(); } } }; } /** * Establish our test environment. * * @param statement * the statement to prepend. * @return the prepended statement. */ private Statement beforeTests(final Statement statement) { return new Statement() { @Override public void evaluate() throws Throwable { jSTestSuiteRunnerService.beforeTests(); // Evaluate the remaining statements. statement.evaluate(); } }; } @Override protected Statement classBlock(final RunNotifier notifier) { Statement statement = super.classBlock(notifier); statement = beforeTests(statement); statement = afterTests(statement); return statement; } @Override protected Description describeChild(URL url) { return Description .createTestDescription(this.getTestClass().getJavaClass(), JSTestSuiteRunnerService.getFormattedPath(url)); } @Override protected List<URL> getChildren() { return urls; } public JSTestSuiteRunnerService getjSTestSuiteRunnerService() { return jSTestSuiteRunnerService; } @Override protected void runChild(URL url, RunNotifier notifier) { Description description = describeChild(url); notifier.fireTestStarted(description); JSTestResult jsTestResult = jSTestSuiteRunnerService.runTest(url); if (jsTestResult != null) { if (jsTestResult.failures > 0) { JSTestFailure failure = new JSTestFailure(description, url, "Failures: " + jsTestResult.failures + ", passes: " + jsTestResult.passes + ":\n" + jsTestResult.message); notifier.fireTestFailure(failure); } else { notifier.fireTestFinished(description); } } else { JSTestFailure failure = new JSTestFailure(description, url, "Timed out waiting for test"); notifier.fireTestFailure(failure); } } }