588af906921e10be19aebd1b782cf20a9abbc16f
[WebKit.git] / Websites / perf.webkit.org / tools / js / buildbot-syncer.js
1 'use strict';
2
3 let assert = require('assert');
4
5 require('./v3-models.js');
6
7 class BuildbotBuildEntry {
8     constructor(syncer, rawData)
9     {
10         assert.equal(syncer.builderName(), rawData['builderName']);
11
12         this._syncer = syncer;
13         this._slaveName = null;
14         this._buildRequestId = null;
15         this._isInProgress = rawData['currentStep'] || (rawData['times'] && !rawData['times'][1]);
16         this._buildNumber = rawData['number'];
17
18         for (let propertyTuple of (rawData['properties'] || [])) {
19             // e.g. ['build_request_id', '16733', 'Force Build Form']
20             const name = propertyTuple[0];
21             const value = propertyTuple[1];
22             if (name == syncer._slavePropertyName)
23                 this._slaveName = value;
24             else if (name == syncer._buildRequestPropertyName)
25                 this._buildRequestId = value;
26         }
27     }
28
29     syncer() { return this._syncer; }
30     buildNumber() { return this._buildNumber; }
31     slaveName() { return this._slaveName; }
32     buildRequestId() { return this._buildRequestId; }
33     isPending() { return typeof(this._buildNumber) != 'number'; }
34     isInProgress() { return this._isInProgress; }
35     hasFinished() { return !this.isPending() && !this.isInProgress(); }
36     url() { return this.isPending() ? this._syncer.url() : this._syncer.urlForBuildNumber(this._buildNumber); }
37
38     buildRequestStatusIfUpdateIsNeeded(request)
39     {
40         assert.equal(request.id(), this._buildRequestId);
41         if (!request)
42             return null;
43         if (this.isPending()) {
44             if (request.isPending())
45                 return 'scheduled';
46         } else if (this.isInProgress()) {
47             if (!request.hasStarted() || request.isScheduled())
48                 return 'running';
49         } else if (this.hasFinished()) {
50             if (!request.hasFinished())
51                 return 'failedIfNotCompleted';
52         }
53         return null;
54     }
55 }
56
57
58 class BuildbotSyncer {
59
60     constructor(remote, object, commonConfigurations)
61     {
62         this._remote = remote;
63         this._type = null;
64         this._configurations = [];
65         this._repositoryGroups = commonConfigurations.repositoryGroups;
66         this._slavePropertyName = commonConfigurations.slaveArgument;
67         this._buildRequestPropertyName = commonConfigurations.buildRequestArgument;
68         this._builderName = object.builder;
69         this._slaveList = object.slaveList;
70         this._entryList = null;
71         this._slavesWithNewRequests = new Set;
72     }
73
74     builderName() { return this._builderName; }
75
76     addTestConfiguration(test, platform, propertiesTemplate)
77     {
78         assert(test instanceof Test);
79         assert(platform instanceof Platform);
80         assert(this._type == null || this._type == 'tester');
81         this._type = 'tester';
82         this._configurations.push({test, platform, propertiesTemplate});
83     }
84     testConfigurations() { return this._type == 'tester' ? this._configurations : []; }
85
86     addBuildConfiguration(platform, propertiesTemplate)
87     {
88         assert(platform instanceof Platform);
89         assert(this._type == null || this._type == 'builder');
90         this._type = 'builder';
91         this._configurations.push({test: null, platform, propertiesTemplate});
92     }
93     buildConfigurations() { return this._type == 'builder' ? this._configurations : []; }
94
95     isTester() { return this._type == 'tester'; }
96
97     repositoryGroups() { return this._repositoryGroups; }
98
99     matchesConfiguration(request)
100     {
101         return this._configurations.some((config) => config.platform == request.platform() && config.test == request.test());
102     }
103
104     scheduleRequest(newRequest, requestsInGroup, slaveName)
105     {
106         assert(!this._slavesWithNewRequests.has(slaveName));
107         let properties = this._propertiesForBuildRequest(newRequest, requestsInGroup);
108
109         assert.equal(!this._slavePropertyName, !slaveName);
110         if (this._slavePropertyName)
111             properties[this._slavePropertyName] = slaveName;
112
113         this._slavesWithNewRequests.add(slaveName);
114         return this._remote.postFormUrlencodedData(this.pathForForceBuild(), properties);
115     }
116
117     scheduleRequestInGroupIfAvailable(newRequest, requestsInGroup, slaveName)
118     {
119         assert(newRequest instanceof BuildRequest);
120
121         if (!this.matchesConfiguration(newRequest))
122             return null;
123
124         let hasPendingBuildsWithoutSlaveNameSpecified = false;
125         let usedSlaves = new Set;
126         for (let entry of this._entryList) {
127             let entryPreventsNewRequest = entry.isPending();
128             if (entry.isInProgress()) {
129                 const requestInProgress = BuildRequest.findById(entry.buildRequestId());
130                 if (!requestInProgress || requestInProgress.testGroupId() != newRequest.testGroupId())
131                     entryPreventsNewRequest = true;
132             }
133             if (entryPreventsNewRequest) {
134                 if (!entry.slaveName())
135                     hasPendingBuildsWithoutSlaveNameSpecified = true;
136                 usedSlaves.add(entry.slaveName());
137             }
138         }
139
140         if (!this._slaveList || hasPendingBuildsWithoutSlaveNameSpecified) {
141             if (usedSlaves.size || this._slavesWithNewRequests.size)
142                 return null;
143             return this.scheduleRequest(newRequest, requestsInGroup, null);
144         }
145
146         if (slaveName) {
147             if (!usedSlaves.has(slaveName) && !this._slavesWithNewRequests.has(slaveName))
148                 return this.scheduleRequest(newRequest, requestsInGroup, slaveName);
149             return null;
150         }
151
152         for (let slaveName of this._slaveList) {
153             if (!usedSlaves.has(slaveName) && !this._slavesWithNewRequests.has(slaveName))
154                 return this.scheduleRequest(newRequest, requestsInGroup, slaveName);
155         }
156
157         return null;
158     }
159
160     pullBuildbot(count)
161     {
162         return this._remote.getJSON(this.pathForPendingBuildsJSON()).then((content) => {
163             let pendingEntries = content.map((entry) => new BuildbotBuildEntry(this, entry));
164             return this._pullRecentBuilds(count).then((entries) => {
165                 let entryByRequest = {};
166
167                 for (let entry of pendingEntries)
168                     entryByRequest[entry.buildRequestId()] = entry;
169
170                 for (let entry of entries)
171                     entryByRequest[entry.buildRequestId()] = entry;
172
173                 let entryList = [];
174                 for (let id in entryByRequest)
175                     entryList.push(entryByRequest[id]);
176
177                 this._entryList = entryList;
178                 this._slavesWithNewRequests.clear();
179
180                 return entryList;
181             });
182         });
183     }
184
185     _pullRecentBuilds(count)
186     {
187         if (!count)
188             return Promise.resolve([]);
189
190         let selectedBuilds = new Array(count);
191         for (let i = 0; i < count; i++)
192             selectedBuilds[i] = -i - 1;
193
194         return this._remote.getJSON(this.pathForBuildJSON(selectedBuilds)).then((content) => {
195             const entries = [];
196             for (let index of selectedBuilds) {
197                 const entry = content[index];
198                 if (entry && !entry['error'])
199                     entries.push(new BuildbotBuildEntry(this, entry));
200             }
201             return entries;
202         });
203     }
204
205     pathForPendingBuildsJSON() { return `/json/builders/${escape(this._builderName)}/pendingBuilds`; }
206     pathForBuildJSON(selectedBuilds)
207     {
208         return `/json/builders/${escape(this._builderName)}/builds/?` + selectedBuilds.map((number) => 'select=' + number).join('&');
209     }
210     pathForForceBuild() { return `/builders/${escape(this._builderName)}/force`; }
211
212     url() { return this._remote.url(`/builders/${escape(this._builderName)}/`); }
213     urlForBuildNumber(number) { return this._remote.url(`/builders/${escape(this._builderName)}/builds/${number}`); }
214
215     _propertiesForBuildRequest(buildRequest, requestsInGroup)
216     {
217         assert(buildRequest instanceof BuildRequest);
218         assert(requestsInGroup[0] instanceof BuildRequest);
219
220         const commitSet = buildRequest.commitSet();
221         assert(commitSet instanceof CommitSet);
222
223         const repositoryByName = {};
224         for (let repository of commitSet.repositories())
225             repositoryByName[repository.name()] = repository;
226
227         const matchingConfiguration = this._configurations.find((config) => config.platform == buildRequest.platform() && config.test == buildRequest.test());
228         assert(matchingConfiguration, `Build request ${buildRequest.id()} does not match a configuration in the builder "${this._builderName}"`);
229         const propertiesTemplate = matchingConfiguration.propertiesTemplate;
230
231         const repositoryGroup = buildRequest.repositoryGroup();
232         assert(repositoryGroup.accepts(commitSet), `Build request ${buildRequest.id()} does not specify a commit set accepted by the repository group ${repositoryGroup.id()}`);
233
234         const repositoryGroupConfiguration = this._repositoryGroups[repositoryGroup.name()];
235         assert(repositoryGroupConfiguration, `Build request ${buildRequest.id()} uses an unsupported repository group "${repositoryGroup.name()}"`);
236
237         const properties = {};
238         for (let propertyName in propertiesTemplate)
239             properties[propertyName] = propertiesTemplate[propertyName];
240
241         const repositoryGroupTemplate = buildRequest.isBuild() ? repositoryGroupConfiguration.buildPropertiesTemplate : repositoryGroupConfiguration.testPropertiesTemplate;
242         for (let propertyName in repositoryGroupTemplate) {
243             let value = repositoryGroupTemplate[propertyName];
244             const type = typeof(value) == 'object' ? value.type : 'string';
245             switch (type) {
246             case 'string':
247                 break;
248             case 'revision':
249                 value = commitSet.revisionForRepository(value.repository);
250                 break;
251             case 'patch':
252                 const patch = commitSet.patchForRepository(value.repository);
253                 if (!patch)
254                     continue;
255                 value = patch.url();
256                 break;
257             case 'roots':
258                 const rootFiles = commitSet.allRootFiles();
259                 if (!rootFiles.length)
260                     continue;
261                 value = JSON.stringify(rootFiles.map((file) => ({url: file.url()})));
262                 break;
263             case 'conditional':
264                 switch (value.condition) {
265                 case 'built':
266                     if (!requestsInGroup.some((otherRequest) => otherRequest.isBuild() && otherRequest.commitSet() == buildRequest.commitSet()))
267                         continue;
268                     break;
269                 }
270                 value = value.value;
271             }
272             properties[propertyName] = value;
273         }
274         properties[this._buildRequestPropertyName] = buildRequest.id();
275
276         return properties;
277     }
278
279     _revisionSetFromCommitSetWithExclusionList(commitSet, exclusionList)
280     {
281         const revisionSet = {};
282         for (let repository of commitSet.repositories()) {
283             if (exclusionList.indexOf(repository.name()) >= 0)
284                 continue;
285             const commit = commitSet.commitForRepository(repository);
286             revisionSet[repository.name()] = {
287                 id: commit.id(),
288                 time: +commit.time(),
289                 repository: repository.name(),
290                 revision: commit.revision(),
291             };
292         }
293         return revisionSet;
294     }
295
296     static _loadConfig(remote, config)
297     {
298         const types = config['types'] || {};
299         const builders = config['builders'] || {};
300
301         assert(config.buildRequestArgument, 'buildRequestArgument must specify the name of the property used to store the build request ID');
302
303         assert.equal(typeof(config.repositoryGroups), 'object', 'repositoryGroups must specify a dictionary from the name to its definition');
304
305         const repositoryGroups = {};
306         for (const name in config.repositoryGroups)
307             repositoryGroups[name] = this._parseRepositoryGroup(name, config.repositoryGroups[name]);
308
309         const commonConfigurations = {
310             repositoryGroups,
311             slaveArgument: config.slaveArgument,
312             buildRequestArgument: config.buildRequestArgument,
313         };
314
315         const syncerByBuilder = new Map;
316         const ensureBuildbotSyncer = (builderInfo) => {
317             let builderSyncer = syncerByBuilder.get(builderInfo.builder);
318             if (!builderSyncer) {
319                 builderSyncer = new BuildbotSyncer(remote, builderInfo, commonConfigurations);
320                 syncerByBuilder.set(builderInfo.builder, builderSyncer);
321             }
322             return builderSyncer;
323         }
324
325         assert(Array.isArray(config['testConfigurations']), `The test configuration must be an array`);
326         this._resolveBuildersWithPlatforms('test', config['testConfigurations'], builders).forEach((entry, configurationIndex) => {
327             assert(Array.isArray(entry['types']), `The test configuration ${configurationIndex} does not specify "types" as an array`);
328             for (const type of entry['types']) {
329                 const typeConfig = this._validateAndMergeConfig({}, entry.builderConfig);
330                 assert(types[type], `"${type}" is not a valid type in the configuration`);
331                 this._validateAndMergeConfig(typeConfig, types[type]);
332
333                 const testPath = typeConfig.test;
334                 const test = Test.findByPath(testPath);
335                 assert(test, `"${testPath.join('", "')}" is not a valid test path in the test configuration ${configurationIndex}`);
336
337                 ensureBuildbotSyncer(entry.builderConfig).addTestConfiguration(test, entry.platform, typeConfig.properties);
338             }
339         });
340
341         const buildConfigurations = config['buildConfigurations'];
342         if (buildConfigurations) {
343             assert(Array.isArray(buildConfigurations), `The test configuration must be an array`);
344             this._resolveBuildersWithPlatforms('test', buildConfigurations, builders).forEach((entry, configurationIndex) => {
345                 const syncer = ensureBuildbotSyncer(entry.builderConfig);
346                 assert(!syncer.isTester(), `The build configuration ${configurationIndex} uses a tester: ${syncer.builderName()}`);
347                 syncer.addBuildConfiguration(entry.platform, entry.builderConfig.properties);
348             });
349         }
350
351         return Array.from(syncerByBuilder.values());
352     }
353
354     static _resolveBuildersWithPlatforms(configurationType, configurationList, builders)
355     {
356         const resolvedConfigurations = [];
357         let configurationIndex = 0;
358         for (const entry of configurationList) {
359             configurationIndex++;
360             assert(Array.isArray(entry['builders']), `The ${configurationType} configuration ${configurationIndex} does not specify "builders" as an array`);
361             assert(Array.isArray(entry['platforms']), `The ${configurationType} configuration ${configurationIndex} does not specify "platforms" as an array`);
362             for (const builderKey of entry['builders']) {
363                 const matchingBuilder = builders[builderKey];
364                 assert(matchingBuilder, `"${builderKey}" is not a valid builder in the configuration`);
365                 assert('builder' in matchingBuilder, `Builder ${builderKey} does not specify a buildbot builder name`);
366                 const builderConfig = this._validateAndMergeConfig({}, matchingBuilder);
367                 for (const platformName of entry['platforms']) {
368                     const platform = Platform.findByName(platformName);
369                     assert(platform, `${platformName} is not a valid platform name`);
370                     resolvedConfigurations.push({types: entry.types, builderConfig, platform});
371                 }
372             }
373         }
374         return resolvedConfigurations;
375     }
376
377     static _parseRepositoryGroup(name, group)
378     {
379         assert.equal(typeof(group.repositories), 'object',
380             `Repository group "${name}" does not specify a dictionary of repositories`);
381         assert(!('description' in group) || typeof(group['description']) == 'string',
382             `Repository group "${name}" have an invalid description`);
383         assert([undefined, true, false].includes(group.acceptsRoots),
384             `Repository group "${name}" contains invalid acceptsRoots value: ${JSON.stringify(group.acceptsRoots)}`);
385
386         const repositoryByName = {};
387         const parsedRepositoryList = [];
388         const patchAcceptingRepositoryList = new Set;
389         for (const repositoryName in group.repositories) {
390             const options = group.repositories[repositoryName];
391             const repository = Repository.findTopLevelByName(repositoryName);
392             assert(repository, `"${repositoryName}" is not a valid repository name`);
393             repositoryByName[repositoryName] = repository;
394             assert.equal(typeof(options), 'object', `"${repositoryName}" specifies a non-dictionary value`);
395             assert([undefined, true, false].includes(options.acceptsPatch),
396                 `"${repositoryName}" contains invalid acceptsPatch value: ${JSON.stringify(options.acceptsPatch)}`);
397             if (options.acceptsPatch)
398                 patchAcceptingRepositoryList.add(repository);
399             repositoryByName[repositoryName] = repository;
400             parsedRepositoryList.push({repository: repository.id(), acceptsPatch: options.acceptsPatch});
401         }
402         assert(parsedRepositoryList.length, `Repository group "${name}" does not specify any repository`);
403
404         assert.equal(typeof(group.testProperties), 'object', `Repository group "${name}" specifies the test configurations with an invalid type`);
405
406         const resolveRepository = (repositoryName) => {
407             const repository = repositoryByName[repositoryName];
408             assert(repository, `Repository group "${name}" an invalid repository "${repositoryName}"`);
409             return repository;
410         }
411
412         const testRepositories = new Set;
413         let specifiesRoots = false;
414         const testPropertiesTemplate = this._parseRepositoryGroupPropertyTemplate('test', name, group.testProperties, (type, value) => {
415             assert(type != 'patch', `Repository group "${name}" specifies a patch for "${value}" in the properties for testing`);
416             switch (type) {
417             case 'revision':
418                 const repository = resolveRepository(value);
419                 testRepositories.add(repository);
420                 return {type, repository};
421             case 'roots':
422                 assert(group.acceptsRoots, `Repository group "${name}" specifies roots in a property but it does not accept roots`);
423                 specifiesRoots = true;
424                 return {type};
425             case 'ifBuilt':
426                 return {type: 'conditional', condition: 'built', value};
427             }
428             return null;
429         });
430         assert(!group.acceptsRoots == !specifiesRoots,
431             `Repository group "${name}" accepts roots but does not specify roots in testProperties`);
432         assert.equal(parsedRepositoryList.length, testRepositories.size,
433             `Repository group "${name}" does not use some of the repositories listed in testing`);
434
435         let buildPropertiesTemplate = null;
436         if ('buildProperties' in group) {
437             assert(patchAcceptingRepositoryList.size, `Repository group "${name}" specifies the properties for building but does not accept any patches`);
438             assert(group.acceptsRoots, `Repository group "${name}" specifies the properties for building but does not accept roots in testing`);
439             const revisionRepositories = new Set;
440             const patchRepositories = new Set;
441             buildPropertiesTemplate = this._parseRepositoryGroupPropertyTemplate('build', name, group.buildProperties, (type, value) => {
442                 assert(type != 'roots', `Repository group "${name}" specifies roots in the properties for building`);
443                 const repository = resolveRepository(value);
444                 switch (type) {
445                 case 'patch':
446                     assert(patchAcceptingRepositoryList.has(repository), `Repository group "${name}" specifies a patch for "${value}" but it does not accept a patch`);
447                     patchRepositories.add(repository);
448                     return {type, repository};
449                 case 'revision':
450                     revisionRepositories.add(repository);
451                     return {type, repository};
452                 }
453                 return null;
454             });
455             for (const repository of patchRepositories)
456                 assert(revisionRepositories.has(repository), `Repository group "${name}" specifies a patch for "${repository.name()}" but does not specify a revision`);
457             assert.equal(patchAcceptingRepositoryList.size, patchRepositories.size,
458                 `Repository group "${name}" does not use some of the repositories listed in building a patch`);
459         }
460
461         return {
462             name: group.name,
463             description: group.description,
464             acceptsRoots: group.acceptsRoots,
465             testPropertiesTemplate: testPropertiesTemplate,
466             buildPropertiesTemplate: buildPropertiesTemplate,
467             repositoryList: parsedRepositoryList,
468         };
469     }
470
471     static _parseRepositoryGroupPropertyTemplate(parsingMode, groupName, properties, makeOption)
472     {
473         const propertiesTemplate = {};
474         for (const propertyName in properties) {
475             let value = properties[propertyName];
476             const isDictionary = typeof(value) == 'object';
477             assert(isDictionary || typeof(value) == 'string',
478                 `Repository group "${groupName}" uses an invalid value "${value}" in property "${propertyName}"`);
479
480             if (!isDictionary) {
481                 propertiesTemplate[propertyName] = value;
482                 continue;
483             }
484
485             const keys = Object.keys(value);
486             assert.equal(keys.length, 1,
487                 `Repository group "${groupName}" specifies more than one type in property "${propertyName}": "${keys.join('", "')}"`);
488             const type = keys[0];
489             const option = makeOption(type, value[type]);
490             assert(option, `Repository group "${groupName}" specifies an invalid type "${type}" in property "${propertyName}"`);
491             propertiesTemplate[propertyName] = option;
492         }
493         return propertiesTemplate;
494     }
495
496     static _validateAndMergeConfig(config, valuesToMerge, excludedProperty)
497     {
498         for (const name in valuesToMerge) {
499             const value = valuesToMerge[name];
500             if (name == excludedProperty)
501                 continue;
502
503             switch (name) {
504             case 'properties': // Fallthrough
505                 assert.equal(typeof(value), 'object', 'Build properties should be a dictionary');
506                 if (!config['properties'])
507                     config['properties'] = {};
508                 const properties = config['properties'];
509                 for (const name in value) {
510                     assert.equal(typeof(value[name]), 'string', `Build properties "${name}" specifies a non-string value of type "${typeof(value)}"`);
511                     properties[name] = value[name];
512                 }
513                 break;
514             case 'test': // Fallthrough
515             case 'slaveList': // Fallthrough
516                 assert(value instanceof Array, `${name} should be an array`);
517                 assert(value.every(function (part) { return typeof part == 'string'; }), `${name} should be an array of strings`);
518                 config[name] = value.slice();
519                 break;
520             case 'builder': // Fallthrough
521                 assert.equal(typeof(value), 'string', `${name} should be of string type`);
522                 config[name] = value;
523                 break;
524             default:
525                 assert(false, `Unrecognized parameter "${name}"`);
526             }
527         }
528         return config;
529     }
530
531 }
532
533 if (typeof module != 'undefined') {
534     module.exports.BuildbotSyncer = BuildbotSyncer;
535     module.exports.BuildbotBuildEntry = BuildbotBuildEntry;
536 }