Update Bot Watcher's Dashboard for Buildbot 0.9
[WebKit-https.git] / Tools / BuildSlaveSupport / build.webkit.org-config / public_html / dashboard / Scripts / BuildbotIteration.js
1 /*
2  * Copyright (C) 2013, 2014, 2016 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 BuildbotIteration = function(queue, dataOrID, finished)
27 {
28     BaseObject.call(this);
29
30     console.assert(queue);
31
32     this.queue = queue;
33
34     if (typeof dataOrID === "object") {
35         this._parseData(dataOrID);
36         return;
37     }
38
39     console.assert(typeof dataOrID === "number");
40     this.id = dataOrID;
41
42     this.loaded = false;
43     this.isLoading = false;
44     this._productive = false;
45
46     this.revision = {};
47
48     this.layoutTestResults = null; // Layout test results can be needed even if all tests passed, e.g. for the leaks bot.
49     this.javaScriptCoreTestResults = null;
50
51     this.failedTestSteps = [];
52
53     this._finished = finished;
54 };
55
56 BaseObject.addConstructorFunctions(BuildbotIteration);
57
58 // JSON result values for both individual steps and the whole iteration.
59 BuildbotIteration.SUCCESS = 0;
60 BuildbotIteration.WARNINGS = 1;
61 BuildbotIteration.FAILURE = 2;
62 BuildbotIteration.SKIPPED = 3;
63 BuildbotIteration.EXCEPTION = 4;
64 BuildbotIteration.RETRY = 5;
65
66 // If none of these steps ran, then we didn't get any real results, and the iteration was not productive.
67 // All test steps are considered productive too.
68 BuildbotIteration.ProductiveSteps = {
69     "Build" : 1,
70     "build ASan archive": 1,
71     "compile-webkit": 1,
72     "scan build": 1,
73 };
74
75 // These have a special meaning for test queue views.
76 BuildbotIteration.TestSteps = {
77     "API tests": "platform api test",
78     "bindings-generation-tests": "bindings tests",
79     "builtins-generator-tests": "builtins generator tests",
80     "jscore-test": "javascript test",
81     "layout-test": "layout test",
82     "perf-test": "performance test",
83     "run-api-tests": "api test",
84     "dashboard-tests": "dashboard test",
85     "webkit-32bit-jsc-test": "javascript test",
86     "webkit-jsc-cloop-test": "javascript cloop test",
87     "webkitperl-test": "webkitperl test",
88     "webkitpy-test": "webkitpy test",
89     "test262-test": "test262 test",
90 };
91
92 BuildbotIteration.Event = {
93     Updated: "updated",
94     UnauthorizedAccess: "unauthorized-access"
95 };
96
97 // See <http://docs.buildbot.net/0.8.8/manual/cfg-properties.html>.
98 function isMultiCodebaseGotRevisionProperty(property)
99 {
100     return typeof property[0] === "object";
101 }
102
103 function parseRevisionProperty(property, key, fallbackKey)
104 {
105     if (!property)
106         return null;
107     var value = property[0];
108
109     // The property got_revision may have the following forms in Buildbot v0.8:
110     // ["got_revision",{"Internal":"1357","WebKitOpenSource":"2468"},"Source"]
111     // OR
112     // ["got_revision","2468","Source"]
113     //
114     // It may have the following forms in Buildbot v0.9:
115     // [{"Internal":"1357","WebKitOpenSource":"2468"}, "Source"]
116     // OR
117     // ["2468", "Source"]
118     if (isMultiCodebaseGotRevisionProperty(property))
119         value = (key in value) ? value[key] : value[fallbackKey];
120     return value;
121 }
122
123 BuildbotIteration.prototype = {
124     constructor: BuildbotIteration,
125     __proto__: BaseObject.prototype,
126
127     get finished()
128     {
129         return this._finished;
130     },
131
132     set finished(x)
133     {
134         this._finished = x;
135     },
136
137     get successful()
138     {
139         return this._result === BuildbotIteration.SUCCESS;
140     },
141
142     get productive()
143     {
144         return this._productive;
145     },
146
147     // It is not a real failure if Buildbot itself failed with codes like EXCEPTION or RETRY.
148     get failed()
149     {
150         return this._result === BuildbotIteration.FAILURE;
151     },
152
153     get firstFailedStepName()
154     {
155         if (!this._firstFailedStep)
156             return undefined;
157         return this._firstFailedStep.name;
158     },
159
160     failureLogURL: function(kind)
161     {
162         if (!this.failed)
163             return undefined;
164
165         console.assert(this._firstFailedStep);
166
167         if (!this._firstFailedStep.logs)
168             return this.queue.buildbot.buildPageURLForIteration(this);
169
170         for (var i = 0; i < this._firstFailedStep.logs.length; ++i) {
171             if (this._firstFailedStep.logs[i][0] == kind)
172                 return this._firstFailedStep.logs[i][1];
173         }
174
175         return undefined;
176     },
177
178     get failureLogs()
179     {
180         if (!this.failed)
181             return undefined;
182
183         console.assert(this._firstFailedStep);
184         return this._firstFailedStep.logs;
185     },
186
187     get previousProductiveIteration()
188     {
189         for (var i = 0; i < this.queue.iterations.length - 1; ++i) {
190             if (this.queue.iterations[i] === this) {
191                 while (++i < this.queue.iterations.length) {
192                     var iteration = this.queue.iterations[i];
193                     if (iteration.productive)
194                         return iteration;
195                 }
196                 break;
197             }
198         }
199         return null;
200     },
201
202     _parseData: function(data)
203     {
204         data = this._adjustBuildDataForBuildbot09(data)
205         console.assert(!this.id || this.id === data.number);
206         this.id = data.number;
207
208         this.revision = {};
209         var revisionProperty = data.properties.got_revision;
210         var branches = this.queue.branches;
211
212         for (var i = 0; i < branches.length; ++i) {
213             var repository = branches[i].repository;
214             var repositoryName = repository.name;
215             var key;
216             var fallbackKey;
217
218             if (repository === Dashboard.Repository.OpenSource) {
219                 key = "WebKit";
220                 fallbackKey = "opensource";
221             } else {
222                 key = repositoryName;
223                 fallbackKey = null;
224             }
225
226             var revision = parseRevisionProperty(revisionProperty, key, fallbackKey);
227             this.revision[repositoryName] = revision;
228         }
229
230         function sourceStampChanges(sourceStamp) {
231             var result = [];
232             var changes = sourceStamp.changes;
233             for (var i = 0; i < changes.length; ++i) {
234                 var change = { revisionNumber: changes[i].revision }
235                 if (changes[i].repository)
236                     change.repository = changes[i].repository;
237                 if (changes[i].branch)
238                     change.branch = changes[i].branch;
239                 // There is also a timestamp, but it's not accurate.
240                 result.push(change);
241             }
242             return result;
243         }
244
245         // The changes array is generally meaningful for svn triggered queues (such as builders),
246         // but not for internally triggered ones (such as testers), due to coalescing.
247         this.changes = [];
248         if (this.queue.buildbot.VERSION_LESS_THAN_09)
249             console.assert(data.sourceStamp || data.sourceStamps)
250         if (data.sourceStamp)
251             this.changes = sourceStampChanges(data.sourceStamp);
252         else if (data.sourceStamps)
253             for (var i = 0; i < data.sourceStamps.length; ++i)
254                 this.changes = this.changes.concat(sourceStampChanges(data.sourceStamps[i]));
255
256         this.startTime = new Date(data.started_at * 1000);
257         this.endTime = new Date(data.complete_at * 1000);
258
259         this.failedTestSteps = [];
260         data.steps.forEach(function(step) {
261             if (!step.complete || step.hidden || !(step.name in BuildbotIteration.TestSteps))
262                 return;
263             var results = new BuildbotTestResults(step);
264             if (step.name === "layout-test")
265                 this.layoutTestResults = results;
266             else if (/(?=.*test)(?=.*jsc)/.test(step.name))
267                 this.javaScriptCoreTestResults = results;
268             if (results.allPassed)
269                 return;
270             this.failedTestSteps.push(results);
271         }, this);
272
273         var masterShellCommandStep = data.steps.findFirst(function(step) { return step.name === "MasterShellCommand"; });
274         if (masterShellCommandStep && masterShellCommandStep.urls) {
275             // Sample masterShellCommandStep.urls data:
276             // "urls": [
277             //     {
278             //         "name": "view results",
279             //         "url": "/results/Apple Sierra Release WK2 (Tests)/r220013 (3245)/results.html"
280             //     }
281             // ]
282             this.resultURLs = masterShellCommandStep.urls[0];
283         }
284         for (var linkName in this.resultURLs) {
285             var url = this.resultURLs[linkName];
286             if (!url.startsWith("http"))
287                 this.resultURLs[linkName] = this.queue.buildbot.baseURL + url;
288         }
289
290         this.loaded = true;
291
292         this._firstFailedStep = data.steps.findFirst(function(step) { return !step.hidden && step.results === BuildbotIteration.FAILURE; });
293
294         console.assert(data.results === null || typeof data.results === "number");
295         this._result = data.results;
296
297         this.text = data.state_string;
298         this.finished = data.complete;
299
300         this._productive = this._finished && this._result !== BuildbotIteration.EXCEPTION && this._result !== BuildbotIteration.RETRY;
301         if (this._productive) {
302             var finishedAnyProductiveStep = false;
303             for (var i = 0; i < data.steps.length; ++i) {
304                 var step = data.steps[i];
305                 if (!step.complete)
306                     break;
307                 if (step.name in BuildbotIteration.ProductiveSteps || step.name in BuildbotIteration.TestSteps) {
308                     finishedAnyProductiveStep = true;
309                     break;
310                 }
311             }
312             this._productive = finishedAnyProductiveStep;
313         }
314     },
315
316     // FIXME: Remove this method after https://bugs.webkit.org/show_bug.cgi?id=175056 is fixed.
317     _adjustBuildDataForBuildbot09: function(data)
318     {
319         if (!this.queue.buildbot.VERSION_LESS_THAN_09)
320             return data;
321
322         data.started_at = data.times[0];
323         data.complete_at = data.times[1];
324         delete data["times"];
325
326         let revisionProperty = data.properties.findFirst((property) => property[0] === "got_revision");
327
328         if (revisionProperty) {
329             // Removing first element from revision property to match with new data format.
330             // Old format: ["got_revision",{"Internal":"1357","WebKitOpenSource":"2468"},"Source"]
331             // New format: [{"Internal":"1357","WebKitOpenSource":"2468"},"Source"]
332             console.assert(revisionProperty[0] === "got_revision")
333             revisionProperty.splice(0, 1);
334         }
335         data.properties.got_revision = revisionProperty;
336
337         for (var i = 0; i < data.steps.length; i++) {
338             data.steps[i].complete = data.steps[i].isFinished;
339             delete data.steps[i]["isFinished"];
340             // Sample state_string: "Exiting early after 20 crashes and 30 timeouts. 31603 tests run. 147 failures 69 new passes".
341             data.steps[i].state_string = data.steps[i].results[1].join(' ');
342             data.steps[i].results = data.steps[i].results[0]; // See URL http://docs.buildbot.net/latest/developer/results.html
343         }
344
345         let masterShellCommandStep = data.steps.findFirst((step) => step.name === "MasterShellCommand");
346         if (masterShellCommandStep)
347             masterShellCommandStep.urls = [masterShellCommandStep.urls];
348
349         data.state_string = data.text.join(" ");
350         delete data["text"];
351
352         data.complete = !data.currentStep;
353         delete data["currentStep"];
354
355         return data;
356     },
357
358     _updateIfDataAvailable: function()
359     {
360         if (!this._steps || !this._buildData)
361             return;
362
363         this.isLoading = false;
364         this._buildData.steps = this._steps;
365
366         this._deprecatedUpdateWithData(this._buildData);
367     },
368
369     _deprecatedUpdateWithData: function(data)
370     {
371         if (this.loaded && this._finished)
372             return;
373
374         this._parseData(data);
375
376         // Update the sorting since it is based on the revision numbers that just became known.
377         this.queue.updateIterationPosition(this);
378
379         this.dispatchEventToListeners(BuildbotIteration.Event.Updated);
380     },
381
382
383     get buildURL()
384     {
385         return this.queue.baseURL + "/builds/" + this.id + "?property=got_revision";
386     },
387
388     get buildStepsURL()
389     {
390         return this.queue.baseURL + "/builds/" + this.id + "/steps";
391     },
392
393     urlFailedToLoad: function(data)
394     {
395         this.isLoading = false;
396         if (data.errorType === JSON.LoadError && data.errorHTTPCode === 401) {
397             this.queue.buildbot.isAuthenticated = false;
398             this.dispatchEventToListeners(BuildbotIteration.Event.UnauthorizedAccess);
399         }
400     },
401
402     update: function()
403     {
404         if (this.loaded && this._finished)
405             return;
406
407         if (this.queue.buildbot.needsAuthentication && this.queue.buildbot.authenticationStatus === Buildbot.AuthenticationStatus.InvalidCredentials)
408             return;
409
410         if (this.isLoading)
411             return;
412
413         this.isLoading = true;
414         if (this.queue.buildbot.VERSION_LESS_THAN_09)
415             this.deprecatedUpdate();
416         else
417             this.actualUpdate();
418     },
419
420     actualUpdate: function()
421     {
422         JSON.load(this.buildStepsURL, function(data) {
423             if (!(data.steps instanceof Array))
424                 return;
425
426             this._steps = data.steps;
427             this._updateIfDataAvailable();
428         }.bind(this), this.urlFailedToLoad, {withCredentials: this.queue.buildbot.needsAuthentication});
429
430         JSON.load(this.buildURL, function(data) {
431             this.queue.buildbot.isAuthenticated = true;
432             if (!(data.builds instanceof Array))
433                 return;
434
435             // Sample data for a single build:
436             // "builds": [
437             //     {
438             //         "builderid": 282,
439             //         "buildid": 5609,
440             //         "complete": true,
441             //         ...
442             //         "workerid": 188
443             //     }
444             // ]
445             this._buildData = data.builds[0];
446             this._updateIfDataAvailable();
447         }.bind(this), this.urlFailedToLoad, {withCredentials: this.queue.buildbot.needsAuthentication});
448     },
449
450     deprecatedUpdate: function()
451     {
452         JSON.load(this.queue.baseURL + "/builds/" + this.id, function(data) {
453             this.isLoading = false;
454             this.queue.buildbot.isAuthenticated = true;
455             if (!data || !data.properties)
456                 return;
457
458             this._deprecatedUpdateWithData(data);
459         }.bind(this), this.urlFailedToLoad, {withCredentials: this.queue.buildbot.needsAuthentication});
460     },
461
462     loadLayoutTestResults: function(callback)
463     {
464         if (this.queue.buildbot.needsAuthentication && this.queue.buildbot.authenticationStatus === Buildbot.AuthenticationStatus.InvalidCredentials)
465             return;
466
467         JSON.load(this.queue.buildbot.layoutTestFullResultsURLForIteration(this), function(data) {
468             this.queue.buildbot.isAuthenticated = true;
469
470             this.layoutTestResults.addFullLayoutTestResults(data);
471             callback();
472         }.bind(this),
473         function(data) {
474             if (data.errorType === JSON.LoadError && data.errorHTTPCode === 401) {
475                 this.queue.buildbot.isAuthenticated = false;
476                 this.dispatchEventToListeners(BuildbotIteration.Event.UnauthorizedAccess);
477             }
478             console.log(data.error);
479             callback();
480         }.bind(this), {jsonpCallbackName: "ADD_RESULTS", withCredentials: this.queue.buildbot.needsAuthentication});
481     },
482
483     loadJavaScriptCoreTestResults: function(testName, callback)
484     {
485         if (this.queue.buildbot.needsAuthentication && this.queue.buildbot.authenticationStatus === Buildbot.AuthenticationStatus.InvalidCredentials)
486             return;
487
488         JSON.load(this.queue.buildbot.javaScriptCoreTestFailuresURLForIteration(this, testName), function(data) {
489             this.queue.buildbot.isAuthenticated = true;
490             this.javaScriptCoreTestResults.addJavaScriptCoreTestFailures(data);
491             callback();
492         }.bind(this),
493         function(data) {
494             if (data.errorType === JSON.LoadError && data.errorHTTPCode === 401) {
495                 this.queue.buildbot.isAuthenticated = false;
496                 this.dispatchEventToListeners(BuildbotIteration.Event.UnauthorizedAccess);
497             }
498             console.log(data.error);
499             callback();
500         }.bind(this), {withCredentials: this.queue.buildbot.needsAuthentication});
501     },
502 };