| // 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.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.classlist'); |
| goog.require('goog.events.EventHandler'); |
| goog.require('goog.functions'); |
| goog.require('goog.string'); |
| 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<string>} |
| * @private |
| */ |
| this.allTests_ = []; |
| |
| /** |
| * Tests that match the filter function. |
| * @type {Array<string>} |
| * @private |
| */ |
| this.activeTests_ = []; |
| |
| /** |
| * An event handler for handling events. |
| * @type {goog.events.EventHandler<!goog.testing.MultiTestRunner>} |
| * @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_); |
| }; |
| 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<string>} |
| */ |
| goog.testing.MultiTestRunner.STATES = [ |
| 'waiting for test runner', |
| 'initializing tests', |
| 'waiting for tests to finish' |
| ]; |
| |
| |
| /** |
| * 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<boolean>} |
| * @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<Object>?} |
| * @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<string>} 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<string>} 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<string>} 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<string>} 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; |
| }; |
| |
| |
| /** |
| * 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('table'); |
| var progressTBody = this.dom_.createDom('tbody'); |
| this.progressRow_ = this.dom_.createDom('tr'); |
| for (var i = 0; i < this.activeTests_.length; i++) { |
| var progressCell = this.dom_.createDom('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('div'); |
| this.progressEl_.className = goog.getCssName('goog-testrunner-progress'); |
| el.appendChild(this.progressEl_); |
| |
| var buttons = this.dom_.createDom('div'); |
| buttons.className = goog.getCssName('goog-testrunner-buttons'); |
| this.startButtonEl_ = this.dom_.createDom('button', null, 'Start'); |
| this.stopButtonEl_ = |
| this.dom_.createDom('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('div'); |
| this.logEl_.className = goog.getCssName('goog-testrunner-log'); |
| el.appendChild(this.logEl_); |
| |
| this.reportEl_ = this.dom_.createElement('div'); |
| this.reportEl_.className = goog.getCssName('goog-testrunner-report'); |
| this.reportEl_.style.display = 'none'; |
| el.appendChild(this.reportEl_); |
| |
| this.statsEl_ = this.dom_.createElement('div'); |
| this.statsEl_.className = goog.getCssName('goog-testrunner-stats'); |
| this.statsEl_.style.display = 'none'; |
| el.appendChild(this.statsEl_); |
| |
| this.logTabEl_ = this.dom_.createDom('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('div', null, 'Report'); |
| this.reportTabEl_.className = goog.getCssName('goog-testrunner-reporttab'); |
| el.appendChild(this.reportTabEl_); |
| |
| this.statsTabEl_ = this.dom_.createDom('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.resetProgressDom_(); |
| goog.dom.removeChildren(this.logEl_); |
| |
| this.resetReport_(); |
| this.clearStats_(); |
| this.showTab_(0); |
| |
| // 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('div', null, msg)); |
| |
| // Autoscroll if we're near the bottom. |
| var top = this.logEl_.scrollTop; |
| var height = this.logEl_.scrollHeight - 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 recieved 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(); |
| |
| this.stats_.push(frame.getStats()); |
| 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('pre', undefined, |
| 'Theses tests did not finish:\n' + unfinished.join('\n'))); |
| } |
| }; |
| |
| |
| /** |
| * 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('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('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.<br>' + |
| passes + ' passed, ' + (executed - passes) + ' failed.<br>' + |
| '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('div'); |
| text = goog.string.htmlEscape(text).replace(/\n/g, '<br>'); |
| if (success) { |
| el.className = goog.getCssName('goog-testrunner-report-success'); |
| } else { |
| text += '<a href="' + this.basePath_ + test + |
| '">Run individually »</a><br> '; |
| 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<!goog.testing.MultiTestRunner.TestFrame>} |
| * @private |
| */ |
| this.eh_ = new goog.events.EventHandler(this); |
| |
| }; |
| 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.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 {?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_ = |
| /** @type {!HTMLIFrameElement} */ (this.dom_.createDom('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.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.isSuccess_ = false; |
| this.finish_(); |
| return; |
| } |
| |
| // Check again in 100ms. |
| goog.Timer.callOnce(this.checkForCompletion_, 100, this); |
| }; |