3 let assert = require('assert');
5 require('./v3-models.js');
7 class BuildbotBuildEntry {
8 constructor(syncer, rawData)
10 assert.equal(syncer.builderName(), rawData['builderName']);
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'];
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;
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); }
38 buildRequestStatusIfUpdateIsNeeded(request)
40 assert.equal(request.id(), this._buildRequestId);
43 if (this.isPending()) {
44 if (request.isPending())
46 } else if (this.isInProgress()) {
47 if (!request.hasStarted() || request.isScheduled())
49 } else if (this.hasFinished()) {
50 if (!request.hasFinished())
51 return 'failedIfNotCompleted';
58 class BuildbotSyncer {
60 constructor(remote, object, commonConfigurations)
62 this._remote = remote;
63 this._testConfigurations = [];
64 this._repositoryGroups = commonConfigurations.repositoryGroups;
65 this._slavePropertyName = commonConfigurations.slaveArgument;
66 this._buildRequestPropertyName = commonConfigurations.buildRequestArgument;
67 this._builderName = object.builder;
68 this._slaveList = object.slaveList;
69 this._entryList = null;
70 this._slavesWithNewRequests = new Set;
73 builderName() { return this._builderName; }
75 addTestConfiguration(test, platform, propertiesTemplate)
77 assert(test instanceof Test);
78 assert(platform instanceof Platform);
79 this._testConfigurations.push({test, platform, propertiesTemplate});
81 testConfigurations() { return this._testConfigurations; }
82 repositoryGroups() { return this._repositoryGroups; }
84 matchesConfiguration(request)
86 return this._testConfigurations.some((config) => config.platform == request.platform() && config.test == request.test());
89 scheduleRequest(newRequest, slaveName)
91 assert(!this._slavesWithNewRequests.has(slaveName));
92 let properties = this._propertiesForBuildRequest(newRequest);
94 assert.equal(!this._slavePropertyName, !slaveName);
95 if (this._slavePropertyName)
96 properties[this._slavePropertyName] = slaveName;
98 this._slavesWithNewRequests.add(slaveName);
99 return this._remote.postFormUrlencodedData(this.pathForForceBuild(), properties);
102 scheduleRequestInGroupIfAvailable(newRequest, slaveName)
104 assert(newRequest instanceof BuildRequest);
106 if (!this.matchesConfiguration(newRequest))
109 let hasPendingBuildsWithoutSlaveNameSpecified = false;
110 let usedSlaves = new Set;
111 for (let entry of this._entryList) {
112 if (entry.isPending()) {
113 if (!entry.slaveName())
114 hasPendingBuildsWithoutSlaveNameSpecified = true;
115 usedSlaves.add(entry.slaveName());
119 if (!this._slaveList || hasPendingBuildsWithoutSlaveNameSpecified) {
120 if (usedSlaves.size || this._slavesWithNewRequests.size)
122 return this.scheduleRequest(newRequest, null);
126 if (!usedSlaves.has(slaveName) && !this._slavesWithNewRequests.has(slaveName))
127 return this.scheduleRequest(newRequest, slaveName);
131 for (let slaveName of this._slaveList) {
132 if (!usedSlaves.has(slaveName) && !this._slavesWithNewRequests.has(slaveName))
133 return this.scheduleRequest(newRequest, slaveName);
141 return this._remote.getJSON(this.pathForPendingBuildsJSON()).then((content) => {
142 let pendingEntries = content.map((entry) => new BuildbotBuildEntry(this, entry));
143 return this._pullRecentBuilds(count).then((entries) => {
144 let entryByRequest = {};
146 for (let entry of pendingEntries)
147 entryByRequest[entry.buildRequestId()] = entry;
149 for (let entry of entries)
150 entryByRequest[entry.buildRequestId()] = entry;
153 for (let id in entryByRequest)
154 entryList.push(entryByRequest[id]);
156 this._entryList = entryList;
157 this._slavesWithNewRequests.clear();
164 _pullRecentBuilds(count)
167 return Promise.resolve([]);
169 let selectedBuilds = new Array(count);
170 for (let i = 0; i < count; i++)
171 selectedBuilds[i] = -i - 1;
173 return this._remote.getJSON(this.pathForBuildJSON(selectedBuilds)).then((content) => {
175 for (let index of selectedBuilds) {
176 const entry = content[index];
177 if (entry && !entry['error'])
178 entries.push(new BuildbotBuildEntry(this, entry));
184 pathForPendingBuildsJSON() { return `/json/builders/${escape(this._builderName)}/pendingBuilds`; }
185 pathForBuildJSON(selectedBuilds)
187 return `/json/builders/${escape(this._builderName)}/builds/?` + selectedBuilds.map((number) => 'select=' + number).join('&');
189 pathForForceBuild() { return `/builders/${escape(this._builderName)}/force`; }
191 url() { return this._remote.url(`/builders/${escape(this._builderName)}/`); }
192 urlForBuildNumber(number) { return this._remote.url(`/builders/${escape(this._builderName)}/builds/${number}`); }
194 _propertiesForBuildRequest(buildRequest)
196 assert(buildRequest instanceof BuildRequest);
198 const commitSet = buildRequest.commitSet();
199 assert(commitSet instanceof CommitSet);
201 const repositoryByName = {};
202 for (let repository of commitSet.repositories())
203 repositoryByName[repository.name()] = repository;
205 const matchingConfiguration = this._testConfigurations.find((config) => config.platform == buildRequest.platform() && config.test == buildRequest.test());
206 assert(matchingConfiguration, `Build request ${buildRequest.id()} does not match a configuration in the builder "${this._builderName}"`);
207 const propertiesTemplate = matchingConfiguration.propertiesTemplate;
209 const repositoryGroup = buildRequest.repositoryGroup();
210 assert(repositoryGroup.accepts(commitSet), `Build request ${buildRequest.id()} does not specify a commit set accepted by the repository group ${repositoryGroup.id()}`);
212 const repositoryGroupConfiguration = this._repositoryGroups[repositoryGroup.name()];
213 assert(repositoryGroupConfiguration, `Build request ${buildRequest.id()} uses an unsupported repository group "${repositoryGroup.name()}"`);
215 const properties = {};
216 for (let propertyName in propertiesTemplate)
217 properties[propertyName] = propertiesTemplate[propertyName];
219 const repositoryGroupTemplate = repositoryGroupConfiguration.propertiesTemplate;
220 for (let propertyName in repositoryGroupTemplate) {
221 const value = repositoryGroupTemplate[propertyName];
222 properties[propertyName] = value instanceof Repository ? commitSet.revisionForRepository(value) : value;
224 properties[this._buildRequestPropertyName] = buildRequest.id();
229 _revisionSetFromCommitSetWithExclusionList(commitSet, exclusionList)
231 const revisionSet = {};
232 for (let repository of commitSet.repositories()) {
233 if (exclusionList.indexOf(repository.name()) >= 0)
235 const commit = commitSet.commitForRepository(repository);
236 revisionSet[repository.name()] = {
238 time: +commit.time(),
239 repository: repository.name(),
240 revision: commit.revision(),
246 static _loadConfig(remote, config)
248 const types = config['types'] || {};
249 const builders = config['builders'] || {};
251 assert(config.buildRequestArgument, 'buildRequestArgument must specify the name of the property used to store the build request ID');
253 assert.equal(typeof(config.repositoryGroups), 'object', 'repositoryGroups must specify a dictionary from the name to its definition');
255 const repositoryGroups = {};
256 for (const name in config.repositoryGroups)
257 repositoryGroups[name] = this._parseRepositoryGroup(name, config.repositoryGroups[name]);
259 const commonConfigurations = {
261 slaveArgument: config.slaveArgument,
262 buildRequestArgument: config.buildRequestArgument,
265 let syncerByBuilder = new Map;
266 const expandedConfigurations = [];
267 for (let entry of config['configurations']) {
268 for (const expandedConfig of this._expandTypesAndPlatforms(entry))
269 expandedConfigurations.push(expandedConfig);
272 for (let entry of expandedConfigurations) {
273 const mergedConfig = {};
274 this._validateAndMergeConfig(mergedConfig, entry);
276 if ('type' in mergedConfig) {
277 const type = mergedConfig['type'];
278 assert(type, `${type} is not a valid type in the configuration`);
279 this._validateAndMergeConfig(mergedConfig, types[type]);
282 const builder = entry['builder'];
283 if (builders[builder])
284 this._validateAndMergeConfig(mergedConfig, builders[builder]);
286 this._createTestConfiguration(remote, syncerByBuilder, mergedConfig, commonConfigurations);
289 return Array.from(syncerByBuilder.values());
292 static _parseRepositoryGroup(name, group)
294 assert(Array.isArray(group.repositories), 'Each repository group must specify a list of repositories');
295 assert(group.repositories.length, 'Each repository group must specify a list of repositories');
296 assert(!('description' in group) || typeof(group['description']) == 'string', 'The description of a repository group must be a string');
297 assert.equal(typeof(group.properties), 'object', 'Each repository group must specify a dictionary of properties');
299 const repositoryByName = {};
300 const repositories = group.repositories.map((repositoryName) => {
301 const repository = Repository.findTopLevelByName(repositoryName);
302 assert(repository, `"${repositoryName}" is not a valid repository name`);
303 repositoryByName[repositoryName] = repository;
306 const propertiesTemplate = {};
307 const usedRepositories = [];
308 for (const propertyName in group.properties) {
309 let value = group.properties[propertyName];
310 const match = value.match(/^<(.+)>$/);
312 const repositoryName = match[1];
313 value = repositoryByName[repositoryName];
314 assert(value, `Repository group "${name}" uses "${repositoryName}" in its property but does not list in the list of repositories`);
315 usedRepositories.push(value);
317 propertiesTemplate[propertyName] = value;
319 assert.equal(repositories.length, usedRepositories.length, `Repository group "${name}" does not use some of the listed repositories`);
322 description: group.description,
324 arguments: group.arguments,
325 repositories: repositories.map((repository) => repository.id()),
329 static _expandTypesAndPlatforms(unresolvedConfig)
331 const typeExpanded = [];
332 if ('types' in unresolvedConfig) {
333 for (let type of unresolvedConfig['types'])
334 typeExpanded.push(this._validateAndMergeConfig({'type': type}, unresolvedConfig, 'types'));
336 typeExpanded.push(unresolvedConfig);
338 const configurations = [];
339 for (let config of typeExpanded) {
340 if ('platforms' in config) {
341 for (let platform of config['platforms'])
342 configurations.push(this._validateAndMergeConfig({'platform': platform}, config, 'platforms'));
344 configurations.push(config);
347 return configurations;
350 static _createTestConfiguration(remote, syncerByBuilder, newConfig, commonConfigurations)
352 assert('platform' in newConfig, 'configuration must specify a platform');
353 assert('test' in newConfig, 'configuration must specify a test');
354 assert('builder' in newConfig, 'configuration must specify a builder');
356 const test = Test.findByPath(newConfig.test);
357 assert(test, `${newConfig.test} is not a valid test path`);
359 const platform = Platform.findByName(newConfig.platform);
360 assert(platform, `${newConfig.platform} is not a valid platform name`);
362 let syncer = syncerByBuilder.get(newConfig.builder);
364 syncer = new BuildbotSyncer(remote, newConfig, commonConfigurations);
365 syncerByBuilder.set(newConfig.builder, syncer);
367 syncer.addTestConfiguration(test, platform, newConfig.properties || {});
370 static _validateAndMergeConfig(config, valuesToMerge, excludedProperty)
372 for (const name in valuesToMerge) {
373 const value = valuesToMerge[name];
374 if (name == excludedProperty)
378 case 'properties': // Fallthrough
380 assert.equal(typeof(value), 'object', 'arguments should be a dictionary');
381 if (!config['properties'])
382 config['properties'] = {};
383 this._validateAndMergeProperties(config['properties'], value);
385 case 'test': // Fallthrough
386 case 'slaveList': // Fallthrough
387 case 'platforms': // Fallthrough
389 assert(value instanceof Array, `${name} should be an array`);
390 assert(value.every(function (part) { return typeof part == 'string'; }), `${name} should be an array of strings`);
391 config[name] = value.slice();
393 case 'type': // Fallthrough
394 case 'builder': // Fallthrough
395 case 'platform': // Fallthrough
396 assert.equal(typeof(value), 'string', `${name} should be of string type`);
397 config[name] = value;
400 assert(false, `Unrecognized parameter ${name}`);
406 static _validateAndMergeProperties(properties, configArguments)
408 for (let name in configArguments) {
409 const value = configArguments[name];
410 if (typeof(value) == 'string') {
411 properties[name] = value;
414 assert.equal(typeof(value), 'object', 'A argument value must be either a string or a dictionary');
416 const keys = Object.keys(value);
417 assert.equal(keys.length, 1, 'arguments value cannot contain more than one key');
418 let namedValue = value[keys[0]];
421 assert.equal(typeof(namedValue), 'string', 'root name must be a string');
423 case 'rootOptions': // Fallthrough
424 case 'rootsExcluding':
425 assert(namedValue instanceof Array, `${keys[0]} must specify an array`);
426 for (let excludedRootName of namedValue)
427 assert.equal(typeof(excludedRootName), 'string', `${keys[0]} must specify an array of strings`);
428 namedValue = namedValue.slice();
431 assert(false, `Unrecognized named argument ${keys[0]}`);
433 properties[name] = {[keys[0]]: namedValue};
439 if (typeof module != 'undefined') {
440 module.exports.BuildbotSyncer = BuildbotSyncer;
441 module.exports.BuildbotBuildEntry = BuildbotBuildEntry;