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