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