33a2746508b4df83eedb510b44235193baff565d
[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._pullRecentBuilds(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     _pullRecentBuilds(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.pathForBuildJSON(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     pathForPendingBuildsJSONDeprecated() { return `/json/builders/${escape(this._builderName)}/pendingBuilds`; }
243     pathForPendingBuilds() { return `/api/v2/builders/${this._builderID}/buildrequests?complete=false&claimed=false`; }
244     pathForBuildJSON(selectedBuilds)
245     {
246         return `/json/builders/${escape(this._builderName)}/builds/?` + selectedBuilds.map((number) => 'select=' + number).join('&');
247     }
248     pathForForceBuild() { return `/builders/${escape(this._builderName)}/force`; }
249
250     url() { return this._remote.url(`/builders/${escape(this._builderName)}/`); }
251     urlForBuildNumberDeprecated(number) { return this._remote.url(`/builders/${escape(this._builderName)}/builds/${number}`); }
252     urlForBuildNumber(number) { return this._remote.url(`/#/builders/${this._builderID}/builds/${number}`); }
253     urlForPendingBuild(buildRequestId) { return this._remote.url(`/#/buildrequests/${buildRequestId}`); }
254
255     _propertiesForBuildRequest(buildRequest, requestsInGroup)
256     {
257         assert(buildRequest instanceof BuildRequest);
258         assert(requestsInGroup[0] instanceof BuildRequest);
259
260         const commitSet = buildRequest.commitSet();
261         assert(commitSet instanceof CommitSet);
262
263         const repositoryByName = {};
264         for (let repository of commitSet.repositories())
265             repositoryByName[repository.name()] = repository;
266
267         const matchingConfiguration = this._configurations.find((config) => config.platform == buildRequest.platform() && config.test == buildRequest.test());
268         assert(matchingConfiguration, `Build request ${buildRequest.id()} does not match a configuration in the builder "${this._builderName}"`);
269         const propertiesTemplate = matchingConfiguration.propertiesTemplate;
270
271         const repositoryGroup = buildRequest.repositoryGroup();
272         assert(repositoryGroup.accepts(commitSet), `Build request ${buildRequest.id()} does not specify a commit set accepted by the repository group ${repositoryGroup.id()}`);
273
274         const repositoryGroupConfiguration = this._repositoryGroups[repositoryGroup.name()];
275         assert(repositoryGroupConfiguration, `Build request ${buildRequest.id()} uses an unsupported repository group "${repositoryGroup.name()}"`);
276
277         const properties = {};
278         for (let propertyName in propertiesTemplate)
279             properties[propertyName] = propertiesTemplate[propertyName];
280
281         const repositoryGroupTemplate = buildRequest.isBuild() ? repositoryGroupConfiguration.buildPropertiesTemplate : repositoryGroupConfiguration.testPropertiesTemplate;
282         for (let propertyName in repositoryGroupTemplate) {
283             let value = repositoryGroupTemplate[propertyName];
284             const type = typeof(value) == 'object' ? value.type : 'string';
285             switch (type) {
286             case 'string':
287                 break;
288             case 'revision':
289                 value = commitSet.revisionForRepository(value.repository);
290                 break;
291             case 'patch':
292                 const patch = commitSet.patchForRepository(value.repository);
293                 if (!patch)
294                     continue;
295                 value = patch.url();
296                 break;
297             case 'roots':
298                 const rootFiles = commitSet.allRootFiles();
299                 if (!rootFiles.length)
300                     continue;
301                 value = JSON.stringify(rootFiles.map((file) => ({url: file.url()})));
302                 break;
303             case 'ownedRevisions':
304                 const ownedRepositories = commitSet.ownedRepositoriesForOwnerRepository(value.ownerRepository);
305                 if (!ownedRepositories)
306                     continue;
307
308                 const revisionInfo = {};
309                 revisionInfo[value.ownerRepository.name()] = ownedRepositories.map((ownedRepository) => {
310                     return {
311                         'revision': commitSet.revisionForRepository(ownedRepository),
312                         'repository': ownedRepository.name(),
313                         'ownerRevision': commitSet.ownerRevisionForRepository(ownedRepository)
314                     };
315                 });
316                 value = JSON.stringify(revisionInfo);
317                 break;
318             case 'conditional':
319                 switch (value.condition) {
320                 case 'built':
321                     const repositoryRequirement = value.repositoryRequirement;
322                     const meetRepositoryRequirement = !repositoryRequirement.length || repositoryRequirement.some((repository) => commitSet.requiresBuildForRepository(repository));
323                     if (!meetRepositoryRequirement || !requestsInGroup.some((otherRequest) => otherRequest.isBuild() && otherRequest.commitSet() == buildRequest.commitSet()))
324                         continue;
325                     break;
326                 case 'requiresBuild':
327                     const requiresBuild = value.repositoriesToCheck.some((repository) => commitSet.requiresBuildForRepository(repository));
328                     if (!requiresBuild)
329                         continue;
330                     break;
331                 }
332                 value = value.value;
333             }
334             properties[propertyName] = value;
335         }
336         properties[this._buildRequestPropertyName] = buildRequest.id();
337
338         return properties;
339     }
340
341     _revisionSetFromCommitSetWithExclusionList(commitSet, exclusionList)
342     {
343         const revisionSet = {};
344         for (let repository of commitSet.repositories()) {
345             if (exclusionList.indexOf(repository.name()) >= 0)
346                 continue;
347             const commit = commitSet.commitForRepository(repository);
348             revisionSet[repository.name()] = {
349                 id: commit.id(),
350                 time: +commit.time(),
351                 repository: repository.name(),
352                 revision: commit.revision(),
353             };
354         }
355         return revisionSet;
356     }
357
358     static _loadConfig(remote, config, builderNameToIDMap)
359     {
360         assert(builderNameToIDMap);
361         const types = config['types'] || {};
362         const builders = config['builders'] || {};
363
364         assert(config.buildRequestArgument, 'buildRequestArgument must specify the name of the property used to store the build request ID');
365
366         assert.equal(typeof(config.repositoryGroups), 'object', 'repositoryGroups must specify a dictionary from the name to its definition');
367
368         const repositoryGroups = {};
369         for (const name in config.repositoryGroups)
370             repositoryGroups[name] = this._parseRepositoryGroup(name, config.repositoryGroups[name]);
371
372         const commonConfigurations = {
373             repositoryGroups,
374             slaveArgument: config.slaveArgument,
375             buildRequestArgument: config.buildRequestArgument,
376             platformArgument: config.platformArgument,
377         };
378
379         const syncerByBuilder = new Map;
380         const ensureBuildbotSyncer = (builderInfo) => {
381             let builderSyncer = syncerByBuilder.get(builderInfo.builder);
382             if (!builderSyncer) {
383                 builderSyncer = new BuildbotSyncer(remote, builderInfo, commonConfigurations);
384                 syncerByBuilder.set(builderInfo.builder, builderSyncer);
385             }
386             return builderSyncer;
387         }
388
389         assert(Array.isArray(config['testConfigurations']), `The test configuration must be an array`);
390         this._resolveBuildersWithPlatforms('test', config['testConfigurations'], builders, builderNameToIDMap).forEach((entry, configurationIndex) => {
391             assert(Array.isArray(entry['types']), `The test configuration ${configurationIndex} does not specify "types" as an array`);
392             for (const type of entry['types']) {
393                 const typeConfig = this._validateAndMergeConfig({}, entry.builderConfig);
394                 assert(types[type], `"${type}" is not a valid type in the configuration`);
395                 this._validateAndMergeConfig(typeConfig, types[type]);
396
397                 const testPath = typeConfig.test;
398                 const test = Test.findByPath(testPath);
399                 assert(test, `"${testPath.join('", "')}" is not a valid test path in the test configuration ${configurationIndex}`);
400
401                 ensureBuildbotSyncer(entry.builderConfig).addTestConfiguration(test, entry.platform, typeConfig.properties);
402             }
403         });
404
405         const buildConfigurations = config['buildConfigurations'];
406         if (buildConfigurations) {
407             assert(Array.isArray(buildConfigurations), `The test configuration must be an array`);
408             this._resolveBuildersWithPlatforms('test', buildConfigurations, builders, builderNameToIDMap).forEach((entry, configurationIndex) => {
409                 const syncer = ensureBuildbotSyncer(entry.builderConfig);
410                 assert(!syncer.isTester(), `The build configuration ${configurationIndex} uses a tester: ${syncer.builderName()}`);
411                 syncer.addBuildConfiguration(entry.platform, entry.builderConfig.properties);
412             });
413         }
414
415         return Array.from(syncerByBuilder.values());
416     }
417
418     static _resolveBuildersWithPlatforms(configurationType, configurationList, builders, builderNameToIDMap)
419     {
420         const resolvedConfigurations = [];
421         let configurationIndex = 0;
422         for (const entry of configurationList) {
423             configurationIndex++;
424             assert(Array.isArray(entry['builders']), `The ${configurationType} configuration ${configurationIndex} does not specify "builders" as an array`);
425             assert(Array.isArray(entry['platforms']), `The ${configurationType} configuration ${configurationIndex} does not specify "platforms" as an array`);
426             for (const builderKey of entry['builders']) {
427                 const matchingBuilder = builders[builderKey];
428                 assert(matchingBuilder, `"${builderKey}" is not a valid builder in the configuration`);
429                 assert('builder' in matchingBuilder, `Builder ${builderKey} does not specify a buildbot builder name`);
430                 assert(matchingBuilder.builder in builderNameToIDMap, `Builder ${matchingBuilder.builder} not found in Buildbot configuration.`);
431                 matchingBuilder['builderID'] = builderNameToIDMap[matchingBuilder.builder];
432                 const builderConfig = this._validateAndMergeConfig({}, matchingBuilder);
433                 for (const platformName of entry['platforms']) {
434                     const platform = Platform.findByName(platformName);
435                     assert(platform, `${platformName} is not a valid platform name`);
436                     resolvedConfigurations.push({types: entry.types, builderConfig, platform});
437                 }
438             }
439         }
440         return resolvedConfigurations;
441     }
442
443     static _parseRepositoryGroup(name, group)
444     {
445         assert.equal(typeof(group.repositories), 'object',
446             `Repository group "${name}" does not specify a dictionary of repositories`);
447         assert(!('description' in group) || typeof(group['description']) == 'string',
448             `Repository group "${name}" have an invalid description`);
449         assert([undefined, true, false].includes(group.acceptsRoots),
450             `Repository group "${name}" contains invalid acceptsRoots value: ${JSON.stringify(group.acceptsRoots)}`);
451
452         const repositoryByName = {};
453         const parsedRepositoryList = [];
454         const patchAcceptingRepositoryList = new Set;
455         for (const repositoryName in group.repositories) {
456             const options = group.repositories[repositoryName];
457             const repository = Repository.findTopLevelByName(repositoryName);
458             assert(repository, `"${repositoryName}" is not a valid repository name`);
459             repositoryByName[repositoryName] = repository;
460             assert.equal(typeof(options), 'object', `"${repositoryName}" specifies a non-dictionary value`);
461             assert([undefined, true, false].includes(options.acceptsPatch),
462                 `"${repositoryName}" contains invalid acceptsPatch value: ${JSON.stringify(options.acceptsPatch)}`);
463             if (options.acceptsPatch)
464                 patchAcceptingRepositoryList.add(repository);
465             repositoryByName[repositoryName] = repository;
466             parsedRepositoryList.push({repository: repository.id(), acceptsPatch: options.acceptsPatch});
467         }
468         assert(parsedRepositoryList.length, `Repository group "${name}" does not specify any repository`);
469
470         assert.equal(typeof(group.testProperties), 'object', `Repository group "${name}" specifies the test configurations with an invalid type`);
471
472         const resolveRepository = (repositoryName) => {
473             const repository = repositoryByName[repositoryName];
474             assert(repository, `Repository group "${name}" an invalid repository "${repositoryName}"`);
475             return repository;
476         }
477
478         const testRepositories = new Set;
479         let specifiesRoots = false;
480         const testPropertiesTemplate = this._parseRepositoryGroupPropertyTemplate('test', name, group.testProperties, (type, value, condition) => {
481             assert(type != 'patch', `Repository group "${name}" specifies a patch for "${value}" in the properties for testing`);
482             switch (type) {
483             case 'revision':
484                 const repository = resolveRepository(value);
485                 testRepositories.add(repository);
486                 return {type, repository};
487             case 'roots':
488                 assert(group.acceptsRoots, `Repository group "${name}" specifies roots in a property but it does not accept roots`);
489                 specifiesRoots = true;
490                 return {type};
491             case 'ifBuilt':
492                 assert('condition', 'condition must set if type is "ifBuilt"');
493                 return {type: 'conditional', condition: 'built', value, repositoryRequirement: condition.map(resolveRepository)};
494             }
495             return null;
496         });
497         assert(!group.acceptsRoots == !specifiesRoots,
498             `Repository group "${name}" accepts roots but does not specify roots in testProperties`);
499         assert.equal(parsedRepositoryList.length, testRepositories.size,
500             `Repository group "${name}" does not use some of the repositories listed in testing`);
501
502         let buildPropertiesTemplate = null;
503         if ('buildProperties' in group) {
504             assert(patchAcceptingRepositoryList.size, `Repository group "${name}" specifies the properties for building but does not accept any patches`);
505             assert(group.acceptsRoots, `Repository group "${name}" specifies the properties for building but does not accept roots in testing`);
506             const revisionRepositories = new Set;
507             const patchRepositories = new Set;
508             buildPropertiesTemplate = this._parseRepositoryGroupPropertyTemplate('build', name, group.buildProperties, (type, value, condition) => {
509                 assert(type != 'roots', `Repository group "${name}" specifies roots in the properties for building`);
510                 let repository = null;
511                 switch (type) {
512                 case 'patch':
513                     repository = resolveRepository(value);
514                     assert(patchAcceptingRepositoryList.has(repository), `Repository group "${name}" specifies a patch for "${value}" but it does not accept a patch`);
515                     patchRepositories.add(repository);
516                     return {type, repository};
517                 case 'revision':
518                     repository = resolveRepository(value);
519                     revisionRepositories.add(repository);
520                     return {type, repository};
521                 case 'ownedRevisions':
522                     return {type, ownerRepository: resolveRepository(value)};
523                 case 'ifRepositorySet':
524                     assert(condition, 'condition must set if type is "ifRepositorySet"');
525                     return {type: 'conditional', condition: 'requiresBuild', value, repositoriesToCheck: condition.map(resolveRepository)};
526                 }
527                 return null;
528             });
529             for (const repository of patchRepositories)
530                 assert(revisionRepositories.has(repository), `Repository group "${name}" specifies a patch for "${repository.name()}" but does not specify a revision`);
531             assert.equal(patchAcceptingRepositoryList.size, patchRepositories.size,
532                 `Repository group "${name}" does not use some of the repositories listed in building a patch`);
533         }
534
535         return {
536             name: group.name,
537             description: group.description,
538             acceptsRoots: group.acceptsRoots,
539             testPropertiesTemplate: testPropertiesTemplate,
540             buildPropertiesTemplate: buildPropertiesTemplate,
541             repositoryList: parsedRepositoryList,
542         };
543     }
544
545     static _parseRepositoryGroupPropertyTemplate(parsingMode, groupName, properties, makeOption)
546     {
547         const propertiesTemplate = {};
548         for (const propertyName in properties) {
549             let value = properties[propertyName];
550             const isDictionary = typeof(value) == 'object';
551             assert(isDictionary || typeof(value) == 'string',
552                 `Repository group "${groupName}" uses an invalid value "${value}" in property "${propertyName}"`);
553
554             if (!isDictionary) {
555                 propertiesTemplate[propertyName] = value;
556                 continue;
557             }
558
559             const keys = Object.keys(value);
560             assert(keys.length == 1 || keys.length == 2,
561                 `Repository group "${groupName}" specifies more than two types in property "${propertyName}": "${keys.join('", "')}"`);
562             let type;
563             let condition = null;
564             let optionValue;
565             if (keys.length == 2) {
566                 assert(keys.includes('value'), `Repository group "${groupName}" with two types in property "${propertyName}": "${keys.join('", "')}" should contains 'value' as one type`);
567                 type = keys.find((key) => key != 'value');
568                 optionValue = value.value;
569                 condition = value[type];
570             }
571             else {
572                 type = keys[0];
573                 optionValue = value[type];
574             }
575             const option = makeOption(type, optionValue, condition);
576             assert(option, `Repository group "${groupName}" specifies an invalid type "${type}" in property "${propertyName}"`);
577             propertiesTemplate[propertyName] = option;
578         }
579         return propertiesTemplate;
580     }
581
582     static _validateAndMergeConfig(config, valuesToMerge, excludedProperty)
583     {
584         for (const name in valuesToMerge) {
585             const value = valuesToMerge[name];
586             if (name == excludedProperty)
587                 continue;
588
589             switch (name) {
590             case 'properties': // Fallthrough
591                 assert.equal(typeof(value), 'object', 'Build properties should be a dictionary');
592                 if (!config['properties'])
593                     config['properties'] = {};
594                 const properties = config['properties'];
595                 for (const name in value) {
596                     assert.equal(typeof(value[name]), 'string', `Build properties "${name}" specifies a non-string value of type "${typeof(value)}"`);
597                     properties[name] = value[name];
598                 }
599                 break;
600             case 'test': // Fallthrough
601             case 'slaveList': // Fallthrough
602                 assert(value instanceof Array, `${name} should be an array`);
603                 assert(value.every(function (part) { return typeof part == 'string'; }), `${name} should be an array of strings`);
604                 config[name] = value.slice();
605                 break;
606             case 'builder': // Fallthrough
607                 assert.equal(typeof(value), 'string', `${name} should be of string type`);
608                 config[name] = value;
609                 break;
610             case 'builderID':
611                 assert(value, 'builderID should not be undefined.');
612                 config[name] = value;
613                 break;
614             default:
615                 assert(false, `Unrecognized parameter "${name}"`);
616             }
617         }
618         return config;
619     }
620
621 }
622
623 if (typeof module != 'undefined') {
624     module.exports.BuildbotSyncer = BuildbotSyncer;
625     module.exports.BuildbotBuildEntry = BuildbotBuildEntry;
626     module.exports.BuildbotBuildEntryDeprecated = BuildbotBuildEntryDeprecated;
627 }