Add links to regression ranges in Trac to the TestFailures page
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / TestFailures / ViewController.js
1 /*
2  * Copyright (C) 2011 Apple 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 function ViewController(buildbot, bugzilla, trac) {
27     this._buildbot = buildbot;
28     this._bugzilla = bugzilla;
29     this._trac = trac;
30
31     var self = this;
32     addEventListener('load', function() { self.loaded() }, false);
33     addEventListener('hashchange', function() { self.parseHash(location.hash) }, false);
34 }
35
36 ViewController.prototype = {
37     loaded: function() {
38         this.parseHash(location.hash);
39     },
40
41     parseHash: function(hash) {
42         var match = /#\/(.*)/.exec(hash);
43         if (match)
44             this._displayBuilder(this._buildbot.builderNamed(decodeURIComponent(match[1])));
45         else
46             this._displayTesters();
47     },
48
49     _displayBuilder: function(builder) {
50         var self = this;
51         builder.startFetchingBuildHistory(function(history, stillFetchingData) {
52             var list = document.createElement('ol');
53             list.id = 'failure-history';
54             Object.keys(history).forEach(function(buildName, buildIndex, buildNameArray) {
55                 var failingTestNames = Object.keys(history[buildName].tests);
56                 if (!failingTestNames.length)
57                     return;
58
59                 var item = document.createElement('li');
60                 list.appendChild(item);
61
62                 var testList = document.createElement('ol');
63                 item.appendChild(testList);
64
65                 testList.className = 'test-list';
66                 for (var testName in history[buildName].tests) {
67                     var testItem = document.createElement('li');
68                     testItem.appendChild(self._domForFailedTest(builder, buildName, testName, history[buildName].tests[testName]));
69                     testList.appendChild(testItem);
70                 }
71
72                 if (history[buildName].tooManyFailures) {
73                     var p = document.createElement('p');
74                     p.className = 'info';
75                     p.appendChild(document.createTextNode('run-webkit-tests exited early due to too many failures/crashes/timeouts'));
76                     item.appendChild(p);
77                 }
78
79                 var passingBuildName;
80                 if (buildIndex + 1 < buildNameArray.length)
81                     passingBuildName = buildNameArray[buildIndex + 1];
82
83                 item.appendChild(self._domForRegressionRange(builder, passingBuildName, buildName));
84
85                 if (passingBuildName || !stillFetchingData)
86                     item.appendChild(self._domForNewAndExistingBugs(builder, buildName, passingBuildName, failingTestNames));
87             });
88
89             var header = document.createElement('h1');
90             header.appendChild(document.createTextNode(builder.name));
91             document.body.innerHTML = '';
92             document.title = builder.name;
93             document.body.appendChild(header);
94             document.body.appendChild(list);
95
96             if (!stillFetchingData)
97                 PersistentCache.prune();
98         });
99     },
100
101     _displayTesters: function() {
102         var list = document.createElement('ul');
103         var latestBuildInfos = [];
104
105         function updateList() {
106             latestBuildInfos.sort(function(a, b) { return a.tester.name.localeCompare(b.tester.name) });
107             while (list.firstChild)
108                 list.removeChild(list.firstChild);
109             latestBuildInfos.forEach(function(buildInfo) {
110                 var link = document.createElement('a');
111                 link.href = '#/' + buildInfo.tester.name;
112                 link.appendChild(document.createTextNode(buildInfo.tester.name));
113
114                 var item = document.createElement('li');
115                 item.appendChild(link);
116                 if (buildInfo.tooManyFailures)
117                     item.appendChild(document.createTextNode(' (too many failures/crashes/timeouts)'));
118                 else
119                     item.appendChild(document.createTextNode(' (' + buildInfo.failureCount + ' failing test' + (buildInfo.failureCount > 1 ? 's' : '') + ')'));
120                 list.appendChild(item);
121             });
122         }
123
124         this._buildbot.getTesters(function(testers) {
125             testers.forEach(function(tester) {
126                 tester.getMostRecentCompletedBuildNumber(function(buildNumber) {
127                     if (buildNumber < 0)
128                         return;
129                     tester.getNumberOfFailingTests(buildNumber, function(failureCount, tooManyFailures) {
130                         if (failureCount <= 0)
131                             return;
132                         latestBuildInfos.push({ tester: tester, failureCount: failureCount, tooManyFailures: tooManyFailures });
133                         updateList();
134                     });
135                 });
136             });
137
138             document.body.innerHTML = '';
139             document.title = 'Testers';
140             document.body.appendChild(list);
141         });
142     },
143
144     _domForRegressionRange: function(builder, passingBuildName, failingBuildName) {
145         var result = document.createDocumentFragment();
146
147         var dlItems = [
148             [document.createTextNode('Failed'), this._domForBuildName(builder, failingBuildName)],
149         ];
150         if (passingBuildName)
151             dlItems.push([document.createTextNode('Passed'), this._domForBuildName(builder, passingBuildName)]);
152         result.appendChild(createDefinitionList(dlItems));
153
154         if (!passingBuildName)
155             return result;
156
157         var firstSuspectRevision = this._buildbot.parseBuildName(passingBuildName).revision + 1;
158         var lastSuspectRevision = this._buildbot.parseBuildName(failingBuildName).revision;
159
160         if (firstSuspectRevision === lastSuspectRevision)
161             return result;
162
163         var link = document.createElement('a');
164         result.appendChild(link);
165
166         link.href = this._trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision);
167         link.appendChild(document.createTextNode('View regression range in Trac'));
168
169         return result;
170     },
171
172     _domForBuildName: function(builder, buildName) {
173         var parsed = this._buildbot.parseBuildName(buildName);
174
175         var sourceLink = document.createElement('a');
176         sourceLink.href = 'http://trac.webkit.org/changeset/' + parsed.revision;
177         sourceLink.appendChild(document.createTextNode('r' + parsed.revision));
178
179         var buildLink = document.createElement('a');
180         buildLink.href = builder.buildURL(parsed.buildNumber);
181         buildLink.appendChild(document.createTextNode(parsed.buildNumber));
182
183         var resultsLink = document.createElement('a');
184         resultsLink.href = builder.resultsPageURL(buildName);
185         resultsLink.appendChild(document.createTextNode('results.html'));
186
187         var result = document.createDocumentFragment();
188         result.appendChild(sourceLink);
189         result.appendChild(document.createTextNode(' ('));
190         result.appendChild(buildLink);
191         result.appendChild(document.createTextNode(') ('));
192         result.appendChild(resultsLink);
193         result.appendChild(document.createTextNode(')'));
194
195         return result;
196     },
197
198     _domForFailedTest: function(builder, buildName, testName, failureType) {
199         var diagnosticInfo = builder.failureDiagnosisTextAndURL(buildName, testName, failureType);
200
201         var result = document.createDocumentFragment();
202         result.appendChild(document.createTextNode(testName));
203         result.appendChild(document.createTextNode(' ('));
204
205         var textNode = document.createTextNode(diagnosticInfo.text);
206         if ('url' in diagnosticInfo) {
207             var link = document.createElement('a');
208             link.href = diagnosticInfo.url;
209             link.appendChild(textNode);
210             result.appendChild(link);
211         } else
212             result.appendChild(textNode);
213
214         result.appendChild(document.createTextNode(')'));
215
216         return result;
217     },
218
219     _domForNewAndExistingBugs: function(tester, failingBuildName, passingBuildName, failingTests) {
220         var result = document.createDocumentFragment();
221
222         if (!this._bugzilla)
223             return result;
224
225         var container = document.createElement('p');
226         result.appendChild(container);
227
228         container.className = 'existing-and-new-bugs';
229
230         var bugsContainer = document.createElement('div');
231         container.appendChild(bugsContainer);
232
233         bugsContainer.appendChild(document.createTextNode('Searching for bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + '\u2026'));
234
235         this._bugzilla.quickSearch('ALL ' + failingTests.join('|'), function(bugs) {
236             if (!bugs.length) {
237                 bugsContainer.parentNode.removeChild(bugsContainer);
238                 return;
239             }
240
241             while (bugsContainer.firstChild)
242                 bugsContainer.removeChild(bugsContainer.firstChild);
243
244             bugsContainer.appendChild(document.createTextNode('Existing bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + ':'));
245
246             var list = document.createElement('ul');
247             bugsContainer.appendChild(list);
248
249             list.className = 'existing-bugs-list';
250
251             function bugToListItem(bug) {
252                 var link = document.createElement('a');
253                 link.href = bug.url;
254                 link.appendChild(document.createTextNode(bug.title));
255
256                 var item = document.createElement('li');
257                 item.appendChild(link);
258
259                 return item;
260             }
261
262             var openBugs = bugs.filter(function(bug) { return Bugzilla.isOpenStatus(bug.status) });
263             var closedBugs = bugs.filter(function(bug) { return !Bugzilla.isOpenStatus(bug.status) });
264
265             list.appendChildren(openBugs.map(bugToListItem));
266
267             if (!closedBugs.length)
268                 return;
269
270             var item = document.createElement('li');
271             list.appendChild(item);
272
273             item.appendChild(document.createTextNode('Closed bugs:'));
274
275             var closedList = document.createElement('ul');
276             item.appendChild(closedList);
277
278             closedList.appendChildren(closedBugs.map(bugToListItem));
279         });
280
281         var parsedFailingBuildName = this._buildbot.parseBuildName(failingBuildName);
282         var regressionRangeString = 'r' + parsedFailingBuildName.revision;
283         if (passingBuildName) {
284             var parsedPassingBuildName = this._buildbot.parseBuildName(passingBuildName);
285             if (parsedFailingBuildName.revision - parsedPassingBuildName.revision > 1)
286                 regressionRangeString = 'r' + parsedPassingBuildName.revision + '-' + regressionRangeString;
287         }
288
289         // FIXME: Some of this code should move into a new method on the Bugzilla class.
290
291         // FIXME: When a newly-added test has been failing since its introduction, it isn't really a
292         // "regression". We should use a different title and keywords in that case.
293         // <http://webkit.org/b/61645>
294
295         var titlePrefix = 'REGRESSION (' + regressionRangeString + '): ';
296         var titleSuffix = ' failing on ' + tester.name;
297         var title = titlePrefix + failingTests.join(', ') + titleSuffix;
298         if (title.length > Bugzilla.maximumBugTitleLength)
299             title = titlePrefix + failingTests.length + ' tests' + titleSuffix;
300         console.assert(title.length <= Bugzilla.maximumBugTitleLength);
301
302         var description;
303         if (failingTests.length === 1) {
304             description = failingTests[0] + ' has been failing on ' + tester.name
305                 + ' since r' + parsedFailingBuildName.revision + '.\n\n';
306         } else if (failingTests.length === 2) {
307             description = failingTests.join(' and ') + ' have been failing on ' + tester.name
308                 + ' since r' + parsedFailingBuildName.revision + '.\n\n';
309         } else {
310             description = 'The following tests have been failing on ' + tester.name
311                 + ' since r' + parsedFailingBuildName.revision + ':\n\n'
312                 + failingTests.map(function(test) { return '    ' + test }).join('\n')
313                 + '\n\n';
314         }
315         if (passingBuildName)
316             description += encodeURI(tester.resultsPageURL(passingBuildName)) + ' passed\n';
317         var failingResultsHTML = tester.resultsPageURL(failingBuildName);
318         description += encodeURI(failingResultsHTML) + ' failed\n';
319
320         var formData = {
321             product: 'WebKit',
322             version: '528+ (Nightly build)',
323             component: 'Tools / Tests',
324             keywords: 'LayoutTestFailure, MakingBotsRed, Regression',
325             short_desc: title,
326             comment: description,
327             bug_file_loc: failingResultsHTML,
328         };
329
330         if (/Windows/.test(tester.name)) {
331             formData.rep_platform = 'PC';
332             if (/Windows 7/.test(tester.name))
333                 formData.op_sys = 'Windows 7';
334             else if (/Windows XP/.test(tester.name))
335                 formData.op_sys = 'Windows XP';
336         } else if (/Leopard/.test(tester.name)) {
337             formData.rep_platform = 'Macintosh';
338             if (/SnowLeopard/.test(tester.name))
339                 formData.op_sys = 'Mac OS X 10.6';
340             else
341                 formData.op_sys = 'Mac OS X 10.5';
342         }
343
344         var form = document.createElement('form');
345         result.appendChild(form);
346         form.className = 'new-bug-form';
347         form.method = 'POST';
348         form.action = this._bugzilla.baseURL + 'enter_bug.cgi';
349         form.target = '_blank';
350
351         for (var key in formData) {
352             var input = document.createElement('input');
353             input.type = 'hidden';
354             input.name = key;
355             input.value = formData[key];
356             form.appendChild(input);
357         }
358
359         var link = document.createElement('a');
360         container.appendChild(link);
361
362         link.addEventListener('click', function(event) { form.submit(); event.preventDefault(); });
363         link.href = '#';
364         link.appendChild(document.createTextNode('File bug for ' + (failingTests.length > 1 ? 'these failures' : 'this failure')));
365
366         return result;
367     },
368 };