package com.google.gwt.maven;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.dev.jjs.JsOutputOption;
import java.util.Arrays;
import org.apache.maven.artifact.Artifact;
import org.apache.maven.artifact.resolver.ArtifactResolutionRequest;
import org.apache.maven.artifact.resolver.ArtifactResolutionResult;
import org.apache.maven.plugin.MojoExecutionException;
import org.apache.maven.plugin.MojoFailureException;
import org.apache.maven.plugin.surefire.AbstractSurefireMojo;
import org.apache.maven.plugin.surefire.SurefireHelper;
import org.apache.maven.plugin.surefire.SurefireReportParameters;
import org.apache.maven.plugin.surefire.booterclient.ChecksumCalculator;
import org.apache.maven.plugin.surefire.booterclient.ForkConfiguration;
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.plugins.annotations.ResolutionScope;
import org.apache.maven.repository.RepositorySystem;
import org.apache.maven.surefire.suite.RunResult;
import org.apache.maven.surefire.util.NestedCheckedException;
import org.codehaus.plexus.util.StringUtils;
import java.io.File;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.LinkedHashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
@Mojo(name = "test", defaultPhase = LifecyclePhase.TEST, threadSafe = true, requiresDependencyResolution = ResolutionScope.TEST)
public class TestMojo extends AbstractSurefireMojo implements SurefireReportParameters {
// GWT-specific properties
/**
* Specifies the TCP port for the embedded web server (defaults to automatically picking an available port)
*/
@Parameter(property = "gwt.port")
private int port;
// whitelist/blacklist?
// TODO(t.broyer): log to file?
/**
* Sets the level of logging detail. Defaults to Maven's own log level.
* <p>
* If this is set lower than what's loggable at the Maven level, then lower
* levels will be log at Maven's lowest logging level. For instance, if this
* is set to {@link TreeLogger.Type.INFO INFO} and Maven has been run in
* quiet mode (showing only errors), then warnings and informational messages
* emitted by GWT will actually be logged as errors by the plugin.
*/
@Parameter(property = "gwt.logLevel")
private TreeLogger.Type logLevel;
private TreeLogger.Type getLogLevel() {
return (logLevel == null) ? MavenTreeLogger.getLogLevel(getLog()) : logLevel;
}
/**
* Whether or not to output transient generated files.
*/
@Parameter(property = "gwt.outputGen", defaultValue = "false")
private boolean outputGen;
/**
* Debugging: causes normally-transient generated types to be saved in the specified directory.
*/
@Parameter(defaultValue = "${project.build.directory}/gwt/gen")
private File gen;
private File getGenDir() {
return outputGen ? gen : null;
}
/**
* Specifies the TCP port for the code server (defaults to automatically picking an available port)
*/
@Parameter(property = "gwt.codeServerPort")
private int codeServerPort;
/**
* The directory into which deployable but not servable output files will be written.
*/
@Parameter(defaultValue = "${project.build.directory}/gwt/extra")
private File deploy;
/**
* Whether or not to output extra files.
*/
@Parameter(property = "gwt.outputExtra", defaultValue = "false")
private boolean outputExtra;
/**
* The directory into which extra files, not intended for deployment, will be written.
*/
@Parameter(defaultValue = "${project.build.directory}/gwt/extra")
private File extra;
private File getExtraDir() {
return outputExtra ? extra : null;
}
/**
* The compiler work directory (must be writeable).
*/
@Parameter(defaultValue = "${project.build.directory}/gwt/work")
private File workDir;
/**
* Script output style: OBFUSCATED, PRETTY, or DETAILED.
*/
@Parameter(property = "gwt.style", defaultValue = "OBFUSCATED")
private JsOutputOption style;
/**
* Troubleshooting: Prevent the Production Mode compiler from performing aggressive optimizations.
*/
@Parameter(property = "gwt.disableAggressiveOptimization", defaultValue = "false")
private boolean disableAggressiveOptimization;
/**
* EXPERIMENTAL: Disables some {@code java.lang.Class} methods (e.g. {@code getName()}).
*/
@Parameter(property = "gwt.disableClassMetadata", defaultValue = "false")
private boolean disableClassMetadata;
/**
* EXPERIMENTAL: Disables run-time checking of cast operations.
*/
@Parameter(property = "gwt.disableCastChecking", defaultValue = "false")
private boolean disableCastChecking;
/**
* Disable runAsync code-splitting.
*/
@Parameter(property = "gwt.disableRunAsync", defaultValue = "false")
private boolean disableRunAsync;
/**
* Disable the check to see if an update version of GWT is available.
*/
@Parameter(property = "gwt.disableUpdateCheck", defaultValue = "false")
private boolean disableUpdateCheck;
/**
* Enable faster, but less-optimized, compilations.
*/
@Parameter(property = "gwt.draftCompile", defaultValue = "false")
private boolean draftCompile;
/**
* The number of local workers to use when compiling permutations.
*/
@Parameter(property = "gwt.localWorkers")
private int localWorkers;
/**
* Causes your test to run in production (compiled) mode (defaults to development mode)
*/
@Parameter(property = "gwt.test.prod")
private boolean prod;
/**
* Set the test method timeout, in minutes.
*/
@Parameter(property = "gwt.test.methodTimeout", defaultValue = "5")
private int testMethodTimeout;
/**
* Set the test begin timeout (time for clients to contact server), in minutes.
*/
@Parameter(property = "gwt.test.beginTimeout", defaultValue = "1")
private int testBeginTimeout;
/**
* Selects the runstyle to use for this test. The name is a suffix of
* {@code com.google.gwt.junit.RunStyle} or is a fully qualified class name, and may be
* followed with a colon and an argument for this runstyle. The specified class must
* extend RunStyle.
*/
@Parameter(property = "gwt.test.runStyle", defaultValue = "HtmlUnit")
private String runStyle;
/**
* Configure batch execution of tests. Value can be one of none, class or module.
*/
@Parameter(property = "gwt.test.batch", defaultValue = "none")
private String batch;
public void setBatch(String batch) {
if (!batch.equals("none") && !batch.equals("class") && !batch.equals("module")) {
throw new IllegalArgumentException("batch");
}
this.batch = batch;
}
/**
* Causes the log window and browser windows to be displayed; useful for debugging.
*/
@Parameter(property = "gwt.test.notHeadless")
private boolean notHeadless;
/**
* Precompile modules as tests are running (speeds up remote tests but requires more memory).
* Value can be one of simple, all or parallel.
*/
@Parameter(property = "gwt.test.precompile", defaultValue = "simple")
private String precompile;
public void setPrecompile(String precompile) {
if (!precompile.equals("simple") && !precompile.equals("all") && !precompile.equals("parallel")) {
throw new IllegalArgumentException("precompile");
}
this.precompile = precompile;
}
/**
* Run each test using an HTML document in quirks mode (rather than standards mode).
*/
@Parameter(property = "gwt.test.quirksMode")
private boolean quirksMode;
/**
* EXPERIMENTAL: Sets the maximum number of attempts for running each test method.
*/
@Parameter(property = "gwt.test.tries", defaultValue = "1")
private int tries;
/**
* Specify the user agents to reduce the number of permutations for remote browser tests;
* e.g. ie6,ie8,safari,gecko1_8,opera.
*/
@Parameter(property = "gwt.test.userAgents")
private String userAgents;
/**
* The directory to write output files into.
*/
@Parameter(defaultValue = "${project.build.directory}/gwt-tests/www")
private File outDir;
@Override
public Map<String, String> getSystemPropertyVariables() {
Map<String, String> props = super.getSystemPropertyVariables();
if (props == null) {
props = new HashMap<String, String>(2);
}
if (!props.containsKey("gwt.args")) {
StringBuilder sb = new StringBuilder();
if (port > 0) {
sb.append(" -port ").append(port);
} else {
sb.append(" -port auto ");
}
sb.append(" -logLevel ").append(getLogLevel());
File gen = getGenDir();
if (gen != null) {
sb.append(" -gen ").append(quote(gen.getAbsolutePath()));
}
if (codeServerPort > 0) {
sb.append(" -codeServerPort ").append(codeServerPort);
} else {
sb.append(" -codeServerPort auto ");
}
sb.append(" -deploy ").append(quote(deploy.getAbsolutePath()));
File extra = getExtraDir();
if (extra != null) {
sb.append(" -extra ").append(quote(extra.getAbsolutePath()));
}
sb.append(" -workDir ").append(quote(workDir.getAbsolutePath()));
sb.append(" -style ").append(style);
if (effectiveIsEnableAssertions()) {
sb.append(" -ea ");
}
if (disableAggressiveOptimization) {
sb.append(" -XdisableAggressiveOptimization ");
}
if (disableClassMetadata) {
sb.append(" -XdisableClassMetadata ");
}
if (disableCastChecking) {
sb.append(" -XdisableCastChecking ");
}
if (disableRunAsync) {
sb.append(" -XdisableRunAsync ");
}
if (disableUpdateCheck) {
sb.append(" -XdisableUpdateCheck ");
}
if (draftCompile) {
sb.append(" -draftCompile ");
}
int workers = localWorkers;
if (workers < 1) {
workers = Runtime.getRuntime().availableProcessors();
if (getLog().isDebugEnabled()) {
getLog().debug("Using " + workers + " local workers");
}
}
sb.append(" -localWorkers ").append(workers);
if (prod) {
sb.append(" -prod ");
}
sb.append(" -testMethodTimeout ").append(testMethodTimeout);
sb.append(" -testBeginTimeout ").append(testBeginTimeout);
sb.append(" -runStyle ").append(quote(runStyle));
sb.append(" -batch ").append(batch);
if (notHeadless) {
sb.append(" -notHeadless ");
}
sb.append(" -precompile ").append(precompile);
if (quirksMode) {
sb.append(" -quirksMode ");
}
sb.append(" -Xtries ").append(tries);
if (StringUtils.isNotBlank(userAgents)) {
sb.append(" -userAgents ").append(quote(userAgents));
}
sb.append(" -war ").append(quote(outDir.getAbsolutePath()));
props.put("gwt.args", sb.toString());
}
if (getLog().isDebugEnabled()) {
getLog().debug("Using gwt.args: " + props.get("gwt.args"));
}
return props;
}
private Object quote(String value) {
if (value.matches(".*[\"\\s].*")) {
return "\"" + value.replace("\"", "\\\"") + "\"";
}
return value;
}
@Override
public void setSystemPropertyVariables(Map<String, String> systemPropertyVariables) {
if (systemPropertyVariables.containsKey("gwt.args")) {
getLog().warn("systemPropertyVariables contains a gwt.args value, this will override all individual options");
}
super.setSystemPropertyVariables(systemPropertyVariables);
}
@Override
protected void addPluginSpecificChecksumItems(ChecksumCalculator checksum) {
checksum.add(port);
checksum.add(String.valueOf(logLevel));
checksum.add(outputGen);
checksum.add(gen);
checksum.add(codeServerPort);
checksum.add(deploy);
checksum.add(outputExtra);
checksum.add(extra);
checksum.add(workDir);
checksum.add(String.valueOf(style));
checksum.add(disableAggressiveOptimization);
checksum.add(disableClassMetadata);
checksum.add(disableCastChecking);
checksum.add(disableRunAsync);
checksum.add(disableUpdateCheck);
checksum.add(draftCompile);
checksum.add(localWorkers);
checksum.add(prod);
checksum.add(testBeginTimeout);
checksum.add(testMethodTimeout);
checksum.add(runStyle);
checksum.add(batch);
checksum.add(notHeadless);
checksum.add(precompile);
checksum.add(quirksMode);
checksum.add(tries);
checksum.add(userAgents);
}
@Component
private RepositorySystem repositorySystem;
private Set<String> gwtDevArtifacts;
@Override
public List<String> getAdditionalClasspathElements() {
LinkedHashSet<String> elts = new LinkedHashSet<String>();
if (additionalClasspathElements != null) {
elts.addAll(additionalClasspathElements);
}
if (gwtDevArtifacts == null) {
gwtDevArtifacts = new LinkedHashSet<String>();
// TODO: filter only scope=provided/runtime dependencies and their transitive runtime dependencies
Artifact gwtJUnitArtifact = projectArtifactMap.get("com.google.gwt.user:gwt-junit");
if (gwtJUnitArtifact != null) {
ArtifactResolutionRequest request = new ArtifactResolutionRequest()
.setArtifact(gwtJUnitArtifact)
.setResolveTransitively(true)
.setLocalRepository(localRepository)
.setRemoteRepositories(remoteRepositories)
.setResolveRoot(false);
ArtifactResolutionResult result = repositorySystem.resolve(request);
for (Artifact artifact : result.getArtifacts()) {
gwtDevArtifacts.add(artifact.getFile().getAbsolutePath());
}
} else {
getLog().debug("Project doesn't depend on gwt-junit, skip adding gwt-junit runtime dependencies.");
}
}
elts.addAll(gwtDevArtifacts);
return new ArrayList<String>(elts);
}
// Properties copied from Surefire
/**
* A list of <include> elements specifying the tests (by pattern) that should be included in testing. When not
* specified and when the <code>test</code> parameter is not specified, the default includes will be <code><br/>
* <includes><br/>
* <include>**/*Suite.java</include><br/>
* <include>**/*SuiteNoBrowser.java</include><br/>
* </includes><br/>
* </code>
* <p/>
* Each include item may also contain a comma-separated sublist of items, which will be treated as multiple
* <include> entries.<br/>
* <p/>
* This parameter is ignored if the TestNG <code>suiteXmlFiles</code> parameter is specified.
*/
@Parameter
private List<String> includes;
/**
* Set this to "true" to ignore a failure during testing. Its use is NOT
* RECOMMENDED, but quite convenient on occasion.
*/
@Parameter(property = "maven.test.failure.ignore", defaultValue = "false")
private boolean testFailureIgnore;
/**
* Base directory where all reports are written to.
*/
@Parameter(defaultValue = "${project.build.directory}/surefire-reports")
private File reportsDirectory;
/**
* Specify this parameter to run individual tests by file name, overriding the
* <code>includes/excludes</code> parameters. Each pattern you specify here
* will be used to create an include pattern formatted like
* <code>**/${test}.java</code>, so you can just type "-Dtest=MyTest" to
* run a single test called "foo/MyTest.java".<br/>
* This parameter overrides the <code>includes/excludes</code> parameters, and
* the TestNG <code>suiteXmlFiles</code> parameter.
* <p/>
* Since 2.7.3, you can execute a limited number of methods in the test by
* adding #myMethod or #my*ethod. For example, "-Dtest=MyTest#myMethod". This
* is supported for junit 4.x and testNg.
*/
@Parameter(property = "test")
private String test;
/**
* Option to print summary of test suites or just print the test cases that
* have errors.
*/
@Parameter(property = "surefire.printSummary", defaultValue = "true")
private boolean printSummary;
/**
* Selects the formatting for the test report to be generated. Can be set as
* "brief" or "plain". Only applies to the output format of the output files
* (target/surefire-reports/testName.txt)
*/
@Parameter(property = "surefire.reportFormat", defaultValue = "brief")
private String reportFormat;
/**
* Option to generate a file test report or just output the test report to the
* console.
*/
@Parameter(property = "surefire.useFile", defaultValue = "true")
private boolean useFile;
/**
* Set this to "true" to cause a failure if the none of the tests specified in
* -Dtest=... are run. Defaults to "true".
*
* @since 2.12
*/
@Parameter(property = "surefire.failIfNoSpecifiedTests")
private Boolean failIfNoSpecifiedTests;
/**
* Attach a debugger to the forked JVM. If set to "true", the process will
* suspend and wait for a debugger to attach on port 5005. If set to some
* other string, that string will be appended to the argLine, allowing you to
* configure arbitrary debuggability options (without overwriting the other
* options specified through the <code>argLine</code> parameter).
*
* @since 2.4
*/
@Parameter(property = "maven.surefire.debug")
private String debugForkedProcess;
/**
* Kill the forked test process after a certain number of seconds. If set to
* 0, wait forever for the process, never timing out.
*
* @since 2.4
*/
@Parameter(property = "surefire.timeout")
private int forkedProcessTimeoutInSeconds;
@Override
protected void handleSummary(RunResult summary, NestedCheckedException firstForkException)
throws MojoExecutionException, MojoFailureException {
assertNoException(firstForkException);
SurefireHelper.reportExecution(this, summary, getLog());
}
private void assertNoException(NestedCheckedException firstForkException)
throws MojoFailureException {
if (firstForkException != null) {
throw new MojoFailureException(firstForkException.getMessage(), firstForkException);
}
}
@Override
protected boolean isSkipExecution() {
return isSkip() || isSkipTests() || isSkipExec();
}
@Override
protected String getPluginName() {
return "GWT tests";
}
@Override
protected String[] getDefaultIncludes() {
return new String[] {"**/*Suite.java"};
}
@Override
public List<String> getIncludes() {
return this.includes;
}
@Override
public void setIncludes(List<String> includes) {
this.includes = includes;
}
@Override
public void setForkMode(String forkMode) {
if (ForkConfiguration.FORK_NEVER.equals(forkMode)) {
getLog().warn("ForkMode=never is know not to work for GWT tests");
}
super.setForkMode(forkMode);
}
//
@Override
public boolean isSkipTests() {
return skipTests;
}
@Override
public void setSkipTests(boolean skipTests) {
this.skipTests = skipTests;
}
@Override
@SuppressWarnings("deprecation")
public boolean isSkipExec() {
return skipExec;
}
@Override
@SuppressWarnings("deprecation")
public void setSkipExec(boolean skipExec) {
this.skipExec = skipExec;
}
@Override
public boolean isSkip() {
return skip;
}
@Override
public void setSkip(boolean skip) {
this.skip = skip;
}
@Override
public File getBasedir() {
return basedir;
}
@Override
public void setBasedir(File basedir) {
this.basedir = basedir;
}
@Override
public File getTestClassesDirectory() {
return testClassesDirectory;
}
@Override
public void setTestClassesDirectory(File testClassesDirectory) {
this.testClassesDirectory = testClassesDirectory;
}
@Override
public File getClassesDirectory() {
return classesDirectory;
}
@Override
public void setClassesDirectory(File classesDirectory) {
this.classesDirectory = classesDirectory;
}
@Override
public List<String> getClasspathDependencyExcludes() {
return classpathDependencyExcludes;
}
@Override
public void setClasspathDependencyExcludes(List<String> classpathDependencyExcludes) {
this.classpathDependencyExcludes = classpathDependencyExcludes;
}
@Override
public String getClasspathDependencyScopeExclude() {
return classpathDependencyScopeExclude;
}
@Override
public void setClasspathDependencyScopeExclude(String classpathDependencyScopeExclude) {
this.classpathDependencyScopeExclude = classpathDependencyScopeExclude;
}
// getAdditionalClasspathElements is defined above
@Override
public void setAdditionalClasspathElements(List<String> additionalClasspathElements) {
this.additionalClasspathElements = additionalClasspathElements;
}
@Override
public File getReportsDirectory() {
return reportsDirectory;
}
@Override
public void setReportsDirectory(File reportsDirectory) {
this.reportsDirectory = reportsDirectory;
}
@Override
public String getTest() {
if (StringUtils.isBlank(test)) {
return null;
}
String[] testArray = StringUtils.split(test, ",");
StringBuilder tests = new StringBuilder();
for (String aTestArray : testArray) {
String singleTest = aTestArray;
int index = singleTest.indexOf('#');
if (index >= 0) {// single test method
singleTest = singleTest.substring(0, index);
}
tests.append(singleTest);
tests.append(",");
}
return tests.toString();
}
@Override
public String getTestMethod() {
if (StringUtils.isBlank(test)) {
return null;
}
// modified by rainLee, see http://jira.codehaus.org/browse/SUREFIRE-745
int index = this.test.indexOf('#');
int index2 = this.test.indexOf(",");
if (index >= 0) {
if (index2 < 0) {
String testStrAfterFirstSharp = this.test.substring(index + 1, this.test.length());
if (!testStrAfterFirstSharp.contains("+")) {// the original way
return testStrAfterFirstSharp;
} else {
return this.test;
}
} else {
return this.test;
}
}
return null;
}
@Override
public void setTest(String test) {
this.test = test;
}
@Override
public boolean isPrintSummary() {
return printSummary;
}
@Override
public void setPrintSummary(boolean printSummary) {
this.printSummary = printSummary;
}
@Override
public String getReportFormat() {
return reportFormat;
}
@Override
public void setReportFormat(String reportFormat) {
this.reportFormat = reportFormat;
}
@Override
public boolean isUseFile() {
return useFile;
}
@Override
public void setUseFile(boolean useFile) {
this.useFile = useFile;
}
@Override
public String getDebugForkedProcess() {
return debugForkedProcess;
}
@Override
public void setDebugForkedProcess(String debugForkedProcess) {
this.debugForkedProcess = debugForkedProcess;
}
@Override
public int getForkedProcessTimeoutInSeconds() {
return forkedProcessTimeoutInSeconds;
}
@Override
public void setForkedProcessTimeoutInSeconds(int forkedProcessTimeoutInSeconds) {
this.forkedProcessTimeoutInSeconds = forkedProcessTimeoutInSeconds;
}
@Override
public boolean isUseSystemClassLoader() {
// GWTTestCase must use system class-loader
return true;
}
@Override
public void setUseSystemClassLoader(boolean useSystemClassLoader) {
throw new UnsupportedOperationException("useSystemClassLoader is read-only");
}
@Override
public boolean isUseManifestOnlyJar() {
// GWTTestCase must not use manifest-only JAR
return false;
}
@Override
public void setUseManifestOnlyJar(boolean useManifestOnlyJar) {
throw new UnsupportedOperationException("useManifestOnlyJar is read-only");
}
@Override
public Boolean getFailIfNoSpecifiedTests() {
return failIfNoSpecifiedTests;
}
@Override
public void setFailIfNoSpecifiedTests(Boolean failIfNoSpecifiedTests) {
this.failIfNoSpecifiedTests = failIfNoSpecifiedTests;
}
@Override
public boolean isTestFailureIgnore() {
return testFailureIgnore;
}
@Override
public void setTestFailureIgnore(boolean testFailureIgnore) {
this.testFailureIgnore = testFailureIgnore;
}
}