package com.github.linsolas.casperjsrunner; import static com.github.linsolas.casperjsrunner.ArgQuoter.quote; import static com.github.linsolas.casperjsrunner.CasperJsRuntimeFinder.findCasperRuntime; import static com.github.linsolas.casperjsrunner.CasperJsVersionRetriever.retrieveVersion; import static com.github.linsolas.casperjsrunner.CommandExecutor.executeCommand; import static com.github.linsolas.casperjsrunner.LogUtils.getLogger; import static com.github.linsolas.casperjsrunner.PathToNameBuilder.buildName; import static com.github.linsolas.casperjsrunner.PatternsChecker.checkPatterns; import org.apache.commons.exec.CommandLine; import org.apache.commons.lang3.StringUtils; import org.apache.maven.artifact.versioning.DefaultArtifactVersion; import org.apache.maven.execution.MavenSession; import org.apache.maven.plugin.AbstractMojo; import org.apache.maven.plugin.MojoExecutionException; import org.apache.maven.plugin.MojoFailureException; import org.apache.maven.plugins.annotations.Component; import org.apache.maven.plugins.annotations.LifecyclePhase; import org.apache.maven.plugins.annotations.Mojo; import org.apache.maven.plugins.annotations.Parameter; import org.apache.maven.toolchain.ToolchainManager; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import java.util.Map; /** * Runs JavaScript and/or CoffeScript test files on CasperJS instance * @author Romain Linsolas * @since 09/04/13 */ @Mojo(name = "test", defaultPhase = LifecyclePhase.TEST, threadSafe = true) public class CasperJSRunnerMojo extends AbstractMojo { // Parameters for the plugin /** * Complete path of the executable for CasperJS. * <br/><b>Default value:</b> * Found from <a href="http://maven.apache.org/guides/mini/guide-using-toolchains.html">toolchain</a> named <b><i>casperjs</b></i>, * then from this parameter, * then from PATH with default value of <b>casperjs</b> on Linux/Mac or <b>casperjs.bat</b> on Windows * @since 1.0.0 */ @Parameter(property = "casperjs.executable") private String casperExecPath; /** * Directory where the tests to execute are stored. * <br/>If <code>${tests.directory}/includes</code> and <code>${tests.directory}/scripts</code> directories exist, * this is changed to <code>${tests.directory}/scripts</code> and all <code>*.js</code> files in <code>${tests.directory}/includes</code> * will automatically be added to the CasperJS <code>--includes</code> list. * @since 1.0.0 */ @Parameter(property = "casperjs.tests.directory", defaultValue = "${basedir}/src/test/casperjs") private File testsDir; /** * Specify this parameter to run individual tests by file name, overriding the <code>testIncludes</code>/<code>testExcludes</code> parameters. * Each pattern you specify here will be used to create an include pattern formatted like <code>**/${test}.{js,coffee}</code>, so you can * just type "-Dtest=MyTest" to run a single test called <code>foo/MyTest.js</code> or <code>foo/MyTest.coffee</code>. * @since 1.0.0 */ @Parameter(property = "casperjs.test") private String test; /** * A list of <code><testsInclude></code> elements specifying the tests (by pattern) that should be included in testing. * <br/><b>Default value:</b> When not specified and when the test parameter is not specified, the default includes will be * (javascript patterns will only be set if <code>includeJS</code> is <code>true</code>, and coffee patterns will only be set * if <code>includeCS</code> is <code>true</code>) <br/><br/> <code><testsIncludes><br/>   <testsInclude>**/Test*.js</testsInclude><br/>   <testsInclude>**/*Test.js</testsInclude><br/>   <testsInclude>**/*TestCase.js</testsInclude><br/>   <testsInclude>**/Test*.coffee</testsInclude><br/>   <testsInclude>**/*Test.coffee</testsInclude><br/>   <testsInclude>**/*TestCase.coffee</testsInclude><br/> </testsIncludes></code> * @since 1.0.1 */ @Parameter private List<String> testsIncludes; /** * A list of <code><testsExclude></code> elements specifying the tests (by pattern) that should be excluded in testing. * @since 1.0.1 */ @Parameter private List<String> testsExcludes; /** * Do we ignore the tests failures. If yes, the plugin will not fail at the end if there was tests failures. * @since 1.0.0 */ @Parameter(property = "casperjs.ignoreTestFailures", defaultValue = "${maven.test.failure.ignore}") private boolean ignoreTestFailures = false; /** * Set the plugin to be verbose during its execution. * @since 1.0.0 */ @Parameter(property = "casperjs.verbose", defaultValue = "${maven.verbose}") private boolean verbose = false; /** * A flag to indicate if the *.js found in <code>tests.directory</code> should be executed. * @since 1.0.0 */ @Parameter(property = "casperjs.include.javascript", defaultValue="true") private boolean includeJS; /** * A flag to indicate if the *.coffee found in <code>tests.directory</code> should be executed. * @since 1.0.0 */ @Parameter(property = "casperjs.include.coffeescript", defaultValue="true") private boolean includeCS; /** * Environment variables to set on the command line, instead of the default, inherited, ones. * @since 1.0.0 */ @Parameter private Map<String, String> environmentVariables; /** * Set this to <code>true</code> to bypass unit tests entirely. * @since 1.0.1 */ @Parameter(property = "casperjs.skip", defaultValue="${maven.test.skip}") private boolean skip = false; // Parameters for the CasperJS options /** * Set the value for the CasperJS option <code>--pre=[pre-test.js]</code>: will add the tests contained in pre-test.js * before executing the test suite. If a <code>pre.js</code> file is found on the <code>${tests.directory}</code>, this * option will be set automatically * @since 1.0.0 */ @Parameter(property = "casperjs.pre") private String pre; /** * Set the value for the CasperJS option <code>--post=[post-test.js]</code>: will add the tests contained in post-test.js * after having executed the whole test suite. If a <code>post.js</code> file is found on the <code>${tests.directory}</code>, * this option will be set automatically * @since 1.0.0 */ @Parameter(property = "casperjs.post") private String post; /** * Set the value for the CasperJS option <code>--includes=[foo.js,bar.js]</code>: will includes the foo.js and bar.js files * before each test file execution. * @since 1.0.0 */ @Parameter(property = "casperjs.includes") private String includes; /** * A list of <code><includesPattern></code> elements specifying the files (by pattern) to set on the <code>--includes</code> * option.<br/>When not specified and the <code>${tests.directory}/includes</code> directory exists, this will be set to <br/><br/> <code><includesPatterns><br/>   <includesPattern>${tests.directory}/includes/**/*.js</includesPattern><br/> </includesPatterns></code> * @since 1.0.1 */ @Parameter private List<String> includesPatterns; /** * Should CasperJS generates XML reports, through the <code>--xunit=[filename]</code> option. * If <code>true</code>, such reports will be generated in the <code>reportsDirectory<code> directory, * with a name of <code>TEST-<test filename>.xml</code>. * @since 1.0.2 */ @Parameter(property = "casperjs.enableXmlReports", defaultValue = "false") private boolean enableXmlReports; /** * Directory where the xUnit reports will be stored. * @since 1.0.2 */ @Parameter(property = "casperjs.reports.directory", defaultValue = "${project.build.directory}/casperjs-reports") private File reportsDir; /** * Set the value for the CasperJS option <code>--log-level=[logLevel]</code>: sets the logging level (see http://casperjs.org/logging.html). * @since 1.0.0 */ @Parameter(property = "casperjs.logLevel") private String logLevel; /** * Set the CasperJS --direct option: will output log messages directly to the console. * Deprecated: use the <code>casperjsVerbose</code> option * @since 1.0.0 */ @Deprecated @Parameter(property = "casperjs.direct", defaultValue = "false") private boolean direct; /** * For CasperJS 1.1.x, set --verbose option, --direct for CasperJS 1.0.x: will output log messages directly to the console * @since 1.0.2 */ @Parameter(property = "casperjs.casperjsVerbose", defaultValue = "false") private boolean casperjsVerbose; /** * Set the CasperJS --fail-fast option: will terminate the current test suite as soon as a first failure is encountered. * @since 1.0.0 */ @Parameter(property = "casperjs.failFast", defaultValue = "false") private boolean failFast; /** * CasperJS 1.1 and above<br/>Set the for the CasperJS option <code>--engine=[engine]</code>: will change the rendering engine * (phantomjs or slimerjs) * @since 1.0.0 */ @Parameter(property = "casperjs.engine") private String engine; /** * A list of <code><argument></code> to add to the casperjs command line. * @since 1.0.0 */ @Parameter private List<String> arguments; // Injected components /** * The directory where output files will be stored */ @Parameter(defaultValue="${project.build.directory}/casperjs") private File targetDir; /** * The current maven session, used by the ToolChainManager */ @Parameter(defaultValue="${session}") private MavenSession session; /** * ToolChainManager, used to retrieve the CasperJS runtime path from user's configured toolchains */ @Component private ToolchainManager toolchainManager; /** * The CasperJS runtime path that we will launch */ private String casperRuntime; /** * The CasperJS runtime version */ private DefaultArtifactVersion casperJsVersion; /** * The directory containing the scripts to include while launching tests */ private File includesDir; /** * The directory containing the tests to launch */ private File scriptsDir; @Override public void execute() throws MojoExecutionException, MojoFailureException { LogUtils.setLog(getLog(), verbose); if (skip) { getLogger().info("Skipping CasperJsRunner execution"); return; } init(); Collection<String> scripts = findScripts(); Result globalResult = executeScripts(scripts); getLogger().info(globalResult.print()); if (!ignoreTestFailures && globalResult.getFailures() > 0) { throw new MojoFailureException("There are " + globalResult.getFailures() + " tests failures"); } } private void init() throws MojoFailureException { casperRuntime = findCasperRuntime(toolchainManager,session,casperExecPath); if (StringUtils.isBlank(casperRuntime)) { throw new MojoFailureException("CasperJS executable not found"); } casperJsVersion = retrieveVersion(casperRuntime, verbose); if (verbose) { getLogger().info("CasperJS version: " + casperJsVersion); } if (direct && (casperJsVersion.getMajorVersion() > 1 || casperJsVersion.getMajorVersion() == 1 && casperJsVersion.getMinorVersion() > 0)) { getLogger().warn("direct option is deprecated, use casperjsVerbose instead"); casperjsVerbose = true; } testsIncludes = checkPatterns(testsIncludes, includeJS, includeCS); if (testsExcludes == null) { testsExcludes = new ArrayList<String>(); } if (includesPatterns == null) { includesPatterns = new ArrayList<String>(); } includesDir = testsDir; scriptsDir = testsDir; File defaultIncludesDir = new File(testsDir, "includes"); File defaultScriptsDir = new File(testsDir, "scripts"); if (defaultScriptsDir.exists() && defaultScriptsDir.isDirectory()) { getLogger().debug("'scripts' subdirectory found, altering 'scriptsDir'"); scriptsDir = defaultScriptsDir; if (defaultIncludesDir.exists() && defaultIncludesDir.isDirectory() && includesPatterns.isEmpty()) { getLogger().debug("'includes' subdirectory found and 'includesPatterns' is empty, altering 'includesDir' and 'includesPatterns'"); includesDir = defaultIncludesDir; includesPatterns.add("**/*.js"); } } if (enableXmlReports) { getLogger().debug("creating directories to hold xunit file(s)"); reportsDir.mkdirs(); } } private Collection<String> findScripts() { return new OrdererScriptsFinderDecorator(new DefaultScriptsFinder(scriptsDir, test, testsIncludes, testsExcludes)).findScripts(); } private Result executeScripts(final Collection<String> files) { Result result = new Result(); for (String file : files) { File f = new File(scriptsDir, file); getLogger().debug("Execution of test " + f.getName()); int res = executeScript(f); if (res == 0) { result.addSuccess(); } else { getLogger().warn("Test '" + f.getName() + "' has failure"); result.addFailure(); } } return result; } private int executeScript(File f) { CommandLine cmdLine = new CommandLine(casperRuntime); // First, native options // Option --verbose / --direct, to output log messages to the console if (casperjsVerbose) { if (casperJsVersion.getMajorVersion() < 1 || casperJsVersion.getMajorVersion() == 1 && casperJsVersion.getMinorVersion() == 0) { cmdLine.addArgument("--direct"); } else { cmdLine.addArgument("--verbose"); } } // Option --log-level, to set the log level if (StringUtils.isNotBlank(logLevel)) { cmdLine.addArgument("--log-level=" + logLevel); } // Option --engine, to select phantomJS or slimerJS engine if (StringUtils.isNotBlank(engine)) { cmdLine.addArgument("--engine=" + engine); } // Then, specific ones for unit testing cmdLine.addArgument("test"); // Option --includes, to includes files before each test execution if (StringUtils.isNotBlank(includes)) { cmdLine.addArgument("--includes=" + includes); } else if (!includesPatterns.isEmpty()) { List<String> incs = new IncludesFinder(includesDir, includesPatterns).findIncludes(); if (incs != null && !incs.isEmpty()) { StringBuilder builder = new StringBuilder(); builder.append("--includes="); for (String inc : incs) { builder.append(new File(includesDir, inc).getAbsolutePath()); builder.append(","); } builder.deleteCharAt(builder.length() - 1); cmdLine.addArgument(builder.toString()); } } // Option --pre, to execute the scripts before the test suite if (StringUtils.isNotBlank(pre)) { cmdLine.addArgument("--pre=" + pre); } else if (new File(testsDir, "pre.js").exists()) { getLogger().debug("Using automatically found 'pre.js' file on " + testsDir.getName() + " directory as --pre"); cmdLine.addArgument("--pre=" + new File(testsDir, "pre.js").getAbsolutePath()); } // Option --post, to execute the scripts after the test suite if (StringUtils.isNotBlank(post)) { cmdLine.addArgument("--post=" + post); } else if (new File(testsDir, "post.js").exists()) { getLogger().debug("Using automatically found 'post.js' file on " + testsDir.getName() + " directory as --post"); cmdLine.addArgument("--post=" + new File(testsDir, "post.js").getAbsolutePath()); } // Option --xunit, to export results in XML file if (enableXmlReports) { cmdLine.addArgument("--xunit=" + new File(reportsDir, "TEST-" + buildName(scriptsDir, f) + ".xml")); } // Option --fast-fast, to terminate the test suite once a failure is // found if (failFast) { cmdLine.addArgument("--fail-fast"); } cmdLine.addArgument(f.getAbsolutePath()); if (arguments != null && !arguments.isEmpty()) { for (String argument : arguments) { cmdLine.addArgument(quote(argument), false); } } return executeCommand(cmdLine, environmentVariables, verbose); } }