Home | History | Annotate | Download | only in scripts
      1 /*
      2  * Copyright (C) 2011 Google Inc. All rights reserved.
      3  *
      4  * Redistribution and use in source and binary forms, with or without
      5  * modification, are permitted provided that the following conditions
      6  * are met:
      7  * 1. Redistributions of source code must retain the above copyright
      8  *    notice, this list of conditions and the following disclaimer.
      9  * 2. Redistributions in binary form must reproduce the above copyright
     10  *    notice, this list of conditions and the following disclaimer in the
     11  *    documentation and/or other materials provided with the distribution.
     12  *
     13  * THIS SOFTWARE IS PROVIDED BY APPLE INC. AND ITS CONTRIBUTORS ``AS IS''
     14  * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO,
     15  * THE IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR
     16  * PURPOSE ARE DISCLAIMED. IN NO EVENT SHALL APPLE INC. OR ITS CONTRIBUTORS
     17  * BE LIABLE FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR
     18  * CONSEQUENTIAL DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF
     19  * SUBSTITUTE GOODS OR SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS
     20  * INTERRUPTION) HOWEVER CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN
     21  * CONTRACT, STRICT LIABILITY, OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE)
     22  * ARISING IN ANY WAY OUT OF THE USE OF THIS SOFTWARE, EVEN IF ADVISED OF
     23  * THE POSSIBILITY OF SUCH DAMAGE.
     24  */
     25 
     26 var results = results || {};
     27 
     28 (function() {
     29 
     30 var kResultsName = 'failing_results.json';
     31 
     32 var PASS = 'PASS';
     33 var TIMEOUT = 'TIMEOUT';
     34 var TEXT = 'TEXT';
     35 var CRASH = 'CRASH';
     36 var IMAGE = 'IMAGE';
     37 var IMAGE_TEXT = 'IMAGE+TEXT';
     38 var AUDIO = 'AUDIO';
     39 var MISSING = 'MISSING';
     40 
     41 var kFailingResults = [TEXT, IMAGE_TEXT, AUDIO];
     42 
     43 var kExpectedImageSuffix = '-expected.png';
     44 var kActualImageSuffix = '-actual.png';
     45 var kImageDiffSuffix = '-diff.png';
     46 var kExpectedAudioSuffix = '-expected.wav';
     47 var kActualAudioSuffix = '-actual.wav';
     48 var kExpectedTextSuffix = '-expected.txt';
     49 var kActualTextSuffix = '-actual.txt';
     50 var kDiffTextSuffix = '-diff.txt';
     51 var kCrashLogSuffix = '-crash-log.txt';
     52 
     53 var kPNGExtension = 'png';
     54 var kTXTExtension = 'txt';
     55 var kWAVExtension = 'wav';
     56 
     57 var kPreferredSuffixOrder = [
     58     kExpectedImageSuffix,
     59     kActualImageSuffix,
     60     kImageDiffSuffix,
     61     kExpectedTextSuffix,
     62     kActualTextSuffix,
     63     kDiffTextSuffix,
     64     kCrashLogSuffix,
     65     kExpectedAudioSuffix,
     66     kActualAudioSuffix,
     67     // FIXME: Add support for the rest of the result types.
     68 ];
     69 
     70 // Kinds of results.
     71 results.kActualKind = 'actual';
     72 results.kExpectedKind = 'expected';
     73 results.kDiffKind = 'diff';
     74 results.kUnknownKind = 'unknown';
     75 
     76 // Types of tests.
     77 results.kImageType = 'image'
     78 results.kAudioType = 'audio'
     79 results.kTextType = 'text'
     80 // FIXME: There are more types of tests.
     81 
     82 function layoutTestResultsURL(platform)
     83 {
     84     return config.kPlatforms[platform].layoutTestResultsURL;
     85 }
     86 
     87 function possibleSuffixListFor(failureTypeList)
     88 {
     89     var suffixList = [];
     90 
     91     function pushImageSuffixes()
     92     {
     93         suffixList.push(kExpectedImageSuffix);
     94         suffixList.push(kActualImageSuffix);
     95         suffixList.push(kImageDiffSuffix);
     96     }
     97 
     98     function pushAudioSuffixes()
     99     {
    100         suffixList.push(kExpectedAudioSuffix);
    101         suffixList.push(kActualAudioSuffix);
    102     }
    103 
    104     function pushTextSuffixes()
    105     {
    106         suffixList.push(kActualTextSuffix);
    107         suffixList.push(kExpectedTextSuffix);
    108         suffixList.push(kDiffTextSuffix);
    109         // '-wdiff.html',
    110         // '-pretty-diff.html',
    111     }
    112 
    113     $.each(failureTypeList, function(index, failureType) {
    114         switch(failureType) {
    115         case IMAGE:
    116             pushImageSuffixes();
    117             break;
    118         case TEXT:
    119             pushTextSuffixes();
    120             break;
    121         case AUDIO:
    122             pushAudioSuffixes();
    123             break;
    124         case IMAGE_TEXT:
    125             pushImageSuffixes();
    126             pushTextSuffixes();
    127             break;
    128         case CRASH:
    129             suffixList.push(kCrashLogSuffix);
    130             break;
    131         case MISSING:
    132             pushImageSuffixes();
    133             pushTextSuffixes();
    134             break;
    135         default:
    136             // FIXME: Add support for the rest of the result types.
    137             // '-expected.html',
    138             // '-expected-mismatch.html',
    139             // ... and possibly more.
    140             break;
    141         }
    142     });
    143 
    144     return base.uniquifyArray(suffixList);
    145 }
    146 
    147 results.failureTypeToExtensionList = function(failureType)
    148 {
    149     switch(failureType) {
    150     case IMAGE:
    151         return [kPNGExtension];
    152     case AUDIO:
    153         return [kWAVExtension];
    154     case TEXT:
    155         return [kTXTExtension];
    156     case MISSING:
    157     case IMAGE_TEXT:
    158         return [kTXTExtension, kPNGExtension];
    159     default:
    160         // FIXME: Add support for the rest of the result types.
    161         // '-expected.html',
    162         // '-expected-mismatch.html',
    163         // ... and possibly more.
    164         return [];
    165     }
    166 };
    167 
    168 results.failureTypeList = function(failureBlob)
    169 {
    170     return failureBlob.split(' ');
    171 };
    172 
    173 results.directoryForBuilder = function(builderName)
    174 {
    175     return config.kPlatforms[config.currentPlatform].resultsDirectoryNameFromBuilderName(builderName);
    176 }
    177 
    178 function resultsDirectoryURL(platform, builderName)
    179 {
    180     if (config.useLocalResults)
    181         return '/localresult?path=';
    182     return layoutTestResultsURL(platform) + '/' + results.directoryForBuilder(builderName) + '/results/layout-test-results/';
    183 }
    184 
    185 function resultsPrefixListingURL(platform, builderName, marker)
    186 {
    187     var url =  layoutTestResultsURL(platform) + '/?prefix=' + results.directoryForBuilder(builderName) + '/&delimiter=/';
    188     if (marker)
    189         return url + '&marker=' + marker;
    190     return url;
    191 }
    192 
    193 function resultsDirectoryURLForBuildNumber(platform, builderName, buildNumber)
    194 {
    195     return layoutTestResultsURL(platform) + '/' + results.directoryForBuilder(builderName) + '/' + buildNumber + '/' ;
    196 }
    197 
    198 function resultsSummaryURL(platform, builderName)
    199 {
    200     return resultsDirectoryURL(platform, builderName) + kResultsName;
    201 }
    202 
    203 function resultsSummaryURLForBuildNumber(platform, builderName, buildNumber)
    204 {
    205     return resultsDirectoryURLForBuildNumber(platform, builderName, buildNumber) + kResultsName;
    206 }
    207 
    208 var g_resultsCache = new base.AsynchronousCache(function (key, callback) {
    209     net.jsonp(key, callback);
    210 });
    211 
    212 results.ResultAnalyzer = base.extends(Object, {
    213     init: function(resultNode)
    214     {
    215         this._isUnexpected = resultNode.is_unexpected;
    216         this._actual = resultNode ? results.failureTypeList(resultNode.actual) : [];
    217         this._expected = resultNode ? this._addImpliedExpectations(results.failureTypeList(resultNode.expected)) : [];
    218     },
    219     _addImpliedExpectations: function(resultsList)
    220     {
    221         if (resultsList.indexOf('FAIL') == -1)
    222             return resultsList;
    223         return resultsList.concat(kFailingResults);
    224     },
    225     _hasPass: function(results)
    226     {
    227         return results.indexOf(PASS) != -1;
    228     },
    229     unexpectedResults: function()
    230     {
    231         return this._actual.filter(function(result) {
    232             return this._expected.indexOf(result) == -1;
    233         }, this);
    234     },
    235     succeeded: function()
    236     {
    237         return this._hasPass(this._actual);
    238     },
    239     flaky: function()
    240     {
    241         return this._actual.length > 1;
    242     },
    243     wontfix: function()
    244     {
    245         return this._expected.indexOf('WONTFIX') != -1;
    246     },
    247     hasUnexpectedFailures: function()
    248     {
    249         return this._isUnexpected;
    250     }
    251 })
    252 
    253 function isExpectedFailure(resultNode)
    254 {
    255     var analyzer = new results.ResultAnalyzer(resultNode);
    256     return !analyzer.hasUnexpectedFailures() && !analyzer.succeeded() && !analyzer.flaky() && !analyzer.wontfix();
    257 }
    258 
    259 function isUnexpectedFailure(resultNode)
    260 {
    261     var analyzer = new results.ResultAnalyzer(resultNode);
    262     return analyzer.hasUnexpectedFailures() && !analyzer.succeeded() && !analyzer.flaky() && !analyzer.wontfix();
    263 }
    264 
    265 function isResultNode(node)
    266 {
    267     return !!node.actual;
    268 }
    269 
    270 results.expectedFailures = function(resultsTree)
    271 {
    272     return base.filterTree(resultsTree.tests, isResultNode, isExpectedFailure);
    273 };
    274 
    275 results.unexpectedFailures = function(resultsTree)
    276 {
    277     return base.filterTree(resultsTree.tests, isResultNode, isUnexpectedFailure);
    278 };
    279 
    280 function resultsByTest(resultsByBuilder, filter)
    281 {
    282     var resultsByTest = {};
    283 
    284     $.each(resultsByBuilder, function(builderName, resultsTree) {
    285         $.each(filter(resultsTree), function(testName, resultNode) {
    286             resultsByTest[testName] = resultsByTest[testName] || {};
    287             resultsByTest[testName][builderName] = resultNode;
    288         });
    289     });
    290 
    291     return resultsByTest;
    292 }
    293 
    294 results.expectedFailuresByTest = function(resultsByBuilder)
    295 {
    296     return resultsByTest(resultsByBuilder, results.expectedFailures);
    297 };
    298 
    299 results.unexpectedFailuresByTest = function(resultsByBuilder)
    300 {
    301     return resultsByTest(resultsByBuilder, results.unexpectedFailures);
    302 };
    303 
    304 results.failureInfoForTestAndBuilder = function(resultsByTest, testName, builderName)
    305 {
    306     var failureInfoForTest = {
    307         'testName': testName,
    308         'builderName': builderName,
    309         'failureTypeList': results.failureTypeList(resultsByTest[testName][builderName].actual),
    310     };
    311 
    312     return failureInfoForTest;
    313 };
    314 
    315 results.collectUnexpectedResults = function(dictionaryOfResultNodes)
    316 {
    317     var collectedResults = [];
    318     $.each(dictionaryOfResultNodes, function(key, resultNode) {
    319         var analyzer = new results.ResultAnalyzer(resultNode);
    320         collectedResults = collectedResults.concat(analyzer.unexpectedResults());
    321     });
    322     return base.uniquifyArray(collectedResults);
    323 };
    324 
    325 // Callback data is [{ buildNumber:, url: }]
    326 function historicalResultsLocations(platform, builderName, callback)
    327 {
    328     var historicalResultsData = [];
    329 
    330     function parseListingDocument(prefixListingDocument) {
    331         $(prefixListingDocument).find("Prefix").each(function() {
    332             var buildString = this.textContent.replace(results.directoryForBuilder(builderName) + '/', '');
    333             if (buildString.match(/\d+\//)) {
    334                 var buildNumber = parseInt(buildString);
    335                 var resultsData = {
    336                     'buildNumber': buildNumber,
    337                     'url': resultsSummaryURLForBuildNumber(platform, builderName, buildNumber)
    338                 };
    339                 historicalResultsData.unshift(resultsData);
    340            }
    341         });
    342         var nextMarker = $(prefixListingDocument).find('NextMarker').get();
    343         if (nextMarker.length) {
    344             var nextListingURL = resultsPrefixListingURL(platform, builderName, nextMarker[0].textContent);
    345             net.get(nextListingURL, parseListingDocument);
    346         } else {
    347             callback(historicalResultsData);
    348         }
    349     }
    350 
    351     builders.mostRecentBuildForBuilder(platform, builderName, function (mostRecentBuildNumber) {
    352         var marker = results.directoryForBuilder(builderName) + "/" + (mostRecentBuildNumber - 100) + "/";
    353         var listingURL = resultsPrefixListingURL(platform, builderName, marker);
    354         net.get(listingURL, parseListingDocument);
    355     });
    356 }
    357 
    358 function walkHistory(platform, builderName, testName, callback)
    359 {
    360     var indexOfNextKeyToFetch = 0;
    361     var keyList = [];
    362 
    363     function continueWalk()
    364     {
    365         if (indexOfNextKeyToFetch >= keyList.length) {
    366             processResultNode(0, null);
    367             return;
    368         }
    369 
    370         var resultsURL = keyList[indexOfNextKeyToFetch].url;
    371         ++indexOfNextKeyToFetch;
    372         g_resultsCache.get(resultsURL, function(resultsTree) {
    373             if ($.isEmptyObject(resultsTree)) {
    374                 continueWalk();
    375                 return;
    376             }
    377             var resultNode = results.resultNodeForTest(resultsTree, testName);
    378             var revision = parseInt(resultsTree['blink_revision'])
    379             if (isNaN(revision))
    380                 revision = 0;
    381             processResultNode(revision, resultNode);
    382         });
    383     }
    384 
    385     function processResultNode(revision, resultNode)
    386     {
    387         var shouldContinue = callback(revision, resultNode);
    388         if (!shouldContinue)
    389             return;
    390         continueWalk();
    391     }
    392 
    393     historicalResultsLocations(platform, builderName, function(resultsLocations) {
    394         keyList = resultsLocations;
    395         continueWalk();
    396     });
    397 }
    398 
    399 results.regressionRangeForFailure = function(builderName, testName, callback)
    400 {
    401     var oldestFailingRevision = 0;
    402     var newestPassingRevision = 0;
    403 
    404     // FIXME: should treat {platform, builderName} as a tuple
    405     walkHistory(config.currentPlatform, builderName, testName, function(revision, resultNode) {
    406         if (!revision) {
    407             callback(oldestFailingRevision, newestPassingRevision);
    408             return false;
    409         }
    410         if (!resultNode) {
    411             newestPassingRevision = revision;
    412             callback(oldestFailingRevision, newestPassingRevision);
    413             return false;
    414         }
    415         if (isUnexpectedFailure(resultNode)) {
    416             oldestFailingRevision = revision;
    417             return true;
    418         }
    419         if (!oldestFailingRevision)
    420             return true;  // We need to keep looking for a failing revision.
    421         newestPassingRevision = revision;
    422         callback(oldestFailingRevision, newestPassingRevision);
    423         return false;
    424     });
    425 };
    426 
    427 function mergeRegressionRanges(regressionRanges)
    428 {
    429     var mergedRange = {};
    430 
    431     mergedRange.oldestFailingRevision = 0;
    432     mergedRange.newestPassingRevision = 0;
    433 
    434     $.each(regressionRanges, function(builderName, range) {
    435         if (!range.oldestFailingRevision && !range.newestPassingRevision)
    436             return
    437 
    438         if (!mergedRange.oldestFailingRevision)
    439             mergedRange.oldestFailingRevision = range.oldestFailingRevision;
    440         if (!mergedRange.newestPassingRevision)
    441             mergedRange.newestPassingRevision = range.newestPassingRevision;
    442 
    443         if (range.oldestFailingRevision && range.oldestFailingRevision < mergedRange.oldestFailingRevision)
    444             mergedRange.oldestFailingRevision = range.oldestFailingRevision;
    445         if (range.newestPassingRevision > mergedRange.newestPassingRevision)
    446             mergedRange.newestPassingRevision = range.newestPassingRevision;
    447     });
    448 
    449     return mergedRange;
    450 }
    451 
    452 results.unifyRegressionRanges = function(builderNameList, testName, callback)
    453 {
    454     var regressionRanges = {};
    455 
    456     var tracker = new base.RequestTracker(builderNameList.length, function() {
    457         var mergedRange = mergeRegressionRanges(regressionRanges);
    458         callback(mergedRange.oldestFailingRevision, mergedRange.newestPassingRevision);
    459     });
    460 
    461     $.each(builderNameList, function(index, builderName) {
    462         results.regressionRangeForFailure(builderName, testName, function(oldestFailingRevision, newestPassingRevision) {
    463             var range = {};
    464             range.oldestFailingRevision = oldestFailingRevision;
    465             range.newestPassingRevision = newestPassingRevision;
    466             regressionRanges[builderName] = range;
    467             tracker.requestComplete();
    468         });
    469     });
    470 };
    471 
    472 results.resultNodeForTest = function(resultsTree, testName)
    473 {
    474     var testNamePath = testName.split('/');
    475     var currentNode = resultsTree['tests'];
    476     $.each(testNamePath, function(index, segmentName) {
    477         if (!currentNode)
    478             return;
    479         currentNode = (segmentName in currentNode) ? currentNode[segmentName] : null;
    480     });
    481     return currentNode;
    482 };
    483 
    484 results.resultKind = function(url)
    485 {
    486     if (/-actual\.[a-z]+$/.test(url))
    487         return results.kActualKind;
    488     else if (/-expected\.[a-z]+$/.test(url))
    489         return results.kExpectedKind;
    490     else if (/diff\.[a-z]+$/.test(url))
    491         return results.kDiffKind;
    492     return results.kUnknownKind;
    493 }
    494 
    495 results.resultType = function(url)
    496 {
    497     if (/\.png$/.test(url))
    498         return results.kImageType;
    499     if (/\.wav$/.test(url))
    500         return results.kAudioType;
    501     return results.kTextType;
    502 }
    503 
    504 function sortResultURLsBySuffix(urls)
    505 {
    506     var sortedURLs = [];
    507     $.each(kPreferredSuffixOrder, function(i, suffix) {
    508         $.each(urls, function(j, url) {
    509             if (!base.endsWith(url, suffix))
    510                 return;
    511             sortedURLs.push(url);
    512         });
    513     });
    514     if (sortedURLs.length != urls.length)
    515         throw "sortResultURLsBySuffix failed to return the same number of URLs."
    516     return sortedURLs;
    517 }
    518 
    519 results.fetchResultsURLs = function(failureInfo, callback)
    520 {
    521     var testNameStem = base.trimExtension(failureInfo.testName);
    522     var urlStem = resultsDirectoryURL(config.currentPlatform, failureInfo.builderName);
    523 
    524     var suffixList = possibleSuffixListFor(failureInfo.failureTypeList);
    525     var resultURLs = [];
    526     var tracker = new base.RequestTracker(suffixList.length, function() {
    527         callback(sortResultURLsBySuffix(resultURLs));
    528     });
    529     $.each(suffixList, function(index, suffix) {
    530         var url = urlStem + testNameStem + suffix;
    531         net.probe(url, {
    532             success: function() {
    533                 resultURLs.push(url);
    534                 tracker.requestComplete();
    535             },
    536             error: function() {
    537                 tracker.requestComplete();
    538             },
    539         });
    540     });
    541 };
    542 
    543 results.fetchResultsByBuilder = function(builderNameList, callback)
    544 {
    545     var resultsByBuilder = {};
    546     var tracker = new base.RequestTracker(builderNameList.length, function() {
    547         callback(resultsByBuilder);
    548     });
    549     $.each(builderNameList, function(index, builderName) {
    550         var resultsURL = resultsSummaryURL(config.currentPlatform, builderName);
    551         net.jsonp(resultsURL, function(resultsTree) {
    552             resultsByBuilder[builderName] = resultsTree;
    553             tracker.requestComplete();
    554         });
    555     });
    556 };
    557 
    558 })();
    559