// Copyright 2008 The Closure Library Authors. All Rights Reserved. // // 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. /** * @fileoverview Utility for running multiple test files that utilize the same * interface as goog.testing.TestRunner. Each test is run in series and their * results aggregated. The main usecase for the MultiTestRunner is to allow * the testing of all tests in a project locally. * */ goog.setTestOnly('goog.testing.MultiTestRunner'); goog.provide('goog.testing.MultiTestRunner'); goog.provide('goog.testing.MultiTestRunner.TestFrame'); goog.require('goog.Timer'); goog.require('goog.array'); goog.require('goog.asserts'); goog.require('goog.dom'); goog.require('goog.dom.TagName'); goog.require('goog.dom.classlist'); goog.require('goog.events.EventHandler'); goog.require('goog.functions'); goog.require('goog.object'); goog.require('goog.string'); goog.require('goog.testing.TestCase'); goog.require('goog.ui.Component'); goog.require('goog.ui.ServerChart'); goog.require('goog.ui.TableSorter'); /** * A component for running multiple tests within the browser. * @param {goog.dom.DomHelper=} opt_domHelper A DOM helper. * @extends {goog.ui.Component} * @constructor * @final */ goog.testing.MultiTestRunner = function(opt_domHelper) { goog.ui.Component.call(this, opt_domHelper); /** * Array of tests to execute, when combined with the base path this should be * a relative path to the test from the page containing the multi testrunner. * @type {Array} * @private */ this.allTests_ = []; /** * Tests that match the filter function. * @type {Array} * @private */ this.activeTests_ = []; /** * An event handler for handling events. * @type {goog.events.EventHandler} * @private */ this.eh_ = new goog.events.EventHandler(this); /** * A table sorter for the stats. * @type {goog.ui.TableSorter} * @private */ this.tableSorter_ = new goog.ui.TableSorter(this.dom_); /** * Array to hold individual test reports for tests that failed. * @type {!Array} * @private */ this.failureReports_ = []; /** * Array of test result objects returned from G_testRunner.getTestResults for * each individual test run. * @private {!Array>>} */ this.allTestResults_ = []; }; goog.inherits(goog.testing.MultiTestRunner, goog.ui.Component); /** * Default maximimum amount of time to spend at each stage of the test. * @type {number} */ goog.testing.MultiTestRunner.DEFAULT_TIMEOUT_MS = 45 * 1000; /** * Messages corresponding to the numeric states. * @type {Array} */ goog.testing.MultiTestRunner.STATES = [ 'waiting for test runner', 'initializing tests', 'waiting for tests to finish' ]; /** * Event type dispatched when tests are completed. * @const */ goog.testing.MultiTestRunner.TESTS_FINISHED = 'testsFinished'; /** * The test suite's name. * @type {string} name * @private */ goog.testing.MultiTestRunner.prototype.name_ = ''; /** * The base path used to resolve files within the allTests_ array. * @type {string} * @private */ goog.testing.MultiTestRunner.prototype.basePath_ = ''; /** * A set of tests that have finished. All extant keys map to true. * @type {Object} * @private */ goog.testing.MultiTestRunner.prototype.finished_ = null; /** * Whether the report should contain verbose information about the passes. * @type {boolean} * @private */ goog.testing.MultiTestRunner.prototype.verbosePasses_ = false; /** * Whether to hide passing tests completely in the report, makes verbosePasses_ * obsolete. * @type {boolean} * @private */ goog.testing.MultiTestRunner.prototype.hidePasses_ = false; /** * Flag used to tell the test runner to stop after the current test. * @type {boolean} * @private */ goog.testing.MultiTestRunner.prototype.stopped_ = false; /** * Flag indicating whether the test runner is active. * @type {boolean} * @private */ goog.testing.MultiTestRunner.prototype.active_ = false; /** * Index of the next test to run. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.startedCount_ = 0; /** * Count of the results received so far. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.resultCount_ = 0; /** * Number of passes so far. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.passes_ = 0; /** * Timestamp for the current start time. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.startTime_ = 0; /** * Only tests whose paths patch this filter function will be * executed. * @type {function(string): boolean} * @private */ goog.testing.MultiTestRunner.prototype.filterFn_ = goog.functions.TRUE; /** * Number of milliseconds to wait for loading and initialization steps. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.timeoutMs_ = goog.testing.MultiTestRunner.DEFAULT_TIMEOUT_MS; /** * An array of objects containing stats about the tests. * @type {Array?} * @private */ goog.testing.MultiTestRunner.prototype.stats_ = null; /** * Reference to the start button element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.startButtonEl_ = null; /** * Reference to the stop button element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.stopButtonEl_ = null; /** * Reference to the log element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.logEl_ = null; /** * Reference to the report element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.reportEl_ = null; /** * Reference to the stats element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.statsEl_ = null; /** * Reference to the progress bar's element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.progressEl_ = null; /** * Reference to the progress bar's inner row element. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.progressRow_ = null; /** * Reference to the log tab. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.logTabEl_ = null; /** * Reference to the report tab. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.reportTabEl_ = null; /** * Reference to the stats tab. * @type {Element} * @private */ goog.testing.MultiTestRunner.prototype.statsTabEl_ = null; /** * The number of tests to run at a time. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.poolSize_ = 1; /** * The size of the stats bucket for the number of files loaded histogram. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.numFilesStatsBucketSize_ = 20; /** * The size of the stats bucket in ms for the run time histogram. * @type {number} * @private */ goog.testing.MultiTestRunner.prototype.runTimeStatsBucketSize_ = 500; /** * Sets the name for the test suite. * @param {string} name The suite's name. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setName = function(name) { this.name_ = name; return this; }; /** * Returns the name for the test suite. * @return {string} The name for the test suite. */ goog.testing.MultiTestRunner.prototype.getName = function() { return this.name_; }; /** * Sets the basepath that tests added using addTests are resolved with. * @param {string} path The relative basepath. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setBasePath = function(path) { this.basePath_ = path; return this; }; /** * Returns the basepath that tests added using addTests are resolved with. * @return {string} The basepath that tests added using addTests are resolved * with. */ goog.testing.MultiTestRunner.prototype.getBasePath = function() { return this.basePath_; }; /** * Sets whether the report should contain verbose information for tests that * pass. * @param {boolean} verbose Whether report should be verbose. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setVerbosePasses = function(verbose) { this.verbosePasses_ = verbose; return this; }; /** * Returns whether the report should contain verbose information for tests that * pass. * @return {boolean} Whether the report should contain verbose information for * tests that pass. */ goog.testing.MultiTestRunner.prototype.getVerbosePasses = function() { return this.verbosePasses_; }; /** * Sets whether the report should contain passing tests at all, makes * setVerbosePasses obsolete. * @param {boolean} hide Whether report should not contain passing tests. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setHidePasses = function(hide) { this.hidePasses_ = hide; return this; }; /** * Returns whether the report should contain passing tests at all, makes * setVerbosePasses obsolete. * @return {boolean} Whether the report should contain passing tests at all, * makes setVerbosePasses obsolete. */ goog.testing.MultiTestRunner.prototype.getHidePasses = function() { return this.hidePasses_; }; /** * Sets the bucket sizes for the histograms. * @param {number} f Bucket size for num files loaded histogram. * @param {number} t Bucket size for run time histogram. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setStatsBucketSizes = function(f, t) { this.numFilesStatsBucketSize_ = f; this.runTimeStatsBucketSize_ = t; return this; }; /** * Sets the number of milliseconds to wait for the page to load, initialize and * run the tests. * @param {number} timeout Time in milliseconds. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setTimeout = function(timeout) { this.timeoutMs_ = timeout; return this; }; /** * Returns the number of milliseconds to wait for the page to load, initialize * and run the tests. * @return {number} The number of milliseconds to wait for the page to load, * initialize and run the tests. */ goog.testing.MultiTestRunner.prototype.getTimeout = function() { return this.timeoutMs_; }; /** * Sets the number of tests that can be run at the same time. This only improves * performance due to the amount of time spent loading the tests. * @param {number} size The number of tests to run at a time. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setPoolSize = function(size) { this.poolSize_ = size; return this; }; /** * Returns the number of tests that can be run at the same time. This only * improves performance due to the amount of time spent loading the tests. * @return {number} The number of tests that can be run at the same time. This * only improves performance due to the amount of time spent loading the * tests. */ goog.testing.MultiTestRunner.prototype.getPoolSize = function() { return this.poolSize_; }; /** * Sets a filter function. Only test paths that match the filter function * will be executed. * @param {function(string): boolean} filterFn Filters test paths. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.setFilterFunction = function(filterFn) { this.filterFn_ = filterFn; return this; }; /** * Returns a filter function. Only test paths that match the filter function * will be executed. * @return {function(string): boolean} A filter function. Only test paths that * match the filter function will be executed. */ goog.testing.MultiTestRunner.prototype.getFilterFunction = function() { return this.filterFn_; }; /** * Adds an array of tests to the tests that the test runner should execute. * @param {Array} tests Adds tests to the test runner. * @return {!goog.testing.MultiTestRunner} Instance for chaining. */ goog.testing.MultiTestRunner.prototype.addTests = function(tests) { goog.array.extend(this.allTests_, tests); return this; }; /** * Returns the list of all tests added to the runner. * @return {Array} The list of all tests added to the runner. */ goog.testing.MultiTestRunner.prototype.getAllTests = function() { return this.allTests_; }; /** * Returns the list of tests that will be run when start() is called. * @return {!Array} The list of tests that will be run when start() is * called. */ goog.testing.MultiTestRunner.prototype.getTestsToRun = function() { return goog.array.filter(this.allTests_, this.filterFn_); }; /** * Returns a list of tests from runner that have been marked as failed. * @return {!Array} A list of tests from runner that have been marked * as failed. */ goog.testing.MultiTestRunner.prototype.getTestsThatFailed = function() { var stats = this.stats_; var failedTests = []; if (stats) { for (var i = 0, stat; stat = stats[i]; i++) { if (!stat.success) { failedTests.push(stat.testFile); } } } return failedTests; }; /** * Returns a list of reports for tests that have finished since last "start". * @return {!Array} A list of tests reports. */ goog.testing.MultiTestRunner.prototype.getFailureReports = function() { return this.failureReports_; }; /** * Returns list of each frame's test results. * @return {!Array>>} */ goog.testing.MultiTestRunner.prototype.getAllTestResults = function() { return this.allTestResults_; }; /** * Deletes and re-creates the progress table inside the progess element. * @private */ goog.testing.MultiTestRunner.prototype.resetProgressDom_ = function() { goog.dom.removeChildren(this.progressEl_); var progressTable = this.dom_.createDom(goog.dom.TagName.TABLE); var progressTBody = this.dom_.createDom(goog.dom.TagName.TBODY); this.progressRow_ = this.dom_.createDom(goog.dom.TagName.TR); for (var i = 0; i < this.activeTests_.length; i++) { var progressCell = this.dom_.createDom(goog.dom.TagName.TD); this.progressRow_.appendChild(progressCell); } progressTBody.appendChild(this.progressRow_); progressTable.appendChild(progressTBody); this.progressEl_.appendChild(progressTable); }; /** @override */ goog.testing.MultiTestRunner.prototype.createDom = function() { goog.testing.MultiTestRunner.superClass_.createDom.call(this); var el = this.getElement(); el.className = goog.getCssName('goog-testrunner'); this.progressEl_ = this.dom_.createDom(goog.dom.TagName.DIV); this.progressEl_.className = goog.getCssName('goog-testrunner-progress'); el.appendChild(this.progressEl_); var buttons = this.dom_.createDom(goog.dom.TagName.DIV); buttons.className = goog.getCssName('goog-testrunner-buttons'); this.startButtonEl_ = this.dom_.createDom(goog.dom.TagName.BUTTON, null, 'Start'); this.stopButtonEl_ = this.dom_.createDom(goog.dom.TagName.BUTTON, {'disabled': true}, 'Stop'); buttons.appendChild(this.startButtonEl_); buttons.appendChild(this.stopButtonEl_); el.appendChild(buttons); this.eh_.listen(this.startButtonEl_, 'click', this.onStartClicked_); this.eh_.listen(this.stopButtonEl_, 'click', this.onStopClicked_); this.logEl_ = this.dom_.createElement(goog.dom.TagName.DIV); this.logEl_.className = goog.getCssName('goog-testrunner-log'); el.appendChild(this.logEl_); this.reportEl_ = this.dom_.createElement(goog.dom.TagName.DIV); this.reportEl_.className = goog.getCssName('goog-testrunner-report'); this.reportEl_.style.display = 'none'; el.appendChild(this.reportEl_); this.statsEl_ = this.dom_.createElement(goog.dom.TagName.DIV); this.statsEl_.className = goog.getCssName('goog-testrunner-stats'); this.statsEl_.style.display = 'none'; el.appendChild(this.statsEl_); this.logTabEl_ = this.dom_.createDom(goog.dom.TagName.DIV, null, 'Log'); this.logTabEl_.className = goog.getCssName('goog-testrunner-logtab') + ' ' + goog.getCssName('goog-testrunner-activetab'); el.appendChild(this.logTabEl_); this.reportTabEl_ = this.dom_.createDom(goog.dom.TagName.DIV, null, 'Report'); this.reportTabEl_.className = goog.getCssName('goog-testrunner-reporttab'); el.appendChild(this.reportTabEl_); this.statsTabEl_ = this.dom_.createDom(goog.dom.TagName.DIV, null, 'Stats'); this.statsTabEl_.className = goog.getCssName('goog-testrunner-statstab'); el.appendChild(this.statsTabEl_); this.eh_.listen(this.logTabEl_, 'click', this.onLogTabClicked_); this.eh_.listen(this.reportTabEl_, 'click', this.onReportTabClicked_); this.eh_.listen(this.statsTabEl_, 'click', this.onStatsTabClicked_); }; /** @override */ goog.testing.MultiTestRunner.prototype.disposeInternal = function() { goog.testing.MultiTestRunner.superClass_.disposeInternal.call(this); this.tableSorter_.dispose(); this.eh_.dispose(); this.startButtonEl_ = null; this.stopButtonEl_ = null; this.logEl_ = null; this.reportEl_ = null; this.progressEl_ = null; this.logTabEl_ = null; this.reportTabEl_ = null; this.statsTabEl_ = null; this.statsEl_ = null; }; /** * Starts executing the tests. */ goog.testing.MultiTestRunner.prototype.start = function() { this.startButtonEl_.disabled = true; this.stopButtonEl_.disabled = false; this.stopped_ = false; this.active_ = true; this.finished_ = {}; this.activeTests_ = this.getTestsToRun(); this.startedCount_ = 0; this.resultCount_ = 0; this.passes_ = 0; this.stats_ = []; this.startTime_ = goog.now(); this.failureReports_ = []; this.resetProgressDom_(); goog.dom.removeChildren(this.logEl_); this.resetReport_(); this.clearStats_(); this.showTab_(0); // No tests to run, finish early and return. if (this.activeTests_.length == 0) { this.finish_(); return; } // Ensure the pool isn't too big. while (this.getChildCount() > this.poolSize_) { this.removeChildAt(0, true).dispose(); } // Start a test in each runner. for (var i = 0; i < this.poolSize_; i++) { if (i >= this.getChildCount()) { var testFrame = new goog.testing.MultiTestRunner.TestFrame( this.basePath_, this.timeoutMs_, this.verbosePasses_, this.dom_); this.addChild(testFrame, true); } this.runNextTest_( /** @type {goog.testing.MultiTestRunner.TestFrame} */ (this.getChildAt(i))); } }; /** * Logs a message to the log window. * @param {string} msg A message to log. */ goog.testing.MultiTestRunner.prototype.log = function(msg) { if (msg != '.') { msg = this.getTimeStamp_() + ' : ' + msg; } this.logEl_.appendChild(this.dom_.createDom(goog.dom.TagName.DIV, null, msg)); // Autoscroll if we're near the bottom. var top = this.logEl_.scrollTop; var height = /** @type {!HTMLElement} */ (this.logEl_).scrollHeight - /** @type {!HTMLElement} */ (this.logEl_).offsetHeight; if (top == 0 || top > height - 50) { this.logEl_.scrollTop = height; } }; /** * Processes a result returned from a TestFrame. If there are tests remaining * it will trigger the next one to be run, otherwise if there are no tests and * all results have been received then it will call finish. * @param {goog.testing.MultiTestRunner.TestFrame} frame The frame that just * finished. */ goog.testing.MultiTestRunner.prototype.processResult = function(frame) { var success = frame.isSuccess(); var report = frame.getReport(); var test = frame.getTestFile(); var stats = frame.getStats(); if (!stats.success) { this.failureReports_.push(report); } this.allTestResults_.push(frame.getTestResults()); this.stats_.push(stats); this.finished_[test] = true; var prefix = success ? '' : '*** FAILURE *** '; this.log( prefix + this.trimFileName_(test) + ' : ' + (success ? 'Passed' : 'Failed')); this.resultCount_++; if (success) { this.passes_++; } this.drawProgressSegment_(test, success); this.writeCurrentSummary_(); if (!(success && this.hidePasses_)) { this.drawTestResult_(test, success, report); } if (!this.stopped_ && this.startedCount_ < this.activeTests_.length) { this.runNextTest_(frame); } else if (this.resultCount_ == this.activeTests_.length) { this.finish_(); } }; /** * Runs the next available test, if there are any left. * @param {goog.testing.MultiTestRunner.TestFrame} frame Where to run the test. * @private */ goog.testing.MultiTestRunner.prototype.runNextTest_ = function(frame) { if (this.startedCount_ < this.activeTests_.length) { var nextTest = this.activeTests_[this.startedCount_++]; this.log(this.trimFileName_(nextTest) + ' : Loading'); frame.runTest(nextTest); } }; /** * Handles the test finishing, processing the results and rendering the report. * @private */ goog.testing.MultiTestRunner.prototype.finish_ = function() { if (this.stopped_) { this.log('Stopped'); } else { this.log('Finished'); } this.startButtonEl_.disabled = false; this.stopButtonEl_.disabled = true; this.active_ = false; this.showTab_(1); this.drawStats_(); // Remove all the test frames while (this.getChildCount() > 0) { this.removeChildAt(0, true).dispose(); } // Compute tests that did not finish before the stop button was hit. var unfinished = []; for (var i = 0; i < this.activeTests_.length; i++) { var test = this.activeTests_[i]; if (!this.finished_[test]) { unfinished.push(test); } } if (unfinished.length) { this.reportEl_.appendChild( goog.dom.createDom( goog.dom.TagName.PRE, undefined, 'These tests did not finish:\n' + unfinished.join('\n'))); } this.dispatchEvent({ 'type': goog.testing.MultiTestRunner.TESTS_FINISHED, 'allTestResults': this.getAllTestResults() }); }; /** * Resets the report, clearing out all children and drawing the initial summary. * @private */ goog.testing.MultiTestRunner.prototype.resetReport_ = function() { goog.dom.removeChildren(this.reportEl_); var summary = this.dom_.createDom(goog.dom.TagName.DIV); summary.className = goog.getCssName('goog-testrunner-progress-summary'); this.reportEl_.appendChild(summary); this.writeCurrentSummary_(); }; /** * Draws the stats for the test run. * @private */ goog.testing.MultiTestRunner.prototype.drawStats_ = function() { this.drawFilesHistogram_(); // Only show time stats if pool size is 1, otherwise times are wrong. if (this.poolSize_ == 1) { this.drawRunTimePie_(); this.drawTimeHistogram_(); } this.drawWorstTestsTable_(); }; /** * Draws the histogram showing number of files loaded. * @private */ goog.testing.MultiTestRunner.prototype.drawFilesHistogram_ = function() { this.drawStatsHistogram_( 'numFilesLoaded', this.numFilesStatsBucketSize_, goog.functions.identity, 500, 'Histogram showing distribution of\nnumber of files loaded per test'); }; /** * Draws the histogram showing how long each test took to complete. * @private */ goog.testing.MultiTestRunner.prototype.drawTimeHistogram_ = function() { this.drawStatsHistogram_( 'totalTime', this.runTimeStatsBucketSize_, function(x) { return x / 1000; }, 500, 'Histogram showing distribution of\ntime spent running tests in s'); }; /** * Draws a stats histogram. * @param {string} statsField Field of the stats object to graph. * @param {number} bucketSize The size for the histogram's buckets. * @param {function(number, ...*): *} valueTransformFn Function for * transforming the x-labels value for display. * @param {number} width The width in pixels of the graph. * @param {string} title The graph's title. * @private */ goog.testing.MultiTestRunner.prototype.drawStatsHistogram_ = function( statsField, bucketSize, valueTransformFn, width, title) { var hist = {}, data = [], xlabels = [], ylabels = []; var max = 0; for (var i = 0; i < this.stats_.length; i++) { var num = this.stats_[i][statsField]; var bucket = Math.floor(num / bucketSize) * bucketSize; if (bucket > max) { max = bucket; } if (!hist[bucket]) { hist[bucket] = 1; } else { hist[bucket]++; } } var maxBucketSize = 0; for (var i = 0; i <= max; i += bucketSize) { xlabels.push(valueTransformFn(i)); var count = hist[i] || 0; if (count > maxBucketSize) { maxBucketSize = count; } data.push(count); } var diff = Math.max(1, Math.ceil(maxBucketSize / 10)); for (var i = 0; i <= maxBucketSize; i += diff) { ylabels.push(i); } var chart = new goog.ui.ServerChart( goog.ui.ServerChart.ChartType.VERTICAL_STACKED_BAR, width, 250, null, goog.ui.ServerChart.CHART_SERVER_HTTPS_URI); chart.setTitle(title); chart.addDataSet(data, 'ff9900'); chart.setLeftLabels(ylabels); chart.setGridY(ylabels.length - 1); chart.setXLabels(xlabels); chart.render(this.statsEl_); }; /** * Draws a pie chart showing the percentage of time spent running the tests * compared to loading them etc. * @private */ goog.testing.MultiTestRunner.prototype.drawRunTimePie_ = function() { var totalTime = 0, runTime = 0; for (var i = 0; i < this.stats_.length; i++) { var stat = this.stats_[i]; totalTime += stat.totalTime; runTime += stat.runTime; } var loadTime = totalTime - runTime; var pie = new goog.ui.ServerChart( goog.ui.ServerChart.ChartType.PIE, 500, 250, null, goog.ui.ServerChart.CHART_SERVER_HTTPS_URI); pie.setMinValue(0); pie.setMaxValue(totalTime); pie.addDataSet([runTime, loadTime], 'ff9900'); pie.setXLabels( ['Test execution (' + runTime + 'ms)', 'Loading (' + loadTime + 'ms)']); pie.render(this.statsEl_); }; /** * Draws a pie chart showing the percentage of time spent running the tests * compared to loading them etc. * @private */ goog.testing.MultiTestRunner.prototype.drawWorstTestsTable_ = function() { this.stats_.sort(function(a, b) { return b['numFilesLoaded'] - a['numFilesLoaded']; }); var tbody = goog.bind(this.dom_.createDom, this.dom_, 'tbody'); var thead = goog.bind(this.dom_.createDom, this.dom_, 'thead'); var tr = goog.bind(this.dom_.createDom, this.dom_, 'tr'); var th = goog.bind(this.dom_.createDom, this.dom_, 'th'); var td = goog.bind(this.dom_.createDom, this.dom_, 'td'); var a = goog.bind(this.dom_.createDom, this.dom_, 'a'); var head = thead( {'style': 'cursor: pointer'}, tr(null, th(null, ' '), th(null, 'Test file'), th('center', 'Num files loaded'), th('center', 'Run time (ms)'), th('center', 'Total time (ms)'))); var body = tbody(); var table = this.dom_.createDom(goog.dom.TagName.TABLE, null, head, body); for (var i = 0; i < this.stats_.length; i++) { var stat = this.stats_[i]; body.appendChild( tr(null, td('center', String(i + 1)), td(null, a({'href': this.basePath_ + stat['testFile'], 'target': '_blank'}, stat['testFile'])), td('center', String(stat['numFilesLoaded'])), td('center', String(stat['runTime'])), td('center', String(stat['totalTime'])))); } this.statsEl_.appendChild(table); this.tableSorter_.setDefaultSortFunction(goog.ui.TableSorter.numericSort); this.tableSorter_.setSortFunction( 1 /* test file name */, goog.ui.TableSorter.alphaSort); this.tableSorter_.decorate(table); }; /** * Clears the stats page. * @private */ goog.testing.MultiTestRunner.prototype.clearStats_ = function() { goog.dom.removeChildren(this.statsEl_); this.tableSorter_.exitDocument(); }; /** * Updates the report's summary. * @private */ goog.testing.MultiTestRunner.prototype.writeCurrentSummary_ = function() { var total = this.activeTests_.length; var executed = this.resultCount_; var passes = this.passes_; var duration = Math.round((goog.now() - this.startTime_) / 1000); var text = executed + ' of ' + total + ' tests executed.
' + passes + ' passed, ' + (executed - passes) + ' failed.
' + 'Duration: ' + duration + 's.'; this.reportEl_.firstChild.innerHTML = text; }; /** * Adds a segment to the progress bar. * @param {string} title Title for the segment. * @param {*} success Whether the segment should indicate a success. * @private */ goog.testing.MultiTestRunner.prototype.drawProgressSegment_ = function( title, success) { var part = this.progressRow_.cells[this.resultCount_ - 1]; part.title = title + ' : ' + (success ? 'SUCCESS' : 'FAILURE'); part.style.backgroundColor = success ? '#090' : '#900'; }; /** * Draws a test result in the report pane. * @param {string} test Test name. * @param {*} success Whether the test succeeded. * @param {string} report The report. * @private */ goog.testing.MultiTestRunner.prototype.drawTestResult_ = function( test, success, report) { var text = goog.string.isEmptyOrWhitespace(report) ? 'No report for ' + test + '\n' : report; var el = this.dom_.createDom(goog.dom.TagName.DIV); text = goog.string.htmlEscape(text).replace(/\n/g, '
'); if (success) { el.className = goog.getCssName('goog-testrunner-report-success'); } else { text += 'Run individually »
 '; el.className = goog.getCssName('goog-testrunner-report-failure'); } el.innerHTML = text; this.reportEl_.appendChild(el); }; /** * Returns the current timestamp. * @return {string} HH:MM:SS. * @private */ goog.testing.MultiTestRunner.prototype.getTimeStamp_ = function() { var d = new Date; return goog.string.padNumber(d.getHours(), 2) + ':' + goog.string.padNumber(d.getMinutes(), 2) + ':' + goog.string.padNumber(d.getSeconds(), 2); }; /** * Trims a filename to be less than 35-characters, ensuring that we do not break * a path part. * @param {string} name The file name. * @return {string} The shortened name. * @private */ goog.testing.MultiTestRunner.prototype.trimFileName_ = function(name) { if (name.length < 35) { return name; } var parts = name.split('/'); var result = ''; while (result.length < 35 && parts.length > 0) { result = '/' + parts.pop() + result; } return '...' + result; }; /** * Shows the report and hides the log if the argument is true. * @param {number} tab Which tab to show. * @private */ goog.testing.MultiTestRunner.prototype.showTab_ = function(tab) { var activeTabCssClass = goog.getCssName('goog-testrunner-activetab'); var logTabElement = goog.asserts.assert(this.logTabEl_); var reportTabElement = goog.asserts.assert(this.reportTabEl_); var statsTabElement = goog.asserts.assert(this.statsTabEl_); if (tab == 0) { this.logEl_.style.display = ''; goog.dom.classlist.add(logTabElement, activeTabCssClass); } else { this.logEl_.style.display = 'none'; goog.dom.classlist.remove(logTabElement, activeTabCssClass); } if (tab == 1) { this.reportEl_.style.display = ''; goog.dom.classlist.add(reportTabElement, activeTabCssClass); } else { this.reportEl_.style.display = 'none'; goog.dom.classlist.remove(reportTabElement, activeTabCssClass); } if (tab == 2) { this.statsEl_.style.display = ''; goog.dom.classlist.add(statsTabElement, activeTabCssClass); } else { this.statsEl_.style.display = 'none'; goog.dom.classlist.remove(statsTabElement, activeTabCssClass); } }; /** * Handles the start button being clicked. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.testing.MultiTestRunner.prototype.onStartClicked_ = function(e) { this.start(); }; /** * Handles the stop button being clicked. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.testing.MultiTestRunner.prototype.onStopClicked_ = function(e) { this.stopped_ = true; this.finish_(); }; /** * Handles the log tab being clicked. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.testing.MultiTestRunner.prototype.onLogTabClicked_ = function(e) { this.showTab_(0); }; /** * Handles the log tab being clicked. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.testing.MultiTestRunner.prototype.onReportTabClicked_ = function(e) { this.showTab_(1); }; /** * Handles the stats tab being clicked. * @param {goog.events.BrowserEvent} e The click event. * @private */ goog.testing.MultiTestRunner.prototype.onStatsTabClicked_ = function(e) { this.showTab_(2); }; /** * Class used to manage the interaction with a single iframe. * @param {string} basePath The base path for tests. * @param {number} timeoutMs The time to wait for the test to load and run. * @param {boolean} verbosePasses Whether to show results for passes. * @param {goog.dom.DomHelper=} opt_domHelper Optional dom helper. * @constructor * @extends {goog.ui.Component} * @final */ goog.testing.MultiTestRunner.TestFrame = function( basePath, timeoutMs, verbosePasses, opt_domHelper) { goog.ui.Component.call(this, opt_domHelper); /** * Base path where tests should be resolved from. * @type {string} * @private */ this.basePath_ = basePath; /** * The timeout for the test. * @type {number} * @private */ this.timeoutMs_ = timeoutMs; /** * Whether to show a summary for passing tests. * @type {boolean} * @private */ this.verbosePasses_ = verbosePasses; /** * An event handler for handling events. * @type {goog.events.EventHandler} * @private */ this.eh_ = new goog.events.EventHandler(this); /** * Object to hold test results. Key is test method or file name (depending on * failure mode) and the value is an array of failure messages. * @private {!Object>} */ this.testResults_ = {}; }; goog.inherits(goog.testing.MultiTestRunner.TestFrame, goog.ui.Component); /** * Reference to the iframe. * @type {HTMLIFrameElement} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.iframeEl_ = null; /** * Whether the iframe for the current test has loaded. * @type {boolean} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.iframeLoaded_ = false; /** * The test file being run. * @type {string} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.testFile_ = ''; /** * The report returned from the test. * @type {string} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.report_ = ''; /** * The total time loading and running the test in milliseconds. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.totalTime_ = 0; /** * The actual runtime of the test in milliseconds. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.runTime_ = 0; /** * The number of files loaded by the test. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.numFilesLoaded_ = 0; /** * Whether the test was successful, null if no result has been returned yet. * @type {?boolean} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.isSuccess_ = null; /** * Timestamp for the when the test was started. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.startTime_ = 0; /** * Timestamp for the last state, used to determine timeouts. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.lastStateTime_ = 0; /** * The state of the active test. * @type {number} * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.currentState_ = 0; /** @override */ goog.testing.MultiTestRunner.TestFrame.prototype.disposeInternal = function() { goog.testing.MultiTestRunner.TestFrame.superClass_.disposeInternal.call(this); this.dom_.removeNode(this.iframeEl_); this.eh_.dispose(); this.iframeEl_ = null; }; /** * Runs a test file in this test frame. * @param {string} testFile The test to run. */ goog.testing.MultiTestRunner.TestFrame.prototype.runTest = function(testFile) { this.lastStateTime_ = this.startTime_ = goog.now(); if (!this.iframeEl_) { this.createIframe_(); } this.iframeLoaded_ = false; this.currentState_ = 0; this.isSuccess_ = null; this.report_ = ''; this.testResults_ = {}; this.testFile_ = testFile; try { this.iframeEl_.src = this.basePath_ + testFile; } catch (e) { // Failures will trigger a JS exception on the local file system. this.report_ = this.testFile_ + ' failed to load : ' + e.message; this.isSuccess_ = false; this.finish_(); return; } this.checkForCompletion_(); }; /** * @return {string} The test file the TestFrame is running. */ goog.testing.MultiTestRunner.TestFrame.prototype.getTestFile = function() { return this.testFile_; }; /** * @return {!Object} Stats about the test run. */ goog.testing.MultiTestRunner.TestFrame.prototype.getStats = function() { return { 'testFile': this.testFile_, 'success': this.isSuccess_, 'runTime': this.runTime_, 'totalTime': this.totalTime_, 'numFilesLoaded': this.numFilesLoaded_ }; }; /** * @return {string} The report for the test run. */ goog.testing.MultiTestRunner.TestFrame.prototype.getReport = function() { return this.report_; }; /** * @return {!Object>} The results * per individual test in the file. Key is the test filename concatenated * with the test name, and the array holds failures. */ goog.testing.MultiTestRunner.TestFrame.prototype.getTestResults = function() { var results = {}; for (var testName in this.testResults_) { var testKey = this.testFile_.replace(/\.html$/, ''); // Concatenate with ":" unless the testName is equivalent to // testFile_, which means the test timed out or had no test methods and // there's no way to get the test method name. if (testName != this.testFile_) { testKey += ':' + testName; } results[testKey] = this.testResults_[testName]; } return results; }; /** * @return {?boolean} Whether the test frame had a success. */ goog.testing.MultiTestRunner.TestFrame.prototype.isSuccess = function() { return this.isSuccess_; }; /** * Handles the TestFrame finishing a single test. * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.finish_ = function() { this.totalTime_ = goog.now() - this.startTime_; // TODO(user): Fire an event instead? if (this.getParent() && this.getParent().processResult) { this.getParent().processResult(this); } }; /** * Creates an iframe to run the tests in. For overriding in unit tests. * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.createIframe_ = function() { this.iframeEl_ = this.dom_.createDom(goog.dom.TagName.IFRAME); this.getElement().appendChild(this.iframeEl_); this.eh_.listen(this.iframeEl_, 'load', this.onIframeLoaded_); }; /** * Handles the iframe loading. * @param {goog.events.BrowserEvent} e The load event. * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.onIframeLoaded_ = function(e) { this.iframeLoaded_ = true; }; /** * Checks the active test for completion, keeping track of the tests' various * execution stages. * @private */ goog.testing.MultiTestRunner.TestFrame.prototype.checkForCompletion_ = function() { var js = goog.dom.getFrameContentWindow(this.iframeEl_); switch (this.currentState_) { case 0: if (this.iframeLoaded_ && js['G_testRunner']) { this.lastStateTime_ = goog.now(); this.currentState_++; } break; case 1: if (js['G_testRunner']['isInitialized']()) { this.lastStateTime_ = goog.now(); this.currentState_++; } break; case 2: if (js['G_testRunner']['isFinished']()) { var tr = js['G_testRunner']; this.isSuccess_ = tr['isSuccess'](); this.report_ = tr['getReport'](this.verbosePasses_); this.testResults_ = tr['getTestResults'](); // If there is a syntax error, or no tests, it's not possible to get the // individual test method results from TestCase. So just create one here // based on the test report and filename. if (goog.object.isEmpty(this.testResults_)) { // Existence of a report is a signal of a test failure by the test // runner. this.testResults_[this.testFile_] = this.isSuccess_ ? [] : [{ 'message': this.report_, 'source': this.testFile_, 'stacktrace': '' }]; } this.runTime_ = tr['getRunTime'](); this.numFilesLoaded_ = tr['getNumFilesLoaded'](); this.finish_(); return; } } // Check to see if the test has timed out. if (goog.now() - this.lastStateTime_ > this.timeoutMs_) { this.report_ = this.testFile_ + ' timed out ' + goog.testing.MultiTestRunner.STATES[this.currentState_]; this.testResults_[this.testFile_] = [{'message': this.report_, 'source': this.testFile_, 'stacktrace': ''}]; this.isSuccess_ = false; this.finish_(); return; } // Check again in 100ms. goog.Timer.callOnce(this.checkForCompletion_, 100, this); };