Rename TestFailureBugForm to FailingTestsBugForm
[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     this._navigationID = 0;
31
32     var self = this;
33     addEventListener('load', function() { self.loaded() }, false);
34     addEventListener('hashchange', function() { self.parseHash(location.hash) }, false);
35 }
36
37 ViewController.prototype = {
38     loaded: function() {
39         this._header = document.createElement('h1');
40         document.body.appendChild(this._header);
41         this._mainContentElement = document.createElement('div');
42         document.body.appendChild(this._mainContentElement);
43         document.body.appendChild(this._domForAuxiliaryUIElements());
44
45         this.parseHash(location.hash);
46     },
47
48     parseHash: function(hash) {
49         ++this._navigationID;
50
51         var match = /#\/(.*)/.exec(hash);
52         if (match)
53             this._displayBuilder(this._buildbot.builderNamed(decodeURIComponent(match[1])));
54         else
55             this._displayTesters();
56     },
57
58     _displayBuilder: function(builder) {
59         this._setTitle(builder.name);
60         this._mainContentElement.removeAllChildren();
61
62         var navigationID = this._navigationID;
63
64         var self = this;
65         (new LayoutTestHistoryAnalyzer(builder)).start(function(data, stillFetchingData) {
66             if (self._navigationID !== navigationID) {
67                 // The user has navigated somewhere else. Stop loading data about this tester.
68                 return false;
69             }
70
71             var list = document.createElement('ol');
72             list.id = 'failure-history';
73
74             var buildNames = Object.keys(data.history)
75             buildNames.forEach(function(buildName, buildIndex, buildNameArray) {
76                 var failingTestNames = Object.keys(data.history[buildName].tests);
77                 if (!failingTestNames.length)
78                     return;
79
80                 var item = document.createElement('li');
81                 list.appendChild(item);
82
83                 var testList = document.createElement('ol');
84                 item.appendChild(testList);
85
86                 testList.className = 'test-list';
87                 for (var testName in data.history[buildName].tests) {
88                     var testItem = document.createElement('li');
89                     testItem.appendChild(self._domForFailedTest(builder, buildName, testName, data.history[buildName].tests[testName]));
90                     testList.appendChild(testItem);
91                 }
92
93                 if (data.history[buildName].tooManyFailures) {
94                     var p = document.createElement('p');
95                     p.className = 'info';
96                     p.appendChild(document.createTextNode('run-webkit-tests exited early due to too many failures/crashes/timeouts'));
97                     item.appendChild(p);
98                 }
99
100                 var passingBuildName;
101                 if (buildIndex + 1 < buildNameArray.length)
102                     passingBuildName = buildNameArray[buildIndex + 1];
103
104                 item.appendChild(self._domForRegressionRange(builder, buildName, passingBuildName, failingTestNames));
105
106                 if (passingBuildName || !stillFetchingData)
107                     item.appendChild(self._domForNewAndExistingBugs(builder, buildName, passingBuildName, failingTestNames));
108             });
109
110             self._mainContentElement.removeAllChildren();
111             self._mainContentElement.appendChild(list);
112             self._mainContentElement.appendChild(self._domForPossiblyFlakyTests(builder, data.possiblyFlaky, buildNames.length));
113
114             if (!stillFetchingData)
115                 PersistentCache.prune();
116
117             return true;
118         });
119     },
120
121     _displayTesters: function() {
122         this._setTitle('Testers');
123         this._mainContentElement.removeAllChildren();
124
125         var list = document.createElement('ul');
126         this._mainContentElement.appendChild(list);
127
128         var latestBuildInfos = [];
129         var navigationID = this._navigationID;
130
131         function updateList() {
132             latestBuildInfos.sort(function(a, b) { return a.tester.name.localeCompare(b.tester.name) });
133             list.removeAllChildren();
134             latestBuildInfos.forEach(function(buildInfo) {
135                 var link = document.createElement('a');
136                 link.href = '#/' + buildInfo.tester.name;
137                 link.appendChild(document.createTextNode(buildInfo.tester.name));
138
139                 var item = document.createElement('li');
140                 item.appendChild(link);
141                 if (buildInfo.tooManyFailures)
142                     item.appendChild(document.createTextNode(' (too many failures/crashes/timeouts)'));
143                 else
144                     item.appendChild(document.createTextNode(' (' + buildInfo.failureCount + ' failing test' + (buildInfo.failureCount > 1 ? 's' : '') + ')'));
145                 list.appendChild(item);
146             });
147         }
148
149         var self = this;
150         this._buildbot.getTesters(function(testers) {
151             if (self._navigationID !== navigationID) {
152                 // The user has navigated somewhere else.
153                 return;
154             }
155             testers.forEach(function(tester) {
156                 tester.getMostRecentCompletedBuildNumber(function(buildNumber) {
157                     if (self._navigationID !== navigationID)
158                         return;
159                     if (buildNumber < 0)
160                         return;
161                     tester.getNumberOfFailingTests(buildNumber, function(failureCount, tooManyFailures) {
162                         if (self._navigationID !== navigationID)
163                             return;
164                         if (failureCount <= 0)
165                             return;
166                         latestBuildInfos.push({ tester: tester, failureCount: failureCount, tooManyFailures: tooManyFailures });
167                         updateList();
168                     });
169                 });
170             });
171         });
172     },
173
174     _domForRegressionRange: function(builder, failingBuildName, passingBuildName, failingTestNames) {
175         var result = document.createDocumentFragment();
176
177         var dlItems = [
178             [document.createTextNode('Failed'), this._domForBuildName(builder, failingBuildName)],
179         ];
180         if (passingBuildName)
181             dlItems.push([document.createTextNode('Passed'), this._domForBuildName(builder, passingBuildName)]);
182         result.appendChild(createDefinitionList(dlItems));
183
184         if (!passingBuildName)
185             return result;
186
187         var firstSuspectRevision = this._buildbot.parseBuildName(passingBuildName).revision + 1;
188         var lastSuspectRevision = this._buildbot.parseBuildName(failingBuildName).revision;
189
190         if (firstSuspectRevision === lastSuspectRevision)
191             return result;
192
193         var suspectsContainer = document.createElement('div');
194         result.appendChild(suspectsContainer);
195
196         var link = document.createElement('a');
197         result.appendChild(link);
198
199         link.href = this._trac.logURL('trunk', firstSuspectRevision, lastSuspectRevision, true);
200         link.appendChild(document.createTextNode('View regression range in Trac'));
201
202         suspectsContainer.appendChild(document.createTextNode('Searching for suspect revisions\u2026'));
203
204         // FIXME: Maybe some of this code should go in LayoutTestHistoryAnalyzer, or some other class?
205         var self = this;
206         self._trac.getCommitDataForRevisionRange('trunk', firstSuspectRevision, lastSuspectRevision, function(commits) {
207             var failingTestNamesWithoutExtensions = failingTestNames.map(removePathExtension);
208             var suspectCommits = commits.filter(function(commit) {
209                 return failingTestNamesWithoutExtensions.some(function(testName) {
210                     return commit.message.contains(testName);
211                 });
212             });
213
214             suspectsContainer.removeAllChildren();
215
216             if (!suspectCommits.length)
217                 return;
218
219             var title = 'Suspect revision' + (suspectCommits.length > 1 ? 's' : '') + ':';
220             suspectsContainer.appendChild(document.createTextNode(title));
221
222             var list = document.createElement('ul');
223             suspectsContainer.appendChild(list);
224             list.className = 'suspect-revisions-list';
225
226             function compareCommits(a, b) {
227                 return b.revision - a.revision;
228             }
229
230             list.appendChildren(sorted(suspectCommits, compareCommits).map(function(commit) {
231                 var item = document.createElement('li');
232                 var link = document.createElement('a');
233                 item.appendChild(link);
234
235                 link.href = self._trac.changesetURL(commit.revision);
236                 link.appendChild(document.createTextNode(commit.title))
237
238                 return item;
239             }));
240         });
241
242         return result;
243     },
244
245     _domForAuxiliaryUIElements: function() {
246         if (!this._bugzilla)
247             return document.createDocumentFragment();
248
249         var aside = document.createElement('aside');
250         aside.appendChild(document.createTextNode('Something not working? Have an idea to improve this page? '));
251         var link = document.createElement('a');
252         aside.appendChild(link);
253
254         link.appendChild(document.createTextNode('File a bug!'));
255         var queryParameters = {
256             product: 'WebKit',
257             component: 'Tools / Tests',
258             version: '528+ (Nightly build)',
259             bug_file_loc: location.href,
260             cc: 'aroben@apple.com',
261             short_desc: 'TestFailures page needs more unicorns!',
262         };
263         link.href = addQueryParametersToURL(this._bugzilla.baseURL + 'enter_bug.cgi', queryParameters);
264         link.target = '_blank';
265
266         return aside;
267     },
268
269     _domForBuildName: function(builder, buildName) {
270         var parsed = this._buildbot.parseBuildName(buildName);
271
272         var sourceLink = document.createElement('a');
273         sourceLink.href = 'http://trac.webkit.org/changeset/' + parsed.revision;
274         sourceLink.appendChild(document.createTextNode('r' + parsed.revision));
275
276         var buildLink = document.createElement('a');
277         buildLink.href = builder.buildURL(parsed.buildNumber);
278         buildLink.appendChild(document.createTextNode(parsed.buildNumber));
279
280         var resultsLink = document.createElement('a');
281         resultsLink.href = builder.resultsPageURL(buildName);
282         resultsLink.appendChild(document.createTextNode('results.html'));
283
284         var result = document.createDocumentFragment();
285         result.appendChild(sourceLink);
286         result.appendChild(document.createTextNode(' ('));
287         result.appendChild(buildLink);
288         result.appendChild(document.createTextNode(') ('));
289         result.appendChild(resultsLink);
290         result.appendChild(document.createTextNode(')'));
291
292         return result;
293     },
294
295     _domForFailedTest: function(builder, buildName, testName, testResult) {
296         var result = document.createDocumentFragment();
297         result.appendChild(document.createTextNode(testName + ': '));
298         result.appendChild(this._domForFailureDiagnosis(builder, buildName, testName, testResult));
299         return result;
300     },
301
302     _domForFailureDiagnosis: function(builder, buildName, testName, testResult) {
303         var diagnosticInfo = builder.failureDiagnosisTextAndURL(buildName, testName, testResult);
304         if (!diagnosticInfo)
305             return document.createTextNode(testResult.failureType);
306
307         var textAndCrashingSymbol = document.createDocumentFragment();
308         textAndCrashingSymbol.appendChild(document.createTextNode(diagnosticInfo.text));
309         if (testResult.crashingSymbol) {
310             var code = document.createElement('code');
311             code.appendChild(document.createTextNode(testResult.crashingSymbol));
312             textAndCrashingSymbol.appendChild(document.createTextNode(' ('));
313             textAndCrashingSymbol.appendChild(code);
314             textAndCrashingSymbol.appendChild(document.createTextNode(')'));
315         }
316
317         if (!('url' in diagnosticInfo))
318             return textAndCrashingSymbol;
319
320         var link = document.createElement('a');
321         link.href = diagnosticInfo.url;
322         link.appendChild(textAndCrashingSymbol);
323         return link;
324     },
325
326     _domForNewAndExistingBugs: function(tester, failingBuildName, passingBuildName, failingTests) {
327         var result = document.createDocumentFragment();
328
329         if (!this._bugzilla)
330             return result;
331
332         var container = document.createElement('p');
333         result.appendChild(container);
334
335         container.className = 'existing-and-new-bugs';
336
337         var bugsContainer = document.createElement('div');
338         container.appendChild(bugsContainer);
339
340         bugsContainer.appendChild(document.createTextNode('Searching for bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + '\u2026'));
341
342         this._bugzilla.quickSearch('ALL ' + failingTests.join('|'), function(bugs) {
343             if (!bugs.length) {
344                 bugsContainer.parentNode.removeChild(bugsContainer);
345                 return;
346             }
347
348             while (bugsContainer.firstChild)
349                 bugsContainer.removeChild(bugsContainer.firstChild);
350
351             bugsContainer.appendChild(document.createTextNode('Existing bugs related to ' + (failingTests.length > 1 ? 'these tests' : 'this test') + ':'));
352
353             var list = document.createElement('ul');
354             bugsContainer.appendChild(list);
355
356             list.className = 'existing-bugs-list';
357
358             function bugToListItem(bug) {
359                 var link = document.createElement('a');
360                 link.href = bug.url;
361                 link.appendChild(document.createTextNode(bug.title));
362
363                 var item = document.createElement('li');
364                 item.appendChild(link);
365
366                 return item;
367             }
368
369             var openBugs = bugs.filter(function(bug) { return Bugzilla.isOpenStatus(bug.status) });
370             var closedBugs = bugs.filter(function(bug) { return !Bugzilla.isOpenStatus(bug.status) });
371
372             list.appendChildren(openBugs.map(bugToListItem));
373
374             if (!closedBugs.length)
375                 return;
376
377             var item = document.createElement('li');
378             list.appendChild(item);
379
380             item.appendChild(document.createTextNode('Closed bugs:'));
381
382             var closedList = document.createElement('ul');
383             item.appendChild(closedList);
384
385             closedList.appendChildren(closedBugs.map(bugToListItem));
386         });
387
388         var bugForm = new FailingTestsBugForm(this._bugzilla, this._trac, tester, failingBuildName, passingBuildName, failingTests);
389
390         var form = bugForm.domElement();
391         result.appendChild(form);
392
393         var link = document.createElement('a');
394         container.appendChild(link);
395
396         link.addEventListener('click', function(event) { form.submit(); event.preventDefault(); });
397         link.href = '#';
398         link.appendChild(document.createTextNode('File bug for ' + (failingTests.length > 1 ? 'these failures' : 'this failure')));
399
400         return result;
401     },
402
403     _domForPossiblyFlakyTests: function(builder, possiblyFlakyTestData, buildCount) {
404         var result = document.createDocumentFragment();
405         var flakyTests = Object.keys(possiblyFlakyTestData);
406         if (!flakyTests.length)
407             return result;
408
409         var flakyHeader = document.createElement('h2');
410         result.appendChild(flakyHeader);
411         flakyHeader.appendChild(document.createTextNode('Possibly Flaky Tests'));
412
413         var flakyList = document.createElement('ol');
414         result.appendChild(flakyList);
415
416         flakyList.id = 'possibly-flaky-tests';
417
418         var self = this;
419         flakyList.appendChildren(sorted(flakyTests).map(function(testName) {
420             var item = document.createElement('li');
421
422             var disclosureTriangle = document.createElement('span');
423             item.appendChild(disclosureTriangle);
424
425             disclosureTriangle.className = 'disclosure-triangle';
426             const blackRightPointingSmallTriangle = '\u25b8';
427             disclosureTriangle.appendChild(document.createTextNode(blackRightPointingSmallTriangle));
428
429             var failures = possiblyFlakyTestData[testName];
430
431             item.appendChild(document.createTextNode(testName + ' (failed ' + failures.length + ' out of ' + buildCount + ' times)'));
432
433             var failureList = document.createElement('ol');
434             item.appendChild(failureList);
435
436             failureList.className = 'flakiness-examples-list';
437
438             disclosureTriangle.addEventListener('click', function() {
439                 item.toggleStyleClass('expanded');
440                 if (!item.hasStyleClass('expanded')) {
441                     failureList.style.height = '';
442                     return;
443                 }
444
445                 if (!failureList.firstChild) {
446                     failureList.appendChildren(failures.map(function(historyItem) {
447                         var item = document.createElement('li');
448                         item.appendChild(self._domForBuildName(builder, historyItem.build));
449                         item.appendChild(document.createTextNode(': '));
450                         item.appendChild(self._domForFailureDiagnosis(builder, historyItem.build, testName, historyItem.result));
451                         return item;
452                     }));
453                 }
454
455                 // CSS transitions can't transition to a value of 'auto', so we find out the actual
456                 // value using getComputedStyle and transition to that.
457                 failureList.style.height = 'auto';
458                 failureList.style.height = getComputedStyle(failureList).height;
459             });
460
461             return item;
462         }));
463
464         return result;
465     },
466
467     _setTitle: function(title) {
468         document.title = title;
469         this._header.textContent = title;
470     },
471 };