001: /*
002: * Copyright 2007 Google Inc.
003: *
004: * Licensed under the Apache License, Version 2.0 (the "License"); you may not
005: * use this file except in compliance with the License. You may obtain a copy of
006: * the License at
007: *
008: * http://www.apache.org/licenses/LICENSE-2.0
009: *
010: * Unless required by applicable law or agreed to in writing, software
011: * distributed under the License is distributed on an "AS IS" BASIS, WITHOUT
012: * WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. See the
013: * License for the specific language governing permissions and limitations under
014: * the License.
015: */
016: package com.google.gwt.junit;
017:
018: import com.google.gwt.core.ext.TreeLogger;
019: import com.google.gwt.core.ext.UnableToCompleteException;
020: import com.google.gwt.dev.BootStrapPlatform;
021: import com.google.gwt.dev.GWTShell;
022: import com.google.gwt.dev.cfg.ModuleDef;
023: import com.google.gwt.dev.cfg.ModuleDefLoader;
024: import com.google.gwt.dev.cfg.Properties;
025: import com.google.gwt.dev.cfg.Property;
026: import com.google.gwt.dev.shell.BrowserWidgetHost;
027: import com.google.gwt.dev.util.log.PrintWriterTreeLogger;
028: import com.google.gwt.junit.benchmarks.BenchmarkReport;
029: import com.google.gwt.junit.client.Benchmark;
030: import com.google.gwt.junit.client.TestResults;
031: import com.google.gwt.junit.client.TimeoutException;
032: import com.google.gwt.junit.client.Trial;
033: import com.google.gwt.junit.remote.BrowserManager;
034: import com.google.gwt.util.tools.ArgHandlerFlag;
035: import com.google.gwt.util.tools.ArgHandlerString;
036:
037: import junit.framework.AssertionFailedError;
038: import junit.framework.TestCase;
039: import junit.framework.TestResult;
040:
041: import java.io.File;
042: import java.rmi.Naming;
043: import java.util.ArrayList;
044: import java.util.Date;
045: import java.util.List;
046: import java.util.regex.Matcher;
047: import java.util.regex.Pattern;
048:
049: /**
050: * This class is responsible for hosting JUnit test case execution. There are
051: * three main pieces to the JUnit system.
052: *
053: * <ul>
054: * <li>Test environment</li>
055: * <li>Client classes</li>
056: * <li>Server classes</li>
057: * </ul>
058: *
059: * <p>
060: * The test environment consists of this class and the non-translatable version
061: * of {@link com.google.gwt.junit.client.GWTTestCase}. These two classes
062: * integrate directly into the real JUnit test process.
063: * </p>
064: *
065: * <p>
066: * The client classes consist of the translatable version of {@link
067: * com.google.gwt.junit.client.GWTTestCase}, translatable JUnit classes, and the
068: * user's own {@link com.google.gwt.junit.client.GWTTestCase}-derived class.
069: * The client communicates to the server via RPC.
070: * </p>
071: *
072: * <p>
073: * The server consists of {@link com.google.gwt.junit.server.JUnitHostImpl}, an
074: * RPC servlet which communicates back to the test environment through a
075: * {@link JUnitMessageQueue}, thus closing the loop.
076: * </p>
077: */
078: public class JUnitShell extends GWTShell {
079:
080: /**
081: * Executes shutdown logic for JUnitShell
082: *
083: * Sadly, there's no simple way to know when all unit tests have finished
084: * executing. So this class is registered as a VM shutdown hook so that work
085: * can be done at the end of testing - for example, writing out the reports.
086: */
087: private class Shutdown implements Runnable {
088:
089: public void run() {
090: try {
091: String reportPath = System
092: .getProperty(Benchmark.REPORT_PATH);
093: if (reportPath == null || reportPath.trim().equals("")) {
094: reportPath = System.getProperty("user.dir");
095: }
096: report.generate(reportPath + File.separator + "report-"
097: + new Date().getTime() + ".xml");
098: } catch (Exception e) {
099: // It really doesn't matter how we got here.
100: // Regardless of the failure, the VM is shutting down.
101: e.printStackTrace();
102: }
103: }
104: }
105:
106: /**
107: * This is a system property that, when set, emulates command line arguments.
108: */
109: private static final String PROP_GWT_ARGS = "gwt.args";
110:
111: /**
112: * This legacy system property, when set, causes us to run in web mode.
113: * (Superceded by passing "-web" into gwt.args).
114: */
115: private static final String PROP_JUNIT_HYBRID_MODE = "gwt.hybrid";
116:
117: /**
118: * The amount of time to wait for all clients to have contacted the server and
119: * begun running the test.
120: */
121: private static final int TEST_BEGIN_TIMEOUT_MILLIS = 60000;
122:
123: /**
124: * Singleton object for hosting unit tests. All test case instances executed
125: * by the TestRunner will use the single unitTestShell.
126: */
127: private static JUnitShell unitTestShell;
128:
129: static {
130: ModuleDefLoader.forceInherit("com.google.gwt.junit.JUnit");
131: ModuleDefLoader.setEnableCachingModules(true);
132: }
133:
134: /**
135: * Called by {@link com.google.gwt.junit.server.JUnitHostImpl} to get an
136: * interface into the test process.
137: *
138: * @return The {@link JUnitMessageQueue} interface that belongs to the
139: * singleton {@link JUnitShell}, or <code>null</code> if no such
140: * singleton exists.
141: */
142: public static JUnitMessageQueue getMessageQueue() {
143: if (unitTestShell == null) {
144: return null;
145: }
146: return unitTestShell.messageQueue;
147: }
148:
149: /**
150: * Called by {@link com.google.gwt.junit.rebind.JUnitTestCaseStubGenerator} to
151: * add test meta data to the test report.
152: *
153: * @return The {@link BenchmarkReport} that belongs to the singleton {@link
154: * JUnitShell}, or <code>null</code> if no such singleton exists.
155: */
156: public static BenchmarkReport getReport() {
157: if (unitTestShell == null) {
158: return null;
159: }
160: return unitTestShell.report;
161: }
162:
163: /**
164: * Entry point for {@link com.google.gwt.junit.client.GWTTestCase}. Gets or
165: * creates the singleton {@link JUnitShell} and invokes its {@link
166: * #runTestImpl(String, TestCase, TestResult)}.
167: */
168: public static void runTest(String moduleName, TestCase testCase,
169: TestResult testResult) throws UnableToCompleteException {
170: getUnitTestShell()
171: .runTestImpl(moduleName, testCase, testResult);
172: }
173:
174: /**
175: * Retrieves the JUnitShell. This should only be invoked during TestRunner
176: * execution of JUnit tests.
177: */
178: private static JUnitShell getUnitTestShell() {
179: if (unitTestShell == null) {
180: BootStrapPlatform.go();
181: JUnitShell shell = new JUnitShell();
182: String[] args = shell.synthesizeArgs();
183: if (!shell.processArgs(args)) {
184: throw new RuntimeException("Invalid shell arguments");
185: }
186:
187: shell.messageQueue = new JUnitMessageQueue(shell.numClients);
188:
189: if (!shell.startUp()) {
190: throw new RuntimeException("Shell failed to start");
191: }
192:
193: shell.report = new BenchmarkReport(shell.getTopLogger());
194: unitTestShell = shell;
195:
196: Runtime.getRuntime().addShutdownHook(
197: new Thread(shell.new Shutdown()));
198: }
199:
200: return unitTestShell;
201: }
202:
203: /**
204: * When headless, all logging goes to the console.
205: */
206: private PrintWriterTreeLogger consoleLogger;
207:
208: /**
209: * Name of the module containing the current/last module to run.
210: */
211: private String currentModuleName;
212:
213: /**
214: * If true, the last attempt to launch failed.
215: */
216: private boolean lastLaunchFailed;
217:
218: /**
219: * Portal to interact with the servlet.
220: */
221: private JUnitMessageQueue messageQueue;
222:
223: /**
224: * The number of test clients executing in parallel. With -remoteweb, users
225: * can specify a number of parallel test clients, but by default we only have
226: * 1.
227: */
228: private int numClients = 1;
229:
230: /**
231: * The result of benchmark runs.
232: */
233: private BenchmarkReport report;
234:
235: /**
236: * What type of test we're running; Local hosted, local web, or remote web.
237: */
238: private RunStyle runStyle = new RunStyleLocalHosted(this );
239:
240: /**
241: * The time at which the current test will fail if the client has not yet
242: * started the test.
243: */
244: private long testBeginTimeout;
245:
246: /**
247: * Class name of the current/last test case to run.
248: */
249: private String testCaseClassName;
250:
251: /**
252: * Enforce the singleton pattern. The call to {@link GWTShell}'s ctor forces
253: * server mode and disables processing extra arguments as URLs to be shown.
254: */
255: private JUnitShell() {
256: super (true, true);
257:
258: registerHandler(new ArgHandlerFlag() {
259:
260: @Override
261: public String getPurpose() {
262: return "Causes your test to run in web (compiled) mode (defaults to hosted mode)";
263: }
264:
265: @Override
266: public String getTag() {
267: return "-web";
268: }
269:
270: @Override
271: public boolean setFlag() {
272: runStyle = new RunStyleLocalWeb(JUnitShell.this );
273: return true;
274: }
275:
276: });
277:
278: registerHandler(new ArgHandlerString() {
279:
280: @Override
281: public String getPurpose() {
282: return "Runs web mode via RMI to a BrowserManagerServer; e.g. rmi://localhost/ie6";
283: }
284:
285: @Override
286: public String getTag() {
287: return "-remoteweb";
288: }
289:
290: @Override
291: public String[] getTagArgs() {
292: return new String[] { "rmiUrl" };
293: }
294:
295: @Override
296: public boolean isUndocumented() {
297: return true;
298: }
299:
300: @Override
301: public boolean setString(String str) {
302: try {
303: String[] urls = str.split(",");
304: numClients = urls.length;
305: BrowserManager[] browserManagers = new BrowserManager[numClients];
306: for (int i = 0; i < numClients; ++i) {
307: browserManagers[i] = (BrowserManager) Naming
308: .lookup(urls[i]);
309: }
310: runStyle = new RunStyleRemoteWeb(JUnitShell.this ,
311: browserManagers);
312: } catch (Exception e) {
313: System.err
314: .println("Error connecting to browser manager at "
315: + str);
316: e.printStackTrace();
317: System.exit(1);
318: return false;
319: }
320: return true;
321: }
322: });
323:
324: registerHandler(new ArgHandlerFlag() {
325:
326: @Override
327: public String getPurpose() {
328: return "Causes the log window and browser windows to be displayed; useful for debugging";
329: }
330:
331: @Override
332: public String getTag() {
333: return "-notHeadless";
334: }
335:
336: @Override
337: public boolean setFlag() {
338: setHeadless(false);
339: return true;
340: }
341: });
342:
343: setRunTomcat(true);
344: setHeadless(true);
345:
346: // Legacy: -Dgwt.hybrid runs web mode
347: if (System.getProperty(PROP_JUNIT_HYBRID_MODE) != null) {
348: runStyle = new RunStyleLocalWeb(this );
349: }
350: }
351:
352: @Override
353: public TreeLogger getTopLogger() {
354: if (consoleLogger != null) {
355: return consoleLogger;
356: } else {
357: return super .getTopLogger();
358: }
359: }
360:
361: @Override
362: protected String doGetDefaultLogLevel() {
363: return "WARN";
364: }
365:
366: /**
367: * Overrides the default module loading behavior. Clears any existing entry
368: * points and adds an entry point for the class being tested. This test class
369: * is then rebound to a generated subclass.
370: */
371: @Override
372: protected ModuleDef doLoadModule(TreeLogger logger,
373: String moduleName) throws UnableToCompleteException {
374:
375: ModuleDef module = super .doLoadModule(logger, moduleName);
376:
377: // Tweak the module for JUnit support
378: //
379: if (moduleName.equals(currentModuleName)) {
380: module.clearEntryPoints();
381: module.addEntryPointTypeName(testCaseClassName);
382: }
383: return module;
384: }
385:
386: /**
387: * Never check for updates in JUnit mode.
388: */
389: @Override
390: protected boolean doShouldCheckForUpdates() {
391: return false;
392: }
393:
394: @Override
395: protected ArgHandlerPort getArgHandlerPort() {
396: return new ArgHandlerPort() {
397: @Override
398: public String[] getDefaultArgs() {
399: return new String[] { "-port", "auto" };
400: }
401: };
402: }
403:
404: @Override
405: protected void initializeLogger() {
406: if (isHeadless()) {
407: consoleLogger = new PrintWriterTreeLogger();
408: consoleLogger.setMaxDetail(getLogLevel());
409: } else {
410: super .initializeLogger();
411: }
412: }
413:
414: /**
415: * Overrides {@link GWTShell#notDone()} to wait for the currently-running test
416: * to complete.
417: */
418: @Override
419: protected boolean notDone() {
420: if (!messageQueue.haveAllClientsRetrievedCurrentTest()
421: && testBeginTimeout < System.currentTimeMillis()) {
422: throw new TimeoutException(
423: "The browser did not contact the server within "
424: + TEST_BEGIN_TIMEOUT_MILLIS + "ms.");
425: }
426:
427: if (messageQueue.hasResult(testCaseClassName)) {
428: return false;
429: }
430:
431: return !runStyle.wasInterrupted();
432: }
433:
434: void compileForWebMode(String moduleName, String userAgentString)
435: throws UnableToCompleteException {
436: ModuleDef module = doLoadModule(getTopLogger(), moduleName);
437: if (userAgentString != null) {
438: Properties props = module.getProperties();
439: Property userAgent = props.find("user.agent");
440: if (userAgent != null) {
441: userAgent.setActiveValue(userAgentString);
442: }
443: }
444: BrowserWidgetHost browserHost = getBrowserHost();
445: assert (browserHost != null);
446: browserHost.compile(module);
447: }
448:
449: /**
450: * Runs a particular test case.
451: */
452: private void runTestImpl(String moduleName, TestCase testCase,
453: TestResult testResult) throws UnableToCompleteException {
454:
455: String newTestCaseClassName = testCase.getClass().getName();
456: boolean sameTest = newTestCaseClassName
457: .equals(testCaseClassName);
458: if (sameTest && lastLaunchFailed) {
459: throw new UnableToCompleteException();
460: }
461:
462: messageQueue.setNextTestName(newTestCaseClassName, testCase
463: .getName());
464:
465: try {
466: lastLaunchFailed = false;
467: testCaseClassName = newTestCaseClassName;
468: currentModuleName = moduleName;
469: runStyle.maybeLaunchModule(moduleName, !sameTest);
470: } catch (UnableToCompleteException e) {
471: lastLaunchFailed = true;
472: testResult.addError(testCase, e);
473: return;
474: }
475:
476: // Wait for test to complete
477: try {
478: // Set a timeout period to automatically fail if the servlet hasn't been
479: // contacted; something probably went wrong (the module failed to load?)
480: testBeginTimeout = System.currentTimeMillis()
481: + TEST_BEGIN_TIMEOUT_MILLIS;
482: pumpEventLoop();
483: } catch (TimeoutException e) {
484: lastLaunchFailed = true;
485: testResult.addError(testCase, e);
486: return;
487: }
488:
489: List<TestResults> results = messageQueue
490: .getResults(testCaseClassName);
491:
492: if (results == null) {
493: return;
494: }
495:
496: boolean parallelTesting = numClients > 1;
497:
498: for (TestResults result : results) {
499: Trial firstTrial = result.getTrials().get(0);
500: Throwable exception = firstTrial.getException();
501:
502: // In the case that we're running multiple clients at once, we need to
503: // let the user know the browser in which the failure happened
504: if (parallelTesting && exception != null) {
505: String msg = "Remote test failed at "
506: + result.getHost() + " on " + result.getAgent();
507: if (exception instanceof AssertionFailedError) {
508: AssertionFailedError newException = new AssertionFailedError(
509: msg + "\n" + exception.getMessage());
510: newException.setStackTrace(exception
511: .getStackTrace());
512: exception = newException;
513: } else {
514: exception = new RuntimeException(msg, exception);
515: }
516: }
517:
518: // A "successful" failure
519: if (exception instanceof AssertionFailedError) {
520: testResult.addFailure(testCase,
521: (AssertionFailedError) exception);
522: } else if (exception != null) {
523: // A real failure
524: testResult.addError(testCase, exception);
525: }
526:
527: if (testCase instanceof Benchmark) {
528: report.addBenchmarkResults(testCase, result);
529: }
530: }
531: }
532:
533: /**
534: * Synthesize command line arguments from a system property.
535: */
536: private String[] synthesizeArgs() {
537: ArrayList<String> argList = new ArrayList<String>();
538:
539: String args = System.getProperty(PROP_GWT_ARGS);
540: if (args != null) {
541: // Match either a non-whitespace, non start of quoted string, or a
542: // quoted string that can have embedded, escaped quoting characters
543: //
544: Pattern pattern = Pattern
545: .compile("[^\\s\"]+|\"[^\"\\\\]*(\\\\.[^\"\\\\]*)*\"");
546: Matcher matcher = pattern.matcher(args);
547: while (matcher.find()) {
548: argList.add(matcher.group());
549: }
550: }
551:
552: return argList.toArray(new String[argList.size()]);
553: }
554: }
|