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
26var results = results || {};
27
28(function() {
29
30var kResultsName = 'failing_results.json';
31
32var kBuildLinkRegexp = /a href="\d+\/"/g;
33var kBuildNumberRegexp = /\d+/;
34
35var PASS = 'PASS';
36var TIMEOUT = 'TIMEOUT';
37var TEXT = 'TEXT';
38var CRASH = 'CRASH';
39var IMAGE = 'IMAGE';
40var IMAGE_TEXT = 'IMAGE+TEXT';
41var AUDIO = 'AUDIO';
42var MISSING = 'MISSING';
43
44var kFailingResults = [TEXT, IMAGE_TEXT, AUDIO];
45
46var kExpectedImageSuffix = '-expected.png';
47var kActualImageSuffix = '-actual.png';
48var kImageDiffSuffix = '-diff.png';
49var kExpectedAudioSuffix = '-expected.wav';
50var kActualAudioSuffix = '-actual.wav';
51var kExpectedTextSuffix = '-expected.txt';
52var kActualTextSuffix = '-actual.txt';
53var kDiffTextSuffix = '-diff.txt';
54var kCrashLogSuffix = '-crash-log.txt';
55
56var kPNGExtension = 'png';
57var kTXTExtension = 'txt';
58var kWAVExtension = 'wav';
59
60var 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.
74results.kActualKind = 'actual';
75results.kExpectedKind = 'expected';
76results.kDiffKind = 'diff';
77results.kUnknownKind = 'unknown';
78
79// Types of tests.
80results.kImageType = 'image'
81results.kAudioType = 'audio'
82results.kTextType = 'text'
83// FIXME: There are more types of tests.
84
85function layoutTestResultsURL(platform)
86{
87    return config.kPlatforms[platform].layoutTestResultsURL;
88}
89
90function 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
150results.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
171results.failureTypeList = function(failureBlob)
172{
173    return failureBlob.split(' ');
174};
175
176results.canRebaseline = function(failureTypeList)
177{
178    return failureTypeList.some(function(element) {
179        return results.failureTypeToExtensionList(element).length > 0;
180    });
181};
182
183results.directoryForBuilder = function(builderName)
184{
185    return config.kPlatforms[config.currentPlatform].resultsDirectoryNameFromBuilderName(builderName);
186}
187
188function resultsDirectoryURL(platform, builderName)
189{
190    if (config.useLocalResults)
191        return '/localresult?path=';
192    return resultsDirectoryListingURL(platform, builderName) + 'results/layout-test-results/';
193}
194
195function resultsDirectoryListingURL(platform, builderName)
196{
197    return layoutTestResultsURL(platform) + '/' + results.directoryForBuilder(builderName) + '/';
198}
199
200function resultsDirectoryURLForBuildNumber(platform, builderName, buildNumber)
201{
202    return resultsDirectoryListingURL(platform, builderName) + buildNumber + '/';
203}
204
205function resultsSummaryURL(platform, builderName)
206{
207    return resultsDirectoryURL(platform, builderName) + kResultsName;
208}
209
210function resultsSummaryURLForBuildNumber(platform, builderName, buildNumber)
211{
212    return resultsDirectoryURLForBuildNumber(platform, builderName, buildNumber) + kResultsName;
213}
214
215var g_resultsCache = new base.AsynchronousCache(function (key, callback) {
216    net.jsonp(key, callback);
217});
218
219results.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
260function isExpectedFailure(resultNode)
261{
262    var analyzer = new results.ResultAnalyzer(resultNode);
263    return !analyzer.hasUnexpectedFailures() && !analyzer.succeeded() && !analyzer.flaky() && !analyzer.wontfix();
264}
265
266function isUnexpectedFailure(resultNode)
267{
268    var analyzer = new results.ResultAnalyzer(resultNode);
269    return analyzer.hasUnexpectedFailures() && !analyzer.succeeded() && !analyzer.flaky() && !analyzer.wontfix();
270}
271
272function isResultNode(node)
273{
274    return !!node.actual;
275}
276
277results.expectedFailures = function(resultsTree)
278{
279    return base.filterTree(resultsTree.tests, isResultNode, isExpectedFailure);
280};
281
282results.unexpectedFailures = function(resultsTree)
283{
284    return base.filterTree(resultsTree.tests, isResultNode, isUnexpectedFailure);
285};
286
287function 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
301results.expectedFailuresByTest = function(resultsByBuilder)
302{
303    return resultsByTest(resultsByBuilder, results.expectedFailures);
304};
305
306results.unexpectedFailuresByTest = function(resultsByBuilder)
307{
308    return resultsByTest(resultsByBuilder, results.unexpectedFailures);
309};
310
311results.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
322results.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: }]
333function 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
350function 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
391results.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
419function 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
444results.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
464results.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
476results.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
487results.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
496function 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
511results.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
535results.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