[TestResultServer] Move the resource loading into a dedicated class
authorzandobersek@gmail.com <zandobersek@gmail.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 22 Oct 2012 07:44:16 +0000 (07:44 +0000)
committerzandobersek@gmail.com <zandobersek@gmail.com@268f45cc-cd09-0410-ab3c-d52691b4dbfc>
Mon, 22 Oct 2012 07:44:16 +0000 (07:44 +0000)
https://bugs.webkit.org/show_bug.cgi?id=99246

Reviewed by Ojan Vafai.

A new 'loader' namespace is created, containing the request method (previously located in dashboard_base.js)
and the new Loader object, which handles the loading of all the necessary data the dashboard might require.

* TestResultServer/static-dashboards/aggregate_results.html: Include the loader.js source file.
* TestResultServer/static-dashboards/builders.js:
(requestBuilderList): Use the request method located in the loader namespace instead of the removed doXHR method.
(onBuilderListLoad): Now parses the response text of the passed-in XHR. When all the builder lists are loaded the
resource loader object is notified appropriately.
(onErrorLoadingBuilderList): The partial function that calls this function also adds an XHR parameter.
* TestResultServer/static-dashboards/dashboard_base.js: Much of the resource loading-related code is moved to loader.js.
The Loader object is now used to load all the required resources.
(parseParameters): Don't push the 'builder' parameter into the current state if the unit tests are being run.
(resourceLoadingComplete): This method gets called when all the resources are loaded and the dashboard should
proceed with generating the page.
(handleLocationChange):
* TestResultServer/static-dashboards/flakiness_dashboard.html: Include the loader.js source file.
* TestResultServer/static-dashboards/flakiness_dashboard.js: The request method has been relocated to the loader namespace.
* TestResultServer/static-dashboards/flakiness_dashboard_unittests.js: The affected test cases are modified appropriately.
(test):
* TestResultServer/static-dashboards/loader.js: Added.
(.): A new namespace is introduced, publicly exporting the request method that performs an XHR operation and a Loader object
which oversees resource loading. The loading is done in steps, first loading the builders list, after that the results files
the current dashboard needs, and lastly the TestExpectations files if they are required by the dashboard. When done the loader
calls the resourceLoadingComplete method located in dashboard_base.js. This signals the dashboard all resources are available
and it can proceed with generating the dashboard page.
* TestResultServer/static-dashboards/loader_unittests.js: Added. Contains unit tests for the Loader object, covering the
incremental loading and the loading of results files and TestExpectations files. The builders list loading is currently not
tested as the unit tests page overrides related methods that possibly affect other tests' behavior.
* TestResultServer/static-dashboards/run-unittests.html: Now includes the loader.js and loader_unittests.js source file.
Refactors the code a bit due to changes in how onBuilderListLoad behaves.
* TestResultServer/static-dashboards/timeline_explorer.html: Now includes the loader.js source file.
* TestResultServer/static-dashboards/treemap.html: Ditto. Also refactors the code to take into account
that all the test files are now loaded before generating the dashboard page.

git-svn-id: https://svn.webkit.org/repository/webkit/trunk@132034 268f45cc-cd09-0410-ab3c-d52691b4dbfc

12 files changed:
Tools/ChangeLog
Tools/TestResultServer/static-dashboards/aggregate_results.html
Tools/TestResultServer/static-dashboards/builders.js
Tools/TestResultServer/static-dashboards/dashboard_base.js
Tools/TestResultServer/static-dashboards/flakiness_dashboard.html
Tools/TestResultServer/static-dashboards/flakiness_dashboard.js
Tools/TestResultServer/static-dashboards/flakiness_dashboard_unittests.js
Tools/TestResultServer/static-dashboards/loader.js [new file with mode: 0644]
Tools/TestResultServer/static-dashboards/loader_unittests.js [new file with mode: 0644]
Tools/TestResultServer/static-dashboards/run-unittests.html
Tools/TestResultServer/static-dashboards/timeline_explorer.html
Tools/TestResultServer/static-dashboards/treemap.html

