Merge from Chromium at DEPS revision r167172
This commit was generated by merge_to_master.py.
Change-Id: Iead6b4948cd90f0aac77a0e5e2b6c1749577569b
diff --git a/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt b/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt
new file mode 100644
index 0000000..536c0a8
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/LICENSE.dygraph.txt
@@ -0,0 +1,22 @@
+Copyright (c) 2009 Dan Vanderkam
+
+Permission is hereby granted, free of charge, to any person
+obtaining a copy of this software and associated documentation
+files (the "Software"), to deal in the Software without
+restriction, including without limitation the rights to use,
+copy, modify, merge, publish, distribute, sublicense, and/or sell
+copies of the Software, and to permit persons to whom the
+Software is furnished to do so, subject to the following
+conditions:
+
+The above copyright notice and this permission notice shall be
+included in all copies or substantial portions of the Software.
+
+THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
+EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES
+OF MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
+NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT
+HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY,
+WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING
+FROM, OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR
+OTHER DEALINGS IN THE SOFTWARE.
diff --git a/Tools/TestResultServer/static-dashboards/README.dygraph.txt b/Tools/TestResultServer/static-dashboards/README.dygraph.txt
new file mode 100644
index 0000000..e800692
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/README.dygraph.txt
@@ -0,0 +1,55 @@
+dygraphs JavaScript charting library
+Copyright (c) 2006-, Dan Vanderkam.
+
+Support: http://groups.google.com/group/dygraphs-users
+Source: http://github.com/danvk/dygraphs
+Issues: http://code.google.com/p/dygraphs/
+
+
+The dygraphs JavaScript library produces produces interactive, zoomable charts of time series.
+
+Features
+- Plots time series without using an external server or Flash
+- Supports multiple data series
+- Supports error bands around data series
+- Displays values on mouseover
+- Interactive zoom
+- Adjustable averaging period
+- Customizable click-through actions
+- Compatible with the Google Visualization API
+
+Demo
+For a gallery and documentation, see http://danvk.org/dygraphs/
+
+Minimal Example
+<html>
+<head>
+<script type="text/javascript" src="dygraph-combined.js"></script>
+</head>
+<body>
+<div id="graphdiv"></div>
+<script type="text/javascript">
+ g = new Dygraph(
+ document.getElementById("graphdiv"), // containing div
+ "Date,Temperature\n" + // the data series
+ "2008-05-07,75\n" +
+ "2008-05-08,70\n" +
+ "2008-05-09,80\n"
+ );
+</script>
+</body>
+</html>
+
+License(s)
+dygraphs uses:
+ - rgbcolor.js (Public Domain)
+ - strftime.js (BSD License)
+ - excanvas.js (Apache License)
+ - YUI compressor (BSD License)
+
+rgbcolor: http://www.phpied.com/rgb-color-parser-in-javascript/
+strftime: http://tech.bluesmoon.info/2008/04/strftime-in-javascript.html
+excanvas: http://code.google.com/p/explorercanvas/
+yui compressor: http://developer.yahoo.com/yui/compressor/
+
+dygraphs is available under the MIT license, included in LICENSE.txt.
diff --git a/Tools/TestResultServer/static-dashboards/README.webtreemap.txt b/Tools/TestResultServer/static-dashboards/README.webtreemap.txt
new file mode 100644
index 0000000..d9d885b
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/README.webtreemap.txt
@@ -0,0 +1,63 @@
+# webtreemap
+
+A simple treemap implementation using web technologies (DOM nodes, CSS
+styling and transitions) rather than a big canvas/svg/plugin.
+
+Play with a [demo][].
+
+[demo]: http://martine.github.com/webtreemap/demo/demo.html
+
+## Creating your own
+
+1. Create a page with a DOM node (i.e. a `<div>`) that will contain
+ your treemap.
+2. Add the treemap to the node via something like
+
+ appendTreemap(document.getElementById('mynode'), mydata);
+3. Style the treemap using CSS.
+
+### Input format
+
+The input data (`mydata` in the overview snippet) is a tree of nodes,
+likely imported via a separate JSON file. Each node (including the
+root) should contain data in the following format.
+
+ {
+ name: (HTML that is displayed via .innerHTML on the caption),
+ data: {
+ "$area": (a number, in arbitrary units)
+ },
+ children: (list of child tree nodes)
+ }
+
+(This strange format for data comes from the the [JavaScript InfoVis
+Toolkit][thejit]. I might change it in the future.)
+
+The `$area` of a node should be the sum of the `$area` of all of its
+`children`.
+
+(At runtime, tree nodes will dynamically will gain two extra
+attributes, `parent` and `dom`; this is only worth pointing out so
+that you don't accidentally conflict with them.)
+
+### CSS styling
+
+The treemap is constructed with one `div` per region with a separate
+`div` for the caption. Each div is styleable via the
+`webtreemap-node` CSS class. The captions are stylable as
+`webtreemap-caption`.
+
+Each level of the tree also gets a per-level CSS class,
+`webtreemap-level0` through `webtreemap-level4`. These can be
+adjusted to e.g. made different levels different colors. To control
+the caption on a per-level basis, use a CSS selector like
+`.webtreemap-level2 > .webtreemap-caption`.
+
+Your best bet is to modify the included `webtreemap.css`, which
+contains comments about required and optional CSS attributes.
+
+## Related projects
+
+* [JavaScript InfoVis Toolkit][thejit]
+
+[thejit]: http://thejit.org/
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/aggregate_results.html b/Tools/TestResultServer/static-dashboards/aggregate_results.html
new file mode 100644
index 0000000..fa033a5
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/aggregate_results.html
@@ -0,0 +1,299 @@
+<!-- Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+<title>Layout test passing status</title>
+<style>
+h2 {
+ margin: 0;
+ font-size: 1.2em;
+}
+h3 {
+ margin-bottom: 0;
+ font-size: 1em;
+}
+.container {
+ display: inline-block;
+ padding: 3px;
+}
+img {
+ border: 1px dotted grey;
+ margin-right: 5px;
+ padding: 2px;
+}
+</style>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script>
+// @fileoverview Creates a dashboard for tracking number of passes/failures per run.
+//
+// Currently, only webkit tests are supported, but adding other test types
+// should just require the following steps:
+// -generate results.json for these tests
+// -copy them to the appropriate location
+// -add the builder name to the list of builders in dashboard_base.js.
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+ var html = htmlForTestTypeSwitcher(true) + '<br>';
+ for (var builder in g_builders)
+ html += htmlForBuilder(builder);
+ document.body.innerHTML = html;
+}
+
+function handleValidHashParameter(key, value)
+{
+ switch(key) {
+ case 'rawValues':
+ g_currentState[key] = value == 'true';
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+g_defaultDashboardSpecificStateValues = {
+ rawValues: false
+};
+
+function htmlForBuilder(builder)
+{
+ var results = g_resultsByBuilder[builder];
+ // Some keys were added later than others, so they don't have as many
+ // builds. Use the shortest.
+ // FIXME: Once 500 runs have finished, we can get rid of passing this
+ // around and just assume all keys have the same number of builders for a
+ // given builder.
+ var numColumns = results[ALL_FIXABLE_COUNT_KEY].length;
+ var html = '<div class=container><h2>' + builder + '</h2>';
+
+ if (g_currentState.rawValues)
+ html += rawValuesHTML(results, numColumns);
+ else {
+ html += '<a href="timeline_explorer.html' + (location.hash ? location.hash + '&' : '#') + 'builder=' + builder + '">' +
+ chartHTML(results, numColumns) + '</a>';
+ }
+
+ html += '</div>';
+ return html;
+}
+
+function rawValuesHTML(results, numColumns)
+{
+ var html = htmlForSummaryTable(results, numColumns) +
+ htmlForTestType(results, FIXABLE_COUNTS_KEY, FIXABLE_DESCRIPTION, numColumns);
+ if (isLayoutTestResults()) {
+ html += htmlForTestType(results, DEFERRED_COUNTS_KEY, DEFERRED_DESCRIPTION, numColumns) +
+ htmlForTestType(results, WONTFIX_COUNTS_KEY, WONTFIX_DESCRIPTION, numColumns);
+ }
+ return html;
+}
+
+function chartHTML(results, numColumns)
+{
+ var shouldShowWebKitRevisions = isTipOfTreeWebKitBuilder();
+ var revisionKey = shouldShowWebKitRevisions ? WEBKIT_REVISIONS_KEY : CHROME_REVISIONS_KEY;
+ var startRevision = results[revisionKey][numColumns - 1];
+ var endRevision = results[revisionKey][0];
+ var revisionLabel = shouldShowWebKitRevisions ? "WebKit Revision" : "Chromium Revision";
+
+ var fixable = results[FIXABLE_COUNT_KEY].slice(0, numColumns);
+ var html = chart("Total failing", {"": fixable}, revisionLabel, startRevision, endRevision);
+
+ var values = valuesPerExpectation(results[FIXABLE_COUNTS_KEY], numColumns);
+ // Don't care about number of passes for the charts.
+ delete(values['P']);
+
+ return html + chart("Detailed breakdown", values, revisionLabel, startRevision, endRevision);
+}
+
+var LABEL_COLORS = ['FF0000', '00FF00', '0000FF', '000000', 'FF6EB4', 'FFA812', '9B30FF', '00FFCC'];
+
+// FIXME: Find a better way to exclude outliers. This is just so we exclude
+// runs where every test failed.
+var MAX_VALUE = 10000;
+
+function filteredValues(values, desiredNumberOfPoints)
+{
+ // Filter out values to make the graph a bit more readable and to keep URLs
+ // from exceeding the browsers max length restriction.
+ var filterAmount = Math.floor(values.length / desiredNumberOfPoints);
+ if (filterAmount < 1)
+ return values;
+
+ return values.filter(function(element, index, array) {
+ // Include the most recent and oldest values and exclude outliers.
+ return (index % filterAmount == 0 || index == array.length - 1) && (array[index] < MAX_VALUE && array[index] != 0);
+ });
+}
+
+function chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints) {
+ var maxValue = 0;
+ for (var expectation in values)
+ maxValue = Math.max(maxValue, Math.max.apply(null, filteredValues(values[expectation], desiredNumberOfPoints)));
+
+ var chartData = '';
+ var labels = '';
+ var numLabels = 0;
+
+ var first = true;
+ for (var expectation in values) {
+ chartData += (first ? 'e:' : ',') + extendedEncode(filteredValues(values[expectation], desiredNumberOfPoints).reverse(), maxValue);
+
+ if (expectation) {
+ numLabels++;
+ labels += (first ? '' : '|') + expectationsMap()[expectation];
+ }
+ first = false;
+ }
+
+ var url = "http://chart.apis.google.com/chart?cht=lc&chs=600x400&chd=" +
+ chartData + "&chg=15,15,1,3&chxt=x,x,y&chxl=1:||" + revisionLabel +
+ "|&chxr=0," + startRevision + "," + endRevision + "|2,0," + maxValue + "&chtt=" + title;
+
+
+ if (labels)
+ url += "&chdl=" + labels + "&chco=" + LABEL_COLORS.slice(0, numLabels).join(',');
+ return url;
+}
+
+function chart(title, values, revisionLabel, startRevision, endRevision)
+{
+ var desiredNumberOfPoints = 400;
+ var url = chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints);
+
+ while (url.length >= 2048) {
+ // Decrease the desired number of points gradually until we get a URL that
+ // doesn't exceed chartserver's max URL length.
+ desiredNumberOfPoints = 3 / 4 * desiredNumberOfPoints;
+ url = chartUrl(title, values, revisionLabel, startRevision, endRevision, desiredNumberOfPoints);
+ }
+
+ return '<img src="' + url + '">';
+}
+
+function htmlForRevisionRows(results, numColumns)
+{
+ return htmlForTableRow('WebKit Revision', results[WEBKIT_REVISIONS_KEY].slice(0, numColumns)) +
+ htmlForTableRow('Chrome Revision', results[CHROME_REVISIONS_KEY].slice(0, numColumns));
+}
+
+function wrapHTMLInTable(description, html)
+{
+ return '<h3>' + description + '</h3><table><tbody>' + html + '</tbody></table>';
+}
+
+function htmlForSummaryTable(results, numColumns)
+{
+ var percent = [];
+ var fixable = results[FIXABLE_COUNT_KEY].slice(0, numColumns);
+ var allFixable = results[ALL_FIXABLE_COUNT_KEY].slice(0, numColumns);
+ for (var i = 0; i < numColumns; i++) {
+ var percentage = 100 * (allFixable[i] - fixable[i]) / allFixable[i];
+ // Round to the nearest tenth of a percent.
+ percent.push(Math.round(percentage * 10) / 10 + '%');
+ }
+ var html = htmlForRevisionRows(results, numColumns) +
+ htmlForTableRow('Percent passed', percent) +
+ htmlForTableRow('Failures (deduped)', fixable) +
+ htmlForTableRow('Fixable Tests', allFixable);
+ return wrapHTMLInTable('Summary', html);
+}
+
+function valuesPerExpectation(counts, numColumns)
+{
+ var values = {};
+ for (var i = 0; i < numColumns; i++) {
+ for (var expectation in expectationsMap()) {
+ if (expectation in counts[i]) {
+ var count = counts[i][expectation];
+ if (!values[expectation])
+ values[expectation] = [];
+ values[expectation].push(count);
+ }
+ }
+ }
+ return values;
+}
+
+function htmlForTestType(results, key, description, numColumns)
+{
+ var counts = results[key];
+ var html = htmlForRevisionRows(results, numColumns);
+ var values = valuesPerExpectation(counts, numColumns);
+ for (var expectation in values)
+ html += htmlForTableRow(expectationsMap()[expectation], values[expectation]);
+ return wrapHTMLInTable(description, html);
+}
+
+function htmlForTableRow(columnName, values)
+{
+ return '<tr><td>' + columnName + '</td><td>' + values.join('</td><td>') + '</td></tr>';
+}
+
+// Taken from http://code.google.com/apis/chart/docs/data_formats.html.
+function extendedEncode(arrVals, maxVal)
+{
+ var map = 'ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz0123456789-.';
+ var mapLength = map.length;
+ var mapLengthSquared = mapLength * mapLength;
+
+ var chartData = '';
+
+ for (var i = 0, len = arrVals.length; i < len; i++) {
+ // In case the array vals were translated to strings.
+ var numericVal = new Number(arrVals[i]);
+ // Scale the value to maxVal.
+ var scaledVal = Math.floor(mapLengthSquared * numericVal / maxVal);
+
+ if(scaledVal > mapLengthSquared - 1)
+ chartData += "..";
+ else if (scaledVal < 0)
+ chartData += '__';
+ else {
+ // Calculate first and second digits and add them to the output.
+ var quotient = Math.floor(scaledVal / mapLength);
+ var remainder = scaledVal - mapLength * quotient;
+ chartData += map.charAt(quotient) + map.charAt(remainder);
+ }
+ }
+
+ return chartData;
+}
+</script>
+</head>
+<body></body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/builders.js b/Tools/TestResultServer/static-dashboards/builders.js
new file mode 100644
index 0000000..2a6b67d
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/builders.js
@@ -0,0 +1,332 @@
+// Copyright (C) 2012 Google Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// @fileoverview File that lists builders, their masters, and logical groupings
+// of them.
+
+function BuilderMaster(name, basePath)
+{
+ this.name = name;
+ this.basePath = basePath;
+}
+
+BuilderMaster.prototype.logPath = function(builder, buildNumber)
+{
+ return this.basePath + 'builders/' + builder + '/builds/' + buildNumber;
+};
+
+BuilderMaster.prototype.builderJsonPath = function()
+{
+ return this.basePath + 'json/builders';
+};
+
+CHROMIUM_WIN_BUILDER_MASTER = new BuilderMaster('ChromiumWin', 'http://build.chromium.org/p/chromium.win/');
+CHROMIUM_MAC_BUILDER_MASTER = new BuilderMaster('ChromiumMac', 'http://build.chromium.org/p/chromium.mac/');
+CHROMIUM_LINUX_BUILDER_MASTER = new BuilderMaster('ChromiumLinux', 'http://build.chromium.org/p/chromium.linux/');
+CHROMIUMOS_BUILDER_MASTER = new BuilderMaster('ChromiumChromiumOS', 'http://build.chromium.org/p/chromium.chromiumos/');
+CHROMIUM_GPU_BUILDER_MASTER = new BuilderMaster('ChromiumGPU', 'http://build.chromium.org/p/chromium.gpu/');
+CHROMIUM_GPU_FYI_BUILDER_MASTER = new BuilderMaster('ChromiumGPUFYI', 'http://build.chromium.org/p/chromium.gpu.fyi/');
+CHROMIUM_PERF_AV_BUILDER_MASTER = new BuilderMaster('ChromiumPerfAv', 'http://build.chromium.org/p/chromium.perf_av/');
+CHROMIUM_WEBKIT_BUILDER_MASTER = new BuilderMaster('ChromiumWebkit', 'http://build.chromium.org/p/chromium.webkit/');
+WEBKIT_BUILDER_MASTER = new BuilderMaster('webkit.org', 'http://build.webkit.org/');
+
+var LEGACY_BUILDER_MASTERS_TO_GROUPS = {
+ 'Chromium': '@DEPS - chromium.org',
+ 'ChromiumWin': '@DEPS - chromium.org',
+ 'ChromiumMac': '@DEPS - chromium.org',
+ 'ChromiumLinux': '@DEPS - chromium.org',
+ 'ChromiumChromiumOS': '@DEPS CrOS - chromium.org',
+ 'ChromiumGPU': '@DEPS - chromium.org',
+ 'ChromiumGPUFYI': '@DEPS FYI - chromium.org',
+ 'ChromiumPerfAv': '@DEPS - chromium.org',
+ 'ChromiumWebkit': '@ToT - chromium.org',
+ 'webkit.org': '@ToT - webkit.org'
+};
+
+function BuilderGroup(isToTWebKit)
+{
+ this.isToTWebKit = isToTWebKit;
+ // Map of builderName (the name shown in the waterfall) to builderPath (the
+ // path used in the builder's URL)
+ this.builders = {};
+ this.groups = 0;
+ this.expectedGroups = 0;
+}
+
+BuilderGroup.prototype.setbuilder = function(builder, flags) {
+ this.builders[builder] = builder.replace(/[ .()]/g, '_');
+ // FIXME: Remove this at some point, we don't actually use DEFAULT_BUILDER
+ // in any meaningful way anymore. We always just default to the
+ // first builder in alphabetical order.
+ if (flags & BuilderGroup.DEFAULT_BUILDER)
+ this.defaultBuilder = builder;
+};
+
+BuilderGroup.prototype.append = function(builders) {
+ builders.forEach(function(builderAndFlags) {
+ var builder = builderAndFlags[0];
+ var flags = builderAndFlags[1];
+ this.setbuilder(builder, flags);
+ }, this);
+ this.groups += 1;
+};
+
+BuilderGroup.prototype.loaded = function() {
+ return this.groups >= this.expectedGroups;
+}
+
+BuilderGroup.prototype.setup = function()
+{
+ // FIXME: instead of copying these to globals, it would be better if
+ // the rest of the code read things from the BuilderGroup instance directly
+ g_defaultBuilderName = this.defaultBuilder;
+ g_builders = this.builders;
+};
+
+BuilderGroup.TOT_WEBKIT = true;
+BuilderGroup.DEPS_WEBKIT = false;
+BuilderGroup.DEFAULT_BUILDER = 1 << 1;
+
+var BUILDER_TO_MASTER = {};
+function associateBuildersWithMaster(builders, master)
+{
+ builders.forEach(function(builderAndFlags) {
+ var builder = builderAndFlags[0];
+ BUILDER_TO_MASTER[builder] = master;
+ });
+}
+
+function requestBuilderList(builderGroups, builderFilter, master, groupName, builderGroup)
+{
+ if (!builderGroups[groupName])
+ builderGroups[groupName] = builderGroup;
+ loader.request(master.builderJsonPath(),
+ partial(onBuilderListLoad, builderGroups, builderFilter, master, groupName),
+ partial(onErrorLoadingBuilderList, master.builderJsonPath(), builderGroups, groupName));
+ builderGroups[groupName].expectedGroups += 1;
+}
+
+function isChromiumDepsGpuTestRunner(builder)
+{
+ return true;
+}
+
+function isChromiumDepsFyiGpuTestRunner(builder)
+{
+ // FIXME: This is kind of wonky, but there's not really a better pattern.
+ return builder.indexOf('(') != -1;
+}
+
+function isChromiumTipOfTreeGpuTestRunner(builder)
+{
+ return builder.indexOf('GPU') != -1;
+}
+
+function isWebkitTestRunner(builder)
+{
+ if (builder.indexOf('EFL') != -1)
+ return builder.indexOf('Build') == -1;
+ if (builder.indexOf('Tests') != -1) {
+ // Apple Windows bots still run old-run-webkit-tests, so they don't upload data.
+ return builder.indexOf('Win') == -1 || (builder.indexOf('Qt') != -1 && builder.indexOf('Chromium') != -1);
+ }
+ return builder.indexOf('GTK') != -1 || builder == 'Qt Linux Release';
+}
+
+function isChromiumContentShellTestRunner(builder)
+{
+ return builder.indexOf('(Content Shell)') != -1;
+}
+
+function isChromiumWebkitTipOfTreeTestRunner(builder)
+{
+ // FIXME: Remove the Android check once the android tests bot is actually uploading results.
+ return builder.indexOf('WebKit') != -1 && builder.indexOf('Builder') == -1 && builder.indexOf('(deps)') == -1 &&
+ builder.indexOf('ASAN') == -1 && !isChromiumContentShellTestRunner(builder) && builder.indexOf('Android') == -1;
+}
+
+function isChromiumWebkitDepsTestRunner(builder)
+{
+ return builder.indexOf('WebKit') != -1 && builder.indexOf('Builder') == -1 && builder.indexOf('(deps)') != -1;
+}
+
+function isChromiumDepsGTestRunner(builder)
+{
+ return builder.indexOf('Builder') == -1;
+}
+
+function isChromiumDepsCrosGTestRunner(builder)
+{
+ return builder.indexOf('Tests') != -1;
+}
+
+function isChromiumTipOfTreeGTestRunner(builder)
+{
+ return !isChromiumTipOfTreeGpuTestRunner(builder) && builder.indexOf('Builder') == -1 && builder.indexOf('Perf') == -1 &&
+ builder.indexOf('WebKit') == -1 && builder.indexOf('Valgrind') == -1 && builder.indexOf('Chrome Frame') == -1;
+}
+
+function isChromiumDepsAVTestRunner(builder)
+{
+ return builder.indexOf('Builder') == -1;
+}
+
+function generateBuildersFromBuilderList(builderList, filter)
+{
+ return builderList.filter(filter).map(function(tester, index) {
+ var builder = [tester];
+ if (!index)
+ builder.push(BuilderGroup.DEFAULT_BUILDER);
+ return builder;
+ });
+}
+
+function onBuilderListLoad(builderGroups, builderFilter, master, groupName, xhr)
+{
+ var builders = generateBuildersFromBuilderList(Object.keys(JSON.parse(xhr.responseText)), builderFilter);
+ associateBuildersWithMaster(builders, master);
+ builderGroups[groupName].append(builders);
+ if (builderGroups[groupName].loaded())
+ g_resourceLoader.buildersListLoaded();
+}
+
+function onErrorLoadingBuilderList(url, builderGroups, groupName, xhr)
+{
+ builderGroups[groupName].groups += 1;
+ console.log('Could not load list of builders from ' + url + '. Try reloading.');
+}
+
+function loadBuildersList(groupName, testType) {
+ switch (testType) {
+ case 'gl_tests':
+ case 'gpu_tests':
+ switch(groupName) {
+ case '@DEPS - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumDepsGpuTestRunner, CHROMIUM_GPU_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@DEPS FYI - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumDepsFyiGpuTestRunner, CHROMIUM_GPU_FYI_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@ToT - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(CHROMIUM_GPU_TESTS_BUILDER_GROUPS, isChromiumTipOfTreeGpuTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+ }
+ break;
+
+ case 'layout-tests':
+ switch(groupName) {
+ case 'Content Shell @ToT - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumContentShellTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@ToT - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@ToT - webkit.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isWebkitTestRunner, WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@DEPS - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitDepsTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ requestBuilderList(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumDepsAVTestRunner, CHROMIUM_PERF_AV_BUILDER_MASTER, groupName, builderGroup);
+ break;
+ }
+ break;
+
+ case 'test_shell_tests':
+ case 'webkit_unit_tests':
+ switch(groupName) {
+ case '@ToT - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@DEPS - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumWebkitDepsTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ requestBuilderList(TEST_SHELL_TESTS_BUILDER_GROUPS, isChromiumDepsAVTestRunner, CHROMIUM_PERF_AV_BUILDER_MASTER, groupName, builderGroup);
+ break;
+ }
+ break;
+
+ default:
+ switch(groupName) {
+ case '@DEPS - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_WIN_BUILDER_MASTER, groupName, builderGroup);
+ requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_MAC_BUILDER_MASTER, groupName, builderGroup);
+ requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsGTestRunner, CHROMIUM_LINUX_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@DEPS CrOS - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.DEPS_WEBKIT);
+ requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumDepsCrosGTestRunner, CHROMIUMOS_BUILDER_MASTER, groupName, builderGroup);
+ break;
+
+ case '@ToT - chromium.org':
+ var builderGroup = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+ requestBuilderList(CHROMIUM_GTESTS_BUILDER_GROUPS, isChromiumTipOfTreeGTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, builderGroup);
+ break;
+ }
+ break;
+ }
+}
+
+var TEST_SHELL_TESTS_BUILDER_GROUPS = {
+ '@ToT - chromium.org': null,
+ '@DEPS - chromium.org': null,
+};
+
+var LAYOUT_TESTS_BUILDER_GROUPS = {
+ '@ToT - chromium.org': null,
+ '@ToT - webkit.org': null,
+ '@DEPS - chromium.org': null,
+ 'Content Shell @ToT - chromium.org': null,
+};
+
+var CHROMIUM_GPU_TESTS_BUILDER_GROUPS = {
+ '@DEPS - chromium.org': null,
+ '@DEPS FYI - chromium.org': null,
+ '@ToT - chromium.org': null,
+};
+
+var CHROMIUM_GTESTS_BUILDER_GROUPS = {
+ '@DEPS - chromium.org': null,
+ '@DEPS CrOS - chromium.org': null,
+ '@ToT - chromium.org': null,
+};
diff --git a/Tools/TestResultServer/static-dashboards/dashboard_base.js b/Tools/TestResultServer/static-dashboards/dashboard_base.js
new file mode 100644
index 0000000..6ec0c8e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/dashboard_base.js
@@ -0,0 +1,915 @@
+// Copyright (C) 2012 Google Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+// @fileoverview Base JS file for pages that want to parse the results JSON
+// from the testing bots. This deals with generic utility functions, visible
+// history, popups and appending the script elements for the JSON files.
+//
+// The calling page is expected to implement the following "abstract"
+// functions/objects:
+var g_pageLoadStartTime = Date.now();
+var g_resourceLoader;
+
+// Generates the contents of the dashboard. The page should override this with
+// a function that generates the page assuming all resources have loaded.
+function generatePage() {}
+
+// Takes a key and a value and sets the g_currentState[key] = value iff key is
+// a valid hash parameter and the value is a valid value for that key.
+//
+// @return {boolean} Whether the key what inserted into the g_currentState.
+function handleValidHashParameter(key, value)
+{
+ return false;
+}
+
+// Default hash parameters for the page. The page should override this to create
+// default states.
+var g_defaultDashboardSpecificStateValues = {};
+
+
+// The page should override this to modify page state due to
+// changing query parameters.
+// @param {Object} params New or modified query params as key: value.
+// @return {boolean} Whether changing this parameter should cause generatePage to be called.
+function handleQueryParameterChange(params)
+{
+ return true;
+}
+
+//////////////////////////////////////////////////////////////////////////////
+// CONSTANTS
+//////////////////////////////////////////////////////////////////////////////
+var GTEST_EXPECTATIONS_MAP_ = {
+ 'P': 'PASS',
+ 'F': 'FAIL',
+ 'N': 'NO DATA',
+ 'X': 'SKIPPED'
+};
+
+var LAYOUT_TEST_EXPECTATIONS_MAP_ = {
+ 'P': 'PASS',
+ 'N': 'NO DATA',
+ 'X': 'SKIP',
+ 'T': 'TIMEOUT',
+ 'F': 'TEXT',
+ 'C': 'CRASH',
+ 'I': 'IMAGE',
+ 'Z': 'IMAGE+TEXT',
+ // We used to glob a bunch of expectations into "O" as OTHER. Expectations
+ // are more precise now though and it just means MISSING.
+ 'O': 'MISSING'
+};
+
+var FAILURE_EXPECTATIONS_ = {
+ 'T': 1,
+ 'F': 1,
+ 'C': 1,
+ 'I': 1,
+ 'Z': 1
+};
+
+// Keys in the JSON files.
+var WONTFIX_COUNTS_KEY = 'wontfixCounts';
+var FIXABLE_COUNTS_KEY = 'fixableCounts';
+var DEFERRED_COUNTS_KEY = 'deferredCounts';
+var WONTFIX_DESCRIPTION = 'Tests never to be fixed (WONTFIX)';
+var FIXABLE_DESCRIPTION = 'All tests for this release';
+var DEFERRED_DESCRIPTION = 'All deferred tests (DEFER)';
+var FIXABLE_COUNT_KEY = 'fixableCount';
+var ALL_FIXABLE_COUNT_KEY = 'allFixableCount';
+var CHROME_REVISIONS_KEY = 'chromeRevision';
+var WEBKIT_REVISIONS_KEY = 'webkitRevision';
+var TIMESTAMPS_KEY = 'secondsSinceEpoch';
+var BUILD_NUMBERS_KEY = 'buildNumbers';
+var TESTS_KEY = 'tests';
+var ONE_DAY_SECONDS = 60 * 60 * 24;
+var ONE_WEEK_SECONDS = ONE_DAY_SECONDS * 7;
+
+// These should match the testtype uploaded to test-results.appspot.com.
+// See http://test-results.appspot.com/testfile.
+var TEST_TYPES = [
+ 'base_unittests',
+ 'browser_tests',
+ 'cacheinvalidation_unittests',
+ 'compositor_unittests',
+ 'content_browsertests',
+ 'content_unittests',
+ 'courgette_unittests',
+ 'crypto_unittests',
+ 'googleurl_unittests',
+ 'gfx_unittests',
+ 'gl_tests',
+ 'gpu_tests',
+ 'gpu_unittests',
+ 'installer_util_unittests',
+ 'interactive_ui_tests',
+ 'ipc_tests',
+ 'jingle_unittests',
+ 'layout-tests',
+ 'media_unittests',
+ 'mini_installer_test',
+ 'net_unittests',
+ 'printing_unittests',
+ 'remoting_unittests',
+ 'safe_browsing_tests',
+ 'sql_unittests',
+ 'sync_unit_tests',
+ 'sync_integration_tests',
+ 'test_shell_tests',
+ 'ui_tests',
+ 'unit_tests',
+ 'views_unittests',
+ 'webkit_unit_tests',
+];
+
+var RELOAD_REQUIRING_PARAMETERS = ['showAllRuns', 'group', 'testType'];
+
+// Enum for indexing into the run-length encoded results in the JSON files.
+// 0 is where the count is length is stored. 1 is the value.
+var RLE = {
+ LENGTH: 0,
+ VALUE: 1
+}
+
+function isFailingResult(value)
+{
+ return 'FSTOCIZ'.indexOf(value) != -1;
+}
+
+// Takes a key and a value and sets the g_currentState[key] = value iff key is
+// a valid hash parameter and the value is a valid value for that key. Handles
+// cross-dashboard parameters then falls back to calling
+// handleValidHashParameter for dashboard-specific parameters.
+//
+// @return {boolean} Whether the key what inserted into the g_currentState.
+function handleValidHashParameterWrapper(key, value)
+{
+ switch(key) {
+ case 'testType':
+ validateParameter(g_crossDashboardState, key, value,
+ function() { return TEST_TYPES.indexOf(value) != -1; });
+ return true;
+
+ case 'group':
+ validateParameter(g_crossDashboardState, key, value,
+ function() {
+ return value in LAYOUT_TESTS_BUILDER_GROUPS ||
+ value in CHROMIUM_GPU_TESTS_BUILDER_GROUPS ||
+ value in CHROMIUM_GTESTS_BUILDER_GROUPS;
+ });
+ return true;
+
+ // FIXME: This should probably be stored on g_crossDashboardState like everything else in this function.
+ case 'builder':
+ validateParameter(g_currentState, key, value,
+ function() { return value in g_builders; });
+ return true;
+
+ case 'useTestData':
+ case 'showAllRuns':
+ g_crossDashboardState[key] = value == 'true';
+ return true;
+
+ case 'buildDir':
+ if (value === 'Debug' || value == 'Release') {
+ g_crossDashboardState['testType'] = 'layout-tests';
+ g_crossDashboardState[key] = value;
+ return true;
+ } else
+ return false;
+
+ default:
+ return handleValidHashParameter(key, value);
+ }
+}
+
+var g_defaultCrossDashboardStateValues = {
+ group: '@ToT - chromium.org',
+ showAllRuns: false,
+ testType: 'layout-tests',
+ buildDir: '',
+ useTestData: false,
+}
+
+// Generic utility functions.
+function $(id)
+{
+ return document.getElementById(id);
+}
+
+function stringContains(a, b)
+{
+ return a.indexOf(b) != -1;
+}
+
+function caseInsensitiveContains(a, b)
+{
+ return a.match(new RegExp(b, 'i'));
+}
+
+function startsWith(a, b)
+{
+ return a.indexOf(b) == 0;
+}
+
+function endsWith(a, b)
+{
+ return a.lastIndexOf(b) == a.length - b.length;
+}
+
+function isValidName(str)
+{
+ return str.match(/[A-Za-z0-9\-\_,]/);
+}
+
+function trimString(str)
+{
+ return str.replace(/^\s+|\s+$/g, '');
+}
+
+function collapseWhitespace(str)
+{
+ return str.replace(/\s+/g, ' ');
+}
+
+function validateParameter(state, key, value, validateFn)
+{
+ if (validateFn())
+ state[key] = value;
+ else
+ console.log(key + ' value is not valid: ' + value);
+}
+
+function queryHashAsMap()
+{
+ var hash = window.location.hash;
+ var paramsList = hash ? hash.substring(1).split('&') : [];
+ var paramsMap = {};
+ var invalidKeys = [];
+ for (var i = 0; i < paramsList.length; i++) {
+ var thisParam = paramsList[i].split('=');
+ if (thisParam.length != 2) {
+ console.log('Invalid query parameter: ' + paramsList[i]);
+ continue;
+ }
+
+ paramsMap[thisParam[0]] = decodeURIComponent(thisParam[1]);
+ }
+
+ // FIXME: remove support for mapping from the master parameter to the group
+ // one once the waterfall starts to pass in the builder name instead.
+ if (paramsMap.master) {
+ paramsMap.group = LEGACY_BUILDER_MASTERS_TO_GROUPS[paramsMap.master];
+ if (!paramsMap.group)
+ console.log('ERROR: Unknown master name: ' + paramsMap.master);
+ window.location.hash = window.location.hash.replace('master=' + paramsMap.master, 'group=' + paramsMap.group);
+ delete paramsMap.master;
+ }
+
+ return paramsMap;
+}
+
+function parseParameter(parameters, key)
+{
+ if (!(key in parameters))
+ return;
+ var value = parameters[key];
+ if (!handleValidHashParameterWrapper(key, value))
+ console.log("Invalid query parameter: " + key + '=' + value);
+}
+
+function parseCrossDashboardParameters()
+{
+ g_crossDashboardState = {};
+ var parameters = queryHashAsMap();
+ for (parameterName in g_defaultCrossDashboardStateValues)
+ parseParameter(parameters, parameterName);
+
+ fillMissingValues(g_crossDashboardState, g_defaultCrossDashboardStateValues);
+ if (currentBuilderGroup() === undefined)
+ g_crossDashboardState.group = g_defaultCrossDashboardStateValues.group;
+}
+
+function parseDashboardSpecificParameters()
+{
+ g_currentState = {};
+ var parameters = queryHashAsMap();
+ for (parameterName in g_defaultDashboardSpecificStateValues)
+ parseParameter(parameters, parameterName);
+}
+
+function parseParameters()
+{
+ var oldCrossDashboardState = g_crossDashboardState;
+ var oldDashboardSpecificState = g_currentState;
+
+ parseCrossDashboardParameters();
+ parseDashboardSpecificParameters();
+ parseParameter(queryHashAsMap(), 'builder');
+
+ var crossDashboardDiffState = diffStates(oldCrossDashboardState, g_crossDashboardState);
+ var dashboardSpecificDiffState = diffStates(oldDashboardSpecificState, g_currentState);
+
+ fillMissingValues(g_currentState, g_defaultDashboardSpecificStateValues);
+ if (!g_crossDashboardState.useTestData)
+ fillMissingValues(g_currentState, {'builder': g_defaultBuilderName});
+
+ // FIXME: dashboard_base shouldn't know anything about specific dashboard specific keys.
+ if (dashboardSpecificDiffState.builder)
+ delete g_currentState.tests;
+ if (g_currentState.tests)
+ delete g_currentState.builder;
+
+ // Some parameters require loading different JSON files when the value changes. Do a reload.
+ if (Object.keys(oldCrossDashboardState).length) {
+ for (var key in g_crossDashboardState) {
+ if (oldCrossDashboardState[key] != g_crossDashboardState[key] && RELOAD_REQUIRING_PARAMETERS.indexOf(key) != -1)
+ window.location.reload();
+ }
+ }
+
+ return dashboardSpecificDiffState;
+}
+
+function diffStates(oldState, newState)
+{
+ // If there is no old state, everything in the current state is new.
+ if (!oldState)
+ return newState;
+
+ var changedParams = {};
+ for (curKey in newState) {
+ var oldVal = oldState[curKey];
+ var newVal = newState[curKey];
+ // Add new keys or changed values.
+ if (!oldVal || oldVal != newVal)
+ changedParams[curKey] = newVal;
+ }
+ return changedParams;
+}
+
+function defaultValue(key)
+{
+ if (key in g_defaultDashboardSpecificStateValues)
+ return g_defaultDashboardSpecificStateValues[key];
+ return g_defaultCrossDashboardStateValues[key];
+}
+
+function fillMissingValues(to, from)
+{
+ for (var state in from) {
+ if (!(state in to))
+ to[state] = from[state];
+ }
+}
+
+// FIXME: Rename this to g_dashboardSpecificState;
+var g_currentState = {};
+var g_crossDashboardState = {};
+parseCrossDashboardParameters();
+
+function isLayoutTestResults()
+{
+ return g_crossDashboardState.testType == 'layout-tests';
+}
+
+function isGPUTestResults()
+{
+ return g_crossDashboardState.testType == 'gpu_tests';
+}
+
+function currentBuilderGroupCategory()
+{
+ switch (g_crossDashboardState.testType) {
+ case 'gl_tests':
+ case 'gpu_tests':
+ return CHROMIUM_GPU_TESTS_BUILDER_GROUPS;
+ case 'layout-tests':
+ return LAYOUT_TESTS_BUILDER_GROUPS;
+ case 'test_shell_tests':
+ case 'webkit_unit_tests':
+ return TEST_SHELL_TESTS_BUILDER_GROUPS;
+ default:
+ return CHROMIUM_GTESTS_BUILDER_GROUPS;
+ }
+}
+
+function currentBuilderGroup()
+{
+ return currentBuilderGroupCategory()[g_crossDashboardState.group]
+}
+
+function builderMaster(builderName)
+{
+ return BUILDER_TO_MASTER[builderName];
+}
+
+function isTipOfTreeWebKitBuilder()
+{
+ return currentBuilderGroup().isToTWebKit;
+}
+
+var g_defaultBuilderName, g_builders;
+function initBuilders()
+{
+ if (g_crossDashboardState.buildDir) {
+ // If buildDir is set, point to the results.json in the local tree. Useful for debugging changes to the python JSON generator.
+ g_defaultBuilderName = 'DUMMY_BUILDER_NAME';
+ g_builders = {'DUMMY_BUILDER_NAME': ''};
+ var loc = document.location.toString();
+ var offset = loc.indexOf('webkit/');
+ } else
+ currentBuilderGroup().setup();
+}
+
+var g_resultsByBuilder = {};
+var g_expectationsByPlatform = {};
+var g_staleBuilders = [];
+var g_buildersThatFailedToLoad = [];
+
+// TODO(aboxhall): figure out whether this is a performance bottleneck and
+// change calling code to understand the trie structure instead if necessary.
+function flattenTrie(trie, prefix)
+{
+ var result = {};
+ for (var name in trie) {
+ var fullName = prefix ? prefix + "/" + name : name;
+ var data = trie[name];
+ if ("results" in data)
+ result[fullName] = data;
+ else {
+ var partialResult = flattenTrie(data, fullName);
+ for (var key in partialResult) {
+ result[key] = partialResult[key];
+ }
+ }
+ }
+ return result;
+}
+
+function isTreeMap()
+{
+ return endsWith(window.location.pathname, 'treemap.html');
+}
+
+function isFlakinessDashboard()
+{
+ return endsWith(window.location.pathname, 'flakiness_dashboard.html');
+}
+
+var g_hasDoneInitialPageGeneration = false;
+// String of error messages to display to the user.
+var g_errorMessages = '';
+
+// Record a new error message.
+// @param {string} errorMsg The message to show to the user.
+function addError(errorMsg)
+{
+ g_errorMessages += errorMsg + '<br>';
+}
+
+// Clear out error and warning messages.
+function clearErrors()
+{
+ g_errorMessages = '';
+}
+
+// If there are errors, show big and red UI for errors so as to be noticed.
+function showErrors()
+{
+ var errors = $('errors');
+
+ if (!g_errorMessages) {
+ if (errors)
+ errors.parentNode.removeChild(errors);
+ return;
+ }
+
+ if (!errors) {
+ errors = document.createElement('H2');
+ errors.style.color = 'red';
+ errors.id = 'errors';
+ document.body.appendChild(errors);
+ }
+
+ errors.innerHTML = g_errorMessages;
+}
+
+function addBuilderLoadErrors()
+{
+ if (g_hasDoneInitialPageGeneration)
+ return;
+
+ if (g_buildersThatFailedToLoad.length)
+ addError('ERROR: Failed to get data from ' + g_buildersThatFailedToLoad.toString() + '.');
+
+ if (g_staleBuilders.length)
+ addError('ERROR: Data from ' + g_staleBuilders.toString() + ' is more than 1 day stale.');
+}
+
+function resourceLoadingComplete()
+{
+ g_resourceLoader = null;
+ handleLocationChange();
+}
+
+function handleLocationChange()
+{
+ if (g_resourceLoader)
+ return;
+
+ addBuilderLoadErrors();
+ g_hasDoneInitialPageGeneration = true;
+
+ var params = parseParameters();
+ var shouldGeneratePage = true;
+ if (Object.keys(params).length)
+ shouldGeneratePage = handleQueryParameterChange(params);
+
+ var newHash = permaLinkURLHash();
+ var winHash = window.location.hash || "#";
+ // Make sure the location is the same as the state we are using internally.
+ // These get out of sync if processQueryParamChange changed state.
+ if (newHash != winHash) {
+ // This will cause another hashchange, and when we loop
+ // back through here next time, we'll go through generatePage.
+ window.location.hash = newHash;
+ } else if (shouldGeneratePage)
+ generatePage();
+}
+
+window.onhashchange = handleLocationChange;
+
+function combinedDashboardState()
+{
+ var combinedState = Object.create(g_currentState);
+ for (var key in g_crossDashboardState)
+ combinedState[key] = g_crossDashboardState[key];
+ return combinedState;
+}
+
+// Sets the page state. Takes varargs of key, value pairs.
+function setQueryParameter(var_args)
+{
+ var state = combinedDashboardState();
+ for (var i = 0; i < arguments.length; i += 2) {
+ var key = arguments[i];
+ state[key] = arguments[i + 1];
+ }
+ // Note: We use window.location.hash rather that window.location.replace
+ // because of bugs in Chrome where extra entries were getting created
+ // when back button was pressed and full page navigation was occuring.
+ // FIXME: file those bugs.
+ window.location.hash = permaLinkURLHash(state);
+}
+
+function permaLinkURLHash(opt_state)
+{
+ var state = opt_state || combinedDashboardState();
+ return '#' + joinParameters(state);
+}
+
+function joinParameters(stateObject)
+{
+ var state = [];
+ for (var key in stateObject) {
+ var value = stateObject[key];
+ if (value != defaultValue(key))
+ state.push(key + '=' + encodeURIComponent(value));
+ }
+ return state.join('&');
+}
+
+function logTime(msg, startTime)
+{
+ console.log(msg + ': ' + (Date.now() - startTime));
+}
+
+function hidePopup()
+{
+ var popup = $('popup');
+ if (popup)
+ popup.parentNode.removeChild(popup);
+}
+
+function showPopup(target, html)
+{
+ var popup = $('popup');
+ if (!popup) {
+ popup = document.createElement('div');
+ popup.id = 'popup';
+ document.body.appendChild(popup);
+ }
+
+ // Set html first so that we can get accurate size metrics on the popup.
+ popup.innerHTML = html;
+
+ var targetRect = target.getBoundingClientRect();
+
+ var x = Math.min(targetRect.left - 10, document.documentElement.clientWidth - popup.offsetWidth);
+ x = Math.max(0, x);
+ popup.style.left = x + document.body.scrollLeft + 'px';
+
+ var y = targetRect.top + targetRect.height;
+ if (y + popup.offsetHeight > document.documentElement.clientHeight)
+ y = targetRect.top - popup.offsetHeight;
+ y = Math.max(0, y);
+ popup.style.top = y + document.body.scrollTop + 'px';
+}
+
+// Create a new function with some of its arguements
+// pre-filled.
+// Taken from goog.partial in the Closure library.
+// @param {Function} fn A function to partially apply.
+// @param {...*} var_args Additional arguments that are partially
+// applied to fn.
+// @return {!Function} A partially-applied form of the function bind() was
+// invoked as a method of.
+function partial(fn, var_args)
+{
+ var args = Array.prototype.slice.call(arguments, 1);
+ return function() {
+ // Prepend the bound arguments to the current arguments.
+ var newArgs = Array.prototype.slice.call(arguments);
+ newArgs.unshift.apply(newArgs, args);
+ return fn.apply(this, newArgs);
+ };
+};
+
+// Returns the appropriate expectatiosn map for the current testType.
+function expectationsMap()
+{
+ return isLayoutTestResults() ? LAYOUT_TEST_EXPECTATIONS_MAP_ : GTEST_EXPECTATIONS_MAP_;
+}
+
+function toggleQueryParameter(param)
+{
+ setQueryParameter(param, !queryParameterValue(param));
+}
+
+function queryParameterValue(parameter)
+{
+ return g_currentState[parameter] || g_crossDashboardState[parameter];
+}
+
+function checkboxHTML(queryParameter, label, isChecked, opt_extraJavaScript)
+{
+ var js = opt_extraJavaScript || '';
+ return '<label style="padding-left: 2em">' +
+ '<input type="checkbox" onchange="toggleQueryParameter(\'' + queryParameter + '\');' + js + '" ' +
+ (isChecked ? 'checked' : '') + '>' + label +
+ '</label> ';
+}
+
+function selectHTML(label, queryParameter, options)
+{
+ var html = '<label style="padding-left: 2em">' + label + ': ' +
+ '<select onchange="setQueryParameter(\'' + queryParameter + '\', this[this.selectedIndex].value)">';
+
+ for (var i = 0; i < options.length; i++) {
+ var value = options[i];
+ html += '<option value="' + value + '" ' +
+ (queryParameterValue(queryParameter) == value ? 'selected' : '') +
+ '>' + value + '</option>'
+ }
+ html += '</select></label> ';
+ return html;
+}
+
+// Returns the HTML for the select element to switch to different testTypes.
+function htmlForTestTypeSwitcher(opt_noBuilderMenu, opt_extraHtml, opt_includeNoneBuilder)
+{
+ var html = '<div style="border-bottom:1px dashed">';
+ html += '' +
+ htmlForDashboardLink('Stats', 'aggregate_results.html') +
+ htmlForDashboardLink('Timeline', 'timeline_explorer.html') +
+ htmlForDashboardLink('Results', 'flakiness_dashboard.html') +
+ htmlForDashboardLink('Treemap', 'treemap.html');
+
+ html += selectHTML('Test type', 'testType', TEST_TYPES);
+
+ if (!opt_noBuilderMenu) {
+ var buildersForMenu = Object.keys(g_builders);
+ if (opt_includeNoneBuilder)
+ buildersForMenu.unshift('--------------');
+ html += selectHTML('Builder', 'builder', buildersForMenu);
+ }
+
+ html += selectHTML('Group', 'group',
+ Object.keys(currentBuilderGroupCategory()));
+
+ if (!isTreeMap())
+ html += checkboxHTML('showAllRuns', 'Show all runs', g_crossDashboardState.showAllRuns);
+
+ if (opt_extraHtml)
+ html += opt_extraHtml;
+ return html + '</div>';
+}
+
+function selectBuilder(builder)
+{
+ setQueryParameter('builder', builder);
+}
+
+function loadDashboard(fileName)
+{
+ var pathName = window.location.pathname;
+ pathName = pathName.substring(0, pathName.lastIndexOf('/') + 1);
+ window.location = pathName + fileName + window.location.hash;
+}
+
+function htmlForTopLink(html, onClick, isSelected)
+{
+ var cssText = isSelected ? 'font-weight: bold;' : 'color:blue;text-decoration:underline;cursor:pointer;';
+ cssText += 'margin: 0 5px;';
+ return '<span style="' + cssText + '" onclick="' + onClick + '">' + html + '</span>';
+}
+
+function htmlForDashboardLink(html, fileName)
+{
+ var pathName = window.location.pathname;
+ var currentFileName = pathName.substring(pathName.lastIndexOf('/') + 1);
+ var isSelected = currentFileName == fileName;
+ var onClick = 'loadDashboard(\'' + fileName + '\')';
+ return htmlForTopLink(html, onClick, isSelected);
+}
+
+function revisionLink(results, index, key, singleUrlTemplate, rangeUrlTemplate)
+{
+ var currentRevision = parseInt(results[key][index], 10);
+ var previousRevision = parseInt(results[key][index + 1], 10);
+
+ function singleUrl()
+ {
+ return singleUrlTemplate.replace('<rev>', currentRevision);
+ }
+
+ function rangeUrl()
+ {
+ return rangeUrlTemplate.replace('<rev1>', currentRevision).replace('<rev2>', previousRevision + 1);
+ }
+
+ if (currentRevision == previousRevision)
+ return 'At <a href="' + singleUrl() + '">r' + currentRevision + '</a>';
+ else if (currentRevision - previousRevision == 1)
+ return '<a href="' + singleUrl() + '">r' + currentRevision + '</a>';
+ else
+ return '<a href="' + rangeUrl() + '">r' + (previousRevision + 1) + ' to r' + currentRevision + '</a>';
+}
+
+function chromiumRevisionLink(results, index)
+{
+ return revisionLink(
+ results,
+ index,
+ CHROME_REVISIONS_KEY,
+ 'http://src.chromium.org/viewvc/chrome?view=rev&revision=<rev>',
+ 'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&range=<rev2>:<rev1>&mode=html');
+}
+
+function webKitRevisionLink(results, index)
+{
+ return revisionLink(
+ results,
+ index,
+ WEBKIT_REVISIONS_KEY,
+ 'http://trac.webkit.org/changeset/<rev>',
+ 'http://trac.webkit.org/log/trunk/?rev=<rev1>&stop_rev=<rev2>&limit=100&verbose=on');
+}
+
+// "Decompresses" the RLE-encoding of test results so that we can query it
+// by build index and test name.
+//
+// @param {Object} results results for the current builder
+// @return Object with these properties:
+// - testNames: array mapping test index to test names.
+// - resultsByBuild: array of builds, for each build a (sparse) array of test results by test index.
+// - flakyTests: array with the boolean value true at test indices that are considered flaky (more than one single-build failure).
+// - flakyDeltasByBuild: array of builds, for each build a count of flaky test results by expectation, as well as a total.
+function decompressResults(builderResults)
+{
+ var builderTestResults = builderResults[TESTS_KEY];
+ var buildCount = builderResults[FIXABLE_COUNTS_KEY].length;
+ var resultsByBuild = new Array(buildCount);
+ var flakyDeltasByBuild = new Array(buildCount);
+
+ // Pre-sizing the test result arrays for each build saves us ~250ms
+ var testCount = 0;
+ for (var testName in builderTestResults)
+ testCount++;
+ for (var i = 0; i < buildCount; i++) {
+ resultsByBuild[i] = new Array(testCount);
+ resultsByBuild[i][testCount - 1] = undefined;
+ flakyDeltasByBuild[i] = {};
+ }
+
+ // Using indices instead of the full test names for each build saves us
+ // ~1500ms
+ var testIndex = 0;
+ var testNames = new Array(testCount);
+ var flakyTests = new Array(testCount);
+
+ // Decompress and "invert" test results (by build instead of by test) and
+ // determine which are flaky.
+ for (var testName in builderTestResults) {
+ var oneBuildFailureCount = 0;
+
+ testNames[testIndex] = testName;
+ var testResults = builderTestResults[testName].results;
+ for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
+ var count = rleResult[RLE.LENGTH];
+ var value = rleResult[RLE.VALUE];
+
+ if (count == 1 && value in FAILURE_EXPECTATIONS_)
+ oneBuildFailureCount++;
+
+ for (var j = 0; j < count; j++) {
+ resultsByBuild[currentBuildIndex++][testIndex] = value;
+ if (currentBuildIndex == buildCount)
+ break;
+ }
+ }
+
+ if (oneBuildFailureCount > 2)
+ flakyTests[testIndex] = true;
+
+ testIndex++;
+ }
+
+ // Now that we know which tests are flaky, count the test results that are
+ // from flaky tests for each build.
+ testIndex = 0;
+ for (var testName in builderTestResults) {
+ if (!flakyTests[testIndex++])
+ continue;
+
+ var testResults = builderTestResults[testName].results;
+ for (var i = 0, rleResult, currentBuildIndex = 0; (rleResult = testResults[i]) && currentBuildIndex < buildCount; i++) {
+ var count = rleResult[RLE.LENGTH];
+ var value = rleResult[RLE.VALUE];
+
+ for (var j = 0; j < count; j++) {
+ var buildTestResults = flakyDeltasByBuild[currentBuildIndex++];
+ function addFlakyDelta(key)
+ {
+ if (!(key in buildTestResults))
+ buildTestResults[key] = 0;
+ buildTestResults[key]++;
+ }
+ addFlakyDelta(value);
+ if (value != 'P' && value != 'N')
+ addFlakyDelta('total');
+ if (currentBuildIndex == buildCount)
+ break;
+ }
+ }
+ }
+
+ return {
+ testNames: testNames,
+ resultsByBuild: resultsByBuild,
+ flakyTests: flakyTests,
+ flakyDeltasByBuild: flakyDeltasByBuild
+ };
+}
+
+document.addEventListener('mousedown', function(e) {
+ // Clear the open popup, unless the click was inside the popup.
+ var popup = $('popup');
+ if (popup && e.target != popup && !(popup.compareDocumentPosition(e.target) & 16))
+ hidePopup();
+}, false);
+
+window.addEventListener('load', function() {
+ // This doesn't seem totally accurate as there is a race between
+ // onload firing and the last script tag being executed.
+ logTime('Time to load JS', g_pageLoadStartTime);
+ g_resourceLoader = new loader.Loader();
+ g_resourceLoader.load();
+}, false);
diff --git a/Tools/TestResultServer/static-dashboards/dygraph-combined.js b/Tools/TestResultServer/static-dashboards/dygraph-combined.js
new file mode 100644
index 0000000..6a88059
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/dygraph-combined.js
@@ -0,0 +1 @@
+DygraphLayout=function(b,a){this.dygraph_=b;this.options={};Dygraph.update(this.options,a?a:{});this.datasets=new Array();this.annotations=new Array()};DygraphLayout.prototype.attr_=function(a){return this.dygraph_.attr_(a)};DygraphLayout.prototype.addDataset=function(a,b){this.datasets[a]=b};DygraphLayout.prototype.setAnnotations=function(d){var e=this.attr_("xValueParser");for(var c=0;c<d.length;c++){var b={};if(!d[c].xval&&!d[c].x){this.dygraph_.error("Annotations must have an 'x' property");return}if(d[c].icon&&!(d[c].hasOwnProperty("width")&&d[c].hasOwnProperty("height"))){this.dygraph_.error("Must set width and height when setting annotation.icon property");return}Dygraph.update(b,d[c]);if(!b.xval){b.xval=e(b.x)}this.annotations.push(b)}};DygraphLayout.prototype.evaluate=function(){this._evaluateLimits();this._evaluateLineCharts();this._evaluateLineTicks();this._evaluateAnnotations()};DygraphLayout.prototype._evaluateLimits=function(){this.minxval=this.maxxval=null;if(this.options.dateWindow){this.minxval=this.options.dateWindow[0];this.maxxval=this.options.dateWindow[1]}else{for(var c in this.datasets){if(!this.datasets.hasOwnProperty(c)){continue}var d=this.datasets[c];var b=d[0][0];if(!this.minxval||b<this.minxval){this.minxval=b}var a=d[d.length-1][0];if(!this.maxxval||a>this.maxxval){this.maxxval=a}}}this.xrange=this.maxxval-this.minxval;this.xscale=(this.xrange!=0?1/this.xrange:1);this.minyval=this.options.yAxis[0];this.maxyval=this.options.yAxis[1];this.yrange=this.maxyval-this.minyval;this.yscale=(this.yrange!=0?1/this.yrange:1)};DygraphLayout.prototype._evaluateLineCharts=function(){this.points=new Array();for(var e in this.datasets){if(!this.datasets.hasOwnProperty(e)){continue}var d=this.datasets[e];for(var b=0;b<d.length;b++){var c=d[b];var a={x:((parseFloat(c[0])-this.minxval)*this.xscale),y:1-((parseFloat(c[1])-this.minyval)*this.yscale),xval:parseFloat(c[0]),yval:parseFloat(c[1]),name:e};if(a.y<=0){a.y=0}if(a.y>=1){a.y=1}this.points.push(a)}}};DygraphLayout.prototype._evaluateLineTicks=function(){this.xticks=new Array();for(var c=0;c<this.options.xTicks.length;c++){var b=this.options.xTicks[c];var a=b.label;var d=this.xscale*(b.v-this.minxval);if((d>=0)&&(d<=1)){this.xticks.push([d,a])}}this.yticks=new Array();for(var c=0;c<this.options.yTicks.length;c++){var b=this.options.yTicks[c];var a=b.label;var d=1-(this.yscale*(b.v-this.minyval));if((d>=0)&&(d<=1)){this.yticks.push([d,a])}}};DygraphLayout.prototype.evaluateWithError=function(){this.evaluate();if(!this.options.errorBars){return}var d=0;for(var g in this.datasets){if(!this.datasets.hasOwnProperty(g)){continue}var c=0;var f=this.datasets[g];for(var c=0;c<f.length;c++,d++){var e=f[c];var a=parseFloat(e[0]);var b=parseFloat(e[1]);if(a==this.points[d].xval&&b==this.points[d].yval){this.points[d].errorMinus=parseFloat(e[2]);this.points[d].errorPlus=parseFloat(e[3])}}}};DygraphLayout.prototype._evaluateAnnotations=function(){var f={};for(var d=0;d<this.annotations.length;d++){var b=this.annotations[d];f[b.xval+","+b.series]=b}this.annotated_points=[];for(var d=0;d<this.points.length;d++){var e=this.points[d];var c=e.xval+","+e.name;if(c in f){e.annotation=f[c];this.annotated_points.push(e)}}};DygraphLayout.prototype.removeAllDatasets=function(){delete this.datasets;this.datasets=new Array()};DygraphLayout.prototype.updateOptions=function(a){Dygraph.update(this.options,a?a:{})};DygraphCanvasRenderer=function(c,b,d,a){this.dygraph_=c;this.options={strokeWidth:0.5,drawXAxis:true,drawYAxis:true,axisLineColor:"black",axisLineWidth:0.5,axisTickSize:3,axisLabelColor:"black",axisLabelFont:"Arial",axisLabelFontSize:9,axisLabelWidth:50,drawYGrid:true,drawXGrid:true,gridLineColor:"rgb(128,128,128)",fillAlpha:0.15,underlayCallback:null};Dygraph.update(this.options,a);this.layout=d;this.element=b;this.container=this.element.parentNode;this.height=this.element.height;this.width=this.element.width;if(!this.isIE&&!(DygraphCanvasRenderer.isSupported(this.element))){throw"Canvas is not supported."}this.xlabels=new Array();this.ylabels=new Array();this.annotations=new Array();this.area={x:this.options.yAxisLabelWidth+2*this.options.axisTickSize,y:0};this.area.w=this.width-this.area.x-this.options.rightGap;this.area.h=this.height-this.options.axisLabelFontSize-2*this.options.axisTickSize;this.container.style.position="relative";this.container.style.width=this.width+"px"};DygraphCanvasRenderer.prototype.clear=function(){if(this.isIE){try{if(this.clearDelay){this.clearDelay.cancel();this.clearDelay=null}var b=this.element.getContext("2d")}catch(d){this.clearDelay=MochiKit.Async.wait(this.IEDelay);this.clearDelay.addCallback(bind(this.clear,this));return}}var b=this.element.getContext("2d");b.clearRect(0,0,this.width,this.height);for(var a=0;a<this.xlabels.length;a++){var c=this.xlabels[a];c.parentNode.removeChild(c)}for(var a=0;a<this.ylabels.length;a++){var c=this.ylabels[a];c.parentNode.removeChild(c)}for(var a=0;a<this.annotations.length;a++){var c=this.annotations[a];c.parentNode.removeChild(c)}this.xlabels=new Array();this.ylabels=new Array();this.annotations=new Array()};DygraphCanvasRenderer.isSupported=function(g){var b=null;try{if(typeof(g)=="undefined"||g==null){b=document.createElement("canvas")}else{b=g}var c=b.getContext("2d")}catch(d){var f=navigator.appVersion.match(/MSIE (\d\.\d)/);var a=(navigator.userAgent.toLowerCase().indexOf("opera")!=-1);if((!f)||(f[1]<6)||(a)){return false}return true}return true};DygraphCanvasRenderer.prototype.render=function(){var b=this.element.getContext("2d");if(this.options.underlayCallback){this.options.underlayCallback(b,this.area,this.layout,this.dygraph_)}if(this.options.drawYGrid){var d=this.layout.yticks;b.save();b.strokeStyle=this.options.gridLineColor;b.lineWidth=this.options.axisLineWidth;for(var c=0;c<d.length;c++){var a=this.area.x;var e=this.area.y+d[c][0]*this.area.h;b.beginPath();b.moveTo(a,e);b.lineTo(a+this.area.w,e);b.closePath();b.stroke()}}if(this.options.drawXGrid){var d=this.layout.xticks;b.save();b.strokeStyle=this.options.gridLineColor;b.lineWidth=this.options.axisLineWidth;for(var c=0;c<d.length;c++){var a=this.area.x+d[c][0]*this.area.w;var e=this.area.y+this.area.h;b.beginPath();b.moveTo(a,e);b.lineTo(a,this.area.y);b.closePath();b.stroke()}}this._renderLineChart();this._renderAxis();this._renderAnnotations()};DygraphCanvasRenderer.prototype._renderAxis=function(){if(!this.options.drawXAxis&&!this.options.drawYAxis){return}var b=this.element.getContext("2d");var g={position:"absolute",fontSize:this.options.axisLabelFontSize+"px",zIndex:10,color:this.options.axisLabelColor,width:this.options.axisLabelWidth+"px",overflow:"hidden"};var d=function(o){var q=document.createElement("div");for(var p in g){if(g.hasOwnProperty(p)){q.style[p]=g[p]}}q.appendChild(document.createTextNode(o));return q};b.save();b.strokeStyle=this.options.axisLineColor;b.lineWidth=this.options.axisLineWidth;if(this.options.drawYAxis){if(this.layout.yticks&&this.layout.yticks.length>0){for(var e=0;e<this.layout.yticks.length;e++){var f=this.layout.yticks[e];if(typeof(f)=="function"){return}var l=this.area.x;var j=this.area.y+f[0]*this.area.h;b.beginPath();b.moveTo(l,j);b.lineTo(l-this.options.axisTickSize,j);b.closePath();b.stroke();var k=d(f[1]);var h=(j-this.options.axisLabelFontSize/2);if(h<0){h=0}if(h+this.options.axisLabelFontSize+3>this.height){k.style.bottom="0px"}else{k.style.top=h+"px"}k.style.left="0px";k.style.textAlign="right";k.style.width=this.options.yAxisLabelWidth+"px";this.container.appendChild(k);this.ylabels.push(k)}var m=this.ylabels[0];var n=this.options.axisLabelFontSize;var a=parseInt(m.style.top)+n;if(a>this.height-n){m.style.top=(parseInt(m.style.top)-n/2)+"px"}}b.beginPath();b.moveTo(this.area.x,this.area.y);b.lineTo(this.area.x,this.area.y+this.area.h);b.closePath();b.stroke()}if(this.options.drawXAxis){if(this.layout.xticks){for(var e=0;e<this.layout.xticks.length;e++){var f=this.layout.xticks[e];if(typeof(dataset)=="function"){return}var l=this.area.x+f[0]*this.area.w;var j=this.area.y+this.area.h;b.beginPath();b.moveTo(l,j);b.lineTo(l,j+this.options.axisTickSize);b.closePath();b.stroke();var k=d(f[1]);k.style.textAlign="center";k.style.bottom="0px";var c=(l-this.options.axisLabelWidth/2);if(c+this.options.axisLabelWidth>this.width){c=this.width-this.options.xAxisLabelWidth;k.style.textAlign="right"}if(c<0){c=0;k.style.textAlign="left"}k.style.left=c+"px";k.style.width=this.options.xAxisLabelWidth+"px";this.container.appendChild(k);this.xlabels.push(k)}}b.beginPath();b.moveTo(this.area.x,this.area.y+this.area.h);b.lineTo(this.area.x+this.area.w,this.area.y+this.area.h);b.closePath();b.stroke()}b.restore()};DygraphCanvasRenderer.prototype._renderAnnotations=function(){var h={position:"absolute",fontSize:this.options.axisLabelFontSize+"px",zIndex:10,overflow:"hidden"};var j=function(q,r,s,a){return function(t){var p=s.annotation;if(p.hasOwnProperty(q)){p[q](p,s,a.dygraph_,t)}else{if(a.dygraph_.attr_(r)){a.dygraph_.attr_(r)(p,s,a.dygraph_,t)}}}};var m=this.layout.annotated_points;for(var g=0;g<m.length;g++){var e=m[g];if(e.canvasx<this.area.x||e.canvasx>this.area.x+this.area.w){continue}var k=e.annotation;var l=6;if(k.hasOwnProperty("tickHeight")){l=k.tickHeight}var c=document.createElement("div");for(var b in h){if(h.hasOwnProperty(b)){c.style[b]=h[b]}}if(!k.hasOwnProperty("icon")){c.className="dygraphDefaultAnnotation"}if(k.hasOwnProperty("cssClass")){c.className+=" "+k.cssClass}var d=k.hasOwnProperty("width")?k.width:16;var n=k.hasOwnProperty("height")?k.height:16;if(k.hasOwnProperty("icon")){var f=document.createElement("img");f.src=k.icon;f.width=d;f.height=n;c.appendChild(f)}else{if(e.annotation.hasOwnProperty("shortText")){c.appendChild(document.createTextNode(e.annotation.shortText))}}c.style.left=(e.canvasx-d/2)+"px";if(k.attachAtBottom){c.style.top=(this.area.h-n-l)+"px"}else{c.style.top=(e.canvasy-n-l)+"px"}c.style.width=d+"px";c.style.height=n+"px";c.title=e.annotation.text;c.style.color=this.colors[e.name];c.style.borderColor=this.colors[e.name];k.div=c;Dygraph.addEvent(c,"click",j("clickHandler","annotationClickHandler",e,this));Dygraph.addEvent(c,"mouseover",j("mouseOverHandler","annotationMouseOverHandler",e,this));Dygraph.addEvent(c,"mouseout",j("mouseOutHandler","annotationMouseOutHandler",e,this));Dygraph.addEvent(c,"dblclick",j("dblClickHandler","annotationDblClickHandler",e,this));this.container.appendChild(c);this.annotations.push(c);var o=this.element.getContext("2d");o.strokeStyle=this.colors[e.name];o.beginPath();if(!k.attachAtBottom){o.moveTo(e.canvasx,e.canvasy);o.lineTo(e.canvasx,e.canvasy-2-l)}else{o.moveTo(e.canvasx,this.area.h);o.lineTo(e.canvasx,this.area.h-2-l)}o.closePath();o.stroke()}};DygraphCanvasRenderer.prototype._renderLineChart=function(){var c=this.element.getContext("2d");var f=this.options.colorScheme.length;var o=this.options.colorScheme;var z=this.options.fillAlpha;var E=this.layout.options.errorBars;var t=this.layout.options.fillGraph;var d=this.layout.options.stackedGraph;var l=this.layout.options.stepPlot;var G=[];for(var H in this.layout.datasets){if(this.layout.datasets.hasOwnProperty(H)){G.push(H)}}var A=G.length;this.colors={};for(var C=0;C<A;C++){this.colors[G[C]]=o[C%f]}for(var C=0;C<this.layout.points.length;C++){var v=this.layout.points[C];v.canvasx=this.area.w*v.x+this.area.x;v.canvasy=this.area.h*v.y+this.area.y}var p=function(j){return j&&!isNaN(j)};var u=c;if(E){if(t){this.dygraph_.warn("Can't use fillGraph option with error bars")}for(var C=0;C<A;C++){var k=G[C];var x=this.colors[k];u.save();var h=NaN;var e=NaN;var g=[-1,-1];var D=this.layout.yscale;var a=new RGBColor(x);var F="rgba("+a.r+","+a.g+","+a.b+","+z+")";u.fillStyle=F;u.beginPath();for(var y=0;y<this.layout.points.length;y++){var v=this.layout.points[y];if(v.name==k){if(!p(v.y)){h=NaN;continue}if(l){var r=[e-v.errorPlus*D,e+v.errorMinus*D];e=v.y}else{var r=[v.y-v.errorPlus*D,v.y+v.errorMinus*D]}r[0]=this.area.h*r[0]+this.area.y;r[1]=this.area.h*r[1]+this.area.y;if(!isNaN(h)){if(l){u.moveTo(h,r[0])}else{u.moveTo(h,g[0])}u.lineTo(v.canvasx,r[0]);u.lineTo(v.canvasx,r[1]);if(l){u.lineTo(h,r[1])}else{u.lineTo(h,g[1])}u.closePath()}g=r;h=v.canvasx}}u.fill()}}else{if(t){var b=1+this.layout.minyval*this.layout.yscale;if(b<0){b=0}else{if(b>1){b=1}}b=this.area.h*b+this.area.y;var q=[];for(var C=A-1;C>=0;C--){var k=G[C];var x=this.colors[k];u.save();var h=NaN;var g=[-1,-1];var D=this.layout.yscale;var a=new RGBColor(x);var F="rgba("+a.r+","+a.g+","+a.b+","+z+")";u.fillStyle=F;u.beginPath();for(var y=0;y<this.layout.points.length;y++){var v=this.layout.points[y];if(v.name==k){if(!p(v.y)){h=NaN;continue}var r;if(d){lastY=q[v.canvasx];if(lastY===undefined){lastY=b}q[v.canvasx]=v.canvasy;r=[v.canvasy,lastY]}else{r=[v.canvasy,b]}if(!isNaN(h)){u.moveTo(h,g[0]);if(l){u.lineTo(v.canvasx,g[0])}else{u.lineTo(v.canvasx,r[0])}u.lineTo(v.canvasx,r[1]);u.lineTo(h,g[1]);u.closePath()}g=r;h=v.canvasx}}u.fill()}}}for(var C=0;C<A;C++){var k=G[C];var x=this.colors[k];var s=this.dygraph_.attr_("strokeWidth",k);c.save();var v=this.layout.points[0];var m=this.dygraph_.attr_("pointSize",k);var h=null,e=null;var w=this.dygraph_.attr_("drawPoints",k);var B=this.layout.points;for(var y=0;y<B.length;y++){var v=B[y];if(v.name==k){if(!p(v.canvasy)){h=e=null}else{var n=(!h&&(y==B.length-1||!p(B[y+1].canvasy)));if(!h){h=v.canvasx;e=v.canvasy}else{if(s){u.beginPath();u.strokeStyle=x;u.lineWidth=s;u.moveTo(h,e);if(l){u.lineTo(v.canvasx,e)}h=v.canvasx;e=v.canvasy;u.lineTo(h,e);u.stroke()}}if(w||n){u.beginPath();u.fillStyle=x;u.arc(v.canvasx,v.canvasy,m,0,2*Math.PI,false);u.fill()}}}}}c.restore()};Dygraph=function(c,b,a){if(arguments.length>0){if(arguments.length==4){this.warn("Using deprecated four-argument dygraph constructor");this.__old_init__(c,b,arguments[2],arguments[3])}else{this.__init__(c,b,a)}}};Dygraph.NAME="Dygraph";Dygraph.VERSION="1.2";Dygraph.__repr__=function(){return"["+this.NAME+" "+this.VERSION+"]"};Dygraph.toString=function(){return this.__repr__()};Dygraph.DEFAULT_ROLL_PERIOD=1;Dygraph.DEFAULT_WIDTH=480;Dygraph.DEFAULT_HEIGHT=320;Dygraph.AXIS_LINE_WIDTH=0.3;Dygraph.DEFAULT_ATTRS={highlightCircleSize:3,pixelsPerXLabel:60,pixelsPerYLabel:30,labelsDivWidth:250,labelsDivStyles:{},labelsSeparateLines:false,labelsShowZeroValues:true,labelsKMB:false,labelsKMG2:false,showLabelsOnHighlight:true,yValueFormatter:function(a){return Dygraph.round_(a,2)},strokeWidth:1,axisTickSize:3,axisLabelFontSize:14,xAxisLabelWidth:50,yAxisLabelWidth:50,xAxisLabelFormatter:Dygraph.dateAxisFormatter,rightGap:5,showRoller:false,xValueFormatter:Dygraph.dateString_,xValueParser:Dygraph.dateParser,xTicker:Dygraph.dateTicker,delimiter:",",logScale:false,sigma:2,errorBars:false,fractions:false,wilsonInterval:true,customBars:false,fillGraph:false,fillAlpha:0.15,connectSeparatedPoints:false,stackedGraph:false,hideOverlayOnMouseOut:true,stepPlot:false};Dygraph.DEBUG=1;Dygraph.INFO=2;Dygraph.WARNING=3;Dygraph.ERROR=3;Dygraph.addedAnnotationCSS=false;Dygraph.prototype.__old_init__=function(f,d,e,b){if(e!=null){var a=["Date"];for(var c=0;c<e.length;c++){a.push(e[c])}Dygraph.update(b,{labels:a})}this.__init__(f,d,b)};Dygraph.prototype.__init__=function(c,b,a){if(a==null){a={}}this.maindiv_=c;this.file_=b;this.rollPeriod_=a.rollPeriod||Dygraph.DEFAULT_ROLL_PERIOD;this.previousVerticalX_=-1;this.fractions_=a.fractions||false;this.dateWindow_=a.dateWindow||null;this.valueRange_=a.valueRange||null;this.wilsonInterval_=a.wilsonInterval||true;this.is_initial_draw_=true;this.annotations_=[];c.innerHTML="";if(c.style.width==""){c.style.width=a.width||Dygraph.DEFAULT_WIDTH+"px"}if(c.style.height==""){c.style.height=a.height||Dygraph.DEFAULT_HEIGHT+"px"}this.width_=parseInt(c.style.width,10);this.height_=parseInt(c.style.height,10);if(c.style.width.indexOf("%")==c.style.width.length-1){this.width_=c.offsetWidth}if(c.style.height.indexOf("%")==c.style.height.length-1){this.height_=c.offsetHeight}if(this.width_==0){this.error("dygraph has zero width. Please specify a width in pixels.")}if(this.height_==0){this.error("dygraph has zero height. Please specify a height in pixels.")}if(a.stackedGraph){a.fillGraph=true}this.user_attrs_={};Dygraph.update(this.user_attrs_,a);this.attrs_={};Dygraph.update(this.attrs_,Dygraph.DEFAULT_ATTRS);this.boundaryIds_=[];this.labelsFromCSV_=(this.attr_("labels")==null);Dygraph.addAnnotationRule();this.createInterface_();this.start_()};Dygraph.prototype.attr_=function(b,a){if(a&&typeof(this.user_attrs_[a])!="undefined"&&this.user_attrs_[a]!=null&&typeof(this.user_attrs_[a][b])!="undefined"){return this.user_attrs_[a][b]}else{if(typeof(this.user_attrs_[b])!="undefined"){return this.user_attrs_[b]}else{if(typeof(this.attrs_[b])!="undefined"){return this.attrs_[b]}else{return null}}}};Dygraph.prototype.log=function(a,b){if(typeof(console)!="undefined"){switch(a){case Dygraph.DEBUG:console.debug("dygraphs: "+b);break;case Dygraph.INFO:console.info("dygraphs: "+b);break;case Dygraph.WARNING:console.warn("dygraphs: "+b);break;case Dygraph.ERROR:console.error("dygraphs: "+b);break}}};Dygraph.prototype.info=function(a){this.log(Dygraph.INFO,a)};Dygraph.prototype.warn=function(a){this.log(Dygraph.WARNING,a)};Dygraph.prototype.error=function(a){this.log(Dygraph.ERROR,a)};Dygraph.prototype.rollPeriod=function(){return this.rollPeriod_};Dygraph.prototype.xAxisRange=function(){if(this.dateWindow_){return this.dateWindow_}var b=this.rawData_[0][0];var a=this.rawData_[this.rawData_.length-1][0];return[b,a]};Dygraph.prototype.yAxisRange=function(){return this.displayedYRange_};Dygraph.prototype.toDomCoords=function(b,f){var c=[null,null];var d=this.plotter_.area;if(b!==null){var a=this.xAxisRange();c[0]=d.x+(b-a[0])/(a[1]-a[0])*d.w}if(f!==null){var e=this.yAxisRange();c[1]=d.y+(e[1]-f)/(e[1]-e[0])*d.h}return c};Dygraph.prototype.toDataCoords=function(b,f){var c=[null,null];var d=this.plotter_.area;if(b!==null){var a=this.xAxisRange();c[0]=a[0]+(b-d.x)/d.w*(a[1]-a[0])}if(f!==null){var e=this.yAxisRange();c[1]=e[0]+(d.h-f)/d.h*(e[1]-e[0])}return c};Dygraph.prototype.numColumns=function(){return this.rawData_[0].length};Dygraph.prototype.numRows=function(){return this.rawData_.length};Dygraph.prototype.getValue=function(b,a){if(b<0||b>this.rawData_.length){return null}if(a<0||a>this.rawData_[b].length){return null}return this.rawData_[b][a]};Dygraph.addEvent=function(c,a,b){var d=function(f){if(!f){var f=window.event}b(f)};if(window.addEventListener){c.addEventListener(a,d,false)}else{c.attachEvent("on"+a,d)}};Dygraph.clipCanvas_=function(b,c){var a=b.getContext("2d");a.beginPath();a.rect(c.left,c.top,c.width,c.height);a.clip()};Dygraph.prototype.createInterface_=function(){var a=this.maindiv_;this.graphDiv=document.createElement("div");this.graphDiv.style.width=this.width_+"px";this.graphDiv.style.height=this.height_+"px";a.appendChild(this.graphDiv);var c={top:0,left:this.attr_("yAxisLabelWidth")+2*this.attr_("axisTickSize")};c.width=this.width_-c.left-this.attr_("rightGap");c.height=this.height_-this.attr_("axisLabelFontSize")-2*this.attr_("axisTickSize");this.clippingArea_=c;this.canvas_=Dygraph.createCanvas();this.canvas_.style.position="absolute";this.canvas_.width=this.width_;this.canvas_.height=this.height_;this.canvas_.style.width=this.width_+"px";this.canvas_.style.height=this.height_+"px";this.hidden_=this.createPlotKitCanvas_(this.canvas_);this.graphDiv.appendChild(this.hidden_);this.graphDiv.appendChild(this.canvas_);this.mouseEventElement_=this.canvas_;Dygraph.clipCanvas_(this.hidden_,this.clippingArea_);Dygraph.clipCanvas_(this.canvas_,this.clippingArea_);var b=this;Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(d){b.mouseMove_(d)});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(d){b.mouseOut_(d)});this.layoutOptions_={xOriginIsZero:false};Dygraph.update(this.layoutOptions_,this.attrs_);Dygraph.update(this.layoutOptions_,this.user_attrs_);Dygraph.update(this.layoutOptions_,{errorBars:(this.attr_("errorBars")||this.attr_("customBars"))});this.layout_=new DygraphLayout(this,this.layoutOptions_);this.renderOptions_={colorScheme:this.colors_,strokeColor:null,axisLineWidth:Dygraph.AXIS_LINE_WIDTH};Dygraph.update(this.renderOptions_,this.attrs_);Dygraph.update(this.renderOptions_,this.user_attrs_);this.plotter_=new DygraphCanvasRenderer(this,this.hidden_,this.layout_,this.renderOptions_);this.createStatusMessage_();this.createRollInterface_();this.createDragInterface_()};Dygraph.prototype.destroy=function(){var a=function(c){while(c.hasChildNodes()){a(c.firstChild);c.removeChild(c.firstChild)}};a(this.maindiv_);var b=function(c){for(var d in c){if(typeof(c[d])==="object"){c[d]=null}}};b(this.layout_);b(this.plotter_);b(this)};Dygraph.prototype.createPlotKitCanvas_=function(a){var b=Dygraph.createCanvas();b.style.position="absolute";b.style.top=a.style.top;b.style.left=a.style.left;b.width=this.width_;b.height=this.height_;b.style.width=this.width_+"px";b.style.height=this.height_+"px";return b};Dygraph.hsvToRGB=function(h,g,k){var c;var d;var l;if(g===0){c=k;d=k;l=k}else{var e=Math.floor(h*6);var j=(h*6)-e;var b=k*(1-g);var a=k*(1-(g*j));var m=k*(1-(g*(1-j)));switch(e){case 1:c=a;d=k;l=b;break;case 2:c=b;d=k;l=m;break;case 3:c=b;d=a;l=k;break;case 4:c=m;d=b;l=k;break;case 5:c=k;d=b;l=a;break;case 6:case 0:c=k;d=m;l=b;break}}c=Math.floor(255*c+0.5);d=Math.floor(255*d+0.5);l=Math.floor(255*l+0.5);return"rgb("+c+","+d+","+l+")"};Dygraph.prototype.setColors_=function(){var e=this.attr_("labels").length-1;this.colors_=[];var a=this.attr_("colors");if(!a){var c=this.attr_("colorSaturation")||1;var b=this.attr_("colorValue")||0.5;var j=Math.ceil(e/2);for(var d=1;d<=e;d++){if(!this.visibility()[d-1]){continue}var g=d%2?Math.ceil(d/2):(j+d/2);var f=(1*g/(1+e));this.colors_.push(Dygraph.hsvToRGB(f,c,b))}}else{for(var d=0;d<e;d++){if(!this.visibility()[d]){continue}var h=a[d%a.length];this.colors_.push(h)}}this.renderOptions_.colorScheme=this.colors_;Dygraph.update(this.plotter_.options,this.renderOptions_);Dygraph.update(this.layoutOptions_,this.user_attrs_);Dygraph.update(this.layoutOptions_,this.attrs_)};Dygraph.prototype.getColors=function(){return this.colors_};Dygraph.findPosX=function(a){var b=0;if(a.offsetParent){while(1){b+=a.offsetLeft;if(!a.offsetParent){break}a=a.offsetParent}}else{if(a.x){b+=a.x}}return b};Dygraph.findPosY=function(b){var a=0;if(b.offsetParent){while(1){a+=b.offsetTop;if(!b.offsetParent){break}b=b.offsetParent}}else{if(b.y){a+=b.y}}return a};Dygraph.prototype.createStatusMessage_=function(){var d=this.user_attrs_.labelsDiv;if(d&&null!=d&&(typeof(d)=="string"||d instanceof String)){this.user_attrs_.labelsDiv=document.getElementById(d)}if(!this.attr_("labelsDiv")){var a=this.attr_("labelsDivWidth");var c={position:"absolute",fontSize:"14px",zIndex:10,width:a+"px",top:"0px",left:(this.width_-a-2)+"px",background:"white",textAlign:"left",overflow:"hidden"};Dygraph.update(c,this.attr_("labelsDivStyles"));var e=document.createElement("div");for(var b in c){if(c.hasOwnProperty(b)){e.style[b]=c[b]}}this.graphDiv.appendChild(e);this.attrs_.labelsDiv=e}};Dygraph.prototype.createRollInterface_=function(){var f=this.attr_("showRoller")?"block":"none";var b={position:"absolute",zIndex:10,top:(this.plotter_.area.h-25)+"px",left:(this.plotter_.area.x+1)+"px",display:f};var e=document.createElement("input");e.type="text";e.size="2";e.value=this.rollPeriod_;for(var a in b){if(b.hasOwnProperty(a)){e.style[a]=b[a]}}var d=this.graphDiv;d.appendChild(e);var c=this;e.onchange=function(){c.adjustRoll(e.value)};return e};Dygraph.pageX=function(c){if(c.pageX){return(!c.pageX||c.pageX<0)?0:c.pageX}else{var d=document;var a=document.body;return c.clientX+(d.scrollLeft||a.scrollLeft)-(d.clientLeft||0)}};Dygraph.pageY=function(c){if(c.pageY){return(!c.pageY||c.pageY<0)?0:c.pageY}else{var d=document;var a=document.body;return c.clientY+(d.scrollTop||a.scrollTop)-(d.clientTop||0)}};Dygraph.prototype.createDragInterface_=function(){var o=this;var c=false;var e=false;var b=null;var a=null;var n=null;var l=null;var f=null;var m=null;var k=null;var g=0;var d=0;var j=function(p){return Dygraph.pageX(p)-g};var h=function(p){return Dygraph.pageY(p)-d};Dygraph.addEvent(this.mouseEventElement_,"mousemove",function(p){if(c){n=j(p);l=h(p);o.drawZoomRect_(b,n,f);f=n}else{if(e){n=j(p);l=h(p);o.dateWindow_[0]=m-(n/o.width_)*k;o.dateWindow_[1]=o.dateWindow_[0]+k;o.drawGraph_(o.rawData_)}}});Dygraph.addEvent(this.mouseEventElement_,"mousedown",function(p){g=Dygraph.findPosX(o.canvas_);d=Dygraph.findPosY(o.canvas_);b=j(p);a=h(p);if(p.altKey||p.shiftKey){if(!o.dateWindow_){return}e=true;k=o.dateWindow_[1]-o.dateWindow_[0];m=(b/o.width_)*k+o.dateWindow_[0]}else{c=true}});Dygraph.addEvent(document,"mouseup",function(p){if(c||e){c=false;b=null;a=null}if(e){e=false;m=null;k=null}});Dygraph.addEvent(this.mouseEventElement_,"mouseout",function(p){if(c){n=null;l=null}});Dygraph.addEvent(this.mouseEventElement_,"mouseup",function(r){if(c){c=false;n=j(r);l=h(r);var u=Math.abs(n-b);var s=Math.abs(l-a);if(u<2&&s<2&&o.lastx_!=undefined&&o.lastx_!=-1){if(o.attr_("clickCallback")!=null){o.attr_("clickCallback")(r,o.lastx_,o.selPoints_)}if(o.attr_("pointClickCallback")){var x=-1;var y=0;for(var v=0;v<o.selPoints_.length;v++){var t=o.selPoints_[v];var q=Math.pow(t.canvasx-n,2)+Math.pow(t.canvasy-l,2);if(x==-1||q<y){y=q;x=v}}var w=o.attr_("highlightCircleSize")+2;if(y<=5*5){o.attr_("pointClickCallback")(r,o.selPoints_[x])}}}if(u>=10){o.doZoom_(Math.min(b,n),Math.max(b,n))}else{o.canvas_.getContext("2d").clearRect(0,0,o.canvas_.width,o.canvas_.height)}b=null;a=null}if(e){e=false;m=null;k=null}});Dygraph.addEvent(this.mouseEventElement_,"dblclick",function(p){if(o.dateWindow_==null){return}o.dateWindow_=null;o.drawGraph_(o.rawData_);var q=o.rawData_[0][0];var r=o.rawData_[o.rawData_.length-1][0];if(o.attr_("zoomCallback")){o.attr_("zoomCallback")(q,r)}})};Dygraph.prototype.drawZoomRect_=function(c,d,b){var a=this.canvas_.getContext("2d");if(b){a.clearRect(Math.min(c,b),0,Math.abs(c-b),this.height_)}if(d&&c){a.fillStyle="rgba(128,128,128,0.33)";a.fillRect(Math.min(c,d),0,Math.abs(d-c),this.height_)}};Dygraph.prototype.doZoom_=function(d,a){var b=this.toDataCoords(d,null);var c=b[0];b=this.toDataCoords(a,null);var e=b[0];this.dateWindow_=[c,e];this.drawGraph_(this.rawData_);if(this.attr_("zoomCallback")){this.attr_("zoomCallback")(c,e)}};Dygraph.prototype.mouseMove_=function(b){var a=Dygraph.pageX(b)-Dygraph.findPosX(this.mouseEventElement_);var r=this.layout_.points;var m=-1;var j=-1;var o=1e+100;var q=-1;for(var f=0;f<r.length;f++){var h=Math.abs(r[f].canvasx-a);if(h>o){continue}o=h;q=f}if(q>=0){m=r[q].xval}if(a>r[r.length-1].canvasx){m=r[r.length-1].xval}this.selPoints_=[];var d=r.length;if(!this.attr_("stackedGraph")){for(var f=0;f<d;f++){if(r[f].xval==m){this.selPoints_.push(r[f])}}}else{var g=0;for(var f=d-1;f>=0;f--){if(r[f].xval==m){var c={};for(var e in r[f]){c[e]=r[f][e]}c.yval-=g;g+=c.yval;this.selPoints_.push(c)}}this.selPoints_.reverse()}if(this.attr_("highlightCallback")){var n=this.lastx_;if(n!==null&&m!=n){this.attr_("highlightCallback")(b,m,this.selPoints_)}}this.lastx_=m;this.updateSelection_()};Dygraph.prototype.updateSelection_=function(){var q=this.canvas_.getContext("2d");if(this.previousVerticalX_>=0){var h=0;var j=this.attr_("labels");for(var g=1;g<j.length;g++){var b=this.attr_("highlightCircleSize",j[g]);if(b>h){h=b}}var o=this.previousVerticalX_;q.clearRect(o-h-1,0,2*h+2,this.height_)}var p=function(c){return c&&!isNaN(c)};if(this.selPoints_.length>0){var d=this.selPoints_[0].canvasx;var e=this.attr_("xValueFormatter")(this.lastx_,this)+":";var f=this.attr_("yValueFormatter");var m=this.colors_.length;if(this.attr_("showLabelsOnHighlight")){for(var g=0;g<this.selPoints_.length;g++){if(!this.attr_("labelsShowZeroValues")&&this.selPoints_[g].yval==0){continue}if(!p(this.selPoints_[g].canvasy)){continue}if(this.attr_("labelsSeparateLines")){e+="<br/>"}var n=this.selPoints_[g];var l=new RGBColor(this.colors_[g%m]);var k=f(n.yval);e+=" <b><font color='"+l.toHex()+"'>"+n.name+"</font></b>:"+k}this.attr_("labelsDiv").innerHTML=e}q.save();for(var g=0;g<this.selPoints_.length;g++){if(!p(this.selPoints_[g].canvasy)){continue}var a=this.attr_("highlightCircleSize",this.selPoints_[g].name);q.beginPath();q.fillStyle=this.plotter_.colors[this.selPoints_[g].name];q.arc(d,this.selPoints_[g].canvasy,a,0,2*Math.PI,false);q.fill()}q.restore();this.previousVerticalX_=d}};Dygraph.prototype.setSelection=function(b){this.selPoints_=[];var c=0;if(b!==false){b=b-this.boundaryIds_[0][0]}if(b!==false&&b>=0){for(var a in this.layout_.datasets){if(b<this.layout_.datasets[a].length){this.selPoints_.push(this.layout_.points[c+b])}c+=this.layout_.datasets[a].length}}if(this.selPoints_.length){this.lastx_=this.selPoints_[0].xval;this.updateSelection_()}else{this.lastx_=-1;this.clearSelection()}};Dygraph.prototype.mouseOut_=function(a){if(this.attr_("unhighlightCallback")){this.attr_("unhighlightCallback")(a)}if(this.attr_("hideOverlayOnMouseOut")){this.clearSelection()}};Dygraph.prototype.clearSelection=function(){var a=this.canvas_.getContext("2d");a.clearRect(0,0,this.width_,this.height_);this.attr_("labelsDiv").innerHTML="";this.selPoints_=[];this.lastx_=-1};Dygraph.prototype.getSelection=function(){if(!this.selPoints_||this.selPoints_.length<1){return -1}for(var a=0;a<this.layout_.points.length;a++){if(this.layout_.points[a].x==this.selPoints_[0].x){return a+this.boundaryIds_[0][0]}}return -1};Dygraph.zeropad=function(a){if(a<10){return"0"+a}else{return""+a}};Dygraph.hmsString_=function(a){var c=Dygraph.zeropad;var b=new Date(a);if(b.getSeconds()){return c(b.getHours())+":"+c(b.getMinutes())+":"+c(b.getSeconds())}else{return c(b.getHours())+":"+c(b.getMinutes())}};Dygraph.dateAxisFormatter=function(b,c){if(c>=Dygraph.MONTHLY){return b.strftime("%b %y")}else{var a=b.getHours()*3600+b.getMinutes()*60+b.getSeconds()+b.getMilliseconds();if(a==0||c>=Dygraph.DAILY){return new Date(b.getTime()+3600*1000).strftime("%d%b")}else{return Dygraph.hmsString_(b.getTime())}}};Dygraph.dateString_=function(b,k){var c=Dygraph.zeropad;var g=new Date(b);var h=""+g.getFullYear();var e=c(g.getMonth()+1);var j=c(g.getDate());var f="";var a=g.getHours()*3600+g.getMinutes()*60+g.getSeconds();if(a){f=" "+Dygraph.hmsString_(b)}return h+"/"+e+"/"+j+f};Dygraph.round_=function(c,b){var a=Math.pow(10,b);return Math.round(c*a)/a};Dygraph.prototype.loadedEvent_=function(a){this.rawData_=this.parseCSV_(a);this.drawGraph_(this.rawData_)};Dygraph.prototype.months=["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"];Dygraph.prototype.quarters=["Jan","Apr","Jul","Oct"];Dygraph.prototype.addXTicks_=function(){var a,c;if(this.dateWindow_){a=this.dateWindow_[0];c=this.dateWindow_[1]}else{a=this.rawData_[0][0];c=this.rawData_[this.rawData_.length-1][0]}var b=this.attr_("xTicker")(a,c,this);this.layout_.updateOptions({xTicks:b})};Dygraph.SECONDLY=0;Dygraph.TWO_SECONDLY=1;Dygraph.FIVE_SECONDLY=2;Dygraph.TEN_SECONDLY=3;Dygraph.THIRTY_SECONDLY=4;Dygraph.MINUTELY=5;Dygraph.TWO_MINUTELY=6;Dygraph.FIVE_MINUTELY=7;Dygraph.TEN_MINUTELY=8;Dygraph.THIRTY_MINUTELY=9;Dygraph.HOURLY=10;Dygraph.TWO_HOURLY=11;Dygraph.SIX_HOURLY=12;Dygraph.DAILY=13;Dygraph.WEEKLY=14;Dygraph.MONTHLY=15;Dygraph.QUARTERLY=16;Dygraph.BIANNUAL=17;Dygraph.ANNUAL=18;Dygraph.DECADAL=19;Dygraph.NUM_GRANULARITIES=20;Dygraph.SHORT_SPACINGS=[];Dygraph.SHORT_SPACINGS[Dygraph.SECONDLY]=1000*1;Dygraph.SHORT_SPACINGS[Dygraph.TWO_SECONDLY]=1000*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_SECONDLY]=1000*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_SECONDLY]=1000*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_SECONDLY]=1000*30;Dygraph.SHORT_SPACINGS[Dygraph.MINUTELY]=1000*60;Dygraph.SHORT_SPACINGS[Dygraph.TWO_MINUTELY]=1000*60*2;Dygraph.SHORT_SPACINGS[Dygraph.FIVE_MINUTELY]=1000*60*5;Dygraph.SHORT_SPACINGS[Dygraph.TEN_MINUTELY]=1000*60*10;Dygraph.SHORT_SPACINGS[Dygraph.THIRTY_MINUTELY]=1000*60*30;Dygraph.SHORT_SPACINGS[Dygraph.HOURLY]=1000*3600;Dygraph.SHORT_SPACINGS[Dygraph.TWO_HOURLY]=1000*3600*2;Dygraph.SHORT_SPACINGS[Dygraph.SIX_HOURLY]=1000*3600*6;Dygraph.SHORT_SPACINGS[Dygraph.DAILY]=1000*86400;Dygraph.SHORT_SPACINGS[Dygraph.WEEKLY]=1000*604800;Dygraph.prototype.NumXTicks=function(e,b,g){if(g<Dygraph.MONTHLY){var h=Dygraph.SHORT_SPACINGS[g];return Math.floor(0.5+1*(b-e)/h)}else{var f=1;var d=12;if(g==Dygraph.QUARTERLY){d=3}if(g==Dygraph.BIANNUAL){d=2}if(g==Dygraph.ANNUAL){d=1}if(g==Dygraph.DECADAL){d=1;f=10}var c=365.2524*24*3600*1000;var a=1*(b-e)/c;return Math.floor(0.5+1*a*d/f)}};Dygraph.prototype.GetXAxis=function(m,h,a){var r=this.attr_("xAxisLabelFormatter");var y=[];if(a<Dygraph.MONTHLY){var c=Dygraph.SHORT_SPACINGS[a];var u="%d%b";var v=c/1000;var w=new Date(m);if(v<=60){var f=w.getSeconds();w.setSeconds(f-f%v)}else{w.setSeconds(0);v/=60;if(v<=60){var f=w.getMinutes();w.setMinutes(f-f%v)}else{w.setMinutes(0);v/=60;if(v<=24){var f=w.getHours();w.setHours(f-f%v)}else{w.setHours(0);v/=24;if(v==7){w.setDate(w.getDate()-w.getDay())}}}}m=w.getTime();for(var k=m;k<=h;k+=c){y.push({v:k,label:r(new Date(k),a)})}}else{var e;var n=1;if(a==Dygraph.MONTHLY){e=[0,1,2,3,4,5,6,7,8,9,10,11,12]}else{if(a==Dygraph.QUARTERLY){e=[0,3,6,9]}else{if(a==Dygraph.BIANNUAL){e=[0,6]}else{if(a==Dygraph.ANNUAL){e=[0]}else{if(a==Dygraph.DECADAL){e=[0];n=10}}}}}var q=new Date(m).getFullYear();var o=new Date(h).getFullYear();var b=Dygraph.zeropad;for(var s=q;s<=o;s++){if(s%n!=0){continue}for(var p=0;p<e.length;p++){var l=s+"/"+b(1+e[p])+"/01";var k=Date.parse(l);if(k<m||k>h){continue}y.push({v:k,label:r(new Date(k),a)})}}}return y};Dygraph.dateTicker=function(a,f,d){var b=-1;for(var e=0;e<Dygraph.NUM_GRANULARITIES;e++){var c=d.NumXTicks(a,f,e);if(d.width_/c>=d.attr_("pixelsPerXLabel")){b=e;break}}if(b>=0){return d.GetXAxis(a,f,b)}else{}};Dygraph.numericTicks=function(v,u,l){if(l.attr_("labelsKMG2")){var f=[1,2,4,8]}else{var f=[1,2,5]}var x,p,a,q;var h=l.attr_("pixelsPerYLabel");for(var t=-10;t<50;t++){if(l.attr_("labelsKMG2")){var c=Math.pow(16,t)}else{var c=Math.pow(10,t)}for(var s=0;s<f.length;s++){x=c*f[s];p=Math.floor(v/x)*x;a=Math.ceil(u/x)*x;q=Math.abs(a-p)/x;var d=l.height_/q;if(d>h){break}}if(d>h){break}}var w=[];var r;var o=[];if(l.attr_("labelsKMB")){r=1000;o=["K","M","B","T"]}if(l.attr_("labelsKMG2")){if(r){l.warn("Setting both labelsKMB and labelsKMG2. Pick one!")}r=1024;o=["k","M","G","T"]}if(p>a){x*=-1}for(var t=0;t<q;t++){var g=p+t*x;var b=Math.abs(g);var e=Dygraph.round_(g,2);if(o.length){var m=r*r*r*r;for(var s=3;s>=0;s--,m/=r){if(b>=m){e=Dygraph.round_(g/m,1)+o[s];break}}}w.push({label:e,v:g})}return w};Dygraph.prototype.addYTicks_=function(c,b){var a=Dygraph.numericTicks(c,b,this);this.layout_.updateOptions({yAxis:[c,b],yTicks:a})};Dygraph.prototype.extremeValues_=function(d){var h=null,f=null;var b=this.attr_("errorBars")||this.attr_("customBars");if(b){for(var c=0;c<d.length;c++){var g=d[c][1][0];if(!g){continue}var a=g-d[c][1][1];var e=g+d[c][1][2];if(a>g){a=g}if(e<g){e=g}if(f==null||e>f){f=e}if(h==null||a<h){h=a}}}else{for(var c=0;c<d.length;c++){var g=d[c][1];if(g===null||isNaN(g)){continue}if(f==null||g>f){f=g}if(h==null||g<h){h=g}}}return[h,f]};Dygraph.prototype.drawGraph_=function(D){var n=this.is_initial_draw_;this.is_initial_draw_=false;var z=null,y=null;this.layout_.removeAllDatasets();this.setColors_();this.attrs_.pointSize=0.5*this.attr_("highlightCircleSize");var d=[];var f=[];for(var w=D[0].length-1;w>=1;w--){if(!this.visibility()[w-1]){continue}var b=this.attr_("connectSeparatedPoints",w);var m=[];for(var u=0;u<D.length;u++){if(D[u][w]!=null||!b){var A=D[u][0];m.push([A,D[u][w]])}}m=this.rollingAverage(m,this.rollPeriod_);var p=this.attr_("errorBars")||this.attr_("customBars");if(this.dateWindow_){var F=this.dateWindow_[0];var g=this.dateWindow_[1];var q=[];var e=null,E=null;for(var t=0;t<m.length;t++){if(m[t][0]>=F&&e===null){e=t}if(m[t][0]<=g){E=t}}if(e===null){e=0}if(e>0){e--}if(E===null){E=m.length-1}if(E<m.length-1){E++}this.boundaryIds_[w-1]=[e,E];for(var t=e;t<=E;t++){q.push(m[t])}m=q}else{this.boundaryIds_[w-1]=[0,m.length-1]}var a=this.extremeValues_(m);var r=a[0];var o=a[1];if(z===null||r<z){z=r}if(y===null||o>y){y=o}if(p){for(var u=0;u<m.length;u++){val=[m[u][0],m[u][1][0],m[u][1][1],m[u][1][2]];m[u]=val}}else{if(this.attr_("stackedGraph")){var s=m.length;var B;for(var u=0;u<s;u++){var h=m[u][0];if(d[h]===undefined){d[h]=0}B=m[u][1];d[h]+=B;m[u]=[h,d[h]];if(!y||d[h]>y){y=d[h]}}}}f[w]=m}for(var w=1;w<f.length;w++){if(!this.visibility()[w-1]){continue}this.layout_.addDataset(this.attr_("labels")[w],f[w])}if(this.valueRange_!=null){this.addYTicks_(this.valueRange_[0],this.valueRange_[1]);this.displayedYRange_=this.valueRange_}else{if(this.attr_("includeZero")&&z>0){z=0}var v=y-z;if(v==0){v=y}var c=y+0.1*v;var C=z-0.1*v;if(C<0&&z>=0){C=0}if(c>0&&y<=0){c=0}if(this.attr_("includeZero")){if(y<0){c=0}if(z>0){C=0}}this.addYTicks_(C,c);this.displayedYRange_=[C,c]}this.addXTicks_();this.layout_.updateOptions({dateWindow:this.dateWindow_});this.layout_.evaluateWithError();this.plotter_.clear();this.plotter_.render();this.canvas_.getContext("2d").clearRect(0,0,this.canvas_.width,this.canvas_.height);if(this.attr_("drawCallback")!==null){this.attr_("drawCallback")(this,n)}};Dygraph.prototype.rollingAverage=function(m,d){if(m.length<2){return m}var d=Math.min(d,m.length-1);var b=[];var s=this.attr_("sigma");if(this.fractions_){var k=0;var h=0;var e=100;for(var x=0;x<m.length;x++){k+=m[x][1][0];h+=m[x][1][1];if(x-d>=0){k-=m[x-d][1][0];h-=m[x-d][1][1]}var B=m[x][0];var v=h?k/h:0;if(this.attr_("errorBars")){if(this.wilsonInterval_){if(h){var t=v<0?0:v,u=h;var A=s*Math.sqrt(t*(1-t)/u+s*s/(4*u*u));var a=1+s*s/h;var F=(t+s*s/(2*h)-A)/a;var o=(t+s*s/(2*h)+A)/a;b[x]=[B,[t*e,(t-F)*e,(o-t)*e]]}else{b[x]=[B,[0,0,0]]}}else{var z=h?s*Math.sqrt(v*(1-v)/h):1;b[x]=[B,[e*v,e*z,e*z]]}}else{b[x]=[B,e*v]}}}else{if(this.attr_("customBars")){var F=0;var C=0;var o=0;var g=0;for(var x=0;x<m.length;x++){var E=m[x][1];var l=E[1];b[x]=[m[x][0],[l,l-E[0],E[2]-l]];if(l!=null&&!isNaN(l)){F+=E[0];C+=l;o+=E[2];g+=1}if(x-d>=0){var r=m[x-d];if(r[1][1]!=null&&!isNaN(r[1][1])){F-=r[1][0];C-=r[1][1];o-=r[1][2];g-=1}}b[x]=[m[x][0],[1*C/g,1*(C-F)/g,1*(o-C)/g]]}}else{var q=Math.min(d-1,m.length-2);if(!this.attr_("errorBars")){if(d==1){return m}for(var x=0;x<m.length;x++){var c=0;var D=0;for(var w=Math.max(0,x-d+1);w<x+1;w++){var l=m[w][1];if(l==null||isNaN(l)){continue}D++;c+=m[w][1]}if(D){b[x]=[m[x][0],c/D]}else{b[x]=[m[x][0],null]}}}else{for(var x=0;x<m.length;x++){var c=0;var f=0;var D=0;for(var w=Math.max(0,x-d+1);w<x+1;w++){var l=m[w][1][0];if(l==null||isNaN(l)){continue}D++;c+=m[w][1][0];f+=Math.pow(m[w][1][1],2)}if(D){var z=Math.sqrt(f)/D;b[x]=[m[x][0],[c/D,s*z,s*z]]}else{b[x]=[m[x][0],[null,null,null]]}}}}}return b};Dygraph.dateParser=function(b,a){var c;var e;if(b.search("-")!=-1){c=b.replace("-","/","g");while(c.search("-")!=-1){c=c.replace("-","/")}e=Date.parse(c)}else{if(b.length==8){c=b.substr(0,4)+"/"+b.substr(4,2)+"/"+b.substr(6,2);e=Date.parse(c)}else{e=Date.parse(b)}}if(!e||isNaN(e)){a.error("Couldn't parse "+b+" as a date")}return e};Dygraph.prototype.detectTypeFromString_=function(b){var a=false;if(b.indexOf("-")>=0||b.indexOf("/")>=0||isNaN(parseFloat(b))){a=true}else{if(b.length==8&&b>"19700101"&&b<"20371231"){a=true}}if(a){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter}else{this.attrs_.xValueFormatter=function(c){return c};this.attrs_.xValueParser=function(c){return parseFloat(c)};this.attrs_.xTicker=Dygraph.numericTicks;this.attrs_.xAxisLabelFormatter=this.attrs_.xValueFormatter}};Dygraph.prototype.parseCSV_=function(h){var n=[];var r=h.split("\n");var b=this.attr_("delimiter");if(r[0].indexOf(b)==-1&&r[0].indexOf("\t")>=0){b="\t"}var a=0;if(this.labelsFromCSV_){a=1;this.attrs_.labels=r[0].split(b)}var k=function(j){var s=parseFloat(j);return isNaN(s)?null:s};var c;var p=false;var d=this.attr_("labels").length;var m=false;for(var g=a;g<r.length;g++){var q=r[g];if(q.length==0){continue}if(q[0]=="#"){continue}var f=q.split(b);if(f.length<2){continue}var l=[];if(!p){this.detectTypeFromString_(f[0]);c=this.attr_("xValueParser");p=true}l[0]=c(f[0],this);if(this.fractions_){for(var e=1;e<f.length;e++){var o=f[e].split("/");l[e]=[k(o[0]),k(o[1])]}}else{if(this.attr_("errorBars")){for(var e=1;e<f.length;e+=2){l[(e+1)/2]=[k(f[e]),k(f[e+1])]}}else{if(this.attr_("customBars")){for(var e=1;e<f.length;e++){var o=f[e].split(";");l[e]=[k(o[0]),k(o[1]),k(o[2])]}}else{for(var e=1;e<f.length;e++){l[e]=k(f[e])}}}}if(n.length>0&&l[0]<n[n.length-1][0]){m=true}n.push(l);if(l.length!=d){this.error("Number of columns in line "+g+" ("+l.length+") does not agree with number of labels ("+d+") "+q)}}if(m){this.warn("CSV is out of order; order it correctly to speed loading.");n.sort(function(s,j){return s[0]-j[0]})}return n};Dygraph.prototype.parseArray_=function(b){if(b.length==0){this.error("Can't plot empty data set");return null}if(b[0].length==0){this.error("Data set cannot contain an empty row");return null}if(this.attr_("labels")==null){this.warn("Using default labels. Set labels explicitly via 'labels' in the options parameter");this.attrs_.labels=["X"];for(var a=1;a<b[0].length;a++){this.attrs_.labels.push("Y"+a)}}if(Dygraph.isDateLike(b[0][0])){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter;this.attrs_.xTicker=Dygraph.dateTicker;var c=Dygraph.clone(b);for(var a=0;a<b.length;a++){if(c[a].length==0){this.error("Row "+(1+a)+" of data is empty");return null}if(c[a][0]==null||typeof(c[a][0].getTime)!="function"||isNaN(c[a][0].getTime())){this.error("x value in row "+(1+a)+" is not a Date");return null}c[a][0]=c[a][0].getTime()}return c}else{this.attrs_.xValueFormatter=function(d){return d};this.attrs_.xTicker=Dygraph.numericTicks;return b}};Dygraph.prototype.parseDataTable_=function(v){var g=v.getNumberOfColumns();var f=v.getNumberOfRows();var e=v.getColumnType(0);if(e=="date"||e=="datetime"){this.attrs_.xValueFormatter=Dygraph.dateString_;this.attrs_.xValueParser=Dygraph.dateParser;this.attrs_.xTicker=Dygraph.dateTicker;this.attrs_.xAxisLabelFormatter=Dygraph.dateAxisFormatter}else{if(e=="number"){this.attrs_.xValueFormatter=function(j){return j};this.attrs_.xValueParser=function(j){return parseFloat(j)};this.attrs_.xTicker=Dygraph.numericTicks;this.attrs_.xAxisLabelFormatter=this.attrs_.xValueFormatter}else{this.error("only 'date', 'datetime' and 'number' types are supported for column 1 of DataTable input (Got '"+e+"')");return null}}var l=[];var s={};var r=false;for(var p=1;p<g;p++){var b=v.getColumnType(p);if(b=="number"){l.push(p)}else{if(b=="string"&&this.attr_("displayAnnotations")){var q=l[l.length-1];if(!s.hasOwnProperty(q)){s[q]=[p]}else{s[q].push(p)}r=true}else{this.error("Only 'number' is supported as a dependent type with Gviz. 'string' is only supported if displayAnnotations is true")}}}var t=[v.getColumnLabel(0)];for(var p=0;p<l.length;p++){t.push(v.getColumnLabel(l[p]))}this.attrs_.labels=t;g=t.length;var u=[];var h=false;var a=[];for(var p=0;p<f;p++){var d=[];if(typeof(v.getValue(p,0))==="undefined"||v.getValue(p,0)===null){this.warn("Ignoring row "+p+" of DataTable because of undefined or null first column.");continue}if(e=="date"||e=="datetime"){d.push(v.getValue(p,0).getTime())}else{d.push(v.getValue(p,0))}if(!this.attr_("errorBars")){for(var n=0;n<l.length;n++){var c=l[n];d.push(v.getValue(p,c));if(r&&s.hasOwnProperty(c)&&v.getValue(p,s[c][0])!=null){var o={};o.series=v.getColumnLabel(c);o.xval=d[0];o.shortText=String.fromCharCode(65+a.length);o.text="";for(var m=0;m<s[c].length;m++){if(m){o.text+="\n"}o.text+=v.getValue(p,s[c][m])}a.push(o)}}}else{for(var n=0;n<g-1;n++){d.push([v.getValue(p,1+2*n),v.getValue(p,2+2*n)])}}if(u.length>0&&d[0]<u[u.length-1][0]){h=true}u.push(d)}if(h){this.warn("DataTable is out of order; order it correctly to speed loading.");u.sort(function(k,j){return k[0]-j[0]})}this.rawData_=u;if(a.length>0){this.setAnnotations(a,true)}};Dygraph.update=function(b,c){if(typeof(c)!="undefined"&&c!==null){for(var a in c){if(c.hasOwnProperty(a)){b[a]=c[a]}}}return b};Dygraph.isArrayLike=function(b){var a=typeof(b);if((a!="object"&&!(a=="function"&&typeof(b.item)=="function"))||b===null||typeof(b.length)!="number"||b.nodeType===3){return false}return true};Dygraph.isDateLike=function(a){if(typeof(a)!="object"||a===null||typeof(a.getTime)!="function"){return false}return true};Dygraph.clone=function(c){var b=[];for(var a=0;a<c.length;a++){if(Dygraph.isArrayLike(c[a])){b.push(Dygraph.clone(c[a]))}else{b.push(c[a])}}return b};Dygraph.prototype.start_=function(){if(typeof this.file_=="function"){this.loadedEvent_(this.file_())}else{if(Dygraph.isArrayLike(this.file_)){this.rawData_=this.parseArray_(this.file_);this.drawGraph_(this.rawData_)}else{if(typeof this.file_=="object"&&typeof this.file_.getColumnRange=="function"){this.parseDataTable_(this.file_);this.drawGraph_(this.rawData_)}else{if(typeof this.file_=="string"){if(this.file_.indexOf("\n")>=0){this.loadedEvent_(this.file_)}else{var b=new XMLHttpRequest();var a=this;b.onreadystatechange=function(){if(b.readyState==4){if(b.status==200){a.loadedEvent_(b.responseText)}}};b.open("GET",this.file_,true);b.send(null)}}else{this.error("Unknown data format: "+(typeof this.file_))}}}}};Dygraph.prototype.updateOptions=function(a){if(a.rollPeriod){this.rollPeriod_=a.rollPeriod}if(a.dateWindow){this.dateWindow_=a.dateWindow}if(a.valueRange){this.valueRange_=a.valueRange}Dygraph.update(this.user_attrs_,a);Dygraph.update(this.renderOptions_,a);this.labelsFromCSV_=(this.attr_("labels")==null);this.layout_.updateOptions({errorBars:this.attr_("errorBars")});if(a.file){this.file_=a.file;this.start_()}else{this.drawGraph_(this.rawData_)}};Dygraph.prototype.resize=function(b,a){if(this.resize_lock){return}this.resize_lock=true;if((b===null)!=(a===null)){this.warn("Dygraph.resize() should be called with zero parameters or two non-NULL parameters. Pretending it was zero.");b=a=null}this.maindiv_.innerHTML="";this.attrs_.labelsDiv=null;if(b){this.maindiv_.style.width=b+"px";this.maindiv_.style.height=a+"px";this.width_=b;this.height_=a}else{this.width_=this.maindiv_.offsetWidth;this.height_=this.maindiv_.offsetHeight}this.createInterface_();this.drawGraph_(this.rawData_);this.resize_lock=false};Dygraph.prototype.adjustRoll=function(a){this.rollPeriod_=a;this.drawGraph_(this.rawData_)};Dygraph.prototype.visibility=function(){if(!this.attr_("visibility")){this.attrs_.visibility=[]}while(this.attr_("visibility").length<this.rawData_[0].length-1){this.attr_("visibility").push(true)}return this.attr_("visibility")};Dygraph.prototype.setVisibility=function(b,c){var a=this.visibility();if(b<0&&b>=a.length){this.warn("invalid series number in setVisibility: "+b)}else{a[b]=c;this.drawGraph_(this.rawData_)}};Dygraph.prototype.setAnnotations=function(b,a){this.annotations_=b;this.layout_.setAnnotations(this.annotations_);if(!a){this.drawGraph_(this.rawData_)}};Dygraph.prototype.annotations=function(){return this.annotations_};Dygraph.prototype.indexFromSetName=function(a){var c=this.attr_("labels");for(var b=0;b<c.length;b++){if(c[b]==a){return b}}return null};Dygraph.addAnnotationRule=function(){if(Dygraph.addedAnnotationCSS){return}var a;if(document.styleSheets.length>0){a=document.styleSheets[0]}else{var c=document.createElement("style");c.type="text/css";document.getElementsByTagName("head")[0].appendChild(c);for(i=0;i<document.styleSheets.length;i++){if(document.styleSheets[i].disabled){continue}a=document.styleSheets[i]}}var b="border: 1px solid black; background-color: white; text-align: center;";if(a.insertRule){a.insertRule(".dygraphDefaultAnnotation { "+b+" }",0)}else{if(a.addRule){a.addRule(".dygraphDefaultAnnotation",b)}}Dygraph.addedAnnotationCSS=true};Dygraph.createCanvas=function(){var a=document.createElement("canvas");isIE=(/MSIE/.test(navigator.userAgent)&&!window.opera);if(isIE){a=G_vmlCanvasManager.initElement(a)}return a};Dygraph.GVizChart=function(a){this.container=a};Dygraph.GVizChart.prototype.draw=function(b,a){this.container.innerHTML="";this.date_graph=new Dygraph(this.container,b,a)};Dygraph.GVizChart.prototype.setSelection=function(b){var a=false;if(b.length){a=b[0].row}this.date_graph.setSelection(a)};Dygraph.GVizChart.prototype.getSelection=function(){var b=[];var c=this.date_graph.getSelection();if(c<0){return b}col=1;for(var a in this.date_graph.layout_.datasets){b.push({row:c,column:col});col++}return b};DateGraph=Dygraph;function RGBColor(g){this.ok=false;if(g.charAt(0)=="#"){g=g.substr(1,6)}g=g.replace(/ /g,"");g=g.toLowerCase();var a={aliceblue:"f0f8ff",antiquewhite:"faebd7",aqua:"00ffff",aquamarine:"7fffd4",azure:"f0ffff",beige:"f5f5dc",bisque:"ffe4c4",black:"000000",blanchedalmond:"ffebcd",blue:"0000ff",blueviolet:"8a2be2",brown:"a52a2a",burlywood:"deb887",cadetblue:"5f9ea0",chartreuse:"7fff00",chocolate:"d2691e",coral:"ff7f50",cornflowerblue:"6495ed",cornsilk:"fff8dc",crimson:"dc143c",cyan:"00ffff",darkblue:"00008b",darkcyan:"008b8b",darkgoldenrod:"b8860b",darkgray:"a9a9a9",darkgreen:"006400",darkkhaki:"bdb76b",darkmagenta:"8b008b",darkolivegreen:"556b2f",darkorange:"ff8c00",darkorchid:"9932cc",darkred:"8b0000",darksalmon:"e9967a",darkseagreen:"8fbc8f",darkslateblue:"483d8b",darkslategray:"2f4f4f",darkturquoise:"00ced1",darkviolet:"9400d3",deeppink:"ff1493",deepskyblue:"00bfff",dimgray:"696969",dodgerblue:"1e90ff",feldspar:"d19275",firebrick:"b22222",floralwhite:"fffaf0",forestgreen:"228b22",fuchsia:"ff00ff",gainsboro:"dcdcdc",ghostwhite:"f8f8ff",gold:"ffd700",goldenrod:"daa520",gray:"808080",green:"008000",greenyellow:"adff2f",honeydew:"f0fff0",hotpink:"ff69b4",indianred:"cd5c5c",indigo:"4b0082",ivory:"fffff0",khaki:"f0e68c",lavender:"e6e6fa",lavenderblush:"fff0f5",lawngreen:"7cfc00",lemonchiffon:"fffacd",lightblue:"add8e6",lightcoral:"f08080",lightcyan:"e0ffff",lightgoldenrodyellow:"fafad2",lightgrey:"d3d3d3",lightgreen:"90ee90",lightpink:"ffb6c1",lightsalmon:"ffa07a",lightseagreen:"20b2aa",lightskyblue:"87cefa",lightslateblue:"8470ff",lightslategray:"778899",lightsteelblue:"b0c4de",lightyellow:"ffffe0",lime:"00ff00",limegreen:"32cd32",linen:"faf0e6",magenta:"ff00ff",maroon:"800000",mediumaquamarine:"66cdaa",mediumblue:"0000cd",mediumorchid:"ba55d3",mediumpurple:"9370d8",mediumseagreen:"3cb371",mediumslateblue:"7b68ee",mediumspringgreen:"00fa9a",mediumturquoise:"48d1cc",mediumvioletred:"c71585",midnightblue:"191970",mintcream:"f5fffa",mistyrose:"ffe4e1",moccasin:"ffe4b5",navajowhite:"ffdead",navy:"000080",oldlace:"fdf5e6",olive:"808000",olivedrab:"6b8e23",orange:"ffa500",orangered:"ff4500",orchid:"da70d6",palegoldenrod:"eee8aa",palegreen:"98fb98",paleturquoise:"afeeee",palevioletred:"d87093",papayawhip:"ffefd5",peachpuff:"ffdab9",peru:"cd853f",pink:"ffc0cb",plum:"dda0dd",powderblue:"b0e0e6",purple:"800080",red:"ff0000",rosybrown:"bc8f8f",royalblue:"4169e1",saddlebrown:"8b4513",salmon:"fa8072",sandybrown:"f4a460",seagreen:"2e8b57",seashell:"fff5ee",sienna:"a0522d",silver:"c0c0c0",skyblue:"87ceeb",slateblue:"6a5acd",slategray:"708090",snow:"fffafa",springgreen:"00ff7f",steelblue:"4682b4",tan:"d2b48c",teal:"008080",thistle:"d8bfd8",tomato:"ff6347",turquoise:"40e0d0",violet:"ee82ee",violetred:"d02090",wheat:"f5deb3",white:"ffffff",whitesmoke:"f5f5f5",yellow:"ffff00",yellowgreen:"9acd32"};for(var c in a){if(g==c){g=a[c]}}var h=[{re:/^rgb\((\d{1,3}),\s*(\d{1,3}),\s*(\d{1,3})\)$/,example:["rgb(123, 234, 45)","rgb(255,234,245)"],process:function(j){return[parseInt(j[1]),parseInt(j[2]),parseInt(j[3])]}},{re:/^(\w{2})(\w{2})(\w{2})$/,example:["#00ff00","336699"],process:function(j){return[parseInt(j[1],16),parseInt(j[2],16),parseInt(j[3],16)]}},{re:/^(\w{1})(\w{1})(\w{1})$/,example:["#fb0","f0f"],process:function(j){return[parseInt(j[1]+j[1],16),parseInt(j[2]+j[2],16),parseInt(j[3]+j[3],16)]}}];for(var b=0;b<h.length;b++){var e=h[b].re;var d=h[b].process;var f=e.exec(g);if(f){channels=d(f);this.r=channels[0];this.g=channels[1];this.b=channels[2];this.ok=true}}this.r=(this.r<0||isNaN(this.r))?0:((this.r>255)?255:this.r);this.g=(this.g<0||isNaN(this.g))?0:((this.g>255)?255:this.g);this.b=(this.b<0||isNaN(this.b))?0:((this.b>255)?255:this.b);this.toRGB=function(){return"rgb("+this.r+", "+this.g+", "+this.b+")"};this.toHex=function(){var l=this.r.toString(16);var k=this.g.toString(16);var j=this.b.toString(16);if(l.length==1){l="0"+l}if(k.length==1){k="0"+k}if(j.length==1){j="0"+j}return"#"+l+k+j}}Date.ext={};Date.ext.util={};Date.ext.util.xPad=function(a,c,b){if(typeof(b)=="undefined"){b=10}for(;parseInt(a,10)<b&&b>1;b/=10){a=c.toString()+a}return a.toString()};Date.prototype.locale="en-GB";if(document.getElementsByTagName("html")&&document.getElementsByTagName("html")[0].lang){Date.prototype.locale=document.getElementsByTagName("html")[0].lang}Date.ext.locales={};Date.ext.locales.en={a:["Sun","Mon","Tue","Wed","Thu","Fri","Sat"],A:["Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday"],b:["Jan","Feb","Mar","Apr","May","Jun","Jul","Aug","Sep","Oct","Nov","Dec"],B:["January","February","March","April","May","June","July","August","September","October","November","December"],c:"%a %d %b %Y %T %Z",p:["AM","PM"],P:["am","pm"],x:"%d/%m/%y",X:"%T"};Date.ext.locales["en-US"]=Date.ext.locales.en;Date.ext.locales["en-US"].c="%a %d %b %Y %r %Z";Date.ext.locales["en-US"].x="%D";Date.ext.locales["en-US"].X="%r";Date.ext.locales["en-GB"]=Date.ext.locales.en;Date.ext.locales["en-AU"]=Date.ext.locales["en-GB"];Date.ext.formats={a:function(a){return Date.ext.locales[a.locale].a[a.getDay()]},A:function(a){return Date.ext.locales[a.locale].A[a.getDay()]},b:function(a){return Date.ext.locales[a.locale].b[a.getMonth()]},B:function(a){return Date.ext.locales[a.locale].B[a.getMonth()]},c:"toLocaleString",C:function(a){return Date.ext.util.xPad(parseInt(a.getFullYear()/100,10),0)},d:["getDate","0"],e:["getDate"," "],g:function(a){return Date.ext.util.xPad(parseInt(Date.ext.util.G(a)/100,10),0)},G:function(c){var e=c.getFullYear();var b=parseInt(Date.ext.formats.V(c),10);var a=parseInt(Date.ext.formats.W(c),10);if(a>b){e++}else{if(a===0&&b>=52){e--}}return e},H:["getHours","0"],I:function(b){var a=b.getHours()%12;return Date.ext.util.xPad(a===0?12:a,0)},j:function(c){var a=c-new Date(""+c.getFullYear()+"/1/1 GMT");a+=c.getTimezoneOffset()*60000;var b=parseInt(a/60000/60/24,10)+1;return Date.ext.util.xPad(b,0,100)},m:function(a){return Date.ext.util.xPad(a.getMonth()+1,0)},M:["getMinutes","0"],p:function(a){return Date.ext.locales[a.locale].p[a.getHours()>=12?1:0]},P:function(a){return Date.ext.locales[a.locale].P[a.getHours()>=12?1:0]},S:["getSeconds","0"],u:function(a){var b=a.getDay();return b===0?7:b},U:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=6-e.getDay();var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0)},V:function(e){var c=parseInt(Date.ext.formats.W(e),10);var a=(new Date(""+e.getFullYear()+"/1/1")).getDay();var b=c+(a>4||a<=1?0:1);if(b==53&&(new Date(""+e.getFullYear()+"/12/31")).getDay()<4){b=1}else{if(b===0){b=Date.ext.formats.V(new Date(""+(e.getFullYear()-1)+"/12/31"))}}return Date.ext.util.xPad(b,0)},w:"getDay",W:function(e){var a=parseInt(Date.ext.formats.j(e),10);var c=7-Date.ext.formats.u(e);var b=parseInt((a+c)/7,10);return Date.ext.util.xPad(b,0,10)},y:function(a){return Date.ext.util.xPad(a.getFullYear()%100,0)},Y:"getFullYear",z:function(c){var b=c.getTimezoneOffset();var a=Date.ext.util.xPad(parseInt(Math.abs(b/60),10),0);var e=Date.ext.util.xPad(b%60,0);return(b>0?"-":"+")+a+e},Z:function(a){return a.toString().replace(/^.*\(([^)]+)\)$/,"$1")},"%":function(a){return"%"}};Date.ext.aggregates={c:"locale",D:"%m/%d/%y",h:"%b",n:"\n",r:"%I:%M:%S %p",R:"%H:%M",t:"\t",T:"%H:%M:%S",x:"locale",X:"locale"};Date.ext.aggregates.z=Date.ext.formats.z(new Date());Date.ext.aggregates.Z=Date.ext.formats.Z(new Date());Date.ext.unsupported={};Date.prototype.strftime=function(a){if(!(this.locale in Date.ext.locales)){if(this.locale.replace(/-[a-zA-Z]+$/,"") in Date.ext.locales){this.locale=this.locale.replace(/-[a-zA-Z]+$/,"")}else{this.locale="en-GB"}}var c=this;while(a.match(/%[cDhnrRtTxXzZ]/)){a=a.replace(/%([cDhnrRtTxXzZ])/g,function(e,d){var g=Date.ext.aggregates[d];return(g=="locale"?Date.ext.locales[c.locale][d]:g)})}var b=a.replace(/%([aAbBCdegGHIjmMpPSuUVwWyY%])/g,function(e,d){var g=Date.ext.formats[d];if(typeof(g)=="string"){return c[g]()}else{if(typeof(g)=="function"){return g.call(c,c)}else{if(typeof(g)=="object"&&typeof(g[0])=="string"){return Date.ext.util.xPad(c[g[0]](),g[1])}else{return d}}}});c=null;return b};
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css
new file mode 100644
index 0000000..063addf
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.css
@@ -0,0 +1,71 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+body {
+ font-family: arial;
+ font-size: 13px;
+}
+h2 {
+ font-size: 16px;
+ margin-bottom: .25em;
+}
+h3 {
+ font-size: 13px;
+ margin: 0;
+}
+input {
+ margin-right: 15px;
+}
+label {
+ padding-left: 2em;
+ white-space: nowrap;
+}
+.forms {
+ display: -webkit-box;
+ -webkit-box-align: baseline;
+}
+.forms > * {
+ display: block;
+}
+.forms span {
+ padding: 0px 3px;
+}
+#tests-form {
+ display: -webkit-box;
+ -webkit-box-align: baseline;
+ -webkit-box-flex: 1;
+}
+#tests-form > * {
+ display: block;
+}
+#tests-input {
+ display: -webkit-box;
+ -webkit-box-flex: 1;
+}
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html
new file mode 100644
index 0000000..625d423
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.html
@@ -0,0 +1,36 @@
+<!-- Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<!DOCTYPE HTML>
+<title>Chromium/WebKit Test History</title>
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script src="flakiness_dashboard.js"></script>
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
new file mode 100644
index 0000000..7589e34
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
@@ -0,0 +1,2607 @@
+// Copyright (C) 2012 Google Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+//////////////////////////////////////////////////////////////////////////////
+// CONSTANTS
+//////////////////////////////////////////////////////////////////////////////
+var ALL = 'ALL';
+var FORWARD = 'forward';
+var BACKWARD = 'backward';
+var GTEST_MODIFIERS = ['FLAKY', 'FAILS', 'MAYBE', 'DISABLED'];
+var TEST_URL_BASE_PATH_TRAC = 'http://trac.webkit.org/browser/trunk/LayoutTests/';
+var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/LayoutTests/";
+var EXPECTATIONS_URL_BASE_PATH = TEST_URL_BASE_PATH + "platform/";
+var TEST_RESULTS_BASE_PATH = 'http://build.chromium.org/f/chromium/layout_test_results/';
+var GPU_RESULTS_BASE_PATH = 'http://chromium-browser-gpu-tests.commondatastorage.googleapis.com/runs/'
+
+var PLATFORMS = {
+ 'CHROMIUM': {
+ expectationsDirectory: 'chromium',
+ subPlatforms: {
+ 'LION': { fallbackPlatforms: ['CHROMIUM'] },
+ 'SNOWLEOPARD': { fallbackPlatforms: ['CHROMIUM'] },
+ 'XP': { fallbackPlatforms: ['CHROMIUM'] },
+ 'VISTA': { fallbackPlatforms: ['CHROMIUM'] },
+ 'WIN7': { fallbackPlatforms: ['CHROMIUM'] },
+ 'LUCID': { fallbackPlatforms: ['CHROMIUM'] },
+ 'ANDROID': { fallbackPlatforms: ['CHROMIUM'], expectationsDirectory: 'chromium-android' }
+ },
+ platformModifierUnions: {
+ 'MAC': ['CHROMIUM_LION', 'CHROMIUM_SNOWLEOPARD'],
+ 'WIN': ['CHROMIUM_XP', 'CHROMIUM_VISTA', 'CHROMIUM_WIN7'],
+ 'LINUX': ['CHROMIUM_LUCID']
+ }
+ },
+ 'APPLE': {
+ subPlatforms: {
+ 'MAC': {
+ expectationsDirectory: 'mac',
+ subPlatforms: {
+ 'LION': {
+ expectationsDirectory: 'mac-lion',
+ subPlatforms: {
+ 'WK1': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC'] },
+ 'WK2': { fallbackPlatforms: ['APPLE_MAC_LION', 'APPLE_MAC', 'WK2'] }
+ }
+ },
+ 'SNOWLEOPARD': {
+ expectationsDirectory: 'mac-snowleopard',
+ subPlatforms: {
+ 'WK1': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC'] },
+ 'WK2': { fallbackPlatforms: ['APPLE_MAC_SNOWLEOPARD', 'APPLE_MAC', 'WK2'] }
+ }
+ }
+ }
+ },
+ 'WIN': {
+ expectationsDirectory: 'win',
+ subPlatforms: {
+ 'XP': { fallbackPlatforms: ['APPLE_WIN'] },
+ 'WIN7': { fallbackPlatforms: ['APPLE_WIN'] }
+ }
+ }
+ }
+ },
+ 'GTK': {
+ expectationsDirectory: 'gtk',
+ subPlatforms: {
+ 'LINUX': {
+ subPlatforms: {
+ 'WK1': { fallbackPlatforms: ['GTK'] },
+ 'WK2': { fallbackPlatforms: ['GTK', 'WK2'], expectationsDirectory: 'gtk-wk2' }
+ }
+ }
+ }
+ },
+ 'QT': {
+ expectationsDirectory: 'qt',
+ subPlatforms: {
+ 'LINUX': { fallbackPlatforms: ['QT'] }
+ }
+ },
+ 'EFL': {
+ expectationsDirectory: 'efl',
+ subPlatforms: {
+ 'LINUX': {
+ subPlatforms: {
+ 'WK1': { fallbackPlatforms: ['EFL'], expectationsDirectory: 'efl-wk1' },
+ 'WK2': { fallbackPlatforms: ['EFL', 'WK2'], expectationsDirectory: 'efl-wk2' }
+ }
+ }
+ }
+ },
+ 'WK2': {
+ basePlatform: true,
+ expectationsDirectory: 'wk2'
+ }
+};
+
+var BUILD_TYPES = {'DEBUG': 'DBG', 'RELEASE': 'RELEASE'};
+var MIN_SECONDS_FOR_SLOW_TEST = 4;
+var MIN_SECONDS_FOR_SLOW_TEST_DEBUG = 2 * MIN_SECONDS_FOR_SLOW_TEST;
+var FAIL_RESULTS = ['IMAGE', 'IMAGE+TEXT', 'TEXT', 'MISSING'];
+var CHUNK_SIZE = 25;
+var MAX_RESULTS = 1500;
+
+// FIXME: Figure out how to make this not be hard-coded.
+var VIRTUAL_SUITES = {
+ 'platform/chromium/virtual/gpu/fast/canvas': 'fast/canvas',
+ 'platform/chromium/virtual/gpu/canvas/philip': 'canvas/philip'
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+ if (g_crossDashboardState.useTestData)
+ return;
+
+ updateDefaultBuilderState();
+ document.body.innerHTML = '<div id="loading-ui">LOADING...</div>';
+ showErrors();
+
+ // tests expands to all tests that match the CSV list.
+ // result expands to all tests that ever have the given result
+ if (g_currentState.tests || g_currentState.result)
+ generatePageForIndividualTests(individualTests());
+ else if (g_currentState.expectationsUpdate)
+ generatePageForExpectationsUpdate();
+ else
+ generatePageForBuilder(g_currentState.builder);
+
+ for (var builder in g_builders)
+ processTestResultsForBuilderAsync(builder);
+
+ postHeightChangedMessage();
+}
+
+function handleValidHashParameter(key, value)
+{
+ switch(key) {
+ case 'tests':
+ validateParameter(g_currentState, key, value,
+ function() {
+ return isValidName(value);
+ });
+ return true;
+
+ case 'result':
+ value = value.toUpperCase();
+ validateParameter(g_currentState, key, value,
+ function() {
+ for (var result in LAYOUT_TEST_EXPECTATIONS_MAP_) {
+ if (value == LAYOUT_TEST_EXPECTATIONS_MAP_[result])
+ return true;
+ }
+ return false;
+ });
+ return true;
+
+ case 'builder':
+ validateParameter(g_currentState, key, value,
+ function() {
+ return value in g_builders;
+ });
+ return true;
+
+ case 'sortColumn':
+ validateParameter(g_currentState, key, value,
+ function() {
+ // Get all possible headers since the actual used set of headers
+ // depends on the values in g_currentState, which are currently being set.
+ var headers = tableHeaders(true);
+ for (var i = 0; i < headers.length; i++) {
+ if (value == sortColumnFromTableHeader(headers[i]))
+ return true;
+ }
+ return value == 'test' || value == 'builder';
+ });
+ return true;
+
+ case 'sortOrder':
+ validateParameter(g_currentState, key, value,
+ function() {
+ return value == FORWARD || value == BACKWARD;
+ });
+ return true;
+
+ case 'resultsHeight':
+ case 'updateIndex':
+ case 'revision':
+ validateParameter(g_currentState, key, Number(value),
+ function() {
+ return value.match(/^\d+$/);
+ });
+ return true;
+
+ case 'showChrome':
+ case 'showCorrectExpectations':
+ case 'showWrongExpectations':
+ case 'showExpectations':
+ case 'showFlaky':
+ case 'showLargeExpectations':
+ case 'legacyExpectationsSemantics':
+ case 'showSkipped':
+ case 'showSlow':
+ case 'showUnexpectedPasses':
+ case 'showWontFixSkip':
+ case 'expectationsUpdate':
+ g_currentState[key] = value == 'true';
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+g_defaultDashboardSpecificStateValues = {
+ sortOrder: BACKWARD,
+ sortColumn: 'flakiness',
+ showExpectations: false,
+ showFlaky: true,
+ showLargeExpectations: false,
+ legacyExpectationsSemantics: true,
+ showChrome: true,
+ showCorrectExpectations: !isLayoutTestResults(),
+ showWrongExpectations: !isLayoutTestResults(),
+ showWontFixSkip: !isLayoutTestResults(),
+ showSlow: !isLayoutTestResults(),
+ showSkipped: !isLayoutTestResults(),
+ showUnexpectedPasses: !isLayoutTestResults(),
+ expectationsUpdate: false,
+ updateIndex: 0,
+ resultsHeight: 300,
+ revision: null,
+ tests: '',
+ result: '',
+};
+
+//////////////////////////////////////////////////////////////////////////////
+// GLOBALS
+//////////////////////////////////////////////////////////////////////////////
+
+var g_perBuilderPlatformAndBuildType = {};
+var g_perBuilderFailures = {};
+// Map of builder to arrays of tests that are listed in the expectations file
+// but have for that builder.
+var g_perBuilderWithExpectationsButNoFailures = {};
+// Map of builder to arrays of paths that are skipped. This shows the raw
+// path used in TestExpectations rather than the test path since we
+// don't actually have any data here for skipped tests.
+var g_perBuilderSkippedPaths = {};
+// Maps test path to an array of {builder, testResults} objects.
+var g_testToResultsMap = {};
+// Tests that the user wants to update expectations for.
+var g_confirmedTests = {};
+
+function traversePlatformsTree(callback)
+{
+ function traverse(platformObject, parentPlatform) {
+ Object.keys(platformObject).forEach(function(platformName) {
+ var platform = platformObject[platformName];
+ platformName = parentPlatform ? parentPlatform + platformName : platformName;
+
+ if (platform.subPlatforms)
+ traverse(platform.subPlatforms, platformName + '_');
+ else if (!platform.basePlatform)
+ callback(platform, platformName);
+ });
+ }
+ traverse(PLATFORMS, null);
+}
+
+function createResultsObjectForTest(test, builder)
+{
+ return {
+ test: test,
+ builder: builder,
+ // HTML for display of the results in the flakiness column
+ html: '',
+ flips: 0,
+ slowestTime: 0,
+ slowestNonTimeoutCrashTime: 0,
+ meetsExpectations: true,
+ isWontFixSkip: false,
+ isFlaky: false,
+ // Sorted string of missing expectations
+ missing: '',
+ // String of extra expectations (i.e. expectations that never occur).
+ extra: '',
+ modifiers: '',
+ bugs: '',
+ expectations : '',
+ rawResults: '',
+ // List of all the results the test actually has.
+ actualResults: []
+ };
+}
+
+function matchingElement(stringToMatch, elementsMap)
+{
+ for (var element in elementsMap) {
+ if (stringContains(stringToMatch, elementsMap[element]))
+ return element;
+ }
+}
+
+function determineWKPlatform(builderName, basePlatform)
+{
+ var isWK2Builder = stringContains(builderName, 'WK2') || stringContains(builderName, 'WEBKIT2');
+ return basePlatform + (isWK2Builder ? '_WK2' : '_WK1');
+}
+
+function nonChromiumPlatform(builderNameUpperCase)
+{
+ if (stringContains(builderNameUpperCase, 'WINDOWS 7'))
+ return 'APPLE_WIN_WIN7';
+ if (stringContains(builderNameUpperCase, 'WINDOWS XP'))
+ return 'APPLE_WIN_XP';
+ if (stringContains(builderNameUpperCase, 'QT LINUX'))
+ return 'QT_LINUX';
+
+ if (stringContains(builderNameUpperCase, 'LION'))
+ return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_LION');
+ if (stringContains(builderNameUpperCase, 'SNOWLEOPARD'))
+ return determineWKPlatform(builderNameUpperCase, 'APPLE_MAC_SNOWLEOPARD');
+ if (stringContains(builderNameUpperCase, 'GTK LINUX'))
+ return determineWKPlatform(builderNameUpperCase, 'GTK_LINUX');
+ if (stringContains(builderNameUpperCase, 'EFL'))
+ return determineWKPlatform(builderNameUpperCase, 'EFL_LINUX');
+}
+
+function chromiumPlatform(builderNameUpperCase)
+{
+ if (stringContains(builderNameUpperCase, 'MAC')) {
+ if (stringContains(builderNameUpperCase, '10.7'))
+ return 'CHROMIUM_LION';
+ // The webkit.org 'Chromium Mac Release (Tests)' bot runs SnowLeopard.
+ return 'CHROMIUM_SNOWLEOPARD';
+ }
+ if (stringContains(builderNameUpperCase, 'WIN7'))
+ return 'CHROMIUM_WIN7';
+ if (stringContains(builderNameUpperCase, 'VISTA'))
+ return 'CHROMIUM_VISTA';
+ if (stringContains(builderNameUpperCase, 'WIN') || stringContains(builderNameUpperCase, 'XP'))
+ return 'CHROMIUM_XP';
+ if (stringContains(builderNameUpperCase, 'LINUX'))
+ return 'CHROMIUM_LUCID';
+ if (stringContains(builderNameUpperCase, 'ANDROID'))
+ return 'CHROMIUM_ANDROID';
+ // The interactive bot is XP, but doesn't have an OS in it's name.
+ if (stringContains(builderNameUpperCase, 'INTERACTIVE'))
+ return 'CHROMIUM_XP';
+}
+
+
+function platformAndBuildType(builderName)
+{
+ if (!g_perBuilderPlatformAndBuildType[builderName]) {
+ var builderNameUpperCase = builderName.toUpperCase();
+
+ var platform = '';
+ if (isLayoutTestResults() && g_crossDashboardState.group == '@ToT - webkit.org' && !stringContains(builderNameUpperCase, 'CHROMIUM'))
+ platform = nonChromiumPlatform(builderNameUpperCase);
+ else
+ platform = chromiumPlatform(builderNameUpperCase);
+
+ if (!platform)
+ console.error('Could not resolve platform for builder: ' + builderName);
+
+ var buildType = stringContains(builderNameUpperCase, 'DBG') || stringContains(builderNameUpperCase, 'DEBUG') ? 'DEBUG' : 'RELEASE';
+ g_perBuilderPlatformAndBuildType[builderName] = {platform: platform, buildType: buildType};
+ }
+ return g_perBuilderPlatformAndBuildType[builderName];
+}
+
+function isDebug(builderName)
+{
+ return platformAndBuildType(builderName).buildType == 'DEBUG';
+}
+
+// Returns the expectation string for the given single character result.
+// This string should match the expectations that are put into
+// test_expectations.py.
+//
+// For example, if we start explicitly listing IMAGE result failures,
+// this function should start returning 'IMAGE'.
+function expectationsFileStringForResult(result)
+{
+ // For the purposes of comparing against the expecations of a test,
+ // consider simplified diff failures as just text failures since
+ // the test_expectations file doesn't treat them specially.
+ if (result == 'S')
+ return 'TEXT';
+
+ if (result == 'N')
+ return '';
+
+ return expectationsMap()[result];
+}
+
+var TestTrie = function(builders, resultsByBuilder)
+{
+ this._trie = {};
+
+ for (var builder in builders) {
+ var testsForBuilder = resultsByBuilder[builder].tests;
+ for (var test in testsForBuilder)
+ this._addTest(test.split('/'), this._trie);
+ }
+}
+
+TestTrie.prototype.forEach = function(callback, startingTriePath)
+{
+ var testsTrie = this._trie;
+ if (startingTriePath) {
+ var splitPath = startingTriePath.split('/');
+ while (splitPath.length && testsTrie)
+ testsTrie = testsTrie[splitPath.shift()];
+ }
+
+ if (!testsTrie)
+ return;
+
+ function traverse(trie, triePath) {
+ if (trie == true)
+ callback(triePath);
+ else {
+ for (var member in trie)
+ traverse(trie[member], triePath ? triePath + '/' + member : member);
+ }
+ }
+ traverse(testsTrie, startingTriePath);
+}
+
+TestTrie.prototype._addTest = function(test, trie)
+{
+ var rootComponent = test.shift();
+ if (!test.length) {
+ if (!trie[rootComponent])
+ trie[rootComponent] = true;
+ return;
+ }
+
+ if (!trie[rootComponent] || trie[rootComponent] == true)
+ trie[rootComponent] = {};
+ this._addTest(test, trie[rootComponent]);
+}
+
+// Map of all tests to true values. This is just so we can have the list of
+// all tests across all the builders.
+var g_allTestsTrie;
+
+function getAllTestsTrie()
+{
+ if (!g_allTestsTrie)
+ g_allTestsTrie = new TestTrie(g_builders, g_resultsByBuilder);
+
+ return g_allTestsTrie;
+}
+
+// Returns an array of tests to be displayed in the individual tests view.
+// Note that a directory can be listed as a test, so we expand that into all
+// tests in the directory.
+function individualTests()
+{
+ if (g_currentState.result)
+ return allTestsWithResult(g_currentState.result);
+
+ if (!g_currentState.tests)
+ return [];
+
+ return individualTestsForSubstringList();
+}
+
+function substringList()
+{
+ // Convert windows slashes to unix slashes.
+ var tests = g_currentState.tests.replace(/\\/g, '/');
+ var separator = stringContains(tests, ' ') ? ' ' : ',';
+ var testList = tests.split(separator);
+
+ if (isLayoutTestResults())
+ return testList;
+
+ var testListWithoutModifiers = [];
+ testList.forEach(function(path) {
+ GTEST_MODIFIERS.forEach(function(modifier) {
+ path = path.replace('.' + modifier + '_', '.');
+ });
+ testListWithoutModifiers.push(path);
+ });
+ return testListWithoutModifiers;
+}
+
+function individualTestsForSubstringList()
+{
+ var testList = substringList();
+
+ // Put the tests into an object first and then move them into an array
+ // as a way of deduping.
+ var testsMap = {};
+ for (var i = 0; i < testList.length; i++) {
+ var path = testList[i];
+
+ // Ignore whitespace entries as they'd match every test.
+ if (path.match(/^\s*$/))
+ continue;
+
+ var hasAnyMatches = false;
+ getAllTestsTrie().forEach(function(triePath) {
+ if (caseInsensitiveContains(triePath, path)) {
+ testsMap[triePath] = 1;
+ hasAnyMatches = true;
+ }
+ });
+
+ // If a path doesn't match any tests, then assume it's a full path
+ // to a test that passes on all builders.
+ if (!hasAnyMatches)
+ testsMap[path] = 1;
+ }
+
+ var testsArray = [];
+ for (var test in testsMap)
+ testsArray.push(test);
+ return testsArray;
+}
+
+// Returns whether this test's slowest time is above the cutoff for
+// being a slow test.
+function isSlowTest(resultsForTest)
+{
+ var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
+ return resultsForTest.slowestNonTimeoutCrashTime > maxTime;
+}
+
+// Returns whether this test's slowest time is *well* below the cutoff for
+// being a slow test.
+function isFastTest(resultsForTest)
+{
+ var maxTime = isDebug(resultsForTest.builder) ? MIN_SECONDS_FOR_SLOW_TEST_DEBUG : MIN_SECONDS_FOR_SLOW_TEST;
+ return resultsForTest.slowestNonTimeoutCrashTime < maxTime / 2;
+}
+
+function allTestsWithResult(result)
+{
+ processTestRunsForAllBuilders();
+ var retVal = [];
+
+ getAllTestsTrie().forEach(function(triePath) {
+ for (var i = 0; i < g_testToResultsMap[triePath].length; i++) {
+ if (g_testToResultsMap[triePath][i].actualResults.indexOf(result) != -1) {
+ retVal.push(triePath);
+ break;
+ }
+ }
+ });
+
+ return retVal;
+}
+
+
+// Adds all the tests for the given builder to the testMapToPopulate.
+function addTestsForBuilder(builder, testMapToPopulate)
+{
+ var tests = g_resultsByBuilder[builder].tests;
+ for (var test in tests) {
+ testMapToPopulate[test] = true;
+ }
+}
+
+// Map of all tests to true values by platform and build type.
+// e.g. g_allTestsByPlatformAndBuildType['XP']['DEBUG'] will have the union
+// of all tests run on the xp-debug builders.
+var g_allTestsByPlatformAndBuildType = {};
+traversePlatformsTree(function(platform, platformName) {
+ g_allTestsByPlatformAndBuildType[platformName] = {};
+});
+
+// Map of all tests to true values by platform and build type.
+// e.g. g_allTestsByPlatformAndBuildType['WIN']['DEBUG'] will have the union
+// of all tests run on the win-debug builders.
+function allTestsWithSamePlatformAndBuildType(platform, buildType)
+{
+ if (!g_allTestsByPlatformAndBuildType[platform][buildType]) {
+ var tests = {};
+ for (var thisBuilder in g_builders) {
+ var thisBuilderBuildInfo = platformAndBuildType(thisBuilder);
+ if (thisBuilderBuildInfo.buildType == buildType && thisBuilderBuildInfo.platform == platform) {
+ addTestsForBuilder(thisBuilder, tests);
+ }
+ }
+ g_allTestsByPlatformAndBuildType[platform][buildType] = tests;
+ }
+
+ return g_allTestsByPlatformAndBuildType[platform][buildType];
+}
+
+function getExpectations(test, platform, buildType)
+{
+ var testObject = g_allExpectations[test];
+ if (!testObject)
+ return null;
+
+ var platformObject = testObject[platform];
+ if (!platformObject)
+ return null;
+
+ return platformObject[buildType];
+}
+
+function filterBugs(modifiers)
+{
+ var bugs = modifiers.match(/\b(Bug|webkit.org|crbug.com|code.google.com)\S*/g);
+ if (!bugs)
+ return {bugs: '', modifiers: modifiers};
+ for (var j = 0; j < bugs.length; j++)
+ modifiers = modifiers.replace(bugs[j], '');
+ return {bugs: bugs.join(' '), modifiers: collapseWhitespace(trimString(modifiers))};
+}
+
+function populateExpectationsData(resultsObject)
+{
+ var buildInfo = platformAndBuildType(resultsObject.builder);
+ var expectations = getExpectations(resultsObject.test, buildInfo.platform, buildInfo.buildType);
+ if (!expectations)
+ return;
+
+ resultsObject.expectations = expectations.expectations;
+ var filteredModifiers = filterBugs(expectations.modifiers);
+ resultsObject.modifiers = filteredModifiers.modifiers;
+ resultsObject.bugs = filteredModifiers.bugs;
+ resultsObject.isWontFixSkip = stringContains(expectations.modifiers, 'WONTFIX') || stringContains(expectations.modifiers, 'SKIP');
+}
+
+function platformObjectForName(platformName)
+{
+ var platformsList = platformName.split("_");
+ var platformObject = PLATFORMS[platformsList.shift()];
+ platformsList.forEach(function(platformName) {
+ platformObject = platformObject.subPlatforms[platformName];
+ });
+ return platformObject;
+}
+
+// Data structure to hold the processed expectations.
+// g_allExpectations[testPath][platform][buildType] gets the object that has
+// expectations and modifiers properties for this platform/buildType.
+//
+// platform and buildType both go through fallback sets of keys from most
+// specific key to least specific. For example, on Windows XP, we first
+// check the platform WIN-XP, if there's no such object, we check WIN,
+// then finally we check ALL. For build types, we check the current
+// buildType, then ALL.
+var g_allExpectations;
+
+function getParsedExpectations(data)
+{
+ var expectations = [];
+ var lines = data.split('\n');
+ lines.forEach(function(line) {
+ line = trimString(line);
+ if (!line || startsWith(line, '#'))
+ return;
+
+ // This code mimics _tokenize_line_using_new_format() in
+ // Tools/Scripts/webkitpy/layout_tests/models/test_expectations.py
+ //
+ // FIXME: consider doing more error checking here.
+ //
+ // FIXME: Clean this all up once we've fully cut over to the new syntax.
+ var tokens = line.split(/\s+/)
+ var parsed_bugs = [];
+ var parsed_modifiers = [];
+ var parsed_path;
+ var parsed_expectations = [];
+ var state = 'start';
+
+ // This clones _configuration_tokens_list in test_expectations.py.
+ // FIXME: unify with the platforms constants at the top of the file.
+ var configuration_tokens = {
+ 'Release': 'RELEASE',
+ 'Debug': 'DEBUG',
+ 'Mac': 'MAC',
+ 'Win': 'WIN',
+ 'Linux': 'LINUX',
+ 'SnowLeopard': 'SNOWLEOPARD',
+ 'Lion': 'LION',
+ 'MountainLion': 'MOUNTAINLION',
+ 'Win7': 'WIN7',
+ 'XP': 'XP',
+ 'Vista': 'VISTA',
+ 'Android': 'ANDROID',
+ };
+
+ var expectation_tokens = {
+ 'Crash': 'CRASH',
+ 'Failure': 'FAIL',
+ 'ImageOnlyFailure': 'IMAGE',
+ 'Missing': 'MISSING',
+ 'Pass': 'PASS',
+ 'Rebaseline': 'REBASELINE',
+ 'Skip': 'SKIP',
+ 'Slow': 'SLOW',
+ 'Timeout': 'TIMEOUT',
+ 'WontFix': 'WONTFIX',
+ };
+
+
+ tokens.forEach(function(token) {
+ if (token.indexOf('Bug') != -1 ||
+ token.indexOf('webkit.org') != -1 ||
+ token.indexOf('crbug.com') != -1 ||
+ token.indexOf('code.google.com') != -1) {
+ parsed_bugs.push(token);
+ } else if (token == '[') {
+ if (state == 'start') {
+ state = 'configuration';
+ } else if (state == 'name_found') {
+ state = 'expectations';
+ }
+ } else if (token == ']') {
+ if (state == 'configuration') {
+ state = 'name';
+ } else if (state == 'expectations') {
+ state = 'done';
+ }
+ } else if (state == 'configuration') {
+ parsed_modifiers.push(configuration_tokens[token]);
+ } else if (state == 'expectations') {
+ if (token == 'Rebaseline' || token == 'Skip' || token == 'Slow' || token == 'WontFix') {
+ parsed_modifiers.push(token.toUpperCase());
+ } else {
+ parsed_expectations.push(expectation_tokens[token]);
+ }
+ } else if (token == '#') {
+ state = 'done';
+ } else if (state == 'name' || state == 'start') {
+ parsed_path = token;
+ state = 'name_found';
+ }
+ });
+
+ if (!parsed_expectations.length) {
+ if (parsed_modifiers.indexOf('Slow') == -1) {
+ parsed_modifiers.push('Skip');
+ parsed_expectations = ['Pass'];
+ }
+ }
+
+ // FIXME: Should we include line number and comment lines here?
+ expectations.push({
+ modifiers: parsed_bugs.concat(parsed_modifiers).join(' '),
+ path: parsed_path,
+ expectations: parsed_expectations.join(' '),
+ });
+ });
+ return expectations;
+}
+
+
+function addTestToAllExpectationsForPlatform(test, platformName, expectations, modifiers)
+{
+ if (!g_allExpectations[test])
+ g_allExpectations[test] = {};
+
+ if (!g_allExpectations[test][platformName])
+ g_allExpectations[test][platformName] = {};
+
+ var allBuildTypes = [];
+ modifiers.split(' ').forEach(function(modifier) {
+ if (modifier in BUILD_TYPES) {
+ allBuildTypes.push(modifier);
+ return;
+ }
+ });
+ if (!allBuildTypes.length)
+ allBuildTypes = Object.keys(BUILD_TYPES);
+
+ allBuildTypes.forEach(function(buildType) {
+ g_allExpectations[test][platformName][buildType] = {modifiers: modifiers, expectations: expectations};
+ });
+}
+
+function processExpectationsForPlatform(platformObject, platformName, expectationsArray)
+{
+ if (!g_allExpectations)
+ g_allExpectations = {};
+
+ if (!expectationsArray)
+ return;
+
+ // Sort the array to hit more specific paths last. More specific
+ // paths (e.g. foo/bar/baz.html) override entries for less-specific ones (e.g. foo/bar).
+ expectationsArray.sort(alphanumericCompare('path'));
+
+ for (var i = 0; i < expectationsArray.length; i++) {
+ var path = expectationsArray[i].path;
+ var modifiers = expectationsArray[i].modifiers;
+ var expectations = expectationsArray[i].expectations;
+
+ var shouldProcessExpectation = false;
+ var hasPlatformModifierUnions = false;
+ if (platformObject.fallbackPlatforms) {
+ platformObject.fallbackPlatforms.forEach(function(fallbackPlatform) {
+ if (shouldProcessExpectation)
+ return;
+
+ var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+ if (!fallbackPlatformObject.platformModifierUnions)
+ return;
+
+ modifiers.split(' ').forEach(function(modifier) {
+ if (modifier in fallbackPlatformObject.platformModifierUnions) {
+ hasPlatformModifierUnions = true;
+ if (fallbackPlatformObject.platformModifierUnions[modifier].indexOf(platformName) != -1)
+ shouldProcessExpectation = true;
+ }
+ });
+ });
+ }
+
+ if (!hasPlatformModifierUnions)
+ shouldProcessExpectation = true;
+
+ if (!shouldProcessExpectation)
+ continue;
+
+ getAllTestsTrie().forEach(function(triePath) {
+ addTestToAllExpectationsForPlatform(triePath, platformName, expectations, modifiers);
+ }, path);
+ }
+}
+
+function processExpectations()
+{
+ // FIXME: An expectations-by-platform object should be passed into this function rather than checking
+ // for a global object. That way this function can be better tested and meaningful errors can
+ // be reported when expectations for a given platform are not found in that object.
+ if (!g_expectationsByPlatform)
+ return;
+
+ traversePlatformsTree(function(platform, platformName) {
+ if (platform.fallbackPlatforms) {
+ platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+ if (fallbackPlatform in g_expectationsByPlatform)
+ processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[fallbackPlatform]);
+ });
+ }
+
+ if (platformName in g_expectationsByPlatform)
+ processExpectationsForPlatform(platform, platformName, g_expectationsByPlatform[platformName]);
+ });
+
+ g_expectationsByPlatform = undefined;
+}
+
+function processMissingTestsWithExpectations(builder, platform, buildType)
+{
+ var noFailures = [];
+ var skipped = [];
+
+ var allTestsForPlatformAndBuildType = allTestsWithSamePlatformAndBuildType(platform, buildType);
+ for (var test in g_allExpectations) {
+ var expectations = getExpectations(test, platform, buildType);
+
+ if (!expectations)
+ continue;
+
+ // Test has expectations, but no result in the builders results.
+ // This means it's either SKIP or passes on all builds.
+ if (!allTestsForPlatformAndBuildType[test] && !stringContains(expectations.modifiers, 'WONTFIX')) {
+ if (stringContains(expectations.modifiers, 'SKIP'))
+ skipped.push(test);
+ else if (!expectations.expectations.match(/^\s*PASS\s*$/)) {
+ // Don't show tests expected to always pass. This is used in ways like
+ // the following:
+ // foo/bar = FAIL
+ // foo/bar/baz.html = PASS
+ noFailures.push({test: test, expectations: expectations.expectations, modifiers: expectations.modifiers});
+ }
+ }
+ }
+
+ g_perBuilderSkippedPaths[builder] = skipped.sort();
+ g_perBuilderWithExpectationsButNoFailures[builder] = noFailures.sort();
+}
+
+function processTestResultsForBuilderAsync(builder)
+{
+ setTimeout(function() { processTestRunsForBuilder(builder); }, 0);
+}
+
+function processTestRunsForAllBuilders()
+{
+ for (var builder in g_builders)
+ processTestRunsForBuilder(builder);
+}
+
+function processTestRunsForBuilder(builderName)
+{
+ if (g_perBuilderFailures[builderName])
+ return;
+
+ if (!g_resultsByBuilder[builderName]) {
+ console.error('No tests found for ' + builderName);
+ g_perBuilderFailures[builderName] = [];
+ return;
+ }
+
+ processExpectations();
+ var start = Date.now();
+
+ var buildInfo = platformAndBuildType(builderName);
+ var platform = buildInfo.platform;
+ var buildType = buildInfo.buildType;
+ processMissingTestsWithExpectations(builderName, platform, buildType);
+
+ var failures = [];
+ var allTestsForThisBuilder = g_resultsByBuilder[builderName].tests;
+
+ for (var test in allTestsForThisBuilder) {
+ var resultsForTest = createResultsObjectForTest(test, builderName);
+ populateExpectationsData(resultsForTest);
+
+ var rawTest = g_resultsByBuilder[builderName].tests[test];
+ resultsForTest.rawTimes = rawTest.times;
+ var rawResults = rawTest.results;
+ resultsForTest.rawResults = rawResults;
+
+ // FIXME: Switch to resultsByBuild
+ var times = resultsForTest.rawTimes;
+ var numTimesSeen = 0;
+ var numResultsSeen = 0;
+ var resultsIndex = 0;
+ var currentResult;
+ for (var i = 0; i < times.length; i++) {
+ numTimesSeen += times[i][RLE.LENGTH];
+
+ while (rawResults[resultsIndex] && numTimesSeen > (numResultsSeen + rawResults[resultsIndex][RLE.LENGTH])) {
+ numResultsSeen += rawResults[resultsIndex][RLE.LENGTH];
+ resultsIndex++;
+ }
+
+ if (rawResults && rawResults[resultsIndex])
+ currentResult = rawResults[resultsIndex][RLE.VALUE];
+
+ var time = times[i][RLE.VALUE]
+
+ // Ignore times for crashing/timeout runs for the sake of seeing if
+ // a test should be marked slow.
+ if (currentResult != 'C' && currentResult != 'T')
+ resultsForTest.slowestNonTimeoutCrashTime = Math.max(resultsForTest.slowestNonTimeoutCrashTime, time);
+ resultsForTest.slowestTime = Math.max(resultsForTest.slowestTime, time);
+ }
+
+ processMissingAndExtraExpectations(resultsForTest);
+ failures.push(resultsForTest);
+
+ if (!g_testToResultsMap[test])
+ g_testToResultsMap[test] = [];
+ g_testToResultsMap[test].push(resultsForTest);
+ }
+
+ g_perBuilderFailures[builderName] = failures;
+ logTime('processTestRunsForBuilder: ' + builderName, start);
+}
+
+function processMissingAndExtraExpectations(resultsForTest)
+{
+ // Heuristic for determining whether expectations apply to a given test:
+ // -If a test result happens < MIN_RUNS_FOR_FLAKE, then consider it a flaky
+ // result and include it in the list of expected results.
+ // -Otherwise, grab the first contiguous set of runs with the same result
+ // for >= MIN_RUNS_FOR_FLAKE and ignore all following runs >=
+ // MIN_RUNS_FOR_FLAKE.
+ // This lets us rule out common cases of a test changing expectations for
+ // a few runs, then being fixed or otherwise modified in a non-flaky way.
+ var rawResults = resultsForTest.rawResults;
+
+ // If the first result is no-data that means the test is skipped or is
+ // being run on a different builder (e.g. moved from one shard to another).
+ // Ignore these results since we have no real data about what's going on.
+ if (rawResults[0][RLE.VALUE] == 'N')
+ return;
+
+ // Only consider flake if it doesn't happen twice in a row.
+ var MIN_RUNS_FOR_FLAKE = 2;
+ var resultsMap = {}
+ var numResultsSeen = 0;
+ var haveSeenNonFlakeResult = false;
+ var numRealResults = 0;
+
+ var seenResults = {};
+ for (var i = 0; i < rawResults.length; i++) {
+ var numResults = rawResults[i][RLE.LENGTH];
+ numResultsSeen += numResults;
+
+ var result = rawResults[i][RLE.VALUE];
+
+ var hasMinRuns = numResults >= MIN_RUNS_FOR_FLAKE;
+ if (haveSeenNonFlakeResult && hasMinRuns)
+ continue;
+ else if (hasMinRuns)
+ haveSeenNonFlakeResult = true;
+ else if (!seenResults[result]) {
+ // Only consider a short-lived result if we've seen it more than once.
+ // Otherwise, we include lots of false-positives due to tests that fail
+ // for a couple runs and then start passing.
+ seenResults[result] = true;
+ continue;
+ }
+
+ var expectation = expectationsFileStringForResult(result);
+ resultsMap[expectation] = true;
+ numRealResults++;
+ }
+
+ resultsForTest.flips = i - 1;
+ resultsForTest.isFlaky = numRealResults > 1;
+
+ var missingExpectations = [];
+ var extraExpectations = [];
+
+ if (isLayoutTestResults()) {
+ var expectationsArray = resultsForTest.expectations ? resultsForTest.expectations.split(' ') : [];
+ extraExpectations = expectationsArray.filter(
+ function(element) {
+ // FIXME: Once all the FAIL lines are removed from
+ // TestExpectations, delete all the legacyExpectationsSemantics
+ // code.
+ if (g_currentState.legacyExpectationsSemantics) {
+ if (element == 'FAIL') {
+ for (var i = 0; i < FAIL_RESULTS.length; i++) {
+ if (resultsMap[FAIL_RESULTS[i]])
+ return false;
+ }
+ return true;
+ }
+ }
+
+ return element && !resultsMap[element] && !stringContains(element, 'BUG');
+ });
+
+ for (var result in resultsMap) {
+ resultsForTest.actualResults.push(result);
+ var hasExpectation = false;
+ for (var i = 0; i < expectationsArray.length; i++) {
+ var expectation = expectationsArray[i];
+ // FIXME: Once all the FAIL lines are removed from
+ // TestExpectations, delete all the legacyExpectationsSemantics
+ // code.
+ if (g_currentState.legacyExpectationsSemantics) {
+ if (expectation == 'FAIL') {
+ for (var j = 0; j < FAIL_RESULTS.length; j++) {
+ if (result == FAIL_RESULTS[j]) {
+ hasExpectation = true;
+ break;
+ }
+ }
+ }
+ }
+
+ if (result == expectation)
+ hasExpectation = true;
+
+ if (hasExpectation)
+ break;
+ }
+ // If we have no expectations for a test and it only passes, then don't
+ // list PASS as a missing expectation. We only want to list PASS if it
+ // flaky passes, so there would be other expectations.
+ if (!hasExpectation && !(!expectationsArray.length && result == 'PASS' && numRealResults == 1))
+ missingExpectations.push(result);
+ }
+
+ // Only highlight tests that take > 2 seconds as needing to be marked as
+ // slow. There are too many tests that take ~2 seconds every couple
+ // hundred runs. It's not worth the manual maintenance effort.
+ // Also, if a test times out, then it should not be marked as slow.
+ var minTimeForNeedsSlow = isDebug(resultsForTest.builder) ? 2 : 1;
+ if (isSlowTest(resultsForTest) && !resultsMap['TIMEOUT'] && (!resultsForTest.modifiers || !stringContains(resultsForTest.modifiers, 'SLOW')))
+ missingExpectations.push('SLOW');
+ else if (isFastTest(resultsForTest) && resultsForTest.modifiers && stringContains(resultsForTest.modifiers, 'SLOW'))
+ extraExpectations.push('SLOW');
+
+ // If there are no missing results or modifiers besides build
+ // type, platform, or bug and the expectations are all extra
+ // that is, extraExpectations - expectations = PASS,
+ // include PASS as extra, since that means this line in
+ // test_expectations can be deleted..
+ if (!missingExpectations.length && !(resultsForTest.modifiers && realModifiers(resultsForTest.modifiers))) {
+ var extraPlusPass = extraExpectations.concat(['PASS']);
+ if (extraPlusPass.sort().toString() == expectationsArray.slice(0).sort().toString())
+ extraExpectations.push('PASS');
+ }
+
+ }
+
+ resultsForTest.meetsExpectations = !missingExpectations.length && !extraExpectations.length;
+ resultsForTest.missing = missingExpectations.sort().join(' ');
+ resultsForTest.extra = extraExpectations.sort().join(' ');
+}
+
+
+var BUG_URL_PREFIX = '<a href="http://';
+var BUG_URL_POSTFIX = '/$1">crbug.com/$1</a> ';
+var WEBKIT_BUG_URL_POSTFIX = '/$1">webkit.org/b/$1</a> ';
+var INTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'b' + BUG_URL_POSTFIX;
+var EXTERNAL_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'crbug.com' + BUG_URL_POSTFIX;
+var WEBKIT_BUG_REPLACE_VALUE = BUG_URL_PREFIX + 'webkit.org/b' + WEBKIT_BUG_URL_POSTFIX;
+
+function htmlForBugs(bugs)
+{
+ bugs = bugs.replace(/crbug.com\/(\d+)(\ |$)/g, EXTERNAL_BUG_REPLACE_VALUE);
+ bugs = bugs.replace(/webkit.org\/b\/(\d+)(\ |$)/g, WEBKIT_BUG_REPLACE_VALUE);
+ return bugs;
+}
+
+function linkHTMLToOpenWindow(url, text)
+{
+ return '<a href="' + url + '" target="_blank">' + text + '</a>';
+}
+
+// FIXME: replaced with chromiumRevisionLink/webKitRevisionLink
+function createBlameListHTML(revisions, index, urlBase, separator, repo)
+{
+ var thisRevision = revisions[index];
+ if (!thisRevision)
+ return '';
+
+ var previousRevision = revisions[index + 1];
+ if (previousRevision && previousRevision != thisRevision) {
+ previousRevision++;
+ return linkHTMLToOpenWindow(urlBase + thisRevision + separator + previousRevision,
+ repo + ' blamelist r' + previousRevision + ':r' + thisRevision);
+ } else
+ return 'At ' + repo + ' revision: ' + thisRevision;
+}
+
+// Returns whether the result for index'th result for testName on builder was
+// a failure.
+function isFailure(builder, testName, index)
+{
+ var currentIndex = 0;
+ var rawResults = g_resultsByBuilder[builder].tests[testName].results;
+ for (var i = 0; i < rawResults.length; i++) {
+ currentIndex += rawResults[i][RLE.LENGTH];
+ if (currentIndex > index)
+ return isFailingResult(rawResults[i][RLE.VALUE]);
+ }
+ console.error('Index exceeds number of results: ' + index);
+}
+
+// Returns an array of indexes for all builds where this test failed.
+function indexesForFailures(builder, testName)
+{
+ var rawResults = g_resultsByBuilder[builder].tests[testName].results;
+ var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
+ var index = 0;
+ var failures = [];
+ for (var i = 0; i < rawResults.length; i++) {
+ var numResults = rawResults[i][RLE.LENGTH];
+ if (isFailingResult(rawResults[i][RLE.VALUE])) {
+ for (var j = 0; j < numResults; j++)
+ failures.push(index + j);
+ }
+ index += numResults;
+ }
+ return failures;
+}
+
+// Returns the path to the failure log for this non-webkit test.
+function pathToFailureLog(testName)
+{
+ return '/steps/' + g_crossDashboardState.testType + '/logs/' + testName.split('.')[1]
+}
+
+function showPopupForBuild(e, builder, index, opt_testName)
+{
+ var html = '';
+
+ var time = g_resultsByBuilder[builder].secondsSinceEpoch[index];
+ if (time) {
+ var date = new Date(time * 1000);
+ html += date.toLocaleDateString() + ' ' + date.toLocaleTimeString();
+ }
+
+ var buildNumber = g_resultsByBuilder[builder].buildNumbers[index];
+ var master = builderMaster(builder);
+ var buildBasePath = master.logPath(builder, buildNumber);
+
+ html += '<ul><li>' + linkHTMLToOpenWindow(buildBasePath, 'Build log') +
+ '</li><li>' +
+ createBlameListHTML(g_resultsByBuilder[builder].webkitRevision, index,
+ 'http://trac.webkit.org/log/?verbose=on&rev=', '&stop_rev=',
+ 'WebKit') +
+ '</li>';
+
+ if (master == WEBKIT_BUILDER_MASTER) {
+ var revision = g_resultsByBuilder[builder].webkitRevision[index];
+ html += '<li><span class=link onclick="setQueryParameter(\'revision\',' +
+ revision + ')">Show results for WebKit r' + revision +
+ '</span></li>';
+ } else {
+ html += '<li>' +
+ createBlameListHTML(g_resultsByBuilder[builder].chromeRevision, index,
+ 'http://build.chromium.org/f/chromium/perf/dashboard/ui/changelog.html?url=/trunk/src&mode=html&range=', ':', 'Chrome') +
+ '</li>';
+
+ var chromeRevision = g_resultsByBuilder[builder].chromeRevision[index];
+ if (chromeRevision && isLayoutTestResults()) {
+ html += '<li><a href="' + TEST_RESULTS_BASE_PATH + g_builders[builder] +
+ '/' + chromeRevision + '/layout-test-results.zip">layout-test-results.zip</a></li>';
+ }
+ }
+
+ if (!isLayoutTestResults() && opt_testName && isFailure(builder, opt_testName, index))
+ html += '<li>' + linkHTMLToOpenWindow(buildBasePath + pathToFailureLog(opt_testName), 'Failure log') + '</li>';
+
+ html += '</ul>';
+ showPopup(e.target, html);
+}
+
+function htmlForTestResults(test)
+{
+ var html = '';
+ var results = test.rawResults.concat();
+ var times = test.rawTimes.concat();
+ var builder = test.builder;
+ var master = builderMaster(builder);
+ var buildNumbers = g_resultsByBuilder[builder].buildNumbers;
+
+ var indexToReplaceCurrentResult = -1;
+ var indexToReplaceCurrentTime = -1;
+ var currentResultArray, currentTimeArray, currentResult, innerHTML, resultString;
+ for (var i = 0; i < buildNumbers.length; i++) {
+ if (i > indexToReplaceCurrentResult) {
+ currentResultArray = results.shift();
+ if (currentResultArray) {
+ currentResult = currentResultArray[RLE.VALUE];
+ // Treat simplified diff failures as just text failures.
+ if (currentResult == 'S')
+ currentResult = 'F';
+ indexToReplaceCurrentResult += currentResultArray[RLE.LENGTH];
+ } else {
+ currentResult = 'N';
+ indexToReplaceCurrentResult += buildNumbers.length;
+ }
+ resultString = expectationsFileStringForResult(currentResult);
+ }
+
+ if (i > indexToReplaceCurrentTime) {
+ currentTimeArray = times.shift();
+ var currentTime = 0;
+ if (currentResultArray) {
+ currentTime = currentTimeArray[RLE.VALUE];
+ indexToReplaceCurrentTime += currentTimeArray[RLE.LENGTH];
+ } else
+ indexToReplaceCurrentTime += buildNumbers.length;
+
+ innerHTML = currentTime || ' ';
+ }
+
+ var extraClassNames = '';
+ var webkitRevision = g_resultsByBuilder[builder].webkitRevision;
+ var isWebkitMerge = webkitRevision[i + 1] && webkitRevision[i] != webkitRevision[i + 1];
+ if (isWebkitMerge && master != WEBKIT_BUILDER_MASTER)
+ extraClassNames += ' merge';
+
+ html += '<td title="' + (resultString || 'NO DATA') + '. Click for more info." class="results ' + currentResult +
+ extraClassNames + '" onclick=\'showPopupForBuild(event, "' + builder + '",' + i + ',"' + test.test + '")\'>' + innerHTML;
+ }
+ return html;
+}
+
+function htmlForTestsWithExpectationsButNoFailures(builder)
+{
+ var tests = g_perBuilderWithExpectationsButNoFailures[builder];
+ var skippedPaths = g_perBuilderSkippedPaths[builder];
+ var showUnexpectedPassesLink = linkHTMLToToggleState('showUnexpectedPasses', 'tests that have not failed in last ' + g_resultsByBuilder[builder].buildNumbers.length + ' runs');
+ var showSkippedLink = linkHTMLToToggleState('showSkipped', 'skipped tests in TestExpectations');
+
+
+ var html = '';
+ if (tests.length || skippedPaths.length) {
+ var buildInfo = platformAndBuildType(builder);
+ html += '<h2 style="display:inline-block">Expectations for ' + buildInfo.platform + '-' + buildInfo.buildType + '</h2> ';
+ if (!g_currentState.showUnexpectedPasses && tests.length)
+ html += showUnexpectedPassesLink;
+ html += ' ';
+ if (!g_currentState.showSkipped && skippedPaths.length)
+ html += showSkippedLink;
+ }
+
+ var open = '<div onclick="selectContents(this)">';
+
+ if (g_currentState.showUnexpectedPasses && tests.length) {
+ html += '<div id="passing-tests">' + showUnexpectedPassesLink;
+ for (var i = 0; i < tests.length; i++)
+ html += open + tests[i].test + '</div>';
+ html += '</div>';
+ }
+
+ if (g_currentState.showSkipped && skippedPaths.length)
+ html += '<div id="skipped-tests">' + showSkippedLink + open + skippedPaths.join('</div>' + open) + '</div></div>';
+ return html + '<br>';
+}
+
+// Returns whether we should exclude test results from the test table.
+function shouldHideTest(testResult)
+{
+ if (testResult.isWontFixSkip)
+ return !g_currentState.showWontFixSkip;
+
+ if (testResult.isFlaky)
+ return !g_currentState.showFlaky;
+
+ if (isSlowTest(testResult))
+ return !g_currentState.showSlow;
+
+ if (testResult.meetsExpectations)
+ return !g_currentState.showCorrectExpectations;
+
+ return !g_currentState.showWrongExpectations;
+}
+
+// Sets the browser's selection to the element's contents.
+function selectContents(element)
+{
+ window.getSelection().selectAllChildren(element);
+}
+
+function createBugHTML(test)
+{
+ var symptom = test.isFlaky ? 'flaky' : 'failing';
+ var title = encodeURIComponent('Layout Test ' + test.test + ' is ' + symptom);
+ var description = encodeURIComponent('The following layout test is ' + symptom + ' on ' +
+ '[insert platform]\n\n' + test.test + '\n\nProbable cause:\n\n' +
+ '[insert probable cause]');
+
+ var component = encodeURIComponent('Tools / Tests');
+ url = 'https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=' + component + '&short_desc=' + title + '&comment=' + description;
+ return '<a href="' + url + '" class="file-bug">FILE BUG</a>';
+}
+
+function isCrossBuilderView()
+{
+ return g_currentState.tests || g_currentState.result || g_currentState.expectationsUpdate;
+}
+
+function tableHeaders(opt_getAll)
+{
+ var headers = [];
+ if (isCrossBuilderView() || opt_getAll)
+ headers.push('builder');
+
+ if (!isCrossBuilderView() || opt_getAll)
+ headers.push('test');
+
+ if (isLayoutTestResults() || opt_getAll)
+ headers.push('bugs', 'modifiers', 'expectations');
+
+ headers.push('slowest run', 'flakiness (numbers are runtimes in seconds)');
+ return headers;
+}
+
+function htmlForSingleTestRow(test)
+{
+ if (!isCrossBuilderView() && shouldHideTest(test)) {
+ // The innerHTML call is considerably faster if we exclude the rows for
+ // items we're not showing than if we hide them using display:none.
+ // For the crossBuilderView, we want to show all rows the user is
+ // explicitly listing tests to view.
+ return '';
+ }
+
+ var headers = tableHeaders();
+ var html = '';
+ for (var i = 0; i < headers.length; i++) {
+ var header = headers[i];
+ if (startsWith(header, 'test') || startsWith(header, 'builder')) {
+ // If isCrossBuilderView() is true, we're just viewing a single test
+ // with results for many builders, so the first column is builder names
+ // instead of test paths.
+ var testCellClassName = 'test-link' + (isCrossBuilderView() ? ' builder-name' : '');
+ var testCellHTML = isCrossBuilderView() ? test.builder : '<span class="link" onclick="setQueryParameter(\'tests\',\'' + test.test +'\');">' + test.test + '</span>';
+
+ html += '<tr><td class="' + testCellClassName + '">' + testCellHTML;
+ } else if (startsWith(header, 'bugs'))
+ html += '<td class=options-container>' + (test.bugs ? htmlForBugs(test.bugs) : createBugHTML(test));
+ else if (startsWith(header, 'modifiers'))
+ html += '<td class=options-container>' + test.modifiers;
+ else if (startsWith(header, 'expectations'))
+ html += '<td class=options-container>' + test.expectations;
+ else if (startsWith(header, 'slowest'))
+ html += '<td>' + (test.slowestTime ? test.slowestTime + 's' : '');
+ else if (startsWith(header, 'flakiness'))
+ html += htmlForTestResults(test);
+ }
+ return html;
+}
+
+function sortColumnFromTableHeader(headerText)
+{
+ return headerText.split(' ', 1)[0];
+}
+
+function htmlForTableColumnHeader(headerName, opt_fillColSpan)
+{
+ // Use the first word of the header title as the sortkey
+ var thisSortValue = sortColumnFromTableHeader(headerName);
+ var arrowHTML = thisSortValue == g_currentState.sortColumn ?
+ '<span class=' + g_currentState.sortOrder + '>' + (g_currentState.sortOrder == FORWARD ? '↑' : '↓' ) + '</span>' : '';
+ return '<th sortValue=' + thisSortValue +
+ // Extend last th through all the rest of the columns.
+ (opt_fillColSpan ? ' colspan=10000' : '') +
+ // Extra span here is so flex boxing actually centers.
+ // There's probably a better way to do this with CSS only though.
+ '><div class=table-header-content><span></span>' + arrowHTML +
+ '<span class=header-text>' + headerName + '</span>' + arrowHTML + '</div></th>';
+}
+
+function htmlForTestTable(rowsHTML, opt_excludeHeaders)
+{
+ var html = '<table class=test-table>';
+ if (!opt_excludeHeaders) {
+ html += '<thead><tr>';
+ var headers = tableHeaders();
+ for (var i = 0; i < headers.length; i++)
+ html += htmlForTableColumnHeader(headers[i], i == headers.length - 1);
+ html += '</tr></thead>';
+ }
+ return html + '<tbody>' + rowsHTML + '</tbody></table>';
+}
+
+function appendHTML(html)
+{
+ var startTime = Date.now();
+ // InnerHTML to a div that's not in the document. This is
+ // ~300ms faster in Safari 4 and Chrome 4 on mac.
+ var div = document.createElement('div');
+ div.innerHTML = html;
+ document.body.appendChild(div);
+ logTime('Time to innerHTML', startTime);
+ postHeightChangedMessage();
+}
+
+function alphanumericCompare(column, reverse)
+{
+ return reversibleCompareFunction(function(a, b) {
+ // Put null entries at the bottom
+ var a = a[column] ? String(a[column]) : 'z';
+ var b = b[column] ? String(b[column]) : 'z';
+
+ if (a < b)
+ return -1;
+ else if (a == b)
+ return 0;
+ else
+ return 1;
+ }, reverse);
+}
+
+function numericSort(column, reverse)
+{
+ return reversibleCompareFunction(function(a, b) {
+ a = parseFloat(a[column]);
+ b = parseFloat(b[column]);
+ return a - b;
+ }, reverse);
+}
+
+function reversibleCompareFunction(compare, reverse)
+{
+ return function(a, b) {
+ return compare(reverse ? b : a, reverse ? a : b);
+ };
+}
+
+function changeSort(e)
+{
+ var target = e.currentTarget;
+ e.preventDefault();
+
+ var sortValue = target.getAttribute('sortValue');
+ while (target && target.tagName != 'TABLE')
+ target = target.parentNode;
+
+ var sort = 'sortColumn';
+ var orderKey = 'sortOrder';
+ if (sortValue == g_currentState[sort] && g_currentState[orderKey] == FORWARD)
+ order = BACKWARD;
+ else
+ order = FORWARD;
+
+ setQueryParameter(sort, sortValue, orderKey, order);
+}
+
+function sortTests(tests, column, order)
+{
+ var resultsProperty, sortFunctionGetter;
+ if (column == 'flakiness') {
+ sortFunctionGetter = numericSort;
+ resultsProperty = 'flips';
+ } else if (column == 'slowest') {
+ sortFunctionGetter = numericSort;
+ resultsProperty = 'slowestTime';
+ } else {
+ sortFunctionGetter = alphanumericCompare;
+ resultsProperty = column;
+ }
+
+ tests.sort(sortFunctionGetter(resultsProperty, order == BACKWARD));
+}
+
+// Sorts a space separated expectations string in alphanumeric order.
+// @param {string} str The expectations string.
+// @return {string} The sorted string.
+function sortExpectationsString(str)
+{
+ return str.split(' ').sort().join(' ');
+}
+
+function addUpdate(testsNeedingUpdate, test, builderName, missing, extra)
+{
+ if (!testsNeedingUpdate[test])
+ testsNeedingUpdate[test] = {};
+
+ var buildInfo = platformAndBuildType(builderName);
+ var builder = buildInfo.platform + ' ' + buildInfo.buildType;
+ if (!testsNeedingUpdate[test][builder])
+ testsNeedingUpdate[test][builder] = {};
+
+ if (missing)
+ testsNeedingUpdate[test][builder].missing = sortExpectationsString(missing);
+
+ if (extra)
+ testsNeedingUpdate[test][builder].extra = sortExpectationsString(extra);
+}
+
+
+// From a string of modifiers, returns a string of modifiers that
+// are for real result changes, like SLOW, and excludes modifiers
+// that specificy things like platform, build_type, bug.
+// @param {string} modifierString String containing all modifiers.
+// @return {string} String containing only modifiers that effect the results.
+function realModifiers(modifierString)
+{
+ var modifiers = modifierString.split(' ');;
+ return modifiers.filter(function(modifier) {
+ if (modifier in BUILD_TYPES || startsWith(modifier, 'BUG'))
+ return false;
+
+ var matchesPlatformOrUnion = false;
+ traversePlatformsTree(function(platform, platformName) {
+ if (matchesPlatformOrUnion)
+ return;
+
+ if (platform.fallbackPlatforms) {
+ platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+ if (matchesPlatformOrUnion)
+ return;
+
+ var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+ if (!fallbackPlatformObject.platformModifierUnions)
+ return;
+
+ matchesPlatformOrUnion = modifier in fallbackPlatformObject.subPlatforms || modifier in fallbackPlatformObject.platformModifierUnions;
+ });
+ }
+ });
+
+ return !matchesPlatformOrUnion;
+ }).join(' ');
+}
+
+function generatePageForExpectationsUpdate()
+{
+ // Always show all runs when auto-updating expectations.
+ if (!g_crossDashboardState.showAllRuns)
+ setQueryParameter('showAllRuns', true);
+
+ processTestRunsForAllBuilders();
+ var testsNeedingUpdate = {};
+ for (var test in g_testToResultsMap) {
+ var results = g_testToResultsMap[test];
+ for (var i = 0; i < results.length; i++) {
+ var thisResult = results[i];
+
+ if (!thisResult.missing && !thisResult.extra)
+ continue;
+
+ var allPassesOrNoDatas = thisResult.rawResults.filter(function (x) { return x[1] != "P" && x[1] != "N"; }).length == 0;
+
+ if (allPassesOrNoDatas)
+ continue;
+
+ addUpdate(testsNeedingUpdate, test, thisResult.builder, thisResult.missing, thisResult.extra);
+ }
+ }
+
+ for (var builder in g_builders) {
+ var tests = g_perBuilderWithExpectationsButNoFailures[builder]
+ for (var i = 0; i < tests.length; i++) {
+ // Anything extra in this case is what is listed in expectations
+ // plus modifiers other than bug, platform, build type.
+ var modifiers = realModifiers(tests[i].modifiers);
+ var extras = tests[i].expectations;
+ extras += modifiers ? ' ' + modifiers : '';
+ addUpdate(testsNeedingUpdate, tests[i].test, builder, null, extras);
+ }
+ }
+
+ // Get the keys in alphabetical order, so it is easy to process groups
+ // of tests.
+ var keys = Object.keys(testsNeedingUpdate).sort();
+ showUpdateInfoForTest(testsNeedingUpdate, keys);
+}
+
+// Show the test results and the json for differing expectations, and
+// allow the user to include or exclude this update.
+//
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function showUpdateInfoForTest(testsNeedingUpdate, keys)
+{
+ var test = keys[g_currentState.updateIndex];
+ document.body.innerHTML = '';
+
+ // FIXME: Make this DOM creation less verbose.
+ var index = document.createElement('div');
+ index.style.cssFloat = 'right';
+ index.textContent = (g_currentState.updateIndex + 1) + ' of ' + keys.length + ' tests';
+ document.body.appendChild(index);
+
+ var buttonRegion = document.createElement('div');
+ var includeBtn = document.createElement('input');
+ includeBtn.type = 'button';
+ includeBtn.value = 'include selected';
+ includeBtn.addEventListener('click', partial(handleUpdate, testsNeedingUpdate, keys), false);
+ buttonRegion.appendChild(includeBtn);
+
+ var previousBtn = document.createElement('input');
+ previousBtn.type = 'button';
+ previousBtn.value = 'previous';
+ previousBtn.addEventListener('click',
+ function() {
+ setUpdateIndex(g_currentState.updateIndex - 1, testsNeedingUpdate, keys);
+ },
+ false);
+ buttonRegion.appendChild(previousBtn);
+
+ var nextBtn = document.createElement('input');
+ nextBtn.type = 'button';
+ nextBtn.value = 'next';
+ nextBtn.addEventListener('click', partial(nextUpdate, testsNeedingUpdate, keys), false);
+ buttonRegion.appendChild(nextBtn);
+
+ var doneBtn = document.createElement('input');
+ doneBtn.type = 'button';
+ doneBtn.value = 'done';
+ doneBtn.addEventListener('click', finishUpdate, false);
+ buttonRegion.appendChild(doneBtn);
+
+ document.body.appendChild(buttonRegion);
+
+ var updates = testsNeedingUpdate[test];
+ var checkboxes = document.createElement('div');
+ for (var builder in updates) {
+ // Create a checkbox for each builder.
+ var checkboxRegion = document.createElement('div');
+ var checkbox = document.createElement('input');
+ checkbox.type = 'checkbox';
+ checkbox.id = builder;
+ checkbox.checked = true;
+ checkboxRegion.appendChild(checkbox);
+ checkboxRegion.appendChild(document.createTextNode(builder + ' : ' + JSON.stringify(updates[builder])));
+ checkboxes.appendChild(checkboxRegion);
+ }
+ document.body.appendChild(checkboxes);
+
+ var div = document.createElement('div');
+ div.innerHTML = htmlForIndividualTestOnAllBuildersWithResultsLinks(test);
+ document.body.appendChild(div);
+ appendExpectations();
+}
+
+
+// When the user has finished selecting expectations to update, provide them
+// with json to copy over.
+function finishUpdate()
+{
+ document.body.innerHTML = 'The next step is to copy the output below ' +
+ 'into a local file and save it. Then, run<br><code>python ' +
+ 'src/webkit/tools/layout_tests/webkitpy/layout_tests/update_expectat' +
+ 'ions_from_dashboard.py path/to/local/file</code><br>in order to ' +
+ 'update the expectations file.<br><textarea id="results" '+
+ 'style="width:600px;height:600px;"> ' +
+ JSON.stringify(g_confirmedTests) + '</textarea>';
+ results.focus();
+ document.execCommand('SelectAll');
+}
+
+// Handle user click on "include selected" button.
+// Includes the tests that are selected and exclude the rest.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function handleUpdate(testsNeedingUpdate, keys)
+{
+ var test = keys[g_currentState.updateIndex];
+ var updates = testsNeedingUpdate[test];
+ for (var builder in updates) {
+ // Add included tests, and delete excluded tests if
+ // they were previously included.
+ if ($(builder).checked) {
+ if (!g_confirmedTests[test])
+ g_confirmedTests[test] = {};
+ g_confirmedTests[test][builder] = testsNeedingUpdate[test][builder];
+ } else if (g_confirmedTests[test] && g_confirmedTests[test][builder]) {
+ delete g_confirmedTests[test][builder];
+ if (!Object.keys(g_confirmedTests[test]).length)
+ delete g_confirmedTests[test];
+ }
+ }
+ nextUpdate(testsNeedingUpdate, keys);
+}
+
+
+// Move to the next item to update.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function nextUpdate(testsNeedingUpdate, keys)
+{
+ setUpdateIndex(g_currentState.updateIndex + 1, testsNeedingUpdate, keys);
+}
+
+
+// Advance the index we are updating at. If we walk over the end
+// or beginning, just loop.
+// @param {string} newIndex The index into the keys to move to.
+// @param {Object} testsNeedingUpdate Tests that need updating.
+// @param {Array.<string>} keys Keys into the testNeedingUpdate object.
+function setUpdateIndex(newIndex, testsNeedingUpdate, keys)
+{
+ if (newIndex == -1)
+ newIndex = keys.length - 1;
+ else if (newIndex == keys.length)
+ newIndex = 0;
+ setQueryParameter("updateIndex", newIndex);
+ showUpdateInfoForTest(testsNeedingUpdate, keys);
+}
+
+function htmlForIndividualTestOnAllBuilders(test)
+{
+ processTestRunsForAllBuilders();
+
+ var testResults = g_testToResultsMap[test];
+ if (!testResults)
+ return '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>';
+
+ var html = '';
+ var shownBuilders = [];
+ for (var j = 0; j < testResults.length; j++) {
+ shownBuilders.push(testResults[j].builder);
+ html += htmlForSingleTestRow(testResults[j]);
+ }
+
+ var skippedBuilders = []
+ for (builder in currentBuilderGroup().builders) {
+ if (shownBuilders.indexOf(builder) == -1)
+ skippedBuilders.push(builder);
+ }
+
+ var skippedBuildersHtml = '';
+ if (skippedBuilders.length) {
+ skippedBuildersHtml = '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+ '<div class=skipped-builder-list><div class=skipped-builder>' + skippedBuilders.join('</div><div class=skipped-builder>') + '</div></div>';
+ }
+
+ return htmlForTestTable(html) + skippedBuildersHtml;
+}
+
+function htmlForIndividualTestOnAllBuildersWithResultsLinks(test)
+{
+ processTestRunsForAllBuilders();
+
+ var testResults = g_testToResultsMap[test];
+ var html = '';
+ html += htmlForIndividualTestOnAllBuilders(test);
+
+ html += '<div class=expectations test=' + test + '><div>' +
+ linkHTMLToToggleState('showExpectations', 'results')
+
+ if (isLayoutTestResults() || isGPUTestResults()) {
+ if (isLayoutTestResults())
+ html += ' | ' + linkHTMLToToggleState('showLargeExpectations', 'large thumbnails');
+ if (testResults && builderMaster(testResults[0].builder) == WEBKIT_BUILDER_MASTER) {
+ var revision = g_currentState.revision || '';
+ html += '<form onsubmit="setQueryParameter(\'revision\', revision.value);' +
+ 'return false;">Show results for WebKit revision: ' +
+ '<input name=revision placeholder="e.g. 65540" value="' + revision +
+ '" id=revision-input></form>';
+ } else
+ html += ' | <b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>';
+ } else {
+ html += ' | <span>Results height:<input ' +
+ 'onchange="setQueryParameter(\'resultsHeight\',this.value)" value="' +
+ g_currentState.resultsHeight + '" style="width:2.5em">px</span>';
+ }
+ html += '</div></div>';
+ return html;
+}
+
+function getExpectationsContainer(expectationsContainers, parentContainer, expectationsType)
+{
+ if (!expectationsContainers[expectationsType]) {
+ var container = document.createElement('div');
+ container.className = 'expectations-container';
+ parentContainer.appendChild(container);
+ expectationsContainers[expectationsType] = container;
+ }
+ return expectationsContainers[expectationsType];
+}
+
+function ensureTrailingSlash(path)
+{
+ if (path.match(/\/$/))
+ return path;
+ return path + '/';
+}
+
+function maybeAddPngChecksum(expectationDiv, pngUrl)
+{
+ // pngUrl gets served from the browser cache since we just loaded it in an
+ // <img> tag.
+ loader.request(pngUrl,
+ function(xhr) {
+ // Convert the first 2k of the response to a byte string.
+ var bytes = xhr.responseText.substring(0, 2048);
+ for (var position = 0; position < bytes.length; ++position)
+ bytes[position] = bytes[position] & 0xff;
+
+ // Look for the comment.
+ var commentKey = 'tEXtchecksum\x00';
+ var checksumPosition = bytes.indexOf(commentKey);
+ if (checksumPosition == -1)
+ return;
+
+ var checksum = bytes.substring(checksumPosition + commentKey.length, checksumPosition + commentKey.length + 32);
+ var checksumContainer = document.createElement('span');
+ checksumContainer.innerText = 'Embedded checksum: ' + checksum;
+ checksumContainer.setAttribute('class', 'pngchecksum');
+ expectationDiv.parentNode.appendChild(checksumContainer);
+ },
+ function(xhr) {},
+ true);
+}
+
+// Adds a specific expectation. If it's an image, it's only added on the
+// image's onload handler. If it's a text file, then a script tag is appended
+// as a hack to see if the file 404s (necessary since it's cross-domain).
+// Once all the expectations for a specific type have loaded or errored
+// (e.g. all the text results), then we go through and identify which platform
+// uses which expectation.
+//
+// @param {Object} expectationsContainers Map from expectations type to
+// container DIV.
+// @param {Element} parentContainer Container element for
+// expectationsContainer divs.
+// @param {string} platform Platform string. Empty string for non-platform
+// specific expectations.
+// @param {string} path Relative path to the expectation.
+// @param {string} base Base path for the expectation URL.
+// @param {string} opt_builder Builder whose actual results this expectation
+// points to.
+// @param {string} opt_suite "virtual suite" that the test belongs to, if any.
+function addExpectationItem(expectationsContainers, parentContainer, platform, path, base, opt_builder, opt_suite)
+{
+ var parts = path.split('.')
+ var fileExtension = parts[parts.length - 1];
+ if (fileExtension == 'html')
+ fileExtension = 'txt';
+
+ var container = getExpectationsContainer(expectationsContainers, parentContainer, fileExtension);
+ var isImage = path.match(/\.png$/);
+
+ // FIXME: Stop using script tags once all the places we pull from support CORS.
+ var platformPart = platform ? ensureTrailingSlash(platform) : '';
+ var suitePart = opt_suite ? ensureTrailingSlash(opt_suite) : '';
+
+ var childContainer = document.createElement('span');
+ childContainer.className = 'unloaded';
+
+ var appendExpectationsItem = function(item) {
+ childContainer.appendChild(expectationsTitle(platformPart + suitePart, path, opt_builder));
+ childContainer.className = 'expectations-item';
+ item.className = 'expectation ' + fileExtension;
+ if (g_currentState.showLargeExpectations)
+ item.className += ' large';
+ childContainer.appendChild(item);
+ handleFinishedLoadingExpectations(container);
+ };
+
+ var url = base + platformPart + path;
+ if (isImage || !startsWith(base, 'http://svn.webkit.org')) {
+ var dummyNode = document.createElement(isImage ? 'img' : 'script');
+ dummyNode.src = url;
+ dummyNode.onload = function() {
+ var item;
+ if (isImage) {
+ item = dummyNode;
+ if (startsWith(base, 'http://svn.webkit.org'))
+ maybeAddPngChecksum(item, url);
+ } else {
+ item = document.createElement('iframe');
+ item.src = url;
+ }
+ appendExpectationsItem(item);
+ }
+ dummyNode.onerror = function() {
+ childContainer.parentNode.removeChild(childContainer);
+ handleFinishedLoadingExpectations(container);
+ }
+
+ // Append script elements now so that they load. Images load without being
+ // appended to the DOM.
+ if (!isImage)
+ childContainer.appendChild(dummyNode);
+ } else {
+ loader.request(url,
+ function(xhr) {
+ var item = document.createElement('pre');
+ item.innerText = xhr.responseText;
+ appendExpectationsItem(item);
+ },
+ function(xhr) {/* Do nothing on errors since they're expected */});
+ }
+
+ container.appendChild(childContainer);
+}
+
+
+// Identifies which expectations are used on which platform once all the
+// expectations of a given type have loaded (e.g. the container for png
+// expectations for this test had no child elements with the class
+// "unloaded").
+//
+// @param {string} container Element containing the expectations for a given
+// test and a given type (e.g. png).
+function handleFinishedLoadingExpectations(container)
+{
+ if (container.getElementsByClassName('unloaded').length)
+ return;
+
+ var titles = container.getElementsByClassName('expectations-title');
+ for (var platform in g_fallbacksMap) {
+ var fallbacks = g_fallbacksMap[platform];
+ var winner = null;
+ var winningIndex = -1;
+ for (var i = 0; i < titles.length; i++) {
+ var title = titles[i];
+
+ if (!winner && title.platform == "") {
+ winner = title;
+ continue;
+ }
+
+ var rawPlatform = title.platform && title.platform.replace('platform/', '');
+ for (var j = 0; j < fallbacks.length; j++) {
+ if ((winningIndex == -1 || winningIndex > j) && rawPlatform == fallbacks[j]) {
+ winningIndex = j;
+ winner = title;
+ break;
+ }
+ }
+ }
+ if (winner)
+ winner.getElementsByClassName('platforms')[0].innerHTML += '<div class=used-platform>' + platform + '</div>';
+ else {
+ console.log('No expectations identified for this test. This means ' +
+ 'there is a logic bug in the dashboard for which expectations a ' +
+ 'platform uses or trac.webkit.org/src.chromium.org is giving 5XXs.');
+ }
+ }
+
+ consolidateUsedPlatforms(container);
+}
+
+// Consolidate platforms when all sub-platforms for a given platform are represented.
+// e.g., if all of the WIN- platforms are there, replace them with just WIN.
+function consolidateUsedPlatforms(container)
+{
+ var allPlatforms = Object.keys(g_fallbacksMap);
+
+ var platformElements = container.getElementsByClassName('platforms');
+ for (var i = 0, platformsLength = platformElements.length; i < platformsLength; i++) {
+ var usedPlatforms = platformElements[i].getElementsByClassName('used-platform');
+ if (!usedPlatforms.length)
+ continue;
+
+ var platforms = {};
+ platforms['MAC'] = {};
+ platforms['WIN'] = {};
+ platforms['LINUX'] = {};
+ allPlatforms.forEach(function(platform) {
+ if (startsWith(platform, 'MAC'))
+ platforms['MAC'][platform] = 1;
+ else if (startsWith(platform, 'WIN'))
+ platforms['WIN'][platform] = 1;
+ else if (startsWith(platform, 'LINUX'))
+ platforms['LINUX'][platform] = 1;
+ });
+
+ for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
+ for (var platform in platforms)
+ delete platforms[platform][usedPlatforms[j].textContent];
+ }
+
+ for (var platform in platforms) {
+ if (!Object.keys(platforms[platform]).length) {
+ var nodesToRemove = [];
+ for (var j = 0, usedPlatformsLength = usedPlatforms.length; j < usedPlatformsLength; j++) {
+ var usedPlatform = usedPlatforms[j];
+ if (startsWith(usedPlatform.textContent, platform))
+ nodesToRemove.push(usedPlatform);
+ }
+
+ nodesToRemove.forEach(function(element) { element.parentNode.removeChild(element); });
+ platformElements[i].insertAdjacentHTML('afterBegin', '<div class=used-platform>' + platform + '</div>');
+ }
+ }
+ }
+}
+
+function addExpectations(expectationsContainers, container, base,
+ platform, text, png, reftest_html_file, reftest_mismatch_html_file, suite)
+{
+ var builder = '';
+ addExpectationItem(expectationsContainers, container, platform, text, base, builder, suite);
+ addExpectationItem(expectationsContainers, container, platform, png, base, builder, suite);
+ addExpectationItem(expectationsContainers, container, platform, reftest_html_file, base, builder, suite);
+ addExpectationItem(expectationsContainers, container, platform, reftest_mismatch_html_file, base, builder, suite);
+}
+
+function expectationsTitle(platform, path, builder)
+{
+ var header = document.createElement('h3');
+ header.className = 'expectations-title';
+
+ var innerHTML;
+ if (builder) {
+ var resultsType;
+ if (endsWith(path, '-crash-log.txt'))
+ resultsType = 'STACKTRACE';
+ else if (endsWith(path, '-actual.txt') || endsWith(path, '-actual.png'))
+ resultsType = 'ACTUAL RESULTS';
+ else if (endsWith(path, '-wdiff.html'))
+ resultsType = 'WDIFF';
+ else
+ resultsType = 'DIFF';
+
+ innerHTML = resultsType + ': ' + builder;
+ } else if (platform === "") {
+ var parts = path.split('/');
+ innerHTML = parts[parts.length - 1];
+ } else
+ innerHTML = platform || path;
+
+ header.innerHTML = '<div class=title>' + innerHTML +
+ '</div><div style="float:left"> </div>' +
+ '<div class=platforms style="float:right"></div>';
+ header.platform = platform;
+ return header;
+}
+
+function loadExpectations(expectationsContainer)
+{
+ var test = expectationsContainer.getAttribute('test');
+ if (isLayoutTestResults())
+ loadExpectationsLayoutTests(test, expectationsContainer);
+ else {
+ var results = g_testToResultsMap[test];
+ for (var i = 0; i < results.length; i++)
+ if (isGPUTestResults())
+ loadGPUResultsForBuilder(results[i].builder, test, expectationsContainer);
+ else
+ loadNonWebKitResultsForBuilder(results[i].builder, test, expectationsContainer);
+ }
+}
+
+function gpuResultsPath(chromeRevision, builder)
+{
+ return chromeRevision + '_' + builder.replace(/[^A-Za-z0-9]+/g, '_');
+}
+
+function loadGPUResultsForBuilder(builder, test, expectationsContainer)
+{
+ var container = document.createElement('div');
+ container.className = 'expectations-container';
+ container.innerHTML = '<div><b>' + builder + '</b></div>';
+ expectationsContainer.appendChild(container);
+
+ var failureIndex = indexesForFailures(builder, test)[0];
+
+ var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndex];
+ var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
+
+ var chromeRevision = g_resultsByBuilder[builder].chromeRevision[failureIndex];
+ var resultsUrl = GPU_RESULTS_BASE_PATH + gpuResultsPath(chromeRevision, builder);
+ var filename = test.split(/\./)[1] + '.png';
+
+ appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
+ appendNonWebKitResults(container, resultsUrl + '/gen/' + filename, 'gpu-test-results', 'Generated');
+ appendNonWebKitResults(container, resultsUrl + '/ref/' + filename, 'gpu-test-results', 'Reference');
+ appendNonWebKitResults(container, resultsUrl + '/diff/' + filename, 'gpu-test-results', 'Diff');
+}
+
+function loadNonWebKitResultsForBuilder(builder, test, expectationsContainer)
+{
+ var failureIndexes = indexesForFailures(builder, test);
+ var container = document.createElement('div');
+ container.innerHTML = '<div><b>' + builder + '</b></div>';
+ expectationsContainer.appendChild(container);
+ for (var i = 0; i < failureIndexes.length; i++) {
+ // FIXME: This doesn't seem to work anymore. Did the paths change?
+ // Once that's resolved, see if we need to try each GTEST_MODIFIERS prefix as well.
+ var buildNumber = g_resultsByBuilder[builder].buildNumbers[failureIndexes[i]];
+ var pathToLog = builderMaster(builder).logPath(builder, buildNumber) + pathToFailureLog(test);
+ appendNonWebKitResults(container, pathToLog, 'non-webkit-results');
+ }
+}
+
+function appendNonWebKitResults(container, url, itemClassName, opt_title)
+{
+ // Use a script tag to detect whether the URL 404s.
+ // Need to use a script tag since the URL is cross-domain.
+ var dummyNode = document.createElement('script');
+ dummyNode.src = url;
+
+ dummyNode.onload = function() {
+ var item = document.createElement('iframe');
+ item.src = dummyNode.src;
+ item.className = itemClassName;
+ item.style.height = g_currentState.resultsHeight + 'px';
+
+ if (opt_title) {
+ var childContainer = document.createElement('div');
+ childContainer.style.display = 'inline-block';
+ var title = document.createElement('div');
+ title.textContent = opt_title;
+ childContainer.appendChild(title);
+ childContainer.appendChild(item);
+ container.replaceChild(childContainer, dummyNode);
+ } else
+ container.replaceChild(item, dummyNode);
+ }
+ dummyNode.onerror = function() {
+ container.removeChild(dummyNode);
+ }
+
+ container.appendChild(dummyNode);
+}
+
+function buildInfoForRevision(builder, revision)
+{
+ var revisions = g_resultsByBuilder[builder].webkitRevision;
+ var revisionStart = 0, revisionEnd = 0, buildNumber = 0;
+ for (var i = 0; i < revisions.length; i++) {
+ if (revision > revisions[i]) {
+ revisionStart = revisions[i - 1];
+ revisionEnd = revisions[i];
+ buildNumber = g_resultsByBuilder[builder].buildNumbers[i - 1];
+ break;
+ }
+ }
+
+ if (revisionEnd)
+ revisionEnd++;
+ else
+ revisionEnd = '';
+
+ return {revisionStart: revisionStart, revisionEnd: revisionEnd, buildNumber: buildNumber};
+}
+
+function lookupVirtualTestSuite(test) {
+ for (var suite in VIRTUAL_SUITES) {
+ if (test.indexOf(suite) != -1)
+ return suite;
+ }
+ return '';
+}
+
+function baseTest(test, suite) {
+ base = VIRTUAL_SUITES[suite];
+ return base ? test.replace(suite, base) : test;
+}
+
+function loadBaselinesForTest(expectationsContainers, expectationsContainer, test) {
+ var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
+ var text = testWithoutSuffix + "-expected.txt";
+ var png = testWithoutSuffix + "-expected.png";
+ var reftest_html_file = testWithoutSuffix + "-expected.html";
+ var reftest_mismatch_html_file = testWithoutSuffix + "-expected-mismatch.html";
+ var suite = lookupVirtualTestSuite(test);
+
+ if (!suite)
+ addExpectationItem(expectationsContainers, expectationsContainer, null, test, TEST_URL_BASE_PATH);
+
+ addExpectations(expectationsContainers, expectationsContainer,
+ TEST_URL_BASE_PATH, '', text, png, reftest_html_file, reftest_mismatch_html_file, suite);
+
+ var fallbacks = allFallbacks();
+ for (var i = 0; i < fallbacks.length; i++) {
+ var fallback = 'platform/' + fallbacks[i];
+ addExpectations(expectationsContainers, expectationsContainer, TEST_URL_BASE_PATH, fallback, text, png,
+ reftest_html_file, reftest_mismatch_html_file, suite);
+ }
+
+ if (suite)
+ loadBaselinesForTest(expectationsContainers, expectationsContainer, baseTest(test, suite));
+}
+
+function loadExpectationsLayoutTests(test, expectationsContainer)
+{
+ // Map from file extension to container div for expectations of that type.
+ var expectationsContainers = {};
+
+ var revisionContainer = document.createElement('div');
+ revisionContainer.textContent = "Showing results for: "
+ expectationsContainer.appendChild(revisionContainer);
+ for (var builder in g_builders) {
+ if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
+ var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+ var buildInfo = buildInfoForRevision(builder, latestRevision);
+ var revisionInfo = document.createElement('div');
+ revisionInfo.style.cssText = 'background:lightgray;margin:0 3px;padding:0 2px;display:inline-block;';
+ revisionInfo.innerHTML = builder + ' r' + buildInfo.revisionEnd +
+ ':r' + buildInfo.revisionStart + ', build ' + buildInfo.buildNumber;
+ revisionContainer.appendChild(revisionInfo);
+ }
+ }
+
+ loadBaselinesForTest(expectationsContainers, expectationsContainer, test);
+
+ var testWithoutSuffix = test.substring(0, test.lastIndexOf('.'));
+ var actualResultSuffixes = ['-actual.txt', '-actual.png', '-crash-log.txt', '-diff.txt', '-wdiff.html', '-diff.png'];
+
+ for (var builder in g_builders) {
+ var actualResultsBase;
+ if (builderMaster(builder) == WEBKIT_BUILDER_MASTER) {
+ var latestRevision = g_currentState.revision || g_resultsByBuilder[builder].webkitRevision[0];
+ var buildInfo = buildInfoForRevision(builder, latestRevision);
+ actualResultsBase = 'http://build.webkit.org/results/' + builder +
+ '/r' + buildInfo.revisionStart + ' (' + buildInfo.buildNumber + ')/';
+ } else
+ actualResultsBase = TEST_RESULTS_BASE_PATH + g_builders[builder] + '/results/layout-test-results/';
+
+ for (var i = 0; i < actualResultSuffixes.length; i++) {
+ addExpectationItem(expectationsContainers, expectationsContainer, null,
+ testWithoutSuffix + actualResultSuffixes[i], actualResultsBase, builder);
+ }
+ }
+
+ // Add a clearing element so floated elements don't bleed out of their
+ // containing block.
+ var br = document.createElement('br');
+ br.style.clear = 'both';
+ expectationsContainer.appendChild(br);
+}
+
+var g_allFallbacks;
+
+// Returns the reverse sorted, deduped list of all platform fallback
+// directories.
+function allFallbacks()
+{
+ if (!g_allFallbacks) {
+ var holder = {};
+ for (var platform in g_fallbacksMap) {
+ var fallbacks = g_fallbacksMap[platform];
+ for (var i = 0; i < fallbacks.length; i++)
+ holder[fallbacks[i]] = 1;
+ }
+
+ g_allFallbacks = [];
+ for (var fallback in holder)
+ g_allFallbacks.push(fallback);
+
+ g_allFallbacks.sort(function(a, b) {
+ if (a == b)
+ return 0;
+ return a < b;
+ });
+ }
+ return g_allFallbacks;
+}
+
+function appendExpectations()
+{
+ var expectations = g_currentState.showExpectations ? document.getElementsByClassName('expectations') : [];
+ // Loading expectations is *very* slow. Use a large timeout to avoid
+ // totally hanging the renderer.
+ performChunkedAction(expectations, function(chunk) {
+ for (var i = 0, len = chunk.length; i < len; i++)
+ loadExpectations(chunk[i]);
+ postHeightChangedMessage();
+
+ }, hideLoadingUI, 10000);
+}
+
+function hideLoadingUI()
+{
+ var loadingDiv = $('loading-ui');
+ if (loadingDiv)
+ loadingDiv.style.display = 'none';
+ postHeightChangedMessage();
+}
+
+function generatePageForIndividualTests(tests)
+{
+ console.log('Number of tests: ' + tests.length);
+ if (g_currentState.showChrome)
+ appendHTML(htmlForNavBar());
+ performChunkedAction(tests, function(chunk) {
+ appendHTML(htmlForIndividualTests(chunk));
+ }, appendExpectations, 500);
+ if (g_currentState.showChrome)
+ $('tests-input').value = g_currentState.tests;
+}
+
+function performChunkedAction(tests, handleChunk, onComplete, timeout, opt_index) {
+ var index = opt_index || 0;
+ setTimeout(function() {
+ var chunk = Array.prototype.slice.call(tests, index * CHUNK_SIZE, (index + 1) * CHUNK_SIZE);
+ if (chunk.length) {
+ handleChunk(chunk);
+ performChunkedAction(tests, handleChunk, onComplete, timeout, ++index);
+ } else
+ onComplete();
+ // No need for a timeout on the first chunked action.
+ }, index ? timeout : 0);
+}
+
+function htmlForIndividualTests(tests)
+{
+ var testsHTML = [];
+ for (var i = 0; i < tests.length; i++) {
+ var test = tests[i];
+ var testNameHtml = '';
+ if (g_currentState.showChrome || tests.length > 1) {
+ if (isLayoutTestResults()) {
+ var suite = lookupVirtualTestSuite(test);
+ var base = suite ? baseTest(test, suite) : test;
+ var tracURL = TEST_URL_BASE_PATH_TRAC + base;
+ testNameHtml += '<h2>' + linkHTMLToOpenWindow(tracURL, test) + '</h2>';
+ } else
+ testNameHtml += '<h2>' + test + '</h2>';
+ }
+
+ testsHTML.push(testNameHtml + htmlForIndividualTestOnAllBuildersWithResultsLinks(test));
+ }
+ return testsHTML.join('<hr>');
+}
+
+function htmlForNavBar()
+{
+ var extraHTML = '';
+ var html = htmlForTestTypeSwitcher(false, extraHTML, isCrossBuilderView());
+ html += '<div class=forms><form id=result-form ' +
+ 'onsubmit="setQueryParameter(\'result\', result.value);' +
+ 'return false;">Show all tests with result: ' +
+ '<input name=result placeholder="e.g. CRASH" id=result-input>' +
+ '</form><form id=tests-form ' +
+ 'onsubmit="setQueryParameter(\'tests\', tests.value);' +
+ 'return false;"><span>Show tests on all platforms: </span>' +
+ '<input name=tests ' +
+ 'placeholder="Comma or space-separated list of tests or partial ' +
+ 'paths to show test results across all builders, e.g., ' +
+ 'foo/bar.html,foo/baz,domstorage" id=tests-input></form>' +
+ '<span class=link onclick="showLegend()">Show legend [type ?]</span></div>';
+ return html;
+}
+
+function checkBoxToToggleState(key, text)
+{
+ var stateEnabled = g_currentState[key];
+ return '<label><input type=checkbox ' + (stateEnabled ? 'checked ' : '') + 'onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + text + '</label> ';
+}
+
+function linkHTMLToToggleState(key, linkText)
+{
+ var stateEnabled = g_currentState[key];
+ return '<span class=link onclick="setQueryParameter(\'' + key + '\', ' + !stateEnabled + ')">' + (stateEnabled ? 'Hide' : 'Show') + ' ' + linkText + '</span>';
+}
+
+function headerForTestTableHtml()
+{
+ return '<h2 style="display:inline-block">Failing tests</h2>' +
+ checkBoxToToggleState('showWontFixSkip', 'WONTFIX/SKIP') +
+ checkBoxToToggleState('showCorrectExpectations', 'tests with correct expectations') +
+ checkBoxToToggleState('showWrongExpectations', 'tests with wrong expectations') +
+ checkBoxToToggleState('showFlaky', 'flaky') +
+ checkBoxToToggleState('showSlow', 'slow');
+}
+
+function generatePageForBuilder(builderName)
+{
+ processTestRunsForBuilder(builderName);
+
+ var results = g_perBuilderFailures[builderName];
+ sortTests(results, g_currentState.sortColumn, g_currentState.sortOrder);
+
+ var testsHTML = '';
+ if (results.length) {
+ var tableRowsHTML = '';
+ for (var i = 0; i < results.length; i++)
+ tableRowsHTML += htmlForSingleTestRow(results[i])
+ testsHTML = htmlForTestTable(tableRowsHTML);
+ } else {
+ testsHTML = '<div>No tests found. ';
+ if (isLayoutTestResults())
+ testsHTML += 'Try showing tests with correct expectations.</div>';
+ else
+ testsHTML += 'This means no tests have failed!</div>';
+ }
+
+ var html = htmlForNavBar();
+
+ if (isLayoutTestResults())
+ html += htmlForTestsWithExpectationsButNoFailures(builderName) + headerForTestTableHtml();
+
+ html += '<br>' + testsHTML;
+ appendHTML(html);
+
+ var ths = document.getElementsByTagName('th');
+ for (var i = 0; i < ths.length; i++) {
+ ths[i].addEventListener('click', changeSort, false);
+ ths[i].className = "sortable";
+ }
+
+ hideLoadingUI();
+}
+
+var VALID_KEYS_FOR_CROSS_BUILDER_VIEW = {
+ tests: 1,
+ result: 1,
+ showChrome: 1,
+ showExpectations: 1,
+ showLargeExpectations: 1,
+ legacyExpectationsSemantics: 1,
+ resultsHeight: 1,
+ revision: 1
+};
+
+function isInvalidKeyForCrossBuilderView(key)
+{
+ return !(key in VALID_KEYS_FOR_CROSS_BUILDER_VIEW) && !(key in g_defaultCrossDashboardStateValues);
+}
+
+function updateDefaultBuilderState()
+{
+ if (isCrossBuilderView())
+ delete g_defaultDashboardSpecificStateValues.builder;
+ else
+ g_defaultDashboardSpecificStateValues.builder = g_defaultBuilderName;
+}
+
+// Sets the page state to regenerate the page.
+// @param {Object} params New or modified query parameters as key: value.
+function handleQueryParameterChange(params)
+{
+ for (key in params) {
+ if (key == 'tests') {
+ // Entering cross-builder view, only keep valid keys for that view.
+ for (var currentKey in g_currentState) {
+ if (isInvalidKeyForCrossBuilderView(currentKey)) {
+ delete g_currentState[currentKey];
+ }
+ }
+ } else if (isInvalidKeyForCrossBuilderView(key)) {
+ delete g_currentState.tests;
+ delete g_currentState.result;
+ }
+ }
+
+ updateDefaultBuilderState();
+ return true;
+}
+
+function hideLegend()
+{
+ var legend = $('legend');
+ if (legend)
+ legend.parentNode.removeChild(legend);
+}
+
+var g_fallbacksMap = {};
+g_fallbacksMap['WIN-XP'] = ['chromium-win-xp', 'chromium-win', 'chromium'];
+g_fallbacksMap['WIN-7'] = ['chromium-win', 'chromium'];
+g_fallbacksMap['MAC-SNOWLEOPARD'] = ['chromium-mac-snowleopard', 'chromium-mac', 'chromium'];
+g_fallbacksMap['MAC-LION'] = ['chromium-mac', 'chromium'];
+g_fallbacksMap['LINUX-32'] = ['chromium-linux-x86', 'chromium-linux', 'chromium-win', 'chromium'];
+g_fallbacksMap['LINUX-64'] = ['chromium-linux', 'chromium-win', 'chromium'];
+
+function htmlForFallbackHelp(fallbacks)
+{
+ return '<ol class=fallback-list><li>' + fallbacks.join('</li><li>') + '</li></ol>';
+}
+
+function showLegend()
+{
+ var legend = $('legend');
+ if (!legend) {
+ legend = document.createElement('div');
+ legend.id = 'legend';
+ document.body.appendChild(legend);
+ }
+
+ var html = '<div id=legend-toggle onclick="hideLegend()">Hide ' +
+ 'legend [type esc]</div><div id=legend-contents>';
+ for (var expectation in expectationsMap())
+ html += '<div class=' + expectation + '>' + expectationsMap()[expectation] + '</div>';
+
+ html += '<div class=merge>WEBKIT MERGE</div>';
+ if (isLayoutTestResults()) {
+ html += '</div><br style="clear:both">' +
+ '</div><h3>Test expectatons fallback order.</h3>';
+
+ for (var platform in g_fallbacksMap)
+ html += '<div class=fallback-header>' + platform + '</div>' + htmlForFallbackHelp(g_fallbacksMap[platform]);
+
+ html += '<div>TIMES:</div>' +
+ htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST) +
+ '<div>DEBUG TIMES:</div>' +
+ htmlForSlowTimes(MIN_SECONDS_FOR_SLOW_TEST_DEBUG);
+ }
+
+ legend.innerHTML = html;
+}
+
+function htmlForSlowTimes(minTime)
+{
+ return '<ul><li><1 second == !SLOW</li><li>>1 second && <' +
+ minTime + ' seconds == SLOW || !SLOW is fine</li><li>>' +
+ minTime + ' seconds == SLOW</li></ul>';
+}
+
+function postHeightChangedMessage()
+{
+ if (window == parent)
+ return;
+
+ var root = document.documentElement;
+ var height = root.offsetHeight;
+ if (root.offsetWidth < root.scrollWidth) {
+ // We have a horizontal scrollbar. Include it in the height.
+ var dummyNode = document.createElement('div');
+ dummyNode.style.overflow = 'scroll';
+ document.body.appendChild(dummyNode);
+ var scrollbarWidth = dummyNode.offsetHeight - dummyNode.clientHeight;
+ document.body.removeChild(dummyNode);
+ height += scrollbarWidth;
+ }
+ parent.postMessage({command: 'heightChanged', height: height}, '*')
+}
+
+if (window != parent)
+ window.addEventListener('blur', hidePopup);
+
+document.addEventListener('keydown', function(e) {
+ if (e.keyIdentifier == 'U+003F' || e.keyIdentifier == 'U+00BF') {
+ // WebKit MAC retursn 3F. WebKit WIN returns BF. This is a bug!
+ // ? key
+ showLegend();
+ } else if (e.keyIdentifier == 'U+001B') {
+ // escape key
+ hideLegend();
+ hidePopup();
+ }
+}, false);
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html
new file mode 100644
index 0000000..c371e78
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded.html
@@ -0,0 +1,83 @@
+<!-- Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<!DOCTYPE HTML>
+<style>
+iframe {
+ width: 100%;
+ display: block;
+}
+#toolbar {
+ display: -webkit-flexbox;
+ -webkit-flex-align: baseline;
+}
+#tests {
+ -webkit-flex: 1;
+ /* WebKit bug. Don't properly wrap input elements for flexing. */
+ display: block;
+}
+</style>
+
+<div>This is a demo page for working on iframe embedding the flakiness dashboard.</div>
+<div id="toolbar">
+ <input id="chrome" type=checkbox></input><label>Hide chrome</label> |
+ <label>Tests:</label><input id=tests placeholder="Type test name here to load a different test in the frame" ></input>
+</div>
+<iframe src="flakiness_dashboard.html"></iframe>
+
+<script>
+var timeoutId;
+document.querySelector('#tests').oninput = function(event) {
+ if (timeoutId)
+ clearTimeout(timeoutId);
+ timeoutId = setTimeout(setFrameSrc, 1000);
+};
+document.querySelector('#chrome').onchange = setFrameSrc;
+
+function setFrameSrc() {
+ var tests = document.querySelector('#tests').value;
+ var hideChrome = document.querySelector('#chrome').checked ? '&showChrome=false' : '';
+ var url = 'flakiness_dashboard.html#tests=' + tests + hideChrome;
+ document.querySelector('iframe').src = url;
+};
+
+function sizeIframeToContents() {
+ document.querySelector('iframe').contentWindow.postMessage({command: 'queryContentHeight'}, '*');
+};
+
+window.addEventListener('message', function(event) {
+ switch(event.data.command) {
+ case 'heightChanged':
+ document.querySelector('iframe').style.height = event.data.height + 'px';
+ break;
+
+ default:
+ console.error('Did not understand message: ' + event.data);
+ }
+});
+</script>
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js
new file mode 100644
index 0000000..5599b3e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_embedded_unittests.js
@@ -0,0 +1,40 @@
+// Copyright (C) 2011 Google Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+test('hidePopupOnBlur', 2, function() {
+ showPopup(document.body, 'dummy content');
+ ok(document.querySelector('#popup'));
+
+ // Cause the window to be blurred.
+ var frame = document.createElement('iframe');
+ document.body.appendChild(frame);
+ frame.focus();
+ document.body.removeChild(frame);
+
+ ok(!document.querySelector('#popup'));
+});
\ No newline at end of file
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css
new file mode 100644
index 0000000..6165bcd
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_tests.css
@@ -0,0 +1,273 @@
+/*
+ * Copyright (C) 2012 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+#errors {
+ color: red;
+ font-size: 16px;
+ margin: 0;
+}
+#result-input {
+ width: 7em;
+}
+.test-link.builder-name {
+ white-space: nowrap;
+}
+.test-link, .options-container {
+ padding: 0 2px;
+}
+.test-table {
+ white-space: nowrap;
+ border-spacing: 1px;
+}
+/* Let the bugs column wrap. */
+.test-table tr > td:nth-child(2) {
+ white-space: normal;
+}
+.test-table {
+ width: 100%;
+}
+.test-table tr {
+ border: 1px solid red;
+ background-color: #E8E8E8;
+}
+.test-table tbody tr:hover {
+ opacity: .7;
+}
+.test-table th {
+ -webkit-user-select: none;
+ -moz-user-select: none;
+}
+.link, .sortable .header-text {
+ color: blue;
+ text-decoration: underline;
+ cursor: pointer;
+}
+.table-header-content,
+.table-header-content * {
+ display: -webkit-box;
+}
+.table-header-content * {
+ -webkit-box-flex: 1;
+ cursor: pointer;
+}
+.results {
+ cursor: pointer;
+ padding: 0 1px;
+ font-size: 10px;
+ text-align: center;
+}
+#legend {
+ position: fixed;
+ top: 5px;
+ right: 5px;
+ width: 400px;
+ padding: 2px;
+ border: 2px solid grey;
+ background-color: white;
+ z-index: 1;
+}
+#legend ul, #legend ol {
+ margin-top: 0;
+ margin-bottom: 5px;
+}
+#legend-contents * {
+ margin: 3px 0;
+ padding: 0 2px;
+ float: left;
+ border: 1px solid grey;
+}
+.P {
+ background-color: #3f3;
+}
+.N {
+ background-color: #fff;
+}
+.X {
+ background-color: lightgray;
+}
+.C {
+ background-color: #c90;
+}
+.T {
+ background-color: #fffc6c;
+}
+.I {
+ background-color: #69f;
+}
+.S {
+ background-color: #c6c;
+}
+.F {
+ background-color: #e98080;
+}
+.O {
+ background-color: #8a7700;
+}
+.Z {
+ background-color: #96f;
+}
+#legend .merge {
+ background-color: black;
+ color: white;
+}
+.test-table .merge {
+ border-right: solid 2px #000;
+ padding-right: 0;
+}
+.separator {
+ border: 1px solid lightgray;
+ height: 0px;
+}
+#passing-tests,
+#skipped-tests {
+ -webkit-column-count: 3;
+ -webkit-column-gap: 25px;
+ -webkit-column-rule: 1px dashed black;
+ -moz-column-count: 3;
+ -moz-column-gap: 25px;
+ -moz-column-rule: 1px dashed black;
+ border-top: 1px dashed black;
+ border-bottom: 1px dashed black;
+}
+.not-found {
+ color: red;
+ font-size: large;
+}
+#loading-ui {
+ position: fixed;
+ top: 0;
+ left: 0;
+ background-color: yellow;
+ padding: 5px;
+ text-align: center;
+ font-weight: bold;
+}
+#popup {
+ background-color: white;
+ z-index: 1;
+ position: absolute;
+ border: 3px solid grey;
+ padding: 3px;
+ -webkit-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+ -moz-box-shadow: 5px 5px 5px rgba(0, 0, 0, 0.5);
+ -webkit-border-radius: 5px;
+ -moz-border-radius: 5px;
+}
+#popup > ul {
+ margin: 0;
+ padding-left: 20px;
+}
+.expectations-container {
+ clear: both;
+}
+.expectations-item {
+ float: left;
+ border: 1px solid grey;
+ display: -webkit-box;
+ display: -moz-box;
+ position: relative;
+ -webkit-box-orient: vertical;
+ -moz-box-orient: vertical;
+}
+.expectations-item .expectation {
+ width: 400px;
+ height: 300px;
+ border: 0;
+ border-top: 1px solid grey;
+ overflow: auto;
+ display: -webkit-box;
+ display: -moz-box;
+ -webkit-box-flex: 1;
+ -moz-box-flex: 1;
+}
+pre.expectation {
+ padding: 8px;
+ margin: 0;
+ box-sizing: border-box;
+}
+.expectations-item .large {
+ width: 800px;
+ height: 600px;
+}
+.non-webkit-results {
+ width: 99%;
+}
+.gpu-test-results {
+ width: 400px;
+}
+.used-platform {
+ float: right;
+ color: darkblue;
+ margin: 0 5px;
+}
+.expectations-title {
+ /* Hack to make a containing block for absolute positioned elements. */
+ position: relative;
+ clear: both;
+}
+.title {
+ /* Position absolutely so the container does not grow to contain this. */
+ position: absolute;
+}
+.platforms {
+ position: absolute;
+ right: 0;
+ z-index: 1;
+}
+.file-bug {
+ font-weight: bold;
+ font-size: 11px;
+}
+.pngchecksum {
+ position: absolute;
+ right: 0;
+ bottom: 0;
+ background-color: #ffffc8;
+ visibility: hidden;
+}
+.expectations-item:hover .pngchecksum {
+ visibility: visible;
+}
+.skipped-builder-list {
+ margin-left: 20px;
+ background-color: #E8E8E8;
+}
+.skipped-builder {
+ display: inline-block;
+ white-space: nowrap;
+}
+.skipped-builder:after {
+ content: '|';
+ margin: 5px;
+}
+.skipped-builder:last-child:after {
+ content: '';
+}
+
diff --git a/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js
new file mode 100644
index 0000000..597c62e
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js
@@ -0,0 +1,799 @@
+// Copyright (C) 2011 Google Inc. All rights reserved.
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+function resetGlobals()
+{
+ allExpectations = null;
+ allTests = null;
+ g_expectationsByPlatform = {};
+ g_resultsByBuilder = {};
+ g_builders = {};
+ g_allExpectations = null;
+ g_allTestsTrie = null;
+ g_currentState = {};
+ g_crossDashboardState = {};
+ for (var key in g_defaultCrossDashboardStateValues)
+ g_crossDashboardState[key] = g_defaultCrossDashboardStateValues[key];
+}
+
+function runExpectationsTest(builder, test, expectations, modifiers)
+{
+ g_builders[builder] = true;
+
+ // Put in some dummy results. processExpectations expects the test to be
+ // there.
+ var tests = {};
+ tests[test] = {'results': [[100, 'F']], 'times': [[100, 0]]};
+ g_resultsByBuilder[builder] = {'tests': tests};
+
+ processExpectations();
+ var resultsForTest = createResultsObjectForTest(test, builder);
+ populateExpectationsData(resultsForTest);
+
+ var message = 'Builder: ' + resultsForTest.builder + ' test: ' + resultsForTest.test;
+ equal(resultsForTest.expectations, expectations, message);
+ equal(resultsForTest.modifiers, modifiers, message);
+}
+
+test('flattenTrie', 1, function() {
+ resetGlobals();
+ var tests = {
+ 'bar.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ 'foo': {
+ 'bar': {
+ 'baz.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ }
+ }
+ };
+ var expectedFlattenedTests = {
+ 'bar.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ 'foo/bar/baz.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ };
+ equal(JSON.stringify(flattenTrie(tests)), JSON.stringify(expectedFlattenedTests))
+});
+
+test('releaseFail', 2, function() {
+ resetGlobals();
+ var builder = 'WebKit Win';
+ var test = 'foo/1.html';
+ var expectationsArray = [
+ {'modifiers': 'RELEASE', 'expectations': 'FAIL'}
+ ];
+ g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]');
+ runExpectationsTest(builder, test, 'FAIL', 'RELEASE');
+});
+
+test('releaseFailDebugCrashReleaseBuilder', 2, function() {
+ resetGlobals();
+ var builder = 'WebKit Win';
+ var test = 'foo/1.html';
+ var expectationsArray = [
+ {'modifiers': 'RELEASE', 'expectations': 'FAIL'},
+ {'modifiers': 'DEBUG', 'expectations': 'CRASH'}
+ ];
+ g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]\n' +
+ '[ Debug ] ' + test + ' [ Crash ]');
+ runExpectationsTest(builder, test, 'FAIL', 'RELEASE');
+});
+
+test('releaseFailDebugCrashDebugBuilder', 2, function() {
+ resetGlobals();
+ var builder = 'WebKit Win (dbg)';
+ var test = 'foo/1.html';
+ var expectationsArray = [
+ {'modifiers': 'RELEASE', 'expectations': 'FAIL'},
+ {'modifiers': 'DEBUG', 'expectations': 'CRASH'}
+ ];
+ g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('[ Release ] ' + test + ' [ Failure ]\n' +
+ '[ Debug ] ' + test + ' [ Crash ]');
+ runExpectationsTest(builder, test, 'CRASH', 'DEBUG');
+});
+
+test('overrideJustBuildType', 12, function() {
+ resetGlobals();
+ var test = 'bar/1.html';
+ g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('bar [ WontFix Failure Pass Timeout ]\n' +
+ '[ Mac ] ' + test + ' [ WontFix Failure ]\n' +
+ '[ Linux Debug ] ' + test + ' [ Crash ]');
+
+ runExpectationsTest('WebKit Win', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+ runExpectationsTest('WebKit Win (dbg)(3)', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+ runExpectationsTest('WebKit Linux', test, 'FAIL PASS TIMEOUT', 'WONTFIX');
+ runExpectationsTest('WebKit Linux (dbg)(3)', test, 'CRASH', 'LINUX DEBUG');
+ runExpectationsTest('WebKit Mac10.7', test, 'FAIL', 'MAC WONTFIX');
+ runExpectationsTest('WebKit Mac10.7 (dbg)(3)', test, 'FAIL', 'MAC WONTFIX');
+});
+
+test('platformAndBuildType', 78, function() {
+ var runPlatformAndBuildTypeTest = function(builder, expectedPlatform, expectedBuildType) {
+ g_perBuilderPlatformAndBuildType = {};
+ buildInfo = platformAndBuildType(builder);
+ var message = 'Builder: ' + builder;
+ equal(buildInfo.platform, expectedPlatform, message);
+ equal(buildInfo.buildType, expectedBuildType, message);
+ }
+ runPlatformAndBuildTypeTest('WebKit Win (deps)', 'CHROMIUM_XP', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Win (deps)(dbg)(1)', 'CHROMIUM_XP', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Win (deps)(dbg)(2)', 'CHROMIUM_XP', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Linux (deps)', 'CHROMIUM_LUCID', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Linux (deps)(dbg)(1)', 'CHROMIUM_LUCID', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Linux (deps)(dbg)(2)', 'CHROMIUM_LUCID', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)(dbg)(1)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Mac10.6 (deps)(dbg)(2)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Win', 'CHROMIUM_XP', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Win7', 'CHROMIUM_WIN7', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Win (dbg)(1)', 'CHROMIUM_XP', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Win (dbg)(2)', 'CHROMIUM_XP', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Linux', 'CHROMIUM_LUCID', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Linux 32', 'CHROMIUM_LUCID', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Linux (dbg)(1)', 'CHROMIUM_LUCID', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Linux (dbg)(2)', 'CHROMIUM_LUCID', 'DEBUG');
+ runPlatformAndBuildTypeTest('WebKit Mac10.6', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+ runPlatformAndBuildTypeTest('WebKit Mac10.6 (dbg)', 'CHROMIUM_SNOWLEOPARD', 'DEBUG');
+ runPlatformAndBuildTypeTest('XP Tests', 'CHROMIUM_XP', 'RELEASE');
+ runPlatformAndBuildTypeTest('Interactive Tests (dbg)', 'CHROMIUM_XP', 'DEBUG');
+
+ g_crossDashboardState.group = '@ToT - webkit.org';
+ g_crossDashboardState.testType = 'layout-tests';
+ runPlatformAndBuildTypeTest('Chromium Win Release (Tests)', 'CHROMIUM_XP', 'RELEASE');
+ runPlatformAndBuildTypeTest('Chromium Linux Release (Tests)', 'CHROMIUM_LUCID', 'RELEASE');
+ runPlatformAndBuildTypeTest('Chromium Mac Release (Tests)', 'CHROMIUM_SNOWLEOPARD', 'RELEASE');
+
+ // FIXME: These platforms should match whatever we use in the TestExpectations format.
+ runPlatformAndBuildTypeTest('Lion Release (Tests)', 'APPLE_MAC_LION_WK1', 'RELEASE');
+ runPlatformAndBuildTypeTest('Lion Debug (Tests)', 'APPLE_MAC_LION_WK1', 'DEBUG');
+ runPlatformAndBuildTypeTest('SnowLeopard Intel Release (Tests)', 'APPLE_MAC_SNOWLEOPARD_WK1', 'RELEASE');
+ runPlatformAndBuildTypeTest('SnowLeopard Intel Leaks', 'APPLE_MAC_SNOWLEOPARD_WK1', 'RELEASE');
+ runPlatformAndBuildTypeTest('SnowLeopard Intel Debug (Tests)', 'APPLE_MAC_SNOWLEOPARD_WK1', 'DEBUG');
+ runPlatformAndBuildTypeTest('GTK Linux 32-bit Release', 'GTK_LINUX_WK1', 'RELEASE');
+ runPlatformAndBuildTypeTest('GTK Linux 32-bit Debug', 'GTK_LINUX_WK1', 'DEBUG');
+ runPlatformAndBuildTypeTest('GTK Linux 64-bit Debug', 'GTK_LINUX_WK1', 'DEBUG');
+ runPlatformAndBuildTypeTest('GTK Linux 64-bit Debug WK2', 'GTK_LINUX_WK2', 'DEBUG');
+ runPlatformAndBuildTypeTest('Qt Linux Release', 'QT_LINUX', 'RELEASE');
+ runPlatformAndBuildTypeTest('Windows 7 Release (Tests)', 'APPLE_WIN_WIN7', 'RELEASE');
+ runPlatformAndBuildTypeTest('Windows XP Debug (Tests)', 'APPLE_WIN_XP', 'DEBUG');
+
+ // FIXME: Should WebKit2 be it's own platform?
+ runPlatformAndBuildTypeTest('SnowLeopard Intel Release (WebKit2 Tests)', 'APPLE_MAC_SNOWLEOPARD_WK2', 'RELEASE');
+ runPlatformAndBuildTypeTest('SnowLeopard Intel Debug (WebKit2 Tests)', 'APPLE_MAC_SNOWLEOPARD_WK2', 'DEBUG');
+ runPlatformAndBuildTypeTest('Windows 7 Release (WebKit2 Tests)', 'APPLE_WIN_WIN7', 'RELEASE');
+});
+
+test('realModifiers', 3, function() {
+ equal(realModifiers('BUG(Foo) LINUX LION WIN DEBUG SLOW'), 'SLOW');
+ equal(realModifiers('BUG(Foo) LUCID MAC XP RELEASE SKIP'), 'SKIP');
+ equal(realModifiers('BUG(Foo)'), '');
+});
+
+test('allTestsWithSamePlatformAndBuildType', 1, function() {
+ // FIXME: test that allTestsWithSamePlatformAndBuildType actually returns the right set of tests.
+ var expectedPlatformsList = ['CHROMIUM_LION', 'CHROMIUM_SNOWLEOPARD', 'CHROMIUM_XP', 'CHROMIUM_VISTA', 'CHROMIUM_WIN7', 'CHROMIUM_LUCID',
+ 'CHROMIUM_ANDROID', 'APPLE_MAC_LION_WK1', 'APPLE_MAC_LION_WK2', 'APPLE_MAC_SNOWLEOPARD_WK1', 'APPLE_MAC_SNOWLEOPARD_WK2',
+ 'APPLE_WIN_XP', 'APPLE_WIN_WIN7', 'GTK_LINUX_WK1', 'GTK_LINUX_WK2', 'QT_LINUX', 'EFL_LINUX_WK1', 'EFL_LINUX_WK2'];
+ var actualPlatformsList = Object.keys(g_allTestsByPlatformAndBuildType);
+ deepEqual(expectedPlatformsList, actualPlatformsList);
+});
+
+test('filterBugs',4, function() {
+ var filtered = filterBugs('Skip crbug.com/123 webkit.org/b/123 Slow Bug(Tony) Debug')
+ equal(filtered.modifiers, 'Skip Slow Debug');
+ equal(filtered.bugs, 'crbug.com/123 webkit.org/b/123 Bug(Tony)');
+
+ filtered = filterBugs('Skip Slow Debug')
+ equal(filtered.modifiers, 'Skip Slow Debug');
+ equal(filtered.bugs, '');
+});
+
+test('getExpectations', 16, function() {
+ resetGlobals();
+ g_builders['WebKit Win'] = true;
+ g_resultsByBuilder = {
+ 'WebKit Win': {
+ 'tests': {
+ 'foo/test1.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ 'foo/test2.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ 'foo/test3.html': {'results': [[100, 'F']], 'times': [[100, 0]]},
+ 'test1.html': {'results': [[100, 'F']], 'times': [[100, 0]]}
+ }
+ }
+ }
+
+ g_expectationsByPlatform['CHROMIUM'] = getParsedExpectations('Bug(123) foo [ Failure Pass Crash ]\n' +
+ 'Bug(Foo) [ Release ] foo/test1.html [ Failure ]\n' +
+ '[ Debug ] foo/test1.html [ Crash ]\n' +
+ 'Bug(456) foo/test2.html [ Failure ]\n' +
+ '[ Linux Debug ] foo/test2.html [ Crash ]\n' +
+ '[ Release ] test1.html [ Failure ]\n' +
+ '[ Debug ] test1.html [ Crash ]\n');
+ g_expectationsByPlatform['CHROMIUM_ANDROID'] = getParsedExpectations('Bug(654) foo/test2.html [ Crash ]\n');
+
+ g_expectationsByPlatform['GTK'] = getParsedExpectations('Bug(42) foo/test2.html [ ImageOnlyFailure ]\n' +
+ '[ Debug ] test1.html [ Crash ]\n');
+ g_expectationsByPlatform['GTK_LINUX_WK1'] = getParsedExpectations('[ Release ] foo/test1.html [ ImageOnlyFailure ]\n' +
+ 'Bug(789) foo/test2.html [ Crash ]\n');
+ g_expectationsByPlatform['GTK_LINUX_WK2'] = getParsedExpectations('Bug(987) foo/test2.html [ Failure ]\n');
+
+ processExpectations();
+
+ var expectations = getExpectations('foo/test1.html', 'CHROMIUM_XP', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+ var expectations = getExpectations('foo/test1.html', 'CHROMIUM_LUCID', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(Foo) RELEASE","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LUCID', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(456)","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LION', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(456)","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test2.html', 'CHROMIUM_LUCID', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"LINUX DEBUG","expectations":"CRASH"}');
+
+ var expectations = getExpectations('foo/test2.html', 'CHROMIUM_ANDROID', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(654)","expectations":"CRASH"}');
+
+ var expectations = getExpectations('test1.html', 'CHROMIUM_ANDROID', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test3.html', 'CHROMIUM_LUCID', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(123)","expectations":"FAIL PASS CRASH"}');
+
+ var expectations = getExpectations('test1.html', 'CHROMIUM_XP', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+ var expectations = getExpectations('test1.html', 'CHROMIUM_LUCID', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test1.html', 'GTK_LINUX_WK1', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"RELEASE","expectations":"IMAGE"}');
+
+ var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK1', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(789)","expectations":"CRASH"}');
+
+ var expectations = getExpectations('test1.html', 'GTK_LINUX_WK1', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+
+ var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK2', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(987)","expectations":"FAIL"}');
+
+ var expectations = getExpectations('foo/test2.html', 'GTK_LINUX_WK2', 'RELEASE');
+ equal(JSON.stringify(expectations), '{"modifiers":"Bug(987)","expectations":"FAIL"}');
+
+ var expectations = getExpectations('test1.html', 'GTK_LINUX_WK2', 'DEBUG');
+ equal(JSON.stringify(expectations), '{"modifiers":"DEBUG","expectations":"CRASH"}');
+});
+
+test('substringList', 2, function() {
+ g_crossDashboardState.testType = 'gtest';
+ g_currentState.tests = 'test.FLAKY_foo test.FAILS_foo1 test.DISABLED_foo2 test.MAYBE_foo3 test.foo4';
+ equal(substringList().toString(), 'test.foo,test.foo1,test.foo2,test.foo3,test.foo4');
+
+ g_crossDashboardState.testType = 'layout-tests';
+ g_currentState.tests = 'foo/bar.FLAKY_foo.html';
+ equal(substringList().toString(), 'foo/bar.FLAKY_foo.html');
+});
+
+test('htmlForTestsWithExpectationsButNoFailures', 4, function() {
+ var builder = 'WebKit Win';
+ g_perBuilderWithExpectationsButNoFailures[builder] = ['passing-test1.html', 'passing-test2.html'];
+ g_perBuilderSkippedPaths[builder] = ['skipped-test1.html'];
+ g_resultsByBuilder[builder] = { buildNumbers: [5, 4, 3, 1] };
+
+ g_currentState.showUnexpectedPasses = true;
+ g_currentState.showSkipped = true;
+
+ g_crossDashboardState.group = '@ToT - chromium.org';
+ g_crossDashboardState.testType = 'layout-tests';
+
+ var container = document.createElement('div');
+ container.innerHTML = htmlForTestsWithExpectationsButNoFailures(builder);
+ equal(container.querySelectorAll('#passing-tests > div').length, 2);
+ equal(container.querySelectorAll('#skipped-tests > div').length, 1);
+
+ g_currentState.showUnexpectedPasses = false;
+ g_currentState.showSkipped = false;
+
+ var container = document.createElement('div');
+ container.innerHTML = htmlForTestsWithExpectationsButNoFailures(builder);
+ equal(container.querySelectorAll('#passing-tests > div').length, 0);
+ equal(container.querySelectorAll('#skipped-tests > div').length, 0);
+});
+
+test('headerForTestTableHtml', 1, function() {
+ var container = document.createElement('div');
+ container.innerHTML = headerForTestTableHtml();
+ equal(container.querySelectorAll('input').length, 5);
+});
+
+test('htmlForTestTypeSwitcherGroup', 6, function() {
+ var container = document.createElement('div');
+ g_crossDashboardState.testType = 'ui_tests';
+ container.innerHTML = htmlForTestTypeSwitcher(true);
+ var selects = container.querySelectorAll('select');
+ equal(selects.length, 2);
+ var group = selects[1];
+ equal(group.parentNode.textContent.indexOf('Group:'), 0);
+ equal(group.children.length, 3);
+
+ g_crossDashboardState.testType = 'layout-tests';
+ container.innerHTML = htmlForTestTypeSwitcher(true);
+ var selects = container.querySelectorAll('select');
+ equal(selects.length, 2);
+ var group = selects[1];
+ equal(group.parentNode.textContent.indexOf('Group:'), 0);
+ equal(group.children.length, 4);
+});
+
+test('htmlForIndividualTestOnAllBuilders', 1, function() {
+ resetGlobals();
+ equal(htmlForIndividualTestOnAllBuilders('foo/nonexistant.html'), '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinksNonexistant', 1, function() {
+ resetGlobals();
+ equal(htmlForIndividualTestOnAllBuildersWithResultsLinks('foo/nonexistant.html'),
+ '<div class="not-found">Test not found. Either it does not exist, is skipped or passes on all platforms.</div>' +
+ '<div class=expectations test=foo/nonexistant.html>' +
+ '<div>' +
+ '<span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+ '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span> | ' +
+ '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b>' +
+ '</div>' +
+ '</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinks', 1, function() {
+ resetGlobals();
+ var test = 'dummytest.html';
+ var builderName = 'dummyBuilder';
+ BUILDER_TO_MASTER[builderName] = CHROMIUM_BUILDER_MASTER;
+ g_testToResultsMap[test] = [createResultsObjectForTest(test, builderName)];
+
+ equal(htmlForIndividualTestOnAllBuildersWithResultsLinks(test),
+ '<table class=test-table><thead><tr>' +
+ '<th sortValue=test><div class=table-header-content><span></span><span class=header-text>test</span></div></th>' +
+ '<th sortValue=bugs><div class=table-header-content><span></span><span class=header-text>bugs</span></div></th>' +
+ '<th sortValue=modifiers><div class=table-header-content><span></span><span class=header-text>modifiers</span></div></th>' +
+ '<th sortValue=expectations><div class=table-header-content><span></span><span class=header-text>expectations</span></div></th>' +
+ '<th sortValue=slowest><div class=table-header-content><span></span><span class=header-text>slowest run</span></div></th>' +
+ '<th sortValue=flakiness colspan=10000><div class=table-header-content><span></span><span class=header-text>flakiness (numbers are runtimes in seconds)</span></div></th>' +
+ '</tr></thead>' +
+ '<tbody></tbody>' +
+ '</table>' +
+ '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+ '<div class=skipped-builder-list>' +
+ '<div class=skipped-builder>WebKit Linux</div><div class=skipped-builder>WebKit Linux (dbg)</div>' +
+ '<div class=skipped-builder>WebKit Mac10.7</div><div class=skipped-builder>WebKit Win</div>' +
+ '</div>' +
+ '<div class=expectations test=dummytest.html>' +
+ '<div><span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+ '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span> | ' +
+ '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+ '</div>');
+});
+
+test('htmlForIndividualTestOnAllBuildersWithResultsLinksWebkitMaster', 1, function() {
+ resetGlobals();
+ var test = 'dummytest.html';
+ var builderName = 'dummyBuilder';
+ BUILDER_TO_MASTER[builderName] = WEBKIT_BUILDER_MASTER;
+ g_testToResultsMap[test] = [createResultsObjectForTest(test, builderName)];
+
+ equal(htmlForIndividualTestOnAllBuildersWithResultsLinks(test),
+ '<table class=test-table><thead><tr>' +
+ '<th sortValue=test><div class=table-header-content><span></span><span class=header-text>test</span></div></th>' +
+ '<th sortValue=bugs><div class=table-header-content><span></span><span class=header-text>bugs</span></div></th>' +
+ '<th sortValue=modifiers><div class=table-header-content><span></span><span class=header-text>modifiers</span></div></th>' +
+ '<th sortValue=expectations><div class=table-header-content><span></span><span class=header-text>expectations</span></div></th>' +
+ '<th sortValue=slowest><div class=table-header-content><span></span><span class=header-text>slowest run</span></div></th>' +
+ '<th sortValue=flakiness colspan=10000><div class=table-header-content><span></span><span class=header-text>flakiness (numbers are runtimes in seconds)</span></div></th>' +
+ '</tr></thead>' +
+ '<tbody></tbody>' +
+ '</table>' +
+ '<div>The following builders either don\'t run this test (e.g. it\'s skipped) or all runs passed:</div>' +
+ '<div class=skipped-builder-list>' +
+ '<div class=skipped-builder>WebKit Linux</div><div class=skipped-builder>WebKit Linux (dbg)</div>' +
+ '<div class=skipped-builder>WebKit Mac10.7</div><div class=skipped-builder>WebKit Win</div>' +
+ '</div>' +
+ '<div class=expectations test=dummytest.html>' +
+ '<div><span class=link onclick="setQueryParameter(\'showExpectations\', true)">Show results</span> | ' +
+ '<span class=link onclick="setQueryParameter(\'showLargeExpectations\', true)">Show large thumbnails</span>' +
+ '<form onsubmit="setQueryParameter(\'revision\', revision.value);return false;">' +
+ 'Show results for WebKit revision: <input name=revision placeholder="e.g. 65540" value="" id=revision-input>' +
+ '</form></div>' +
+ '</div>');
+});
+
+test('htmlForIndividualTests', 4, function() {
+ resetGlobals();
+ var test1 = 'foo/nonexistant.html';
+ var test2 = 'bar/nonexistant.html';
+
+ g_currentState.showChrome = false;
+
+ var tests = [test1, test2];
+ equal(htmlForIndividualTests(tests),
+ '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+ htmlForIndividualTestOnAllBuilders(test1) +
+ '<div class=expectations test=foo/nonexistant.html>' +
+ '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+ '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+ '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+ '</div>' +
+ '<hr>' +
+ '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/bar/nonexistant.html" target="_blank">bar/nonexistant.html</a></h2>' +
+ htmlForIndividualTestOnAllBuilders(test2) +
+ '<div class=expectations test=bar/nonexistant.html>' +
+ '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+ '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+ '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+ '</div>');
+
+ tests = [test1];
+ equal(htmlForIndividualTests(tests), htmlForIndividualTestOnAllBuilders(test1) +
+ '<div class=expectations test=foo/nonexistant.html>' +
+ '<div><span class=link onclick=\"setQueryParameter(\'showExpectations\', true)\">Show results</span> | ' +
+ '<span class=link onclick=\"setQueryParameter(\'showLargeExpectations\', true)\">Show large thumbnails</span> | ' +
+ '<b>Only shows actual results/diffs from the most recent *failure* on each bot.</b></div>' +
+ '</div>');
+
+ g_currentState.showChrome = true;
+
+ equal(htmlForIndividualTests(tests),
+ '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+ htmlForIndividualTestOnAllBuildersWithResultsLinks(test1));
+
+ tests = [test1, test2];
+ equal(htmlForIndividualTests(tests),
+ '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/foo/nonexistant.html" target="_blank">foo/nonexistant.html</a></h2>' +
+ htmlForIndividualTestOnAllBuildersWithResultsLinks(test1) + '<hr>' +
+ '<h2><a href="http://trac.webkit.org/browser/trunk/LayoutTests/bar/nonexistant.html" target="_blank">bar/nonexistant.html</a></h2>' +
+ htmlForIndividualTestOnAllBuildersWithResultsLinks(test2));
+});
+
+test('htmlForSingleTestRow', 1, function() {
+ resetGlobals();
+ var builder = 'dummyBuilder';
+ BUILDER_TO_MASTER[builder] = CHROMIUM_WEBKIT_BUILDER_MASTER;
+ var test = createResultsObjectForTest('foo/exists.html', builder);
+ g_currentState.showCorrectExpectations = true;
+ g_resultsByBuilder[builder] = {buildNumbers: [2, 1], webkitRevision: [1234, 1233]};
+ test.rawResults = [[1, 'F'], [2, 'I']];
+ test.rawTimes = [[1, 0], [2, 5]];
+ var expected = '<tr>' +
+ '<td class="test-link"><span class="link" onclick="setQueryParameter(\'tests\',\'foo/exists.html\');">foo/exists.html</span>' +
+ '<td class=options-container><a href="https://bugs.webkit.org/enter_bug.cgi?assigned_to=webkit-unassigned%40lists.webkit.org&product=WebKit&form_name=enter_bug&component=Tools%20%2F%20Tests&short_desc=Layout%20Test%20foo%2Fexists.html%20is%20failing&comment=The%20following%20layout%20test%20is%20failing%20on%20%5Binsert%20platform%5D%0A%0Afoo%2Fexists.html%0A%0AProbable%20cause%3A%0A%0A%5Binsert%20probable%20cause%5D" class="file-bug">FILE BUG</a>' +
+ '<td class=options-container>' +
+ '<td class=options-container>' +
+ '<td><td title="TEXT. Click for more info." class="results F merge" onclick=\'showPopupForBuild(event, "dummyBuilder",0,"foo/exists.html")\'> ' +
+ '<td title="IMAGE. Click for more info." class="results I" onclick=\'showPopupForBuild(event, "dummyBuilder",1,"foo/exists.html")\'>5';
+
+ equal(htmlForSingleTestRow(test), expected);
+});
+
+test('lookupVirtualTestSuite', 2, function() {
+ equal(lookupVirtualTestSuite('fast/canvas/foo.html'), '');
+ equal(lookupVirtualTestSuite('platform/chromium/virtual/gpu/fast/canvas/foo.html'), 'platform/chromium/virtual/gpu/fast/canvas');
+});
+
+test('baseTest', 2, function() {
+ equal(baseTest('fast/canvas/foo.html', ''), 'fast/canvas/foo.html');
+ equal(baseTest('platform/chromium/virtual/gpu/fast/canvas/foo.html', 'platform/chromium/virtual/gpu/fast/canvas'), 'fast/canvas/foo.html');
+});
+
+// FIXME: Create builders_tests.js and move this there.
+test('generateChromiumDepsFyiGpuBuildersFromBuilderList', 1, function() {
+ var builderList = ["Linux Audio", "Linux Release (ATI)", "Linux Release (Intel)", "Mac Release (ATI)", "Win7 Audio", "Win7 Release (ATI)", "Win7 Release (Intel)", "WinXP Debug (NVIDIA)", "WinXP Release (NVIDIA)"];
+ var expectedBuilders = [["Linux Release (ATI)", 2], ["Linux Release (Intel)"], ["Mac Release (ATI)"], ["Win7 Release (ATI)"], ["Win7 Release (Intel)"], ["WinXP Debug (NVIDIA)"], ["WinXP Release (NVIDIA)"] ];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsFyiGpuTestRunner), expectedBuilders);
+});
+
+test('generateChromiumTipOfTreeGpuBuildersFromBuilderList', 1, function() {
+ var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf",
+ "Linux Tests", "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN", "WebKit Linux (dbg)", "WebKit Linux (deps)",
+ "WebKit Linux 32", "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)",
+ "WebKit Mac10.6", "WebKit Mac10.6 (dbg)", "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)",
+ "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)", "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+ var expectedBuilders = [["GPU Linux (NVIDIA)", 2], ["GPU Linux (dbg) (NVIDIA)"], ["GPU Mac"], ["GPU Mac (dbg)"], ["GPU Win7 (NVIDIA)"], ["GPU Win7 (dbg) (NVIDIA)"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumTipOfTreeGpuTestRunner), expectedBuilders);
+});
+
+test('generateWebkitBuildersFromBuilderList', 1, function() {
+ var builderList = ["Chromium Android Release", "Chromium Linux Release", "Chromium Linux Release (Grid Layout)", "Chromium Linux Release (Perf)", "Chromium Linux Release (Tests)",
+ "Chromium Mac Release", "Chromium Mac Release (Perf)", "Chromium Mac Release (Tests)", "Chromium Win Release", "Chromium Win Release (Perf)", "Chromium Win Release (Tests)",
+ "EFL Linux Release", "GTK Linux 32-bit Release", "GTK Linux 64-bit Debug", "GTK Linux 64-bit Release", "Lion Debug (Build)", "Lion Debug (Tests)", "Lion Debug (WebKit2 Tests)",
+ "Lion Leaks", "Lion Release (Build)", "Lion Release (Perf)", "Lion Release (Tests)", "Lion Release (WebKit2 Tests)", "Qt Linux 64-bit Release (Perf)",
+ "Qt Linux 64-bit Release (WebKit2 Perf)", "Qt Linux ARMv7 Release", "Qt Linux MIPS Release", "Qt Linux Release", "Qt Linux Release minimal", "Qt Linux SH4 Release",
+ "Qt SnowLeopard Release", "Qt Windows 32-bit Debug", "Qt Windows 32-bit Release", "SnowLeopard Intel Debug (Build)", "SnowLeopard Intel Debug (Tests)",
+ "SnowLeopard Intel Debug (WebKit2 Tests)", "SnowLeopard Intel Release (Build)", "SnowLeopard Intel Release (Tests)", "SnowLeopard Intel Release (WebKit2 Tests)",
+ "WinCE Release (Build)", "WinCairo Release", "Windows 7 Release (Tests)", "Windows 7 Release (WebKit2 Tests)", "Windows Debug (Build)", "Windows Release (Build)", "Windows XP Debug (Tests)",
+ "EFL Linux 32-bit Release (Build)", "EFL Linux 64-bit Debug"];
+ var expectedBuilders = [["Chromium Linux Release (Tests)", 2], ["Chromium Mac Release (Tests)"], ["EFL Linux Release"], ["GTK Linux 32-bit Release"], ["GTK Linux 64-bit Debug"],
+ ["GTK Linux 64-bit Release"], ["Lion Debug (Tests)"], ["Lion Debug (WebKit2 Tests)"], ["Lion Release (Tests)"], ["Lion Release (WebKit2 Tests)"], ["Qt Linux Release"],
+ ["SnowLeopard Intel Debug (Tests)"], ["SnowLeopard Intel Debug (WebKit2 Tests)"], ["SnowLeopard Intel Release (Tests)"], ["SnowLeopard Intel Release (WebKit2 Tests)"], ["EFL Linux 64-bit Debug"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isWebkitTestRunner), expectedBuilders);
+});
+
+test('generateChromiumWebkitTipOfTreeBuildersFromBuilderList', 1, function() {
+ var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf", "Linux Tests",
+ "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN", "WebKit Linux (dbg)", "WebKit Linux (deps)", "WebKit Linux 32",
+ "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)", "WebKit Mac10.6", "WebKit Mac10.6 (dbg)",
+ "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)", "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)",
+ "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder",
+ "Linux (Content Shell)"];
+ var expectedBuilders = [["WebKit Linux", 2], ["WebKit Linux (dbg)"], ["WebKit Linux 32"], ["WebKit Mac10.6"],
+ ["WebKit Mac10.6 (dbg)"], ["WebKit Mac10.7"], ["WebKit Win"], ["WebKit Win (dbg)(1)"], ["WebKit Win (dbg)(2)"], ["WebKit Win7"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumWebkitTipOfTreeTestRunner), expectedBuilders);
+});
+
+test('generateChromiumWebkitDepsBuildersFromBuilderList', 1, function() {
+ var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf", "Linux Tests",
+ "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux ASAN", "WebKit Linux (dbg)", "WebKit Linux (deps)", "WebKit Linux 32",
+ "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)", "WebKit Mac10.6", "WebKit Mac10.6 (dbg)",
+ "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)", "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)",
+ "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+ var expectedBuilders = [["WebKit Linux (deps)", 2], ["WebKit Mac10.6 (deps)"], ["WebKit Win (deps)"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumWebkitDepsTestRunner), expectedBuilders);
+});
+
+test('generateChromiumDepsGTestBuildersFromBuilderList', 1, function() {
+ var builderList = ["Android Builder", "Chrome Frame Tests (ie6)", "Chrome Frame Tests (ie7)", "Chrome Frame Tests (ie8)", "Interactive Tests (dbg)", "Linux", "Linux Builder (dbg)",
+ "Linux Builder (dbg)(shared)", "Linux Builder x64", "Linux Clang (dbg)", "Linux Sync", "Linux Tests (dbg)(1)", "Linux Tests (dbg)(2)", "Linux Tests (dbg)(shared)", "Linux Tests x64",
+ "Linux x64", "Mac", "Mac 10.6 Tests (dbg)(1)", "Mac 10.6 Tests (dbg)(2)",
+ "Mac 10.6 Tests (dbg)(3)", "Mac 10.6 Tests (dbg)(4)", "Mac Builder", "Mac Builder (dbg)", "Mac10.6 Sync",
+ "Mac10.6 Tests (1)", "Mac10.6 Tests (2)", "Mac10.6 Tests (3)", "NACL Tests", "NACL Tests (x64)", "Vista Tests (1)", "Vista Tests (2)", "Vista Tests (3)", "Win", "Win Aura",
+ "Win Builder", "Win Builder (dbg)", "Win Builder 2010 (dbg)", "Win7 Sync", "Win7 Tests (1)", "Win7 Tests (2)", "Win7 Tests (3)", "Win7 Tests (dbg)(1)", "Win7 Tests (dbg)(2)",
+ "Win7 Tests (dbg)(3)", "Win7 Tests (dbg)(4)", "Win7 Tests (dbg)(5)", "Win7 Tests (dbg)(6)", "XP Tests (1)", "XP Tests (2)", "XP Tests (3)", "XP Tests (dbg)(1)", "XP Tests (dbg)(2)",
+ "XP Tests (dbg)(3)", "XP Tests (dbg)(4)", "XP Tests (dbg)(5)", "XP Tests (dbg)(6)"];
+ var expectedBuilders = [["Interactive Tests (dbg)", 2], ["Linux Sync"], ["Linux Tests (dbg)(1)"], ["Linux Tests (dbg)(2)"], ["Linux Tests (dbg)(shared)"], ["Linux Tests x64"],
+ ["Mac 10.6 Tests (dbg)(1)"], ["Mac 10.6 Tests (dbg)(2)"], ["Mac 10.6 Tests (dbg)(3)"],
+ ["Mac 10.6 Tests (dbg)(4)"], ["Mac10.6 Sync"], ["Mac10.6 Tests (1)"], ["Mac10.6 Tests (2)"], ["Mac10.6 Tests (3)"], ["NACL Tests"],
+ ["NACL Tests (x64)"], ["Vista Tests (1)"], ["Vista Tests (2)"], ["Vista Tests (3)"], ["Win7 Sync"], ["Win7 Tests (1)"], ["Win7 Tests (2)"], ["Win7 Tests (3)"], ["Win7 Tests (dbg)(1)"],
+ ["Win7 Tests (dbg)(2)"], ["Win7 Tests (dbg)(3)"], ["Win7 Tests (dbg)(4)"], ["Win7 Tests (dbg)(5)"], ["Win7 Tests (dbg)(6)"], ["XP Tests (1)"], ["XP Tests (2)"], ["XP Tests (3)"],
+ ["XP Tests (dbg)(1)"], ["XP Tests (dbg)(2)"], ["XP Tests (dbg)(3)"], ["XP Tests (dbg)(4)"], ["XP Tests (dbg)(5)"], ["XP Tests (dbg)(6)"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsGTestRunner), expectedBuilders);
+});
+
+test('generateChromiumDepsCrosGTestBuildersFromBuilderList', 1, function() {
+ var builderList = ["ChromiumOS (amd64)", "ChromiumOS (arm)", "ChromiumOS (tegra2)", "ChromiumOS (x86)", "Linux ChromiumOS (Clang dbg)", "Linux ChromiumOS Builder", "Linux ChromiumOS Builder (dbg)",
+ "Linux ChromiumOS Tests (1)", "Linux ChromiumOS Tests (2)", "Linux ChromiumOS Tests (dbg)(1)", "Linux ChromiumOS Tests (dbg)(2)", "Linux ChromiumOS Tests (dbg)(3)"];
+ var expectedBuilders = [["Linux ChromiumOS Tests (1)", 2], ["Linux ChromiumOS Tests (2)"], ["Linux ChromiumOS Tests (dbg)(1)"], ["Linux ChromiumOS Tests (dbg)(2)"], ["Linux ChromiumOS Tests (dbg)(3)"]];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumDepsCrosGTestRunner), expectedBuilders);
+});
+
+test('generateChromiumTipOfTreeGTestBuildersFromBuilderList', 1, function() {
+ var builderList = ["Chrome Frame Tests", "GPU Linux (NVIDIA)", "GPU Linux (dbg) (NVIDIA)", "GPU Mac", "GPU Mac (dbg)", "GPU Win7 (NVIDIA)", "GPU Win7 (dbg) (NVIDIA)", "Linux Perf",
+ "Linux Tests", "Linux Valgrind", "Mac Builder (dbg)", "Mac10.6 Perf", "Mac10.6 Tests", "Vista Perf", "Vista Tests", "WebKit Linux", "WebKit Linux (dbg)", "WebKit Linux (deps)",
+ "WebKit Linux 32", "WebKit Mac Builder", "WebKit Mac Builder (dbg)", "WebKit Mac Builder (deps)",
+ "WebKit Mac10.6", "WebKit Mac10.6 (dbg)", "WebKit Mac10.6 (deps)", "WebKit Mac10.7", "WebKit Win", "WebKit Win (dbg)(1)", "WebKit Win (dbg)(2)",
+ "WebKit Win (deps)", "WebKit Win Builder", "WebKit Win Builder (dbg)", "WebKit Win Builder (deps)", "WebKit Win7", "Win (dbg)", "Win Builder"];
+ var expectedBuilders = [['Linux Tests', BuilderGroup.DEFAULT_BUILDER], ['Mac10.6 Tests'], ['Vista Tests'], ['Win (dbg)']];
+ deepEqual(generateBuildersFromBuilderList(builderList, isChromiumTipOfTreeGTestRunner), expectedBuilders);
+});
+
+test('queryHashAsMap', 2, function() {
+ equal(window.location.hash, '#useTestData=true');
+ deepEqual(queryHashAsMap(), {useTestData: 'true'});
+});
+
+test('parseCrossDashboardParameters', 2, function() {
+ equal(window.location.hash, '#useTestData=true');
+ parseCrossDashboardParameters();
+
+ var expectedParameters = {};
+ for (var key in g_defaultCrossDashboardStateValues)
+ expectedParameters[key] = g_defaultCrossDashboardStateValues[key];
+ expectedParameters.useTestData = true;
+
+ deepEqual(g_crossDashboardState, expectedParameters);
+});
+
+test('diffStates', 5, function() {
+ var newState = {a: 1, b: 2};
+ deepEqual(diffStates(null, newState), newState);
+
+ var oldState = {a: 1};
+ deepEqual(diffStates(oldState, newState), {b: 2});
+
+ // FIXME: This is kind of weird. I think the existing users of this code work correctly, but it's a confusing result.
+ var oldState = {c: 1};
+ deepEqual(diffStates(oldState, newState), {a:1, b: 2});
+
+ var oldState = {a: 1, b: 2};
+ deepEqual(diffStates(oldState, newState), {});
+
+ var oldState = {a: 2, b: 3};
+ deepEqual(diffStates(oldState, newState), {a: 1, b: 2});
+});
+
+test('addBuilderLoadErrors', 1, function() {
+ clearErrors();
+ g_hasDoneInitialPageGeneration = false;
+ g_buildersThatFailedToLoad = ['builder1', 'builder2'];
+ g_staleBuilders = ['staleBuilder1'];
+ addBuilderLoadErrors();
+ equal(g_errorMessages, 'ERROR: Failed to get data from builder1,builder2.<br>ERROR: Data from staleBuilder1 is more than 1 day stale.<br>');
+});
+
+test('builderGroupIsToTWebKitAttribute', 2, function() {
+ var dummyMaster = new BuilderMaster('dummy.org', 'http://build.dummy.org');
+ var testBuilderGroups = {
+ '@ToT - dummy.org': new BuilderGroup(BuilderGroup.TOT_WEBKIT),
+ '@DEPS - dummy.org': new BuilderGroup(BuilderGroup.DEPS_WEBKIT),
+ }
+ testBuilderGroups['@ToT - dummy.org'].expectedGroups = 1;
+ testBuilderGroups['@DEPS - dummy.org'].expectedGroups = 1;
+
+ var testJSONData = "{ \"Dummy Builder 1\": null, \"Dummy Builder 2\": null }";
+ onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+ equal(testBuilderGroups['@ToT - dummy.org'].isToTWebKit, true);
+ onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@DEPS - dummy.org', {responseText: testJSONData});
+ equal(testBuilderGroups['@DEPS - dummy.org'].isToTWebKit, false);
+});
+
+test('builderGroupExpectedGroups', 4, function() {
+ var dummyMaster = new BuilderMaster('dummy.org', 'http://build.dummy.org');
+ var testBuilderGroups = {
+ '@ToT - dummy.org': new BuilderGroup(BuilderGroup.TOT_WEBKIT),
+ }
+ testBuilderGroups['@ToT - dummy.org'].expectedGroups = 3;
+
+ var testJSONData = "{ \"Dummy Builder 1\": null }";
+ equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 3);
+ onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+ equal(testBuilderGroups['@ToT - dummy.org'].groups, 1);
+ var testJSONData = "{ \"Dummy Builder 2\": null }";
+ onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@ToT - dummy.org', {responseText: testJSONData});
+ equal(testBuilderGroups['@ToT - dummy.org'].groups, 2);
+ onErrorLoadingBuilderList('http://build.dummy.org', testBuilderGroups, '@ToT - dummy.org');
+ equal(testBuilderGroups['@ToT - dummy.org'].groups, 3);
+});
+
+test('requestBuilderListAddsBuilderGroupEntry', 2, function() {
+ var testBuilderGroups = { '@ToT - dummy.org': null };
+
+ var requestFunction = loader.request;
+ loader.request = function() {};
+
+ try {
+ var builderFilter = null;
+ var master = { builderJsonPath: function() {} };
+ var groupName = '@ToT - dummy.org';
+ var builderGroup = { expectedGroups: 0 };
+ requestBuilderList(testBuilderGroups, builderFilter, master, groupName, builderGroup);
+
+ equal(testBuilderGroups['@ToT - dummy.org'], builderGroup);
+ equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 1);
+ } finally {
+ loader.request = requestFunction;
+ }
+})
+
+test('sortTests', 4, function() {
+ var test1 = createResultsObjectForTest('foo/test1.html', 'dummyBuilder');
+ var test2 = createResultsObjectForTest('foo/test2.html', 'dummyBuilder');
+ var test3 = createResultsObjectForTest('foo/test3.html', 'dummyBuilder');
+ test1.modifiers = 'b';
+ test2.modifiers = 'a';
+ test3.modifiers = '';
+
+ var tests = [test1, test2, test3];
+ sortTests(tests, 'modifiers', FORWARD);
+ deepEqual(tests, [test2, test1, test3]);
+ sortTests(tests, 'modifiers', BACKWARD);
+ deepEqual(tests, [test3, test1, test2]);
+
+ test1.bugs = 'b';
+ test2.bugs = 'a';
+ test3.bugs = '';
+
+ var tests = [test1, test2, test3];
+ sortTests(tests, 'bugs', FORWARD);
+ deepEqual(tests, [test2, test1, test3]);
+ sortTests(tests, 'bugs', BACKWARD);
+ deepEqual(tests, [test3, test1, test2]);
+});
+
+test('popup', 2, function() {
+ showPopup(document.body, 'dummy content');
+ ok(document.querySelector('#popup'));
+ hidePopup();
+ ok(!document.querySelector('#popup'));
+});
+
+test('gpuResultsPath', 3, function() {
+ equal(gpuResultsPath('777777', 'Win7 Release (ATI)'), '777777_Win7_Release_ATI_');
+ equal(gpuResultsPath('123', 'GPU Linux (dbg)(NVIDIA)'), '123_GPU_Linux_dbg_NVIDIA_');
+ equal(gpuResultsPath('12345', 'GPU Mac'), '12345_GPU_Mac');
+});
+
+test('TestTrie', 3, function() {
+ var builders = {
+ "Dummy Chromium Windows Builder": true,
+ "Dummy GTK Linux Builder": true,
+ "Dummy Apple Mac Lion Builder": true
+ };
+
+ var resultsByBuilder = {
+ "Dummy Chromium Windows Builder": {
+ tests: {
+ "foo": true,
+ "foo/bar/1.html": true,
+ "foo/bar/baz": true
+ }
+ },
+ "Dummy GTK Linux Builder": {
+ tests: {
+ "bar": true,
+ "foo/1.html": true,
+ "foo/bar/2.html": true,
+ "foo/bar/baz/1.html": true,
+ }
+ },
+ "Dummy Apple Mac Lion Builder": {
+ tests: {
+ "foo/bar/3.html": true,
+ "foo/bar/baz/foo": true,
+ }
+ }
+ };
+ var expectedTrie = {
+ "foo": {
+ "bar": {
+ "1.html": true,
+ "2.html": true,
+ "3.html": true,
+ "baz": {
+ "1.html": true,
+ "foo": true
+ }
+ },
+ "1.html": true
+ },
+ "bar": true
+ }
+
+ var trie = new TestTrie(builders, resultsByBuilder);
+ deepEqual(trie._trie, expectedTrie);
+
+ var leafsOfCompleteTrieTraversal = [];
+ var expectedLeafs = ["foo/bar/1.html", "foo/bar/baz/1.html", "foo/bar/baz/foo", "foo/bar/2.html", "foo/bar/3.html", "foo/1.html", "bar"];
+ trie.forEach(function(triePath) {
+ leafsOfCompleteTrieTraversal.push(triePath);
+ });
+ deepEqual(leafsOfCompleteTrieTraversal, expectedLeafs);
+
+ var leafsOfPartialTrieTraversal = [];
+ expectedLeafs = ["foo/bar/1.html", "foo/bar/baz/1.html", "foo/bar/baz/foo", "foo/bar/2.html", "foo/bar/3.html"];
+ trie.forEach(function(triePath) {
+ leafsOfPartialTrieTraversal.push(triePath);
+ }, "foo/bar");
+ deepEqual(leafsOfPartialTrieTraversal, expectedLeafs);
+});
diff --git a/Tools/TestResultServer/static-dashboards/loader.js b/Tools/TestResultServer/static-dashboards/loader.js
new file mode 100644
index 0000000..be9e708
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/loader.js
@@ -0,0 +1,247 @@
+// Copyright (C) 2012 Google Inc. All rights reserved.
+// Copyright (C) 2012 Zan Dobersek <zandobersek@gmail.com>
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+var loader = loader || {};
+
+(function() {
+
+var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
+var CHROMIUM_EXPECTATIONS_URL = 'http://svn.webkit.org/repository/webkit/trunk/LayoutTests/platform/chromium/TestExpectations';
+
+function pathToBuilderResultsFile(builderName) {
+ return TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
+ '&master=' + builderMaster(builderName).name +
+ '&testtype=' + g_crossDashboardState.testType + '&name=';
+}
+
+loader.request = function(url, success, error, opt_isBinaryData)
+{
+ var xhr = new XMLHttpRequest();
+ xhr.open('GET', url, true);
+ if (opt_isBinaryData)
+ xhr.overrideMimeType('text/plain; charset=x-user-defined');
+ xhr.onreadystatechange = function(e) {
+ if (xhr.readyState == 4) {
+ if (xhr.status == 200)
+ success(xhr);
+ else
+ error(xhr);
+ }
+ }
+ xhr.send();
+}
+
+loader.Loader = function()
+{
+ this._loadingSteps = [
+ this._loadBuildersList,
+ this._loadResultsFiles,
+ this._loadExpectationsFiles,
+ ];
+}
+
+loader.Loader.prototype = {
+ load: function()
+ {
+ this._loadNext();
+ },
+ buildersListLoaded: function()
+ {
+ initBuilders();
+ this._loadNext();
+ },
+ _loadNext: function()
+ {
+ var loadingStep = this._loadingSteps.shift();
+ if (!loadingStep) {
+ resourceLoadingComplete();
+ return;
+ }
+ loadingStep.apply(this);
+ },
+ _loadBuildersList: function()
+ {
+ loadBuildersList(g_crossDashboardState.group, g_crossDashboardState.testType);
+ },
+ _loadResultsFiles: function()
+ {
+ parseParameters();
+
+ for (var builderName in g_builders)
+ this._loadResultsFileForBuilder(builderName);
+ },
+ _loadResultsFileForBuilder: function(builderName)
+ {
+ var resultsFilename;
+ if (isTreeMap())
+ resultsFilename = 'times_ms.json';
+ else if (g_crossDashboardState.showAllRuns)
+ resultsFilename = 'results.json';
+ else
+ resultsFilename = 'results-small.json';
+
+ var resultsFileLocation = pathToBuilderResultsFile(builderName) + resultsFilename;
+ loader.request(resultsFileLocation,
+ partial(function(loader, builderName, xhr) {
+ loader._handleResultsFileLoaded(builderName, xhr.responseText);
+ }, this, builderName),
+ partial(function(loader, builderName, xhr) {
+ loader._handleResultsFileLoadError(builderName);
+ }, this, builderName));
+ },
+ _handleResultsFileLoaded: function(builderName, fileData)
+ {
+ if (isTreeMap())
+ this._processTimesJSONData(builderName, fileData);
+ else
+ this._processResultsJSONData(builderName, fileData);
+
+ // We need this work-around for webkit.org/b/50589.
+ if (!g_resultsByBuilder[builderName]) {
+ this._handleResultsFileLoadError(builderName);
+ return;
+ }
+
+ this._handleResourceLoad();
+ },
+ _processTimesJSONData: function(builderName, fileData)
+ {
+ // FIXME: We should probably include the builderName in the JSON
+ // rather than relying on only loading one JSON file per page.
+ g_resultsByBuilder[builderName] = JSON.parse(fileData);
+ },
+ _processResultsJSONData: function(builderName, fileData)
+ {
+ var builds = JSON.parse(fileData);
+
+ var json_version = builds['version'];
+ for (var builderName in builds) {
+ if (builderName == 'version')
+ continue;
+
+ // If a test suite stops being run on a given builder, we don't want to show it.
+ // Assume any builder without a run in two weeks for a given test suite isn't
+ // running that suite anymore.
+ // FIXME: Grab which bots run which tests directly from the buildbot JSON instead.
+ var lastRunSeconds = builds[builderName].secondsSinceEpoch[0];
+ if ((Date.now() / 1000) - lastRunSeconds > ONE_WEEK_SECONDS)
+ continue;
+
+ if ((Date.now() / 1000) - lastRunSeconds > ONE_DAY_SECONDS)
+ g_staleBuilders.push(builderName);
+
+ if (json_version >= 4)
+ builds[builderName][TESTS_KEY] = flattenTrie(builds[builderName][TESTS_KEY]);
+ g_resultsByBuilder[builderName] = builds[builderName];
+ }
+ },
+ _handleResultsFileLoadError: function(builderName)
+ {
+ var error = 'Failed to load results file for ' + builderName + '.';
+
+ if (isLayoutTestResults()) {
+ console.error(error);
+ g_buildersThatFailedToLoad.push(builderName);
+ } else {
+ // Avoid to show error/warning messages for non-layout tests. We may be
+ // checking the builders that are not running the tests.
+ console.info('info:' + error);
+ }
+
+ // Remove this builder from builders, so we don't try to use the
+ // data that isn't there.
+ delete g_builders[builderName];
+
+ // Change the default builder name if it has been deleted.
+ if (g_defaultBuilderName == builderName) {
+ g_defaultBuilderName = null;
+ for (var availableBuilderName in g_builders) {
+ g_defaultBuilderName = availableBuilderName;
+ g_defaultDashboardSpecificStateValues.builder = availableBuilderName;
+ break;
+ }
+ if (!g_defaultBuilderName) {
+ var error = 'No tests results found for ' + g_crossDashboardState.testType + '. Reload the page to try fetching it again.';
+ console.error(error);
+ addError(error);
+ }
+ }
+
+ // Proceed as if the resource had loaded.
+ this._handleResourceLoad();
+ },
+ _handleResourceLoad: function()
+ {
+ if (this._haveResultsFilesLoaded())
+ this._loadNext();
+ },
+ _haveResultsFilesLoaded: function()
+ {
+ for (var builder in g_builders) {
+ if (!g_resultsByBuilder[builder])
+ return false;
+ }
+ return true;
+ },
+ _loadExpectationsFiles: function()
+ {
+ if (!isFlakinessDashboard() && !g_crossDashboardState.useTestData) {
+ this._loadNext();
+ return;
+ }
+
+ var expectationsFilesToRequest = {};
+ traversePlatformsTree(function(platform, platformName) {
+ if (platform.fallbackPlatforms)
+ platform.fallbackPlatforms.forEach(function(fallbackPlatform) {
+ var fallbackPlatformObject = platformObjectForName(fallbackPlatform);
+ if (fallbackPlatformObject.expectationsDirectory && !(fallbackPlatform in expectationsFilesToRequest))
+ expectationsFilesToRequest[fallbackPlatform] = EXPECTATIONS_URL_BASE_PATH + fallbackPlatformObject.expectationsDirectory + '/TestExpectations';
+ });
+
+ if (platform.expectationsDirectory)
+ expectationsFilesToRequest[platformName] = EXPECTATIONS_URL_BASE_PATH + platform.expectationsDirectory + '/TestExpectations';
+ });
+
+ for (platformWithExpectations in expectationsFilesToRequest)
+ loader.request(expectationsFilesToRequest[platformWithExpectations],
+ partial(function(loader, platformName, xhr) {
+ g_expectationsByPlatform[platformName] = getParsedExpectations(xhr.responseText);
+
+ delete expectationsFilesToRequest[platformName];
+ if (!Object.keys(expectationsFilesToRequest).length)
+ loader._loadNext();
+ }, this, platformWithExpectations),
+ partial(function(platformName, xhr) {
+ console.error('Could not load expectations file for ' + platformName);
+ }, platformWithExpectations));
+ }
+}
+
+})();
diff --git a/Tools/TestResultServer/static-dashboards/loader_unittests.js b/Tools/TestResultServer/static-dashboards/loader_unittests.js
new file mode 100644
index 0000000..e2f546c
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/loader_unittests.js
@@ -0,0 +1,106 @@
+// Copyright (C) Zan Dobersek <zandobersek@gmail.com>
+//
+// Redistribution and use in source and binary forms, with or without
+// modification, are permitted provided that the following conditions are
+// met:
+//
+// * Redistributions of source code must retain the above copyright
+// notice, this list of conditions and the following disclaimer.
+// * Redistributions in binary form must reproduce the above
+// copyright notice, this list of conditions and the following disclaimer
+// in the documentation and/or other materials provided with the
+// distribution.
+// * Neither the name of Google Inc. nor the names of its
+// contributors may be used to endorse or promote products derived from
+// this software without specific prior written permission.
+//
+// THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+// "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+// LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+// A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+// OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+// SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+// LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+// DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+// THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+// (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+// OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+
+module('loader');
+
+test('loading steps', 1, function() {
+ var loadedSteps = [];
+ var resourceLoader = new loader.Loader();
+ function loadingStep1() {
+ loadedSteps.push('step 1');
+ resourceLoader.load();
+ }
+ function loadingStep2() {
+ loadedSteps.push('step 2');
+ resourceLoader.load();
+ }
+
+ var loadingCompleteCallback = resourceLoadingComplete;
+ resourceLoadingComplete = function() {
+ deepEqual(loadedSteps, ['step 1', 'step 2']);
+ }
+
+ try {
+ resourceLoader._loadingSteps = [loadingStep1, loadingStep2];
+ resourceLoader.load();
+ } finally {
+ resourceLoadingComplete = loadingCompleteCallback;
+ }
+});
+
+test('results files loading', 5, function() {
+ var expectedLoadedBuilders = ["WebKit Linux", "WebKit Win"];
+ var loadedBuilders = [];
+ var resourceLoader = new loader.Loader();
+ resourceLoader._loadNext = function() {
+ deepEqual(loadedBuilders.sort(), expectedLoadedBuilders);
+ loadedBuilders.forEach(function(builderName) {
+ ok('secondsSinceEpoch' in g_resultsByBuilder[builderName]);
+ deepEqual(g_resultsByBuilder[builderName].tests, {});
+ });
+ }
+
+ var requestFunction = loader.request;
+ loader.request = function(url, successCallback, errorCallback) {
+ var builderName = /builder=([\w ]+)&/.exec(url)[1];
+ loadedBuilders.push(builderName);
+ successCallback({responseText: '{"version": 4, "' + builderName + '": {"secondsSinceEpoch": [' + Date.now() + '], "tests": {}}}'});
+ }
+
+ g_builders = {"WebKit Linux": true, "WebKit Win": true};
+
+ try {
+ resourceLoader._loadResultsFiles();
+ } finally {
+ g_builders = undefined;
+ g_resultsByBuilder = undefined;
+ loader.request = requestFunction;
+ }
+});
+
+test('expectations files loading', 1, function() {
+ var expectedLoadedPlatforms = ["chromium", "chromium-android", "efl", "efl-wk1", "efl-wk2", "gtk",
+ "gtk-wk2", "mac", "mac-lion", "mac-snowleopard", "qt", "win", "wk2"];
+ var loadedPlatforms = [];
+ var resourceLoader = new loader.Loader();
+ resourceLoader._loadNext = function() {
+ deepEqual(loadedPlatforms.sort(), expectedLoadedPlatforms);
+ }
+
+ var requestFunction = loader.request;
+ loader.request = function(url, successCallback, errorCallback) {
+ loadedPlatforms.push(/LayoutTests\/platform\/(.+)\/TestExpectations/.exec(url)[1]);
+ successCallback({responseText: ''});
+ }
+
+ try {
+ resourceLoader._loadExpectationsFiles();
+ } finally {
+ loader.request = requestFunction;
+ }
+});
diff --git a/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html b/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html
new file mode 100644
index 0000000..835be21
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/run-embedded-unittests.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2012 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<html>
+<head>
+<link rel="stylesheet" href="../../../Source/ThirdParty/qunit/qunit/qunit.css">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.min.js"></script>
+<script src="../../../Source/ThirdParty/qunit/qunit/qunit.js"></script>
+</head>
+<body>
+<h1 id="qunit-header">Test Results Server JavaScript Unit Tests</h1>
+<h2 id="qunit-banner"></h2>
+<div id="qunit-testrunner-toolbar"></div>
+<h2 id="qunit-userAgent"></h2>
+<ol id="qunit-tests"></ol>
+
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+
+<script>
+// Don't request the actual builders off the bots when running unittests.
+function loadBuildersList() {};
+function g_handleBuildersListLoaded() {};
+
+// Mimic being embedded. All our embedded checks compare window and parent.
+window.parent = null;
+</script>
+
+<script src="dashboard_base.js"></script>
+<script src="flakiness_dashboard.js"></script>
+
+<script>
+window.location.href = '#useTestData=true';
+var builderGroup = '@ToT - chromium.org';
+var builders = {'Webkit Linux': '', 'Webkit Linux (dbg)': '', 'Webkit Mac10.7': '', 'Webkit Win': ''};
+onBuilderListLoad(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, builderGroup, BuilderGroup.TOT_WEBKIT, builders);
+initBuilders();
+</script>
+
+<!-- FIXME: Split this up into multiple unittest.js, e.g. one for builders.js and one for dashboard_base.js. -->
+<script src="flakiness_dashboard_embedded_unittests.js"></script>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/run-unittests.html b/Tools/TestResultServer/static-dashboards/run-unittests.html
new file mode 100644
index 0000000..9999c71
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/run-unittests.html
@@ -0,0 +1,67 @@
+<!DOCTYPE html>
+<!--
+Copyright (C) 2012 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions
+are met:
+1. Redistributions of source code must retain the above copyright
+ notice, this list of conditions and the following disclaimer.
+2. Redistributions in binary form must reproduce the above copyright
+ notice, this list of conditions and the following disclaimer in the
+ documentation and/or other materials provided with the distribution.
+
+THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
+AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
+THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
+PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
+BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
+CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
+SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
+INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
+CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
+ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
+THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<html>
+<head>
+<link rel="stylesheet" href="../../../Source/ThirdParty/qunit/qunit/qunit.css">
+<script src="https://ajax.googleapis.com/ajax/libs/jquery/1.6.2/jquery.min.js"></script>
+<script src="https://ajax.googleapis.com/ajax/libs/jqueryui/1.8.15/jquery-ui.min.js"></script>
+<script src="../../../Source/ThirdParty/qunit/qunit/qunit.js"></script>
+</head>
+<body>
+<h1 id="qunit-header">Test Results Server JavaScript Unit Tests</h1>
+<h2 id="qunit-banner"></h2>
+<div id="qunit-testrunner-toolbar"></div>
+<h2 id="qunit-userAgent"></h2>
+<ol id="qunit-tests"></ol>
+
+<link rel="stylesheet" href="flakiness_dashboard.css"></link>
+<link rel="stylesheet" href="flakiness_dashboard_tests.css"></link>
+<script src="builders.js"></script>
+
+<script>
+// Don't request the actual builders off the bots when running unittests.
+function loadBuildersList() {};
+</script>
+
+<script src="dashboard_base.js"></script>
+<script src="loader.js"></script>
+<script src="flakiness_dashboard.js"></script>
+
+<script>
+window.location.href = '#useTestData=true';
+var groupName = '@ToT - chromium.org';
+var builders = '{"WebKit Linux": true, "WebKit Linux (dbg)": true, "WebKit Mac10.7": true, "WebKit Win": true}';
+LAYOUT_TESTS_BUILDER_GROUPS[groupName] = new BuilderGroup(BuilderGroup.TOT_WEBKIT);
+LAYOUT_TESTS_BUILDER_GROUPS[groupName].expectedGroups = 4;
+onBuilderListLoad(LAYOUT_TESTS_BUILDER_GROUPS, isChromiumWebkitTipOfTreeTestRunner, CHROMIUM_WEBKIT_BUILDER_MASTER, groupName, {responseText: builders});
+initBuilders();
+</script>
+
+<!-- FIXME: Split this up into multiple unittest.js, e.g. one for builders.js and one for dashboard_base.js. -->
+<script src="flakiness_dashboard_unittests.js"></script>
+<script src="loader_unittests.js"></script>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/timeline_explorer.html b/Tools/TestResultServer/static-dashboards/timeline_explorer.html
new file mode 100644
index 0000000..b3fa41b
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/timeline_explorer.html
@@ -0,0 +1,460 @@
+<!-- Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<!DOCTYPE HTML>
+<html>
+
+<head>
+<title>Test Timeline Explorer</title>
+<style>
+body {
+ font-family: Helvetica, Arial, sans-serif;
+ font-size: 12px;
+}
+
+#timeline-container {
+ position: relative;
+}
+
+#inspector-container {
+ width: 300px;
+ float: right;
+ border-left: 1px dashed;
+ padding-left: 5px;
+ margin-left: 5px;
+}
+
+#inspector-container caption {
+ background: #eee;
+ font-weight: bold;
+ padding: 2px;
+ font-size: 14px;
+ text-align: left;
+}
+
+#inspector-table {
+ width: 100%;
+}
+
+#inspector-table td {
+ white-space: nowrap;
+}
+
+#inspector-table .label {
+ color: #666;
+ text-align: right;
+ width: 10em;
+}
+
+#inspector-table .delta.positive {
+ color: #090;
+}
+
+#inspector-table .delta.negative {
+ color: #900;
+}
+
+#inspector-container .buttons {
+ padding: 1em 0;
+ text-align: center;
+}
+
+#inspector-container #build-buttons {
+ border-top: 1px dashed;
+}
+
+#indicator {
+ top: 0;
+ width: 1px;
+ position: absolute;
+ background: red;
+}
+</style>
+<script src="dygraph-combined.js"></script>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script>
+var FAILING_TESTS_DATASET_NAME = 'Failing tests';
+
+var g_dygraph;
+var g_buildIndicesByTimestamp = {};
+var g_currentBuildIndex = -1;
+var g_currentBuilderTestResults;
+
+//////////////////////////////////////////////////////////////////////////////
+// Methods and objects from dashboard_base.js to override.
+//////////////////////////////////////////////////////////////////////////////
+function generatePage()
+{
+ g_buildIndicesByTimestamp = {};
+ var results = g_resultsByBuilder[g_currentState.builder];
+
+ for (var i = 0; i < results[FIXABLE_COUNTS_KEY].length; i++) {
+ var buildDate = new Date(results[TIMESTAMPS_KEY][i] * 1000);
+ g_buildIndicesByTimestamp[buildDate.getTime()] = i;
+ }
+
+ if (g_currentState.buildTimestamp != -1 && g_currentState.buildTimestamp in g_buildIndicesByTimestamp) {
+ var newBuildIndex = g_buildIndicesByTimestamp[g_currentState.buildTimestamp];
+
+ if (newBuildIndex == g_currentBuildIndex) {
+ // This happens when selectBuild is called, which updates the UI
+ // immediately, in addition to updating the location hash (we don't
+ // just rely on the hash change since we don't want to regenerate the
+ // whole page just because the user clicked on something)
+ return;
+ } else if (newBuildIndex)
+ g_currentBuildIndex = newBuildIndex;
+ }
+
+ initCurrentBuilderTestResults();
+
+ $('test-type-switcher').innerHTML = htmlForTestTypeSwitcher( false,
+ checkboxHTML('ignoreFlakyTests', 'Ignore flaky tests', g_currentState.ignoreFlakyTests, 'g_currentBuildIndex = -1')
+ );
+
+ updateTimelineForBuilder();
+}
+
+function initCurrentBuilderTestResults()
+{
+ var startTime = Date.now();
+ g_currentBuilderTestResults = decompressResults(g_resultsByBuilder[g_currentState.builder]);
+ console.log( 'Time to get test results by build: ' + (Date.now() - startTime));
+}
+
+function handleValidHashParameter(key, value)
+{
+ switch(key) {
+ case 'buildTimestamp':
+ g_currentState.buildTimestamp = parseInt(value, 10);
+ return true;
+ case 'ignoreFlakyTests':
+ g_currentState.ignoreFlakyTests = value == 'true';
+ return true;
+ default:
+ return false;
+ }
+}
+
+g_defaultDashboardSpecificStateValues = {
+ buildTimestamp: -1,
+ ignoreFlakyTests: true
+};
+
+function shouldShowWebKitRevisionsOnly()
+{
+ return isTipOfTreeWebKitBuilder();
+}
+
+function updateTimelineForBuilder()
+{
+ var builder = g_currentState.builder;
+ var results = g_resultsByBuilder[builder];
+ var graphData = [];
+
+ var annotations = [];
+
+ // Dygraph prefers to be handed data in chronological order.
+ for (var i = results[FIXABLE_COUNTS_KEY].length - 1; i >= 0; i--) {
+ var buildDate = new Date(results[TIMESTAMPS_KEY][i] * 1000);
+ // FIXME: Find a better way to exclude outliers. This is just so we
+ // exclude runs where every test failed.
+ var failureCount = Math.min(results[FIXABLE_COUNT_KEY][i], 10000);
+
+ if (g_currentState.ignoreFlakyTests)
+ failureCount -= g_currentBuilderTestResults.flakyDeltasByBuild[i].total || 0;
+
+ graphData.push([buildDate, failureCount]);
+
+ if (!shouldShowWebKitRevisionsOnly() && (results[WEBKIT_REVISIONS_KEY][i] != results[WEBKIT_REVISIONS_KEY][i + 1])) {
+ annotations.push({
+ series: FAILING_TESTS_DATASET_NAME,
+ x: buildDate,
+ shortText: 'R',
+ text: 'WebKit roll: r' + results[WEBKIT_REVISIONS_KEY][i + 1] + ' to ' + results[WEBKIT_REVISIONS_KEY][i]
+ });
+ }
+ }
+
+ var windowWidth = document.documentElement.clientWidth;
+ var windowHeight = document.documentElement.clientHeight;
+ var switcherNode = $('test-type-switcher');
+ var inspectorNode = $('inspector-container');
+ var graphWidth = windowWidth - 20 - inspectorNode.offsetWidth;
+ var graphHeight = windowHeight - switcherNode.offsetTop - switcherNode.offsetHeight - 20;
+
+ var containerNode = $('timeline-container');
+ containerNode.style.height = graphHeight + 'px';
+ containerNode.style.width = graphWidth + 'px';
+ inspectorNode.style.height = graphHeight + 'px';
+
+ g_dygraph = new Dygraph(
+ containerNode,
+ graphData, {
+ labels: ['Date', FAILING_TESTS_DATASET_NAME],
+ width: graphWidth,
+ height: graphHeight,
+ clickCallback: function(event, date) {
+ selectBuild(results, builder, g_dygraph, g_buildIndicesByTimestamp[date]);
+ },
+ drawCallback: function(dygraph, isInitial) {
+ if (isInitial)
+ return;
+ updateBuildIndicator(results, dygraph);
+ },
+ // xValueParser is necessary for annotations to work, even though we
+ // already have Date instances
+ xValueParser: function(input) { return input.getTime(); }
+ });
+ if (annotations.length)
+ g_dygraph.setAnnotations(annotations);
+
+ inspectorNode.style.visibility = 'visible';
+
+ if (g_currentBuildIndex != -1)
+ selectBuild(results, builder, g_dygraph, g_currentBuildIndex);
+}
+
+function selectBuild(results, builder, dygraph, index)
+{
+ g_currentBuildIndex = index;
+ updateBuildIndicator(results, dygraph);
+ updateBuildInspector(results, builder, dygraph, index);
+ setQueryParameter('buildTimestamp', results[TIMESTAMPS_KEY][index] * 1000);
+}
+
+function updateBuildIndicator(results, dygraph)
+{
+ var indicatorNode = $('indicator');
+
+ if (!indicatorNode) {
+ var containerNode = $('timeline-container');
+ indicatorNode = document.createElement('div');
+ indicatorNode.id = 'indicator';
+ indicatorNode.style.height = containerNode.offsetHeight + 'px';
+ containerNode.appendChild(indicatorNode);
+ }
+
+ if (g_currentBuildIndex == -1)
+ indicatorNode.style.display = 'none';
+ else {
+ indicatorNode.style.display = 'block';
+ var buildDate = new Date(results[TIMESTAMPS_KEY][g_currentBuildIndex] * 1000);
+ var domCoords = dygraph.toDomCoords(buildDate, 0);
+ indicatorNode.style.left = domCoords[0] + 'px';
+ }
+}
+
+function updateBuildInspector(results, builder, dygraph, index)
+{
+ var html = '<table id="inspector-table"><caption>Details</caption>';
+
+ function addRow(label, value)
+ {
+ html += '<tr><td class="label">' + label + '</td><td>' + value + '</td></tr>';
+ }
+
+ // Builder and results links
+ var buildNumber = results[BUILD_NUMBERS_KEY][index];
+ addRow('', '');
+ var master = builderMaster(builder);
+ var buildUrl = master.logPath(builder, results[BUILD_NUMBERS_KEY][index]);
+ if (master == WEBKIT_BUILDER_MASTER) {
+ var resultsUrl = 'http://build.webkit.org/results/' + builder + '/r' + results[WEBKIT_REVISIONS_KEY][index] +
+ ' (' + results[BUILD_NUMBERS_KEY][index] + ')';
+ } else {
+ var resultsUrl = 'http://build.chromium.org/f/chromium/layout_test_results/' +
+ g_builders[builder] + '/' + results[CHROME_REVISIONS_KEY][index];
+ }
+
+ addRow('Build:', '<a href="' + buildUrl + '" target="_blank">' + buildNumber + '</a> (<a href="' + resultsUrl + '" target="_blank">results</a>)');
+
+ // Revision link(s)
+ if (!shouldShowWebKitRevisionsOnly())
+ addRow('Chromium change:', chromiumRevisionLink(results, index));
+ addRow('WebKit change:', webKitRevisionLink(results, index));
+
+ // Test status/counts
+ addRow('', '');
+
+ function addNumberRow(label, currentValue, previousValue)
+ {
+ var delta = currentValue - previousValue;
+ var deltaText = ''
+ if (delta < 0)
+ deltaText = ' <span class="delta negative">' + delta + '</span>';
+ else if (delta > 0)
+ deltaText = ' <span class="delta positive">+' + delta + '</span>';
+
+ addRow(label, currentValue + deltaText);
+ }
+
+ var expectations = expectationsMap();
+ var flakyDeltasByBuild = g_currentBuilderTestResults.flakyDeltasByBuild;
+ for (var expectationKey in expectations) {
+ if (expectationKey in results[FIXABLE_COUNTS_KEY][index]) {
+ var currentCount = results[FIXABLE_COUNTS_KEY][index][expectationKey];
+ var previousCount = results[FIXABLE_COUNTS_KEY][index + 1][expectationKey];
+ if (g_currentState.ignoreFlakyTests) {
+ currentCount -= flakyDeltasByBuild[index][expectationKey] || 0;
+ previousCount -= flakyDeltasByBuild[index + 1][expectationKey] || 0;
+ }
+ addNumberRow(expectations[expectationKey], currentCount, previousCount);
+ }
+ }
+
+ var currentTotal = results[FIXABLE_COUNT_KEY][index];
+ var previousTotal = results[FIXABLE_COUNT_KEY][index + 1];
+ if (g_currentState.ignoreFlakyTests) {
+ currentTotal -= flakyDeltasByBuild[index].total || 0;
+ previousTotal -= flakyDeltasByBuild[index + 1].total || 0;
+ }
+ addNumberRow('Total failing tests:', currentTotal, previousTotal);
+
+ html += '</table>';
+
+ html += '<div id="changes-button" class="buttons">';
+ html += '<button>Show changed test results</button>';
+ html += '</div>';
+
+ html += '<div id="build-buttons" class="buttons">';
+ html += '<button>Previous build</button> <button>Next build</button>';
+ html += '</div>';
+
+ var inspectorNode = $('inspector-container');
+ inspectorNode.innerHTML = html;
+
+ inspectorNode.getElementsByTagName('button')[0].onclick = function() {
+ showResultsDelta(index, buildNumber, buildUrl, resultsUrl);
+ };
+ inspectorNode.getElementsByTagName('button')[1].onclick = function() {
+ selectBuild(results, builder, dygraph, index + 1);
+ };
+ inspectorNode.getElementsByTagName('button')[2].onclick = function() {
+ selectBuild(results, builder, dygraph, index - 1);
+ };
+}
+
+function showResultsDelta(index, buildNumber, buildUrl, resultsUrl)
+{
+ var flakyTests = g_currentBuilderTestResults.flakyTests;
+ var currentResults = g_currentBuilderTestResults.resultsByBuild[index];
+ var testNames = g_currentBuilderTestResults.testNames;
+ var previousResults = g_currentBuilderTestResults.resultsByBuild[index + 1];
+ var expectations = expectationsMap();
+
+ var deltas = {};
+ function addDelta(category, testIndex)
+ {
+ if (g_currentState.ignoreFlakyTests && flakyTests[testIndex])
+ return;
+ if (!(category in deltas))
+ deltas[category] = [];
+ var testName = testNames[testIndex];
+ var flakinessDashboardUrl = 'flakiness_dashboard.html' + (location.hash ? location.hash + '&' : '#') + 'tests=' + testName;
+ var html = '<a href="' + flakinessDashboardUrl + '">' + testName + '</a>';
+ if (flakyTests[testIndex])
+ html += ' <span style="color: #f66">possibly flaky</span>';
+ deltas[category].push(html);
+ }
+
+ for (var testIndex = 0; testIndex < currentResults.length; testIndex++) {
+ if (currentResults[testIndex] === undefined)
+ continue;
+
+ if (previousResults[testIndex] !== undefined) {
+ if (currentResults[testIndex] == previousResults[testIndex])
+ continue;
+ addDelta('Was <b>' + expectations[previousResults[testIndex]] + '</b> now <b>' + expectations[currentResults[testIndex]] + '</b>', testIndex);
+ } else
+ addDelta('Newly <b>' + expectations[currentResults[testIndex]] + '</b>', testIndex);
+ }
+
+ for (var testIndex = 0; testIndex < previousResults.length; testIndex++) {
+ if (previousResults[testIndex] === undefined)
+ continue;
+ if (currentResults[testIndex] === undefined)
+ addDelta('Was <b>' + expectations[previousResults[testIndex]] + '</b>', testIndex);
+ }
+
+ var html = '';
+
+ html += '<head><base target="_blank"></head>';
+ html += '<h1>Changes in test results</h1>';
+
+ html += '<p>For build <a href="' + buildUrl + '" target="_blank">' +
+ buildNumber + '</a> ' + '(<a href="' + resultsUrl +
+ '" target="_blank">results</a>)</p>';
+
+ for (var deltaCategory in deltas) {
+ html += '<p><div>' + deltaCategory + ' (' + deltas[deltaCategory].length + ')</div><ul>';
+ deltas[deltaCategory].forEach(function(deltaHtml) {
+ html += '<li>' + deltaHtml + '</li>';
+ });
+ html += '</ul></p>';
+ }
+
+ var deltaWindow = window.open();
+ deltaWindow.document.write(html);
+}
+
+document.addEventListener('keydown', function(e) {
+ if (g_currentBuildIndex == -1)
+ return;
+
+ switch (e.keyIdentifier) {
+ case 'Left':
+ selectBuild(
+ g_resultsByBuilder[g_currentState.builder],
+ g_currentState.builder,
+ g_dygraph,
+ g_currentBuildIndex + 1);
+ break;
+ case 'Right':
+ selectBuild(
+ g_resultsByBuilder[g_currentState.builder],
+ g_currentState.builder,
+ g_dygraph,
+ g_currentBuildIndex - 1);
+ break;
+ }
+});
+</script>
+</head>
+<body>
+ <div id="test-type-switcher"></div>
+
+ <div id="inspector-container" style="visibility: hidden">
+ <p>Click on a point on the graph to see details about that build.</p>
+ <p>Click and drag on the graph to zoom in to that period.</p>
+ </div>
+ <div id="timeline-container">Loading...</div>
+</body>
+</html>
diff --git a/Tools/TestResultServer/static-dashboards/treemap.html b/Tools/TestResultServer/static-dashboards/treemap.html
new file mode 100644
index 0000000..aa7ae43
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/treemap.html
@@ -0,0 +1,364 @@
+<!-- Copyright (C) 2011 Google Inc. All rights reserved.
+
+Redistribution and use in source and binary forms, with or without
+modification, are permitted provided that the following conditions are
+met:
+
+ * Redistributions of source code must retain the above copyright
+notice, this list of conditions and the following disclaimer.
+ * Redistributions in binary form must reproduce the above
+copyright notice, this list of conditions and the following disclaimer
+in the documentation and/or other materials provided with the
+distribution.
+ * Neither the name of Google Inc. nor the names of its
+contributors may be used to endorse or promote products derived from
+this software without specific prior written permission.
+
+THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+"AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+(INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+-->
+<!DOCTYPE html>
+<title>Test Runtimes</title>
+<link rel='stylesheet' href='webtreemap.css'></link>
+<style>
+body {
+ display: -moz-box;
+ display: -webkit-box;
+ display: box;
+ -moz-box-orient: vertical;
+ -webkit-box-orient: vertical;
+ box-orient: vertical;
+
+ position: absolute;
+ top: 0;
+ right: 0;
+ bottom: 0;
+ left: 0;
+}
+
+td:first-child {
+ text-align: left;
+}
+
+td {
+ text-align: right;
+}
+
+#map {
+ display: -moz-box;
+ display: -webkit-box;
+ display: box;
+
+ -moz-box-flex: 1;
+ -webkit-box-flex: 1;
+ box-flex: 1;
+
+ position: relative;
+ cursor: pointer;
+ -webkit-user-select: none;
+ -moz-user-select: none;
+}
+
+.extra-dom {
+ display: none;
+ border: none;
+ border-top: 1px dashed;
+ padding: 4px;
+ margin: 0;
+ overflow: auto;
+ cursor: auto;
+ -webkit-user-select: text;
+ -moz-user-select: text;
+}
+
+#focused-leaf {
+ display: -webkit-box;
+ display: -moz-box;
+ -webkit-box-orient: vertical;
+ -moz-box-orient: vertical;
+}
+
+#focused-leaf > .extra-dom {
+ display: -webkit-box;
+ display: -moz-box;
+ -webkit-box-flex: 1;
+ -moz-box-flex: 1;
+}
+
+#focused-leaf.webtreemap-node:hover {
+ background: white;
+}
+
+#focused-leaf .webtreemap-caption:hover {
+ background: #eee;
+}
+
+.error {
+ color: red;
+ font-style: italic;
+}
+</style>
+<script src="builders.js"></script>
+<script src="loader.js"></script>
+<script src="dashboard_base.js"></script>
+<script src='webtreemap.js'></script>
+
+<div id='header-container'></div>
+<p>Click on a box to zoom in. Click on the outermost box to zoom out. <a href="" onclick="showAverages();return false;">Show averages</a></p>
+<div id='map'></div>
+
+<script>
+var TEST_URL_BASE_PATH = "http://svn.webkit.org/repository/webkit/trunk/";
+
+function humanReadableTime(milliseconds)
+{
+ if (milliseconds < 1000)
+ return Math.floor(milliseconds) + 'ms';
+ else if (milliseconds < 60000)
+ return (milliseconds / 1000).toPrecision(2) + 's';
+
+ var minutes = Math.floor(milliseconds / 60000);
+ var seconds = Math.floor((milliseconds - minutes * 60000) / 1000);
+ return minutes + 'm' + seconds + 's';
+}
+
+// This looks like:
+// { "data": {"$area": (sum of all timings)},
+// "name": (name of this node),
+// "children": [ (child nodes, in the same format as this) ] }
+// childCount is added just to be includes in the node's name
+function convertToWebTreemapFormat(treename, tree, path)
+{
+ var total = 0;
+ var childCount = 0;
+ var children = [];
+ for (var name in tree) {
+ var treeNode = tree[name];
+ if (typeof treeNode == "number") {
+ var time = treeNode;
+ var node = {
+ "data": {"$area": time},
+ "name": name + " (" + humanReadableTime(time) + ")"
+ };
+ children.push(node);
+ total += time;
+ childCount++;
+ } else {
+ var newPath = path ? path + '/' + name : name;
+ var subtree = convertToWebTreemapFormat(name, treeNode, newPath);
+ children.push(subtree);
+ total += subtree["data"]["$area"];
+ childCount += subtree["childCount"];
+ }
+ }
+
+ children.sort(function(a, b) {
+ aTime = a.data["$area"]
+ bTime = b.data["$area"]
+ return bTime - aTime;
+ });
+
+ return {
+ "data": {"$area": total},
+ "name": treename + " (" + humanReadableTime(total) + " - " + childCount + " tests)",
+ "children": children,
+ "childCount": childCount,
+ "path": path
+ };
+}
+
+function listOfAllNonLeafNodes(tree, list)
+{
+ if (!tree.children)
+ return;
+
+ if (!list)
+ list = [];
+ list.push(tree);
+
+ tree.children.forEach(function(child) {
+ listOfAllNonLeafNodes(child, list);
+ });
+ return list;
+}
+
+function reverseSortByAverage(list)
+{
+ list.sort(function(a, b) {
+ var avgA = a.data['$area'] / a.childCount;
+ var avgB = b.data['$area'] / b.childCount;
+ return avgB - avgA;
+ });
+}
+
+function showAverages()
+{
+ if (!document.getElementById('map'))
+ return;
+
+ var table = document.createElement('table');
+ table.innerHTML = '<th>directory</th><th># tests</th><th>avg time / test</th>';
+
+ var allNodes = listOfAllNonLeafNodes(g_webTree);
+ reverseSortByAverage(allNodes);
+ allNodes.forEach(function(node) {
+ var average = node.data['$area'] / node.childCount;
+ if (average > 100 && node.childCount != 1) {
+ var tr = document.createElement('tr');
+ tr.innerHTML = '<td></td><td>' + node.childCount + '</td><td>' + humanReadableTime(average) + '</td>';
+ tr.querySelector('td').innerText = node.path;
+ table.appendChild(tr);
+ }
+ });
+
+ var map = document.getElementById('map');
+ map.parentNode.replaceChild(table, map);
+}
+
+var g_isGeneratingPage = false;
+var g_webTree;
+
+function generatePage()
+{
+ $('header-container').innerHTML = htmlForTestTypeSwitcher();
+
+ g_isGeneratingPage = true;
+
+ var rawTree = g_resultsByBuilder[g_currentState.builder];
+ g_webTree = convertToWebTreemapFormat('LayoutTests', rawTree);
+ appendTreemap($('map'), g_webTree);
+
+ if (g_currentState.treemapfocus)
+ focusPath(g_webTree, g_currentState.treemapfocus)
+
+ g_isGeneratingPage = false;
+}
+
+function focusPath(tree, path)
+{
+ var parts = decodeURIComponent(path).split('/');
+ if (extractName(tree) != parts[0]) {
+ console.error('Could not focus tree rooted at ' + parts[0]);
+ return;
+ }
+
+ for (var i = 1; i < parts.length; i++) {
+ var children = tree.children;
+ for (var j = 0; j < children.length; j++) {
+ var child = children[j];
+ if (extractName(child) == parts[i]) {
+ tree = child;
+ focus(tree);
+ break;
+ }
+ }
+ if (j == children.length) {
+ console.error('Could not find tree at ' + parts[i]);
+ break;
+ }
+ }
+
+}
+
+function handleValidHashParameter(key, value)
+{
+ switch(key) {
+ case 'builder':
+ validateParameter(g_currentState, key, value,
+ function() { return value in g_builders; });
+ return true;
+
+ case 'treemapfocus':
+ validateParameter(g_currentState, key, value,
+ function() {
+ // FIXME: There's probably a simpler regexp here. Just trying to match ascii + forward-slash.
+ // e.g. LayoutTests/foo/bar.html
+ return (value.match(/^(\w+\/\w*)*$/));
+ });
+ return true;
+
+ default:
+ return false;
+ }
+}
+
+g_defaultDashboardSpecificStateValues = {
+ treemapfocus: '',
+}
+
+function handleQueryParameterChange(params)
+{
+ for (var param in params) {
+ if (param != 'treemapfocus') {
+ $('map').innerHTML = 'Loading...';
+ return true;
+ }
+ }
+ return false;
+}
+
+// Overrides handleResourceLoadError in dashboard_base.js.
+function handleResourceLoadError(builderName, e)
+{
+ $('map').innerHTML = '<span class=error>Could not load data for ' + builderName + '. ' +
+ 'Either there was a server-side error or ' + builderName + ' does not run ' +
+ g_crossDashboardState.testType + '.</span>';
+}
+
+function extractName(node)
+{
+ return node.name.split(' ')[0];
+}
+
+function fullName(node)
+{
+ var buffer = [extractName(node)];
+ while (node.parent) {
+ node = node.parent;
+ buffer.unshift(extractName(node));
+ }
+ return buffer.join('/');
+}
+
+function handleFocus(tree)
+{
+ var currentlyFocusedNode = $('focused-leaf');
+ if (currentlyFocusedNode)
+ currentlyFocusedNode.id = '';
+
+ if (!tree.children)
+ tree.dom.id = 'focused-leaf';
+
+ var name = fullName(tree);
+
+ if (!tree.children && !tree.extraDom && isLayoutTestResults()) {
+ tree.extraDom = document.createElement('pre');
+ tree.extraDom.className = 'extra-dom';
+ tree.dom.appendChild(tree.extraDom);
+
+ loader.request(TEST_URL_BASE_PATH + name,
+ function(xhr) {
+ tree.extraDom.onmousedown = function(e) {
+ e.stopPropagation();
+ };
+ tree.extraDom.textContent = xhr.responseText;
+ },
+ function (xhr) {
+ tree.extraDom.textContent = "Could not load test."
+ });
+ }
+
+ // We don't want the focus calls during generatePage to try to modify the query state.
+ if (!g_isGeneratingPage)
+ setQueryParameter('treemapfocus', name);
+}
+</script>
diff --git a/Tools/TestResultServer/static-dashboards/webtreemap.css b/Tools/TestResultServer/static-dashboards/webtreemap.css
new file mode 100644
index 0000000..a078650
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/webtreemap.css
@@ -0,0 +1,78 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+.webtreemap-node {
+ /* Required attributes. */
+ position: absolute;
+ overflow: hidden; /* To hide overlong captions. */
+ background: white; /* Nodes must be opaque for zIndex layering. */
+ border: solid 1px black; /* Calculations assume 1px border. */
+
+ /* Optional: CSS animation. */
+ -webkit-transition: top 0.3s,
+ left 0.3s,
+ width 0.3s,
+ height 0.3s;
+}
+
+/* Optional: highlight nodes on mouseover. */
+.webtreemap-node:hover {
+ background: #eee;
+}
+
+/* Optional: Different borders depending on level. */
+.webtreemap-level0 {
+ border: solid 1px #444;
+}
+.webtreemap-level1 {
+ border: solid 1px #666;
+}
+.webtreemap-level2 {
+ border: solid 1px #888;
+}
+.webtreemap-level3 {
+ border: solid 1px #aaa;
+}
+.webtreemap-level4 {
+ border: solid 1px #ccc;
+}
+
+/* Optional: styling on node captions. */
+.webtreemap-caption {
+ font-family: sans-serif;
+ font-size: 11px;
+ padding: 2px;
+ text-align: center;
+}
+
+/* Optional: styling on captions on mouse hover. */
+/*.webtreemap-node:hover > .webtreemap-caption {
+ text-decoration: underline;
+}*/
diff --git a/Tools/TestResultServer/static-dashboards/webtreemap.js b/Tools/TestResultServer/static-dashboards/webtreemap.js
new file mode 100644
index 0000000..35c03c6
--- /dev/null
+++ b/Tools/TestResultServer/static-dashboards/webtreemap.js
@@ -0,0 +1,243 @@
+/*
+ * Copyright (C) 2011 Google Inc. All rights reserved.
+ *
+ * Redistribution and use in source and binary forms, with or without
+ * modification, are permitted provided that the following conditions are
+ * met:
+ *
+ * * Redistributions of source code must retain the above copyright
+ * notice, this list of conditions and the following disclaimer.
+ * * Redistributions in binary form must reproduce the above
+ * copyright notice, this list of conditions and the following disclaimer
+ * in the documentation and/or other materials provided with the
+ * distribution.
+ * * Neither the name of Google Inc. nor the names of its
+ * contributors may be used to endorse or promote products derived from
+ * this software without specific prior written permission.
+ *
+ * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS
+ * "AS IS" AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT
+ * LIMITED TO, THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR
+ * A PARTICULAR PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT
+ * OWNER OR CONTRIBUTORS BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL,
+ * SPECIAL, EXEMPLARY, OR CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT
+ * LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR SERVICES; LOSS OF USE,
+ * DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER CAUSED AND ON ANY
+ * THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, OR TORT
+ * (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE
+ * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
+ */
+
+// Size of border around nodes.
+// We could support arbitrary borders using getComputedStyle(), but I am
+// skeptical the extra complexity (and performance hit) is worth it.
+var kBorderWidth = 1;
+
+// Padding around contents.
+// TODO: do this with a nested div to allow it to be CSS-styleable.
+var kPadding = 4;
+
+var focused = null;
+
+// Callback for embedding page to update after a focus.
+function handleFocus(tree) {}
+
+function focus(tree) {
+ focused = tree;
+
+ // Hide all visible siblings of all our ancestors by lowering them.
+ var level = 0;
+ var root = tree;
+ while (root.parent) {
+ root = root.parent;
+ level += 1;
+ for (var i = 0, sibling; sibling = root.children[i]; ++i) {
+ if (sibling.dom)
+ sibling.dom.style.zIndex = 0;
+ }
+ }
+ var width = root.dom.offsetWidth;
+ var height = root.dom.offsetHeight;
+ // Unhide (raise) and maximize us and our ancestors.
+ for (var t = tree; t.parent; t = t.parent) {
+ // Shift off by border so we don't get nested borders.
+ // TODO: actually make nested borders work (need to adjust width/height).
+ position(t.dom, -kBorderWidth, -kBorderWidth, width, height);
+ t.dom.style.zIndex = 1;
+ }
+ // And layout into the topmost box.
+ layout(tree, level, width, height);
+ handleFocus(tree);
+}
+
+function makeDom(tree, level) {
+ var dom = document.createElement('div');
+ dom.style.zIndex = 1;
+ dom.className = 'webtreemap-node webtreemap-level' + Math.min(level, 4);
+
+ dom.onmousedown = function(e) {
+ if (e.button == 0) {
+ if (focused && tree == focused && focused.parent) {
+ focus(focused.parent);
+ } else {
+ focus(tree);
+ }
+ }
+ e.stopPropagation();
+ return true;
+ };
+
+ var caption = document.createElement('div');
+ caption.className = 'webtreemap-caption';
+ caption.innerHTML = tree.name;
+ dom.appendChild(caption);
+
+ tree.dom = dom;
+ return dom;
+}
+
+function position(dom, x, y, width, height) {
+ // CSS width/height does not include border.
+ width -= kBorderWidth*2;
+ height -= kBorderWidth*2;
+
+ dom.style.left = x + 'px';
+ dom.style.top = y + 'px';
+ dom.style.width = Math.max(width, 0) + 'px';
+ dom.style.height = Math.max(height, 0) + 'px';
+}
+
+// Given a list of rectangles |nodes|, the 1-d space available
+// |space|, and a starting rectangle index |start|, compute an span of
+// rectangles that optimizes a pleasant aspect ratio.
+//
+// Returns [end, sum], where end is one past the last rectangle and sum is the
+// 2-d sum of the rectangles' areas.
+function selectSpan(nodes, space, start) {
+ // Add rectangle one by one, stopping when aspect ratios begin to go
+ // bad. Result is [start,end) covering the best run for this span.
+ // http://scholar.google.com/scholar?cluster=5972512107845615474
+ var node = nodes[start];
+ var rmin = node.data['$area']; // Smallest seen child so far.
+ var rmax = rmin; // Largest child.
+ var rsum = 0; // Sum of children in this span.
+ var last_score = 0; // Best score yet found.
+ for (var end = start; node = nodes[end]; ++end) {
+ var size = node.data['$area'];
+ if (size < rmin)
+ rmin = size;
+ if (size > rmax)
+ rmax = size;
+ rsum += size;
+
+ // This formula is from the paper, but you can easily prove to
+ // yourself it's taking the larger of the x/y aspect ratio or the
+ // y/x aspect ratio. The additional magic fudge constant of 5
+ // makes us prefer wider rectangles to taller ones.
+ var score = Math.max(5*space*space*rmax / (rsum*rsum),
+ 1*rsum*rsum / (space*space*rmin));
+ if (last_score && score > last_score) {
+ rsum -= size; // Undo size addition from just above.
+ break;
+ }
+ last_score = score;
+ }
+ return [end, rsum];
+}
+
+function layout(tree, level, width, height) {
+ if (!('children' in tree))
+ return;
+
+ var total = tree.data['$area'];
+
+ // XXX why do I need an extra -1/-2 here for width/height to look right?
+ var x1 = 0, y1 = 0, x2 = width - 1, y2 = height - 2;
+ x1 += kPadding; y1 += kPadding;
+ x2 -= kPadding; y2 -= kPadding;
+ y1 += 14; // XXX get first child height for caption spacing
+
+ var pixels_to_units = Math.sqrt(total / ((x2 - x1) * (y2 - y1)));
+
+ for (var start = 0, child; child = tree.children[start]; ++start) {
+ if (x2 - x1 < 60 || y2 - y1 < 40) {
+ if (child.dom) {
+ child.dom.style.zIndex = 0;
+ position(child.dom, -2, -2, 0, 0);
+ }
+ continue;
+ }
+
+ // In theory we can dynamically decide whether to split in x or y based
+ // on aspect ratio. In practice, changing split direction with this
+ // layout doesn't look very good.
+ // var ysplit = (y2 - y1) > (x2 - x1);
+ var ysplit = true;
+
+ var space; // Space available along layout axis.
+ if (ysplit)
+ space = (y2 - y1) * pixels_to_units;
+ else
+ space = (x2 - x1) * pixels_to_units;
+
+ var span = selectSpan(tree.children, space, start);
+ var end = span[0], rsum = span[1];
+
+ // Now that we've selected a span, lay out rectangles [start,end) in our
+ // available space.
+ var x = x1, y = y1;
+ for (var i = start; i < end; ++i) {
+ child = tree.children[i];
+ if (!child.dom) {
+ child.parent = tree;
+ child.dom = makeDom(child, level + 1);
+ tree.dom.appendChild(child.dom);
+ } else {
+ child.dom.style.zIndex = 1;
+ }
+ var size = child.data['$area'];
+ var frac = size / rsum;
+ if (ysplit) {
+ width = rsum / space;
+ height = size / width;
+ } else {
+ height = rsum / space;
+ width = size / height;
+ }
+ width /= pixels_to_units;
+ height /= pixels_to_units;
+ width = Math.round(width);
+ height = Math.round(height);
+ position(child.dom, x, y, width, height);
+ if ('children' in child) {
+ layout(child, level + 1, width, height);
+ }
+ if (ysplit)
+ y += height;
+ else
+ x += width;
+ }
+
+ // Shrink our available space based on the amount we used.
+ if (ysplit)
+ x1 += Math.round((rsum / space) / pixels_to_units);
+ else
+ y1 += Math.round((rsum / space) / pixels_to_units);
+
+ // end points one past where we ended, which is where we want to
+ // begin the next iteration, but subtract one to balance the ++ in
+ // the loop.
+ start = end - 1;
+ }
+}
+
+function appendTreemap(dom, data) {
+ var style = getComputedStyle(dom, null);
+ var width = parseInt(style.width);
+ var height = parseInt(style.height);
+ if (!data.dom)
+ makeDom(data, 0);
+ dom.appendChild(data.dom);
+ position(data.dom, 0, 0, width, height);
+ layout(data, 0, width, height);
+}
\ No newline at end of file