blob: 4629b263cad27d0cd02c5b504b2ee6674c7afa16 [file] [log] [blame]
/*
* Copyright 2007 Google Inc.
*
* Licensed 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 com.google.gwt.junit;
import com.google.gwt.core.ext.TreeLogger;
import com.google.gwt.core.ext.UnableToCompleteException;
import com.google.gwt.dev.BootStrapPlatform;
import com.google.gwt.dev.GWTShell;
import com.google.gwt.dev.cfg.ModuleDef;
import com.google.gwt.dev.cfg.ModuleDefLoader;
import com.google.gwt.dev.shell.BrowserWidgetHost;
import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
import com.google.gwt.junit.benchmarks.BenchmarkReport;
import com.google.gwt.junit.client.Benchmark;
import com.google.gwt.junit.client.TestResults;
import com.google.gwt.junit.client.TimeoutException;
import com.google.gwt.junit.client.Trial;
import com.google.gwt.junit.remote.BrowserManager;
import com.google.gwt.util.tools.ArgHandlerFlag;
import com.google.gwt.util.tools.ArgHandlerString;
import junit.framework.AssertionFailedError;
import junit.framework.TestCase;
import junit.framework.TestResult;
import java.io.File;
import java.rmi.Naming;
import java.util.ArrayList;
import java.util.Date;
import java.util.List;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* This class is responsible for hosting JUnit test case execution. There are
* three main pieces to the JUnit system.
*
* <ul>
* <li>Test environment</li>
* <li>Client classes</li>
* <li>Server classes</li>
* </ul>
*
* <p>
* The test environment consists of this class and the non-translatable version
* of {@link com.google.gwt.junit.client.GWTTestCase}. These two classes
* integrate directly into the real JUnit test process.
* </p>
*
* <p>
* The client classes consist of the translatable version of {@link
* com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, and the
* user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived class.
* The client communicates to the server via RPC.
* </p>
*
* <p>
* The server consists of {@link com.google.gwt.junit.server.JUnitHostImpl}, an
* RPC servlet which communicates back to the test environment through a
* {@link JUnitMessageQueue}, thus closing the loop.
* </p>
*/
public class JUnitShell extends GWTShell {
/**
* Executes shutdown logic for JUnitShell
*
* Sadly, there's no simple way to know when all unit tests have finished
* executing. So this class is registered as a VM shutdown hook so that work
* can be done at the end of testing - for example, writing out the reports.
*/
private class Shutdown implements Runnable {
public void run() {
try {
String reportPath = System.getProperty(Benchmark.REPORT_PATH);
if (reportPath == null || reportPath.trim().equals("")) {
reportPath = System.getProperty("user.dir");
}
report.generate(reportPath + File.separator + "report-"
+ new Date().getTime() + ".xml");
} catch (Exception e) {
// It really doesn't matter how we got here.
// Regardless of the failure, the VM is shutting down.
e.printStackTrace();
}
}
}
/**
* This is a system property that, when set, emulates command line arguments.
*/
private static final String PROP_GWT_ARGS = "gwt.args";
/**
* This legacy system property, when set, causes us to run in web mode.
* (Superceded by passing "-web" into gwt.args).
*/
private static final String PROP_JUNIT_HYBRID_MODE = "gwt.hybrid";
/**
* The amount of time to wait for all clients to have contacted the server and
* begun running the test.
*/
private static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000;
/**
* Singleton object for hosting unit tests. All test case instances executed
* by the TestRunner will use the single unitTestShell.
*/
private static JUnitShell unitTestShell;
static {
ModuleDefLoader.forceInherit("com.google.gwt.junit.JUnit");
ModuleDefLoader.setEnableCachingModules(true);
}
/**
* Called by {@link com.google.gwt.junit.server.JUnitHostImpl} to get an
* interface into the test process.
*
* @return The {@link JUnitMessageQueue} interface that belongs to the
* singleton {@link JUnitShell}, or <code>null</code> if no such
* singleton exists.
*/
public static JUnitMessageQueue getMessageQueue() {
if (unitTestShell == null) {
return null;
}
return unitTestShell.messageQueue;
}
/**
* Called by {@link com.google.gwt.junit.rebind.JUnitTestCaseStubGenerator} to
* add test meta data to the test report.
*
* @return The {@link BenchmarkReport} that belongs to the singleton {@link
* JUnitShell}, or <code>null</code> if no such singleton exists.
*/
public static BenchmarkReport getReport() {
if (unitTestShell == null) {
return null;
}
return unitTestShell.report;
}
/**
* Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
* creates the singleton {@link JUnitShell} and invokes its {@link
* #runTestImpl(String, TestCase, TestResult)}.
*/
public static void runTest(String moduleName, TestCase testCase,
TestResult testResult) throws UnableToCompleteException {
getUnitTestShell().runTestImpl(moduleName, testCase, testResult);
}
/**
* Retrieves the JUnitShell. This should only be invoked during TestRunner
* execution of JUnit tests.
*/
private static JUnitShell getUnitTestShell() {
if (unitTestShell == null) {
BootStrapPlatform.go();
JUnitShell shell = new JUnitShell();
String[] args = shell.synthesizeArgs();
if (!shell.processArgs(args)) {
throw new RuntimeException("Invalid shell arguments");
}
shell.messageQueue = new JUnitMessageQueue(shell.numClients);
if (!shell.startUp()) {
throw new RuntimeException("Shell failed to start");
}
shell.report = new BenchmarkReport(shell.getTopLogger());
unitTestShell = shell;
Runtime.getRuntime().addShutdownHook(new Thread(shell.new Shutdown()));
}
return unitTestShell;
}
/**
* When headless, all logging goes to the console.
*/
private PrintWriterTreeLogger consoleLogger;
/**
* Name of the module containing the current/last module to run.
*/
private String currentModuleName;
/**
* If true, the last attempt to launch failed.
*/
private boolean lastLaunchFailed;
/**
* Portal to interact with the servlet.
*/
private JUnitMessageQueue messageQueue;
/**
* The number of test clients executing in parallel. With -remoteweb, users
* can specify a number of parallel test clients, but by default we only have
* 1.
*/
private int numClients = 1;
/**
* The result of benchmark runs.
*/
private BenchmarkReport report;
/**
* What type of test we're running; Local hosted, local web, or remote web.
*/
private RunStyle runStyle = new RunStyleLocalHosted(this);
/**
* The time at which the current test will fail if the client has not yet
* started the test.
*/
private long testBeginTimeout;
/**
* Class name of the current/last test case to run.
*/
private String testCaseClassName;
/**
* Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces
* server mode and disables processing extra arguments as URLs to be shown.
*/
private JUnitShell() {
super(true, true);
registerHandler(new ArgHandlerFlag() {
public String getPurpose() {
return "Causes your test to run in web (compiled) mode (defaults to hosted mode)";
}
public String getTag() {
return "-web";
}
public boolean setFlag() {
runStyle = new RunStyleLocalWeb(JUnitShell.this);
return true;
}
});
registerHandler(new ArgHandlerString() {
public String getPurpose() {
return "Runs web mode via RMI to a BrowserManagerServer; e.g. rmi://localhost/ie6";
}
public String getTag() {
return "-remoteweb";
}
public String[] getTagArgs() {
return new String[] {"rmiUrl"};
}
public boolean isUndocumented() {
return true;
}
public boolean setString(String str) {
try {
String[] urls = str.split(",");
numClients = urls.length;
BrowserManager[] browserManagers = new BrowserManager[numClients];
for (int i = 0; i < numClients; ++i) {
browserManagers[i] = (BrowserManager) Naming.lookup(urls[i]);
}
runStyle = new RunStyleRemoteWeb(JUnitShell.this, browserManagers);
} catch (Exception e) {
System.err.println("Error connecting to browser manager at " + str);
e.printStackTrace();
System.exit(1);
return false;
}
return true;
}
});
registerHandler(new ArgHandlerFlag() {
public String getPurpose() {
return "Causes the log window and browser windows to be displayed; useful for debugging";
}
public String getTag() {
return "-notHeadless";
}
public boolean setFlag() {
setHeadless(false);
return true;
}
});
setRunTomcat(true);
setHeadless(true);
// Legacy: -Dgwt.hybrid runs web mode
if (System.getProperty(PROP_JUNIT_HYBRID_MODE) != null) {
runStyle = new RunStyleLocalWeb(this);
}
}
public TreeLogger getTopLogger() {
if (consoleLogger != null) {
return consoleLogger;
} else {
return super.getTopLogger();
}
}
protected String doGetDefaultLogLevel() {
return "WARN";
}
/**
* Overrides the default module loading behavior. Clears any existing entry
* points and adds an entry point for the class being tested. This test class
* is then rebound to a generated subclass.
*/
protected ModuleDef doLoadModule(TreeLogger logger, String moduleName)
throws UnableToCompleteException {
ModuleDef module = super.doLoadModule(logger, moduleName);
// Tweak the module for JUnit support
//
if (moduleName.equals(currentModuleName)) {
module.clearEntryPoints();
module.addEntryPointTypeName(testCaseClassName);
}
return module;
}
/**
* Never check for updates in JUnit mode.
*/
protected boolean doShouldCheckForUpdates() {
return false;
}
protected ArgHandlerPort getArgHandlerPort() {
return new ArgHandlerPort() {
public String[] getDefaultArgs() {
return new String[] {"-port", "auto"};
}
};
}
protected void initializeLogger() {
if (isHeadless()) {
consoleLogger = new PrintWriterTreeLogger();
consoleLogger.setMaxDetail(getLogLevel());
} else {
super.initializeLogger();
}
}
/**
* Overrides {@link GWTShell#notDone()} to wait for the currently-running test
* to complete.
*/
protected boolean notDone() {
if (!messageQueue.haveAllClientsRetrievedCurrentTest()
&& testBeginTimeout < System.currentTimeMillis()) {
throw new TimeoutException(
"The browser did not contact the server within "
+ TEST_BEGIN_TIMEOUT_MILLIS + "ms.");
}
if (messageQueue.hasResult(testCaseClassName)) {
return false;
}
return !runStyle.wasInterrupted();
}
void compileForWebMode(String moduleName) throws UnableToCompleteException {
BrowserWidgetHost browserHost = getBrowserHost();
assert (browserHost != null);
browserHost.compile(new String[] {moduleName});
}
/**
* Runs a particular test case.
*/
private void runTestImpl(String moduleName, TestCase testCase,
TestResult testResult) throws UnableToCompleteException {
String newTestCaseClassName = testCase.getClass().getName();
boolean sameTest = newTestCaseClassName.equals(testCaseClassName);
if (sameTest && lastLaunchFailed) {
throw new UnableToCompleteException();
}
messageQueue.setNextTestName(newTestCaseClassName, testCase.getName());
try {
lastLaunchFailed = false;
testCaseClassName = newTestCaseClassName;
currentModuleName = moduleName;
runStyle.maybeLaunchModule(moduleName, !sameTest);
} catch (UnableToCompleteException e) {
lastLaunchFailed = true;
testResult.addError(testCase, e);
return;
}
// Wait for test to complete
try {
// Set a timeout period to automatically fail if the servlet hasn't been
// contacted; something probably went wrong (the module failed to load?)
testBeginTimeout = System.currentTimeMillis() + TEST_BEGIN_TIMEOUT_MILLIS;
pumpEventLoop();
} catch (TimeoutException e) {
lastLaunchFailed = true;
testResult.addError(testCase, e);
return;
}
List/* JUnitMessageQueue.TestResult */results
= messageQueue.getResults(testCaseClassName);
if (results == null) {
return;
}
boolean parallelTesting = numClients > 1;
for (int i = 0; i < results.size(); ++i) {
TestResults result = (TestResults) results.get(i);
Trial firstTrial = (Trial) result.getTrials().get(0);
Throwable exception = firstTrial.getException();
// In the case that we're running multiple clients at once, we need to
// let the user know the browser in which the failure happened
if (parallelTesting && exception != null) {
String msg = "Remote test failed at " + result.getHost() + " on "
+ result.getAgent();
if (exception instanceof AssertionFailedError) {
AssertionFailedError newException = new AssertionFailedError(msg
+ "\n" + exception.getMessage());
newException.setStackTrace(exception.getStackTrace());
exception = newException;
} else {
exception = new RuntimeException(msg, exception);
}
}
// A "successful" failure
if (exception instanceof AssertionFailedError) {
testResult.addFailure(testCase, (AssertionFailedError) exception);
} else if (exception != null) {
// A real failure
testResult.addError(testCase, exception);
}
if (testCase instanceof Benchmark) {
report.addBenchmarkResults(testCase, result);
}
}
}
/**
* Synthesize command line arguments from a system property.
*/
private String[] synthesizeArgs() {
ArrayList argList = new ArrayList();
String args = System.getProperty(PROP_GWT_ARGS);
if (args != null) {
// Match either a non-whitespace, non start of quoted string, or a
// quoted string that can have embedded, escaped quoting characters
//
Pattern pattern = Pattern.compile("[^\\s\"]+|\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"");
Matcher matcher = pattern.matcher(args);
while (matcher.find()) {
argList.add(matcher.group());
}
}
return (String[]) argList.toArray(new String[argList.size()]);
}
}