[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 ddebaa5..d782b0d 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 9a76b06..fa033a5 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 3dd6052..43902ff 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 a3a4ec1..4fc0bb4 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 6be0dda..625d423 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 41a2d6a..7589e34 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 75864fa..597c62e 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 e6879d9..9999c71 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 dd85053..b3fa41b 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 684957b..aa7ae43 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();