index ddebaa5220ca8fcd4cd248dfa0869c883a55e37d..d782b0de3de0a35c6fb63c749d09ffa1eeb9a070 100644 (file)
@@ -1,3 +1,44 @@
+2012-10-22  Zan Dobersek  <zandobersek@gmail.com>
+
+        [TestResultServer] Move the resource loading into a dedicated class
+        https://bugs.webkit.org/show_bug.cgi?id=99246
+
+        Reviewed by Ojan Vafai.
+
+        A new 'loader' namespace is created, containing the request method (previously located in dashboard_base.js)
+        and the new Loader object, which handles the loading of all the necessary data the dashboard might require.
+
+        * TestResultServer/static-dashboards/aggregate_results.html: Include the loader.js source file.
+        * TestResultServer/static-dashboards/builders.js:
+        (requestBuilderList): Use the request method located in the loader namespace instead of the removed doXHR method.
+        (onBuilderListLoad): Now parses the response text of the passed-in XHR. When all the builder lists are loaded the
+        resource loader object is notified appropriately.
+        (onErrorLoadingBuilderList): The partial function that calls this function also adds an XHR parameter.
+        * TestResultServer/static-dashboards/dashboard_base.js: Much of the resource loading-related code is moved to loader.js.
+        The Loader object is now used to load all the required resources.
+        (parseParameters): Don't push the 'builder' parameter into the current state if the unit tests are being run.
+        (resourceLoadingComplete): This method gets called when all the resources are loaded and the dashboard should
+        proceed with generating the page.
+        (handleLocationChange):
+        * TestResultServer/static-dashboards/flakiness_dashboard.html: Include the loader.js source file.
+        * TestResultServer/static-dashboards/flakiness_dashboard.js: The request method has been relocated to the loader namespace.
+        * TestResultServer/static-dashboards/flakiness_dashboard_unittests.js: The affected test cases are modified appropriately.
+        (test):
+        * TestResultServer/static-dashboards/loader.js: Added.
+        (.): A new namespace is introduced, publicly exporting the request method that performs an XHR operation and a Loader object
+        which oversees resource loading. The loading is done in steps, first loading the builders list, after that the results files
+        the current dashboard needs, and lastly the TestExpectations files if they are required by the dashboard. When done the loader
+        calls the resourceLoadingComplete method located in dashboard_base.js. This signals the dashboard all resources are available
+        and it can proceed with generating the dashboard page.
+        * TestResultServer/static-dashboards/loader_unittests.js: Added. Contains unit tests for the Loader object, covering the
+        incremental loading and the loading of results files and TestExpectations files. The builders list loading is currently not
+        tested as the unit tests page overrides related methods that possibly affect other tests' behavior.
+        * TestResultServer/static-dashboards/run-unittests.html: Now includes the loader.js and loader_unittests.js source file.
+        Refactors the code a bit due to changes in how onBuilderListLoad behaves.
+        * TestResultServer/static-dashboards/timeline_explorer.html: Now includes the loader.js source file.
+        * TestResultServer/static-dashboards/treemap.html: Ditto. Also refactors the code to take into account
+        that all the test files are now loaded before generating the dashboard page.
+
 2012-10-21  Jochen Eisinger  <jochen@chromium.org>
 
         [chromium] introduce a public API for the TestRunner library
index 9a76b06b4ed726c9bb6250cd94fe93ad9097dd4f..fa033a54d4fdab2eb563835ff436414f791ba418 100644 (file)
@@ -51,6 +51,7 @@ img {
 }
 </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.
index 3dd6052da243b9176ac2e91ba740d3a53f02756c..43902ffe76d0442a19d6940a58175d05e78aab7f 100644 (file)
@@ -122,26 +122,13 @@ function associateBuildersWithMaster(builders, master)
     });
 }
 
-function doXHR(url, onLoad, builderGroups, groupName)
-{
-    var xhr = new XMLHttpRequest();
-    xhr.open('GET', url, true);
-    xhr.onload = function() {
-        if (xhr.status == 200)
-            onLoad(JSON.parse(xhr.response));
-        else
-            onErrorLoadingBuilderList(url, builderGroups, groupName);
-    };
-    xhr.onerror = function() { onErrorLoadingBuilderList(url, builderGroups, groupName); };
-    xhr.send();
-}
-
 function requestBuilderList(builderGroups, builderFilter, master, groupName, builderGroup)
 {
     if (!builderGroups[groupName])
         builderGroups[groupName] = builderGroup;
-    var onLoad = partial(onBuilderListLoad, builderGroups, builderFilter, master, groupName);
-    doXHR(master.builderJsonPath(), onLoad, builderGroups, groupName);
+    loader.request(master.builderJsonPath(),
+                   partial(onBuilderListLoad, builderGroups, builderFilter, master, groupName),
+                   partial(onErrorLoadingBuilderList, master.builderJsonPath(), builderGroups, groupName));
     builderGroups[groupName].expectedGroups += 1;
 }
 
@@ -220,16 +207,16 @@ function generateBuildersFromBuilderList(builderList, filter)
     });
 }
 
-function onBuilderListLoad(builderGroups, builderFilter, master, groupName, json)
+function onBuilderListLoad(builderGroups, builderFilter, master, groupName, xhr)
 {
-    var builders = generateBuildersFromBuilderList(Object.keys(json), builderFilter);
+    var builders = generateBuildersFromBuilderList(Object.keys(JSON.parse(xhr.responseText)), builderFilter);
     associateBuildersWithMaster(builders, master);
     builderGroups[groupName].append(builders);
     if (builderGroups[groupName].loaded())
-        g_handleBuildersListLoaded();
+        g_resourceLoader.buildersListLoaded();
 }
 
-function onErrorLoadingBuilderList(url, builderGroups, groupName)
+function onErrorLoadingBuilderList(url, builderGroups, groupName, xhr)
 {
     builderGroups[groupName].groups += 1;
     console.log('Could not load list of builders from ' + url + '. Try reloading.');
index a3a4ec126115a21108065e3d8e008033e6736a06..4fc0bb44801d299e6b027da62c76fb737b6708fb 100644 (file)
@@ -33,6 +33,7 @@
 // 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.
@@ -159,8 +160,6 @@ var RLE = {
     VALUE: 1
 }
 
-var TEST_RESULTS_SERVER = 'http://test-results.appspot.com/';
-
 function isFailingResult(value)
 {
     return 'FSTOCIZ'.indexOf(value) != -1;
@@ -262,24 +261,6 @@ function collapseWhitespace(str)
     return str.replace(/\s+/g, ' ');
 }
 
-function request(url, success, error, opt_isBinaryData)
-{
-    console.log('XMLHttpRequest: ' + url);
-    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();
-}
-
 function validateParameter(state, key, value, validateFn)
 {
     if (validateFn())
@@ -359,7 +340,8 @@ function parseParameters()
     var dashboardSpecificDiffState = diffStates(oldDashboardSpecificState, g_currentState);
 
     fillMissingValues(g_currentState, g_defaultDashboardSpecificStateValues);
-    fillMissingValues(g_currentState, {'builder': g_defaultBuilderName});
+    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)
@@ -410,34 +392,10 @@ function fillMissingValues(to, from)
     }
 }
 
-// Load a script.
-// @param {string} path Path to the script to load.
-// @param {Function=} opt_onError Optional function to call if the script
-//         does not load.
-// @param {Function=} opt_onLoad Optional function to call when the script
-//         is loaded.    Called with the script element as its 1st argument.
-function appendScript(path, opt_onError, opt_onLoad)
-{
-    var script = document.createElement('script');
-    script.src = path;
-    if (opt_onLoad) {
-        script.onreadystatechange = function() {
-            if (this.readyState == 'complete')
-                opt_onLoad(script);
-        };
-        script.onload = function() { opt_onLoad(script); };
-    }
-    if (opt_onError)
-        script.onerror = opt_onError;
-    document.getElementsByTagName('head')[0].appendChild(script);
-}
-
 // FIXME: Rename this to g_dashboardSpecificState;
 var g_currentState = {};
 var g_crossDashboardState = {};
-var g_waitingOnExpectations;
 parseCrossDashboardParameters();
-loadBuildersList(g_crossDashboardState.group, g_crossDashboardState.testType);
 
 function isLayoutTestResults()
 {
@@ -498,32 +456,6 @@ var g_expectationsByPlatform = {};
 var g_staleBuilders = [];
 var g_buildersThatFailedToLoad = [];
 
-function ADD_RESULTS(builds)
-{
-    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];
-    }
-
-    handleResourceLoad();
-}
-
 // 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)
@@ -544,44 +476,6 @@ function flattenTrie(trie, prefix)
     return result;
 }
 
-function pathToBuilderResultsFile(builderName)
-{
-    return TEST_RESULTS_SERVER + 'testfile?builder=' + builderName +
-            '&master=' + builderMaster(builderName).name +
-            '&testtype=' + g_crossDashboardState.testType + '&name=';
-}
-
-function requestExpectationsFiles()
-{
-    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)
-        request(expectationsFilesToRequest[platformWithExpectations],
-                partial(function(platformName, xhr) {
-                    g_expectationsByPlatform[platformName] = getParsedExpectations(xhr.responseText);
-
-                    delete expectationsFilesToRequest[platformName];
-                    if (!Object.keys(expectationsFilesToRequest).length) {
-                        g_waitingOnExpectations = false;
-                        handleResourceLoad();
-                    }
-                }, platformWithExpectations),
-                partial(function(platformName, xhr) {
-                    console.error('Could not load expectations file for ' + platformName);
-                }, platformWithExpectations));
-}
-
 function isTreeMap()
 {
     return endsWith(window.location.pathname, 'treemap.html');
@@ -592,106 +486,10 @@ function isFlakinessDashboard()
     return endsWith(window.location.pathname, 'flakiness_dashboard.html');
 }
 
-function appendJSONScriptElementFor(builderName)
-{
-    var resultsFilename;
-    if (isTreeMap())
-        resultsFilename = 'times_ms.json';
-    else if (g_crossDashboardState.showAllRuns)
-        resultsFilename = 'results.json';
-    else
-        resultsFilename = 'results-small.json';
-
-    appendScript(pathToBuilderResultsFile(builderName) + resultsFilename + '&callback=ADD_RESULTS',
-            partial(handleResourceLoadError, builderName),
-            partial(handleScriptLoaded, builderName));
-}
-
-function appendJSONScriptElements()
-{
-    clearErrors();
-
-    if (isTreeMap())
-        return;
-
-    parseParameters();
-
-    if (g_crossDashboardState.useTestData)
-        return;
-
-    for (var builderName in g_builders)
-        appendJSONScriptElementFor(builderName);
-
-    g_waitingOnExpectations = isLayoutTestResults() && isFlakinessDashboard();
-    if (g_waitingOnExpectations)
-        requestExpectationsFiles();
-}
-
 var g_hasDoneInitialPageGeneration = false;
 // String of error messages to display to the user.
 var g_errorMessages = '';
 
-function handleResourceLoad()
-{
-    // In case we load a results.json that's not in the list of builders,
-    // make sure to only call handleLocationChange once from the resource loads.
-    if (!g_hasDoneInitialPageGeneration)
-        handleLocationChange();
-}
-
-function handleScriptLoaded(builderName, script)
-{
-    // We need this work-around for webkit.org/b/50589.
-    if (!g_resultsByBuilder[builderName]) {
-        var error = new Error("Builder data was empty");
-        error.target = script;
-        handleResourceLoadError(builderName, error);
-    }
-}
-
-// Handle resource loading errors - 404s, 500s, etc.    Recover from the error to
-// still show as much data as possible, but show some UI about the failure, and
-// do not try using this resource again until user refreshes.
-//
-// @param {string} builderName Name of builder that the resource failed for.
-// @param {Event} e The error event.
-function handleResourceLoadError(builderName, e)
-{
-    var error = e.target.src + ' failed to load 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.
-    handleResourceLoad();
-}
-
-
 // Record a new error message.
 // @param {string} errorMsg The message to show to the user.
 function addError(errorMsg)
@@ -726,25 +524,6 @@ function showErrors()
     errors.innerHTML = g_errorMessages;
 }
 
-// @return {boolean} Whether the json files have all completed loading.
-function haveJsonFilesLoaded()
-{
-    if (!currentBuilderGroup())
-        return false;
-
-    if (g_waitingOnExpectations)
-        return false;
-
-    if (isTreeMap())
-        return true;
-
-    for (var builder in g_builders) {
-        if (!g_resultsByBuilder[builder])
-            return false;
-    }
-    return true;
-}
-
 function addBuilderLoadErrors()
 {
     if (g_hasDoneInitialPageGeneration)
@@ -757,11 +536,14 @@ function addBuilderLoadErrors()
         addError('ERROR: Data from ' + g_staleBuilders.toString() + ' is more than 1 day stale.');
 }
 
-function handleLocationChange()
+function resourceLoadingComplete()
 {
-    if(!haveJsonFilesLoaded())
-        return;
+    g_resourceLoader = null;
+    handleLocationChange();
+}
 
+function handleLocationChange()
+{
     addBuilderLoadErrors();
     g_hasDoneInitialPageGeneration = true;
 
@@ -1117,13 +899,6 @@ function decompressResults(builderResults)
     };
 }
 
-function g_handleBuildersListLoaded() {
-    if (!currentBuilderGroup())
-        return;
-    initBuilders();
-    appendJSONScriptElements();
-}
-
 document.addEventListener('mousedown', function(e) {
     // Clear the open popup, unless the click was inside the popup.
     var popup = $('popup');
@@ -1135,4 +910,6 @@ 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);
index 6be0dda0458b66f2f7af02d080f7d3d1056dd26f..625d4238d7c24a989d41ef1d416e1cddee59ef33 100644 (file)
@@ -31,5 +31,6 @@ OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE.
 <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>
index 41a2d6a51cfbceba981d012964113b1ef972b456..7589e34242ce137b1fe65cec8d2b773d2b5b70a2 100644 (file)
@@ -1860,7 +1860,7 @@ function maybeAddPngChecksum(expectationDiv, pngUrl)
 {
     // pngUrl gets served from the browser cache since we just loaded it in an
     // <img> tag.
-    request(pngUrl,
+    loader.request(pngUrl,
         function(xhr) {
             // Convert the first 2k of the response to a byte string.
             var bytes = xhr.responseText.substring(0, 2048);
@@ -1954,7 +1954,7 @@ function addExpectationItem(expectationsContainers, parentContainer, platform, p
         if (!isImage)
             childContainer.appendChild(dummyNode);
     } else {
-        request(url,
+        loader.request(url,
             function(xhr) {
                 var item = document.createElement('pre');
                 item.innerText = xhr.responseText;
index 75864fae76bbbd4e1b4379b83bb46248996ff4c2..597c62e2a02f24e7701b3395db521f2779bbe3fe 100644 (file)
@@ -387,6 +387,11 @@ test('htmlForIndividualTestOnAllBuildersWithResultsLinks', 1, function() {
             '</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> | ' +
@@ -412,6 +417,11 @@ test('htmlForIndividualTestOnAllBuildersWithResultsLinksWebkitMaster', 1, functi
             '</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>' +
@@ -642,9 +652,9 @@ test('builderGroupIsToTWebKitAttribute', 2, function() {
     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', JSON.parse(testJSONData));
+    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', JSON.parse(testJSONData));
+    onBuilderListLoad(testBuilderGroups, function() { return true; }, dummyMaster, '@DEPS - dummy.org', {responseText: testJSONData});
     equal(testBuilderGroups['@DEPS - dummy.org'].isToTWebKit, false);
 });
 
@@ -657,10 +667,10 @@ test('builderGroupExpectedGroups', 4, function() {
 
     var testJSONData = "{ \"Dummy Builder 1\": null }";
     equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 3);
-    onBuilderListLoad(testBuilderGroups,  function() { return true; }, dummyMaster, '@ToT - dummy.org', JSON.parse(testJSONData));
+    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', JSON.parse(testJSONData));
+    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);
@@ -669,9 +679,10 @@ test('builderGroupExpectedGroups', 4, function() {
 test('requestBuilderListAddsBuilderGroupEntry', 2, function() {
     var testBuilderGroups = { '@ToT - dummy.org': null };
 
-    var oldDoXHR = doXHR;
+    var requestFunction = loader.request;
+    loader.request = function() {};
+
     try {
-        doXHR = function() {};
         var builderFilter = null;
         var master = { builderJsonPath: function() {} };
         var groupName = '@ToT - dummy.org';
@@ -681,7 +692,7 @@ test('requestBuilderListAddsBuilderGroupEntry', 2, function() {
         equal(testBuilderGroups['@ToT - dummy.org'], builderGroup);
         equal(testBuilderGroups['@ToT - dummy.org'].expectedGroups, 1);
     } finally {
-        doXHR = oldDoXHR;
+        loader.request = requestFunction;
     }
 })
 
diff --git a/Tools/TestResultServer/static-dashboards/loader.js b/Tools/TestResultServer/static-dashboards/loader.js
new file mode 100644 (file)
index 0000000..be9e708
--- /dev/null
@@ -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 (file)
index 0000000..e2f546c
--- /dev/null
@@ -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;
+    }
+});
index e6879d931866c8cf2a912db13eb12e33913ba4f1..9999c71f9670c90de1482af429ec7374aa41ca5f 100644 (file)
@@ -44,23 +44,24 @@ THE POSSIBILITY OF SUCH DAMAGE.
 <script>
 // Don't request the actual builders off the bots when running unittests.
 function loadBuildersList() {};
-function g_handleBuildersListLoaded() {};
 </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': '', 'Webkit Linux (dbg)': '', 'Webkit Mac10.7': '', 'Webkit Win': ''};
+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, BuilderGroup.TOT_WEBKIT, builders);
+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>
index dd8505332d22e36af96fea13169e16f853937dbf..b3fa41bc350f5c57c5c160fa7c5a1ea6ccb0ef02 100644 (file)
@@ -97,6 +97,7 @@ body {
 </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';
index 684957bc64cf71b95b19a934caeab96dc4ee366a..aa7ae437ef7c1e41243e7e157939863d2ae90971 100644 (file)
@@ -108,6 +108,7 @@ td {
 }
 </style>
 <script src="builders.js"></script>
+<script src="loader.js"></script>
 <script src="dashboard_base.js"></script>
 <script src='webtreemap.js'></script>
 
@@ -223,35 +224,12 @@ function showAverages()
     map.parentNode.replaceChild(table, map);
 }
 
-var g_resultsByBuilder = {};
-
-function ADD_RESULTS(data)
-{
-    // FIXME: We should probably include the builderName in the JSON
-    // rather than relying on only loading one JSON file per page.
-    if (!g_resultsByBuilder[g_currentState.builder])
-        g_resultsByBuilder[g_currentState.builder] = data;
-
-    handleLocationChange();
-}
-
-function g_handleBuildersListLoaded() {
-    g_buildersListLoaded = true;
-    initBuilders(g_currentState);
-    $('header-container').innerHTML = htmlForTestTypeSwitcher();
-    parseParameters();
-    appendJSONScriptElementFor(g_currentState.builder);
-}
-
 var g_isGeneratingPage = false;
 var g_webTree;
 
 function generatePage()
 {
-    if (!g_resultsByBuilder[g_currentState.builder]) {
-        handleResourceLoadError(g_currentState.builder);
-        return;
-    }
+    $('header-container').innerHTML = htmlForTestTypeSwitcher();
 
     g_isGeneratingPage = true;
 
@@ -319,12 +297,6 @@ g_defaultDashboardSpecificStateValues = {
 
 function handleQueryParameterChange(params)
 {
-    if (!g_buildersListLoaded)
-        return false;
-
-    if (!g_resultsByBuilder[g_currentState.builder])
-        appendJSONScriptElementFor(g_currentState.builder);
-
     for (var param in params) {
         if (param != 'treemapfocus') {
             $('map').innerHTML = 'Loading...';
@@ -373,7 +345,7 @@ function handleFocus(tree)
         tree.extraDom.className = 'extra-dom';
         tree.dom.appendChild(tree.extraDom);
 
-        request(TEST_URL_BASE_PATH + name,
+        loader.request(TEST_URL_BASE_PATH + name,
             function(xhr) {
                 tree.extraDom.onmousedown = function(e) {
                     e.stopPropagation();