1 // Copyright (c) 2013 The Chromium Authors. All rights reserved.
2 // Use of this source code is governed by a BSD-style license that can be
3 // found in the LICENSE file.
7 base.requireStylesheet('ui.trace_viewer');
8 base.requireStylesheet('base.unittest');
10 base.require('base.key_event_manager');
11 base.require('base.promise');
12 base.require('base.settings');
13 base.require('base.unittest.test_error');
14 base.require('base.unittest.assertions');
16 base.exportTo('base.unittest', function() {
28 var showCondensed_ = false;
29 var testType_ = TestTypes.UNITTEST;
31 function showCondensed(val) {
35 function testType(val) {
37 testType_ = TestTypes.PERFTEST;
39 testType_ = TestTypes.UNITTEST;
42 function logWarningMessage(message) {
43 var messagesEl = document.querySelector('#messages');
44 messagesEl.setAttribute('hasMessages', true);
46 var li = document.createElement('li');
47 li.innerText = message;
49 var list = document.querySelector('#message-list');
53 function TestRunner(tests) {
55 this.suiteNames_ = {};
56 this.tests_ = tests || [];
57 this.moduleCount_ = 0;
67 TestRunner.prototype = {
68 __proto__: Object.prototype,
71 this.clear_(document.querySelector('#test-results'));
72 this.clear_(document.querySelector('#exception-list'));
78 addSuite: function(suite) {
79 if (this.suiteNames_[suite.name] === true)
80 logWarningMessage('Duplicate test suite name detected: ' + suite.name);
82 this.suites_.push(suite);
83 this.suiteNames_[suite.name] = true;
87 return this.suites_.length;
90 clear_: function(el) {
92 el.removeChild(el.firstChild);
95 runSuites_: function(opt_idx) {
96 var idx = opt_idx || 0;
98 var suiteCount = this.suites_.length;
99 if (idx >= suiteCount) {
100 var harness = document.querySelector('#test-results');
101 harness.appendChild(document.createElement('br'));
102 harness.appendChild(document.createTextNode('Test Run Complete'));
106 var suite = this.suites_[idx];
107 suite.showLongResults = (suiteCount === 1);
109 return suite.runTests(this.tests_).then(function(ignored) {
110 this.stats_.duration += suite.duration;
111 this.stats_.tests += suite.testCount;
112 this.stats_.failures += suite.failureCount;
115 return this.runSuites_(idx + 1);
119 onAnimationFrameError: function(e, opt_stack) {
121 console.error(e.message, e.stack);
125 var exception = {e: e, stack: opt_stack};
126 this.stats_.exceptions.push(exception);
127 this.appendException(exception);
131 updateStats_: function() {
132 var statEl = document.querySelector('#stats');
134 this.suites_.length + ' suites, ' +
135 '<span class="passed">' + this.stats_.tests + '</span> tests, ' +
136 '<span class="failed">' + this.stats_.failures +
137 '</span> failures, ' +
138 '<span class="exception">' + this.stats_.exceptions.length +
139 '</span> exceptions,' +
140 ' in ' + this.stats_.duration.toFixed(2) + 'ms.';
143 appendException: function(exc) {
144 var exceptionsEl = document.querySelector('#exceptions');
145 exceptionsEl.setAttribute('hasExceptions', this.stats_.exceptions.length);
147 var excEl = document.createElement('li');
148 excEl.innerHTML = exc.e + '<pre>' + exc.stack + '</pre>';
150 var exceptionsEl = document.querySelector('#exception-list');
151 exceptionsEl.appendChild(excEl);
155 function TestSuite(name, suite) {
158 this.testNames_ = {};
160 this.showLongResults = false;
161 this.duration_ = 0.0;
162 this.resultsEl_ = undefined;
164 global.setupOnce = function(fn) { this.setupOnceFn_ = fn; }.bind(this);
165 global.setup = function(fn) { this.setupFn_ = fn; }.bind(this);
166 global.teardown = function(fn) { this.teardownFn_ = fn; }.bind(this);
168 global.test = function(name, test, options) {
169 options = options || {};
171 if (this.testNames_[name] === true)
172 logWarningMessage('Duplicate test name detected: ' + name);
175 // If the test cares about DPI settings then we first push a test
176 // that fakes the DPI as the low or hi Dpi version, depending on what
177 // we're current using.
178 if (options.dpiAware) {
179 var defaultDevicePixelRatio = window.devicePixelRatio;
180 var dpi = defaultDevicePixelRatio > 1 ? 1 : 2;
182 var testWrapper = function() {
183 window.devicePixelRatio = dpi;
184 test.bind(this).call();
185 window.devicePixelRatio = defaultDevicePixelRatio;
191 testName += '_hiDPI';
194 testName += '_loDPI';
197 this.tests_.push(new Test(newName, testWrapper, options || {}));
200 this.tests_.push(new Test(testName, test, options || {}));
201 this.testNames_[name] = true;
204 global.perfTest = function(name, test, options) {
205 if (this.testNames_[name] === true)
206 logWarningMessage('Duplicate test name detected: ' + name);
208 this.tests_.push(new PerfTest(name, test, options || {}));
209 this.testNames_[name] = true;
212 global.timedPerfTest = function(name, test, options) {
213 if (options === undefined || options.iterations === undefined)
214 throw new Error('timedPerfTest must have iteration option provided.');
216 name += '_' + options.iterations;
217 if (this.testNames_[name] === true)
218 logWarningMessage('Duplicate test name detected: ' + name);
220 options.results = options.results || TimingTestResult;
221 var testWrapper = function(results) {
222 results.testCount = options.iterations;
223 for (var i = 0; i < options.iterations; ++i) {
224 var start = window.performance.now();
225 test.bind(this).call();
226 results.add(window.performance.now() - start);
230 this.tests_.push(new PerfTest(name, testWrapper, options));
231 this.testNames_[name] = true;
236 global.setupOnce = undefined;
237 global.setup = undefined;
238 global.teardown = undefined;
239 global.test = undefined;
240 global.perfTest = undefined;
241 global.timedPerfTest = undefined;
244 TestSuite.prototype = {
245 __proto__: Object.prototype,
252 return (this.failureCount > 0) ? TestStatus.FAILED : TestStatus.PASSED;
256 return this.tests_.length;
260 return this.failures.length;
264 return this.failures_;
268 return this.duration_;
271 displayInfo: function() {
272 this.resultsEl_ = document.createElement('div');
273 this.resultsEl_.className = 'test-result';
275 var resultsPanel = document.querySelector('#test-results');
276 resultsPanel.appendChild(this.resultsEl_);
278 if (this.showLongResults) {
279 this.resultsEl_.innerText = this.name;
281 var link = '/src/tests.html?suite=';
282 link += this.name.replace(/\./g, '/');
283 link += '&type=' + (testType_ === TestTypes.PERFTEST ? 'perf' : 'unit');
285 var suiteInfo = document.createElement('a');
286 suiteInfo.href = link;
287 suiteInfo.innerText = this.name;
288 this.resultsEl_.appendChild(suiteInfo);
291 var statusEl = document.createElement('span');
292 statusEl.classList.add('results');
293 statusEl.classList.add('pending');
294 statusEl.innerText = 'pending';
295 this.resultsEl_.appendChild(statusEl);
298 runTests: function(testsToRun) {
299 this.testsToRun_ = testsToRun;
301 if (this.setupOnceFn_ !== undefined)
302 this.setupOnceFn_.bind(this).call();
305 if (testsToRun.length) {
306 remainingTests = this.tests_.reduce(function(remainingTests, test) {
307 if (this.testsToRun_.indexOf(test.name) !== -1)
308 remainingTests.push(test);
309 return remainingTests;
312 remainingTests = this.tests_.slice(0);
315 return this.runRemainingTests_(remainingTests).then(
316 function resolve(ignored) {
317 this.duration_ = this.tests_.reduce(function(total, test) {
318 return total += test.duration;
320 this.outputResults();
324 this.outputResults();
329 runRemainingTests_: function(remainingTests) {
330 if (!remainingTests.length)
331 return base.Promise.resolve();
332 var test = remainingTests.pop();
334 // Clear settings storage before each test.
335 global.sessionStorage.clear();
336 base.Settings.setAlternativeStorageInstance(global.sessionStorage);
337 base.onAnimationFrameError =
338 testRunners[testType_].onAnimationFrameError.bind(
339 testRunners[testType_]);
340 base.KeyEventManager.resetInstanceForUnitTesting();
342 var testWorkAreaEl_ = document.createElement('div');
343 this.resultsEl_.appendChild(testWorkAreaEl_);
345 test.workArea = testWorkAreaEl_;
346 if (this.setupFn_ !== undefined)
347 this.setupFn_.bind(test).call();
350 return test.run.then(function(ignore) {
351 test.status = TestStatus.PASSED;
352 suite.testTearDown_(test);
353 return suite.runRemainingTests_(remainingTests);
355 var stack = error && error.stack ? error.stack : '';
356 console.error("Rejected, cause: \'" + error + "\'", stack);
357 test.status = TestStatus.FAILED;
358 test.failure = error;
359 suite.failures_.push({
363 suite.testTearDown_(test);
364 return suite.runRemainingTests_(remainingTests);
368 testTearDown_: function(test) {
369 this.resultsEl_.removeChild(test.workArea);
371 if (this.teardownFn_ !== undefined)
372 this.teardownFn_.bind(test).call();
375 outputResults: function() {
376 if ((this.results === TestStatus.PASSED) && showCondensed_ &&
377 !this.showLongResults) {
378 var parent = this.resultsEl_.parentNode;
379 parent.removeChild(this.resultsEl_);
380 this.resultsEl_ = undefined;
382 parent.appendChild(document.createTextNode('.'));
386 var status = this.resultsEl_.querySelector('.results');
387 status.classList.remove('pending');
388 if (this.results === TestStatus.PASSED) {
389 status.innerText = 'passed';
390 status.classList.add('passed');
392 status.innerText = 'FAILED';
393 status.classList.add('failed');
396 status.innerText += ' (' + this.duration_.toFixed(2) + 'ms)';
398 var child = this.showLongResults ? this.outputLongResults() :
399 this.outputShortResults();
400 if (child !== undefined)
401 this.resultsEl_.appendChild(child);
404 outputShortResults: function() {
405 if (this.results === TestStatus.PASSED)
408 var parent = document.createElement('div');
410 var failureList = this.failures;
411 for (var i = 0; i < failureList.length; ++i) {
412 var fail = failureList[i];
414 var preEl = document.createElement('pre');
415 preEl.className = 'failure';
416 preEl.innerText = 'Test: ' + fail.test + '\n' + fail.error.stack;
417 parent.appendChild(preEl);
423 outputLongResults: function() {
424 var parent = document.createElement('div');
426 this.tests_.forEach(function(test) {
427 if (this.testsToRun_.length !== 0 &&
428 this.testsToRun_.indexOf(test.name) === -1)
431 // Construct an individual result div.
432 var testEl = document.createElement('div');
433 testEl.className = 'individual-result';
435 var link = '/src/tests.html?suite=';
436 link += this.name.replace(/\./g, '/');
437 link += '&test=' + test.name.replace(/\./g, '/');
439 (testType_ === TestTypes.PERFTEST ? 'perf' : 'unit');
441 var suiteInfo = document.createElement('a');
442 suiteInfo.href = link;
443 suiteInfo.innerText = test.name;
444 testEl.appendChild(suiteInfo);
446 parent.appendChild(testEl);
448 var resultEl = document.createElement('span');
449 resultEl.classList.add('results');
450 testEl.appendChild(resultEl);
451 if (test.status === TestStatus.PASSED) {
452 resultEl.classList.add('passed');
454 'passed (' + test.output() + ')';
455 } else if (test.status === TestStatus.PENDING) {
456 resultEl.classList.add('failed');
457 resultEl.innerText = 'PENDING...TIMEOUT';
459 resultEl.classList.add('failed');
460 resultEl.innerText = 'FAILED';
462 var preEl = document.createElement('pre');
463 preEl.className = 'failure';
464 preEl.innerText = test.failure.stack || test.failure;
465 testEl.appendChild(preEl);
468 if (test.hasAppendedContent)
469 testEl.appendChild(test.appendedContent);
475 toString: function() {
480 function Test(name, test, options) {
483 this.isPerfTest_ = false;
484 this.options_ = options;
485 this.failure_ = undefined;
487 this.status_ = TestStatus.PENDING;
489 this.appendedContent_ = undefined;
493 __proto__: Object.prototype,
497 return new base.Promise(function(r) {
498 var startTime = window.performance.now();
500 var maybePromise = test.test_();
502 // An async test may not have completed.
503 maybePromise.then(function(ignored) {
504 test.duration = window.performance.now() - startTime;
508 test.duration = window.performance.now() - startTime;
512 test.duration = window.performance.now() - startTime;
519 return this.failure_;
531 return this.isPerfTest_;
535 return this.testRuns_;
547 return this.duration_;
551 return this.options_;
554 set duration(duration) {
555 this.duration_ = duration;
558 get hasAppendedContent() {
559 return (this.appendedContent_ !== undefined);
562 get appendedContent() {
563 return this.appendedContent_;
567 return this.testWorkArea_;
570 set workArea(workArea) {
571 this.testWorkArea_ = workArea;
574 addHTMLOutput: function(element) {
575 this.testWorkArea_.appendChild(element);
576 this.appendedContent_ = element;
579 toString: function() {
584 return this.duration_.toFixed(2) + 'ms';
588 function PerfTest(name, test, options) {
589 Test.apply(this, arguments);
590 this.isPerfTest_ = true;
592 var resultObject = options.results || TestResult;
593 this.results_ = new resultObject();
596 PerfTest.prototype = {
597 __proto__: Test.prototype,
601 this.test_.call(this, this.results_);
602 this.status_ = TestStatus.PASSED;
604 console.error(e, e.stack);
610 return this.results_.output();
614 var testRunners = {};
615 var totalSuiteCount_ = 0;
617 function allSuitesLoaded_() {
618 return (testRunners[TestTypes.UNITTEST].suiteCount +
619 testRunners[TestTypes.PERFTEST].suiteCount) >= totalSuiteCount_;
622 function testSuite(name, suite) {
623 testRunners[TestTypes.UNITTEST].addSuite(new TestSuite(name, suite));
624 if (allSuitesLoaded_())
628 function perfTestSuite(name, suite) {
629 testRunners[TestTypes.PERFTEST].addSuite(new TestSuite(name, suite));
630 if (allSuitesLoaded_())
634 function Suites(suitePaths, tests) {
635 // Assume one suite per file.
636 totalSuiteCount_ = suitePaths.length;
638 testRunners[TestTypes.UNITTEST] = new TestRunner(tests);
639 testRunners[TestTypes.PERFTEST] = new TestRunner(tests);
642 suitePaths.forEach(function(path) {
643 var moduleName = path.slice(5, path.length - 3);
644 moduleName = moduleName.replace(/\//g, '.');
645 modules.push(moduleName);
647 base.require(modules);
650 function runSuites() {
651 testRunners[testType_].run();
654 function TestResult() {
657 TestResult.prototype = {
658 add: function(result) {
659 this.results_.push(result);
663 return this.results_.join(', ');
667 function TimingTestResult() {
668 TestResult.apply(this, arguments);
671 TimingTestResult.prototype = {
672 __proto__: TestResult.prototype,
674 set testCount(runs) {
675 this.runCount_ = runs;
680 this.results_.forEach(function(t) { totalTime += t; });
681 return totalTime.toFixed(2) + 'ms) ' + this.runCount_ + ' runs, ' +
682 'avg ' + (totalTime / this.runCount_).toFixed(2) + 'ms/run';
687 showCondensed: showCondensed,
689 testSuite: testSuite,
690 perfTestSuite: perfTestSuite,
691 runSuites: runSuites,
694 TestSuite_: TestSuite