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