d648c86c934ea1df0ec74915409904e24765783b
[WebKit-https.git] / Websites / perf.webkit.org / unit-tests / buildbot-syncer-tests.js
1 'use strict';
2
3 let assert = require('assert');
4
5 require('../tools/js/v3-models.js');
6 let MockRemoteAPI = require('./resources/mock-remote-api.js').MockRemoteAPI;
7 let MockModels = require('./resources/mock-v3-models.js').MockModels;
8
9 let BuildbotBuildEntry = require('../tools/js/buildbot-syncer.js').BuildbotBuildEntry;
10 let BuildbotBuildEntryDeprecated = require('../tools/js/buildbot-syncer.js').BuildbotBuildEntryDeprecated;
11 let BuildbotSyncer = require('../tools/js/buildbot-syncer.js').BuildbotSyncer;
12
13 function sampleiOSConfig()
14 {
15     return {
16         'slaveArgument': 'slavename',
17         'buildRequestArgument': 'build_request_id',
18         'repositoryGroups': {
19             'ios-svn-webkit': {
20                 'repositories': {'WebKit': {}, 'iOS': {}},
21                 'testProperties': {
22                     'desired_image': {'revision': 'iOS'},
23                     'opensource': {'revision': 'WebKit'},
24                 }
25             }
26         },
27         'types': {
28             'speedometer': {
29                 'test': ['Speedometer'],
30                 'properties': {'test_name': 'speedometer'}
31             },
32             'jetstream': {
33                 'test': ['JetStream'],
34                 'properties': {'test_name': 'jetstream'}
35             },
36             'dromaeo-dom': {
37                 'test': ['Dromaeo', 'DOM Core Tests'],
38                 'properties': {'tests': 'dromaeo-dom'}
39             },
40         },
41         'builders': {
42             'iPhone-bench': {
43                 'builder': 'ABTest-iPhone-RunBenchmark-Tests',
44                 'properties': {'forcescheduler': 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler'},
45                 'slaveList': ['ABTest-iPhone-0'],
46             },
47             'iPad-bench': {
48                 'builder': 'ABTest-iPad-RunBenchmark-Tests',
49                 'properties': {'forcescheduler': 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler'},
50                 'slaveList': ['ABTest-iPad-0', 'ABTest-iPad-1'],
51             },
52             'iOS-builder': {
53                 'builder': 'ABTest-iOS-Builder',
54                 'properties': {'forcescheduler': 'ABTest-Builder-ForceScheduler'},
55             },
56         },
57         'buildConfigurations': [
58             {'builders': ['iOS-builder'], 'platforms': ['iPhone', 'iPad']},
59         ],
60         'testConfigurations': [
61             {'builders': ['iPhone-bench'], 'types': ['speedometer', 'jetstream', 'dromaeo-dom'], 'platforms': ['iPhone']},
62             {'builders': ['iPad-bench'], 'types': ['speedometer', 'jetstream'], 'platforms': ['iPad']},
63         ]
64     };
65 }
66
67 function sampleiOSConfigWithExpansions()
68 {
69     return {
70         "triggerableName": "build-webkit-ios",
71         "buildRequestArgument": "build-request-id",
72         "repositoryGroups": { },
73         "types": {
74             "iphone-plt": {
75                 "test": ["PLT-iPhone"],
76                 "properties": {"test_name": "plt"}
77             },
78             "ipad-plt": {
79                 "test": ["PLT-iPad"],
80                 "properties": {"test_name": "plt"}
81             },
82             "speedometer": {
83                 "test": ["Speedometer"],
84                 "properties": {"tests": "speedometer"}
85             },
86         },
87         "builders": {
88             "iphone": {
89                 "builder": "iPhone AB Tests",
90                 "properties": {"forcescheduler": "force-iphone-ab-tests"},
91             },
92             "iphone-2": {
93                 "builder": "iPhone 2 AB Tests",
94                 "properties": {"forcescheduler": "force-iphone-2-ab-tests"},
95             },
96             "ipad": {
97                 "builder": "iPad AB Tests",
98                 "properties": {"forcescheduler": "force-ipad-ab-tests"},
99             },
100         },
101         "testConfigurations": [
102             {
103                 "builders": ["iphone", "iphone-2"],
104                 "platforms": ["iPhone", "iOS 10 iPhone"],
105                 "types": ["iphone-plt", "speedometer"],
106             },
107             {
108                 "builders": ["ipad"],
109                 "platforms": ["iPad"],
110                 "types": ["ipad-plt", "speedometer"],
111             },
112         ]
113     }
114 }
115
116 function smallConfiguration()
117 {
118     return {
119         'buildRequestArgument': 'id',
120         'repositoryGroups': {
121             'ios-svn-webkit': {
122                 'repositories': {'iOS': {}, 'WebKit': {}},
123                 'testProperties': {
124                     'os': {'revision': 'iOS'},
125                     'wk': {'revision': 'WebKit'}
126                 }
127             }
128         },
129         'types': {
130             'some-test': {
131                 'test': ['Some test'],
132             }
133         },
134         'builders': {
135             'some-builder': {
136                 'builder': 'some builder',
137             }
138         },
139         'testConfigurations': [{
140             'builders': ['some-builder'],
141             'platforms': ['Some platform'],
142             'types': ['some-test'],
143         }]
144     };
145 }
146
147 function builderNameToIDMap()
148 {
149     return {
150         'some builder' : '100',
151         'ABTest-iPhone-RunBenchmark-Tests': '101',
152         'ABTest-iPad-RunBenchmark-Tests': '102',
153         'ABTest-iOS-Builder': '103',
154         'iPhone AB Tests' : '104',
155         'iPhone 2 AB Tests': '105',
156         'iPad AB Tests': '106'
157     }    
158 }
159
160 function smallPendingBuild()
161 {
162     return {
163         'builderName': 'some builder',
164         'builds': [],
165         'properties': [],
166         'source': {
167             'branch': '',
168             'changes': [],
169             'codebase': 'WebKit',
170             'hasPatch': false,
171             'project': '',
172             'repository': '',
173             'revision': ''
174         },
175     };
176 }
177
178 function smallInProgressBuild()
179 {
180     return {
181         'builderName': 'some builder',
182         'builds': [],
183         'properties': [],
184         'currentStep': { },
185         'eta': 123,
186         'number': 456,
187         'source': {
188             'branch': '',
189             'changes': [],
190             'codebase': 'WebKit',
191             'hasPatch': false,
192             'project': '',
193             'repository': '',
194             'revision': ''
195         },
196     };
197 }
198
199 function smallFinishedBuild()
200 {
201     return {
202         'builderName': 'some builder',
203         'builds': [],
204         'properties': [],
205         'currentStep': null,
206         'eta': null,
207         'number': 789,
208         'source': {
209             'branch': '',
210             'changes': [],
211             'codebase': 'WebKit',
212             'hasPatch': false,
213             'project': '',
214             'repository': '',
215             'revision': ''
216         },
217         'times': [0, 1],
218     };
219 }
220
221 function createSampleBuildRequest(platform, test)
222 {
223     assert(platform instanceof Platform);
224     assert(test instanceof Test);
225
226     const webkit197463 = CommitLog.ensureSingleton('111127', {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'});
227     const shared111237 = CommitLog.ensureSingleton('111237', {'id': '111237', 'time': 1456931874000, 'repository': MockModels.sharedRepository, 'revision': '80229'});
228     const ios13A452 = CommitLog.ensureSingleton('88930', {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'});
229
230     const commitSet = CommitSet.ensureSingleton('4197', {customRoots: [], revisionItems: [{commit: webkit197463}, {commit: shared111237}, {commit: ios13A452}]});
231
232     return BuildRequest.ensureSingleton('16733-' + platform.id(), {'triggerable': MockModels.triggerable,
233         repositoryGroup: MockModels.svnRepositoryGroup,
234         'commitSet': commitSet, 'status': 'pending', 'platform': platform, 'test': test});
235 }
236
237 function createSampleBuildRequestWithPatch(platform, test, order)
238 {
239     assert(platform instanceof Platform);
240     assert(!test || test instanceof Test);
241
242     const webkit197463 = CommitLog.ensureSingleton('111127', {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'});
243     const shared111237 = CommitLog.ensureSingleton('111237', {'id': '111237', 'time': 1456931874000, 'repository': MockModels.sharedRepository, 'revision': '80229'});
244     const ios13A452 = CommitLog.ensureSingleton('88930', {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'});
245
246     const patch = new UploadedFile(453, {'createdAt': new Date('2017-05-01T19:16:53Z'), 'filename': 'patch.dat', 'extension': '.dat', 'author': 'some user',
247         size: 534637, sha256: '169463c8125e07c577110fe144ecd63942eb9472d438fc0014f474245e5df8a1'});
248
249     const root = new UploadedFile(456, {'createdAt': new Date('2017-05-01T21:03:27Z'), 'filename': 'root.dat', 'extension': '.dat', 'author': 'some user',
250         size: 16452234, sha256: '03eed7a8494ab8794c44b7d4308e55448fc56f4d6c175809ba968f78f656d58d'});
251
252     const commitSet = CommitSet.ensureSingleton('53246456', {customRoots: [root], revisionItems: [{commit: webkit197463, patch, requiresBuild: true}, {commit: shared111237}, {commit: ios13A452}]});
253
254     return BuildRequest.ensureSingleton(`6345645376-${order}`, {'triggerable': MockModels.triggerable,
255         repositoryGroup: MockModels.svnRepositoryGroup,
256         'commitSet': commitSet, 'status': 'pending', 'platform': platform, 'test': test, 'order': order});
257 }
258
259 function createSampleBuildRequestWithOwnedCommit(platform, test, order)
260 {
261     assert(platform instanceof Platform);
262     assert(!test || test instanceof Test);
263
264     const webkit197463 = CommitLog.ensureSingleton('111127', {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'});
265     const owner111289 = CommitLog.ensureSingleton('111289', {'id': '111289', 'time': 1456931874000, 'repository': MockModels.ownerRepository, 'revision': 'owner-001'});
266     const owned111222 = CommitLog.ensureSingleton('111222', {'id': '111222', 'time': 1456932774000, 'repository': MockModels.ownedRepository, 'revision': 'owned-002'});
267     const ios13A452 = CommitLog.ensureSingleton('88930', {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'});
268
269     const root = new UploadedFile(456, {'createdAt': new Date('2017-05-01T21:03:27Z'), 'filename': 'root.dat', 'extension': '.dat', 'author': 'some user',
270         size: 16452234, sha256: '03eed7a8494ab8794c44b7d4308e55448fc56f4d6c175809ba968f78f656d58d'});
271
272     const commitSet = CommitSet.ensureSingleton('53246486', {customRoots: [root], revisionItems: [{commit: webkit197463}, {commit: owner111289}, {commit: owned111222, commitOwner: owner111289, requiresBuild: true}, {commit: ios13A452}]});
273
274     return BuildRequest.ensureSingleton(`6345645370-${order}`, {'triggerable': MockModels.triggerable,
275         repositoryGroup: MockModels.svnRepositoryWithOwnedRepositoryGroup,
276         'commitSet': commitSet, 'status': 'pending', 'platform': platform, 'test': test, 'order': order});
277 }
278
279 function createSampleBuildRequestWithOwnedCommitAndPatch(platform, test, order)
280 {
281     assert(platform instanceof Platform);
282     assert(!test || test instanceof Test);
283
284     const webkit197463 = CommitLog.ensureSingleton('111127', {'id': '111127', 'time': 1456955807334, 'repository': MockModels.webkit, 'revision': '197463'});
285     const owner111289 = CommitLog.ensureSingleton('111289', {'id': '111289', 'time': 1456931874000, 'repository': MockModels.ownerRepository, 'revision': 'owner-001'});
286     const owned111222 = CommitLog.ensureSingleton('111222', {'id': '111222', 'time': 1456932774000, 'repository': MockModels.ownedRepository, 'revision': 'owned-002'});
287     const ios13A452 = CommitLog.ensureSingleton('88930', {'id': '88930', 'time': 0, 'repository': MockModels.ios, 'revision': '13A452'});
288
289     const patch = new UploadedFile(453, {'createdAt': new Date('2017-05-01T19:16:53Z'), 'filename': 'patch.dat', 'extension': '.dat', 'author': 'some user',
290         size: 534637, sha256: '169463c8125e07c577110fe144ecd63942eb9472d438fc0014f474245e5df8a1'});
291
292     const commitSet = CommitSet.ensureSingleton('53246486', {customRoots: [], revisionItems: [{commit: webkit197463, patch, requiresBuild: true}, {commit: owner111289}, {commit: owned111222, commitOwner: owner111289, requiresBuild: true}, {commit: ios13A452}]});
293
294     return BuildRequest.ensureSingleton(`6345645370-${order}`, {'triggerable': MockModels.triggerable,
295         repositoryGroup: MockModels.svnRepositoryWithOwnedRepositoryGroup,
296         'commitSet': commitSet, 'status': 'pending', 'platform': platform, 'test': test, 'order': order});
297 }
298
299 function samplePendingBuildData(buildRequestId, buildTime, builderId)
300 {
301     return {
302         "builderid": builderId || 102,
303         "buildrequestid": buildRequestId || 16733,
304         "buildsetid": 894720,
305         "claimed": false,
306         "claimed_at": null,
307         "claimed_by_masterid": null,
308         "complete": false,
309         "complete_at": null,
310         "priority": 0,
311         "results": -1,
312         "submitted_at": buildTime || 1458704983,
313         "waited_for": false
314     };
315 }
316
317 function samplePendingBuild(buildRequestId, buildTime, builderName)
318 {
319     return {
320         "buildrequests" : [samplePendingBuildData(buildRequestId, buildTime, builderNameToIDMap()[builderName])]
321     };
322 }
323
324 function sampleBuildData(workerName, isComplete, buildRequestId, buildNumber, builderId)
325 {
326     return {
327         "builderid": builderId || 102,
328         "number": buildNumber || 614,
329         "buildrequestid": buildRequestId || 16733,
330         "complete": isComplete,
331         "complete_at": null,
332         "buildid": 418744,
333         "masterid": 1,
334         "results": null,
335         "started_at": 1513725109,
336         "state_string": "building",
337         "workerid": 41,
338         "properties": {
339             "platform": ["mac", "Unknown"],
340             "scheduler": ["ABTest-iPad-RunBenchmark-Tests-ForceScheduler", "Scheduler"],
341             "slavename": [workerName || "ABTest-iPad-0", "Worker (deprecated)"],
342             "workername": [workerName || "ABTest-iPad-0", "Worker"
343             ]
344         }
345     };
346 }
347
348 function sampleInProgressBuildData(workerName)
349 {
350     return sampleBuildData(workerName, false);
351 }
352
353 function sampleInProgressBuild(workerName)
354 {
355     return {
356         "builds": [sampleInProgressBuildData(workerName)]
357     };
358 }
359
360 function sampleFinishedBuildData(buildRequestId, workerName, builderName)
361 {
362     return sampleBuildData(workerName, true, buildRequestId || 18935, 1755, builderNameToIDMap()[builderName]);
363 }
364
365 function sampleFinishedBuild(buildRequestId, workerName, builderName)
366 {
367     return {
368         "builds": [sampleFinishedBuildData(buildRequestId, workerName, builderName)]
369     };
370 }
371
372 function samplePendingBuildDeprecated(buildRequestId, buildTime, slaveName)
373 {
374     return {
375         'builderName': 'ABTest-iPad-RunBenchmark-Tests',
376         'builds': [],
377         'properties': [
378             ['build_request_id', buildRequestId || '16733', 'Force Build Form'],
379             ['desired_image', '13A452', 'Force Build Form'],
380             ['owner', '<unknown>', 'Force Build Form'],
381             ['test_name', 'speedometer', 'Force Build Form'],
382             ['reason', 'force build','Force Build Form'],
383             ['slavename', slaveName, ''],
384             ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler']
385         ],
386         'source': {
387             'branch': '',
388             'changes': [],
389             'codebase': 'compiler-rt',
390             'hasPatch': false,
391             'project': '',
392             'repository': '',
393             'revision': ''
394         },
395         'submittedAt': buildTime || 1458704983
396     };
397 }
398
399 function sampleInProgressBuildDeprecated(slaveName)
400 {
401     return {
402         'blame': [],
403         'builderName': 'ABTest-iPad-RunBenchmark-Tests',
404         'currentStep': {
405             'eta': 0.26548067698460565,
406             'expectations': [['output', 845, 1315.0]],
407             'hidden': false,
408             'isFinished': false,
409             'isStarted': true,
410             'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
411             'name': 'Some step',
412             'results': [null,[]],
413             'statistics': {},
414             'step_number': 1,
415             'text': [''],
416             'times': [1458718657.581628, null],
417             'urls': {}
418         },
419         'eta': 6497.991612434387,
420         'logs': [['stdio','https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
421         'number': 614,
422         'properties': [
423             ['build_request_id', '16733', 'Force Build Form'],
424             ['buildername', 'ABTest-iPad-RunBenchmark-Tests', 'Builder'],
425             ['buildnumber', 614, 'Build'],
426             ['desired_image', '13A452', 'Force Build Form'],
427             ['owner', '<unknown>', 'Force Build Form'],
428             ['reason', 'force build', 'Force Build Form'],
429             ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler'],
430             ['slavename', slaveName || 'ABTest-iPad-0', 'BuildSlave'],
431         ],
432         'reason': 'A build was forced by \'<unknown>\': force build',
433         'results': null,
434         'slave': 'ABTest-iPad-0',
435         'sourceStamps': [{'branch': '', 'changes': [], 'codebase': 'compiler-rt', 'hasPatch': false, 'project': '', 'repository': '', 'revision': ''}],
436         'steps': [
437             {
438                 'eta': null,
439                 'expectations': [['output',2309,2309.0]],
440                 'hidden': false,
441                 'isFinished': true,
442                 'isStarted': true,
443                 'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
444                 'name': 'Finished step',
445                 'results': [0, []],
446                 'statistics': {},
447                 'step_number': 0,
448                 'text': [''],
449                 'times': [1458718655.419865, 1458718655.453633],
450                 'urls': {}
451             },
452             {
453                 'eta': 0.26548067698460565,
454                 'expectations': [['output', 845, 1315.0]],
455                 'hidden': false,
456                 'isFinished': false,
457                 'isStarted': true,
458                 'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
459                 'name': 'Some step',
460                 'results': [null,[]],
461                 'statistics': {},
462                 'step_number': 1,
463                 'text': [''],
464                 'times': [1458718657.581628, null],
465                 'urls': {}
466             },
467             {
468                 'eta': null,
469                 'expectations': [['output', null, null]],
470                 'hidden': false,
471                 'isFinished': false,
472                 'isStarted': false,
473                 'logs': [],
474                 'name': 'Some other step',
475                 'results': [null, []],
476                 'statistics': {},
477                 'step_number': 2,
478                 'text': [],
479                 'times': [null, null],
480                 'urls': {}
481             },
482         ],
483         'text': [],
484         'times': [1458718655.415821, null]
485     };
486 }
487
488 function sampleFinishedBuildDeprecated(buildRequestId, slaveName)
489 {
490     return {
491         'blame': [],
492         'builderName': 'ABTest-iPad-RunBenchmark-Tests',
493         'currentStep': null,
494         'eta': null,
495         'logs': [['stdio','https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755/steps/shell/logs/stdio']],
496         'number': 1755,
497         'properties': [
498             ['build_request_id', buildRequestId || '18935', 'Force Build Form'],
499             ['buildername', 'ABTest-iPad-RunBenchmark-Tests', 'Builder'],
500             ['buildnumber', 1755, 'Build'],
501             ['desired_image', '13A452', 'Force Build Form'],
502             ['owner', '<unknown>', 'Force Build Form'],
503             ['reason', 'force build', 'Force Build Form'],
504             ['scheduler', 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler', 'Scheduler'],
505             ['slavename', slaveName || 'ABTest-iPad-0', 'BuildSlave'],
506         ],
507         'reason': 'A build was forced by \'<unknown>\': force build',
508         'results': 2,
509         'slave': 'ABTest-iPad-0',
510         'sourceStamps': [{'branch': '', 'changes': [], 'codebase': 'compiler-rt', 'hasPatch': false, 'project': '', 'repository': '', 'revision': ''}],
511         'steps': [
512             {
513                 'eta': null,
514                 'expectations': [['output',2309,2309.0]],
515                 'hidden': false,
516                 'isFinished': true,
517                 'isStarted': true,
518                 'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/shell/logs/stdio']],
519                 'name': 'Finished step',
520                 'results': [0, []],
521                 'statistics': {},
522                 'step_number': 0,
523                 'text': [''],
524                 'times': [1458718655.419865, 1458718655.453633],
525                 'urls': {}
526             },
527             {
528                 'eta': null,
529                 'expectations': [['output', 845, 1315.0]],
530                 'hidden': false,
531                 'isFinished': true,
532                 'isStarted': true,
533                 'logs': [['stdio', 'https://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614/steps/Some%20step/logs/stdio']],
534                 'name': 'Some step',
535                 'results': [null,[]],
536                 'statistics': {},
537                 'step_number': 1,
538                 'text': [''],
539                 'times': [1458718657.581628, null],
540                 'urls': {}
541             },
542             {
543                 'eta': null,
544                 'expectations': [['output', null, null]],
545                 'hidden': false,
546                 'isFinished': true,
547                 'isStarted': true,
548                 'logs': [],
549                 'name': 'Some other step',
550                 'results': [null, []],
551                 'statistics': {},
552                 'step_number': 2,
553                 'text': [],
554                 'times': [null, null],
555                 'urls': {}
556             },
557         ],
558         'text': [],
559         'times': [1458937478.25837, 1458946147.173785]
560     };
561 }
562
563 describe('BuildbotSyncer', () => {
564     MockModels.inject();
565     let requests = MockRemoteAPI.inject('http://build.webkit.org');
566
567     describe('_loadConfig', () => {
568
569         it('should create BuildbotSyncer objects for a configuration that specify all required options', () => {
570             assert.equal(BuildbotSyncer._loadConfig(MockRemoteAPI, smallConfiguration(), builderNameToIDMap()).length, 1);
571         });
572
573         it('should throw when some required options are missing', () => {
574             assert.throws(() => {
575                 const config = smallConfiguration();
576                 delete config.builders;
577                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
578             }, /"some-builder" is not a valid builder in the configuration/);
579             assert.throws(() => {
580                 const config = smallConfiguration();
581                 delete config.types;
582                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
583             }, /"some-test" is not a valid type in the configuration/);
584             assert.throws(() => {
585                 const config = smallConfiguration();
586                 delete config.testConfigurations[0].builders;
587                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
588             }, /The test configuration 1 does not specify "builders" as an array/);
589             assert.throws(() => {
590                 const config = smallConfiguration();
591                 delete config.testConfigurations[0].platforms;
592                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
593             }, /The test configuration 1 does not specify "platforms" as an array/);
594             assert.throws(() => {
595                 const config = smallConfiguration();
596                 delete config.testConfigurations[0].types;
597                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
598             }, /The test configuration 0 does not specify "types" as an array/);
599             assert.throws(() => {
600                 const config = smallConfiguration();
601                 delete config.buildRequestArgument;
602                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
603             }, /buildRequestArgument must specify the name of the property used to store the build request ID/);
604         });
605
606         it('should throw when a test name is not an array of strings', () => {
607             assert.throws(() => {
608                 const config = smallConfiguration();
609                 config.testConfigurations[0].types = 'some test';
610                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
611             }, /The test configuration 0 does not specify "types" as an array/);
612             assert.throws(() => {
613                 const config = smallConfiguration();
614                 config.testConfigurations[0].types = [1];
615                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
616             }, /"1" is not a valid type in the configuration/);
617         });
618
619         it('should throw when properties is not an object', () => {
620             assert.throws(() => {
621                 const config = smallConfiguration();
622                 config.builders[Object.keys(config.builders)[0]].properties = 'hello';
623                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
624             }, /Build properties should be a dictionary/);
625             assert.throws(() => {
626                 const config = smallConfiguration();
627                 config.types[Object.keys(config.types)[0]].properties = 'hello';
628                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
629             }, /Build properties should be a dictionary/);
630         });
631
632         it('should throw when testProperties is specifed in a type or a builder', () => {
633             assert.throws(() => {
634                 const config = smallConfiguration();
635                 const firstType = Object.keys(config.types)[0];
636                 config.types[firstType].testProperties = {};
637                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
638             }, /Unrecognized parameter "testProperties"/);
639             assert.throws(() => {
640                 const config = smallConfiguration();
641                 const firstBuilder = Object.keys(config.builders)[0];
642                 config.builders[firstBuilder].testProperties = {};
643                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
644             }, /Unrecognized parameter "testProperties"/);
645         });
646
647         it('should throw when buildProperties is specifed in a type or a builder', () => {
648             assert.throws(() => {
649                 const config = smallConfiguration();
650                 const firstType = Object.keys(config.types)[0];
651                 config.types[firstType].buildProperties = {};
652                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
653             }, /Unrecognized parameter "buildProperties"/);
654             assert.throws(() => {
655                 const config = smallConfiguration();
656                 const firstBuilder = Object.keys(config.builders)[0];
657                 config.builders[firstBuilder].buildProperties = {};
658                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
659             }, /Unrecognized parameter "buildProperties"/);
660         });
661
662         it('should throw when properties for a type is malformed', () => {
663             const firstType = Object.keys(smallConfiguration().types)[0];
664             assert.throws(() => {
665                 const config = smallConfiguration();
666                 config.types[firstType].properties = 'hello';
667                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
668             }, /Build properties should be a dictionary/);
669             assert.throws(() => {
670                 const config = smallConfiguration();
671                 config.types[firstType].properties = {'some': {'otherKey': 'some root'}};
672                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
673             }, /Build properties "some" specifies a non-string value of type "object"/);
674             assert.throws(() => {
675                 const config = smallConfiguration();
676                 config.types[firstType].properties = {'some': {'otherKey': 'some root'}};
677                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
678             }, /Build properties "some" specifies a non-string value of type "object"/);
679             assert.throws(() => {
680                 const config = smallConfiguration();
681                 config.types[firstType].properties = {'some': {'revision': 'WebKit'}};
682                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
683             }, /Build properties "some" specifies a non-string value of type "object"/);
684             assert.throws(() => {
685                 const config = smallConfiguration();
686                 config.types[firstType].properties = {'some': 1};
687                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
688             }, / Build properties "some" specifies a non-string value of type "object"/);
689         });
690
691         it('should throw when properties for a builder is malformed', () => {
692             const firstBuilder = Object.keys(smallConfiguration().builders)[0];
693             assert.throws(() => {
694                 const config = smallConfiguration();
695                 config.builders[firstBuilder].properties = 'hello';
696                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
697             }, /Build properties should be a dictionary/);
698             assert.throws(() => {
699                 const config = smallConfiguration();
700                 config.builders[firstBuilder].properties = {'some': {'otherKey': 'some root'}};
701                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
702             }, /Build properties "some" specifies a non-string value of type "object"/);
703             assert.throws(() => {
704                 const config = smallConfiguration();
705                 config.builders[firstBuilder].properties = {'some': {'otherKey': 'some root'}};
706                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
707             }, /Build properties "some" specifies a non-string value of type "object"/);
708             assert.throws(() => {
709                 const config = smallConfiguration();
710                 config.builders[firstBuilder].properties = {'some': {'revision': 'WebKit'}};
711                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
712             }, /Build properties "some" specifies a non-string value of type "object"/);
713             assert.throws(() => {
714                 const config = smallConfiguration();
715                 config.builders[firstBuilder].properties = {'some': 1};
716                 BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
717             }, /Build properties "some" specifies a non-string value of type "object"/);
718         });
719
720         it('should create BuildbotSyncer objects for valid configurations', () => {
721             let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
722             assert.equal(syncers.length, 3);
723             assert.ok(syncers[0] instanceof BuildbotSyncer);
724             assert.ok(syncers[1] instanceof BuildbotSyncer);
725             assert.ok(syncers[2] instanceof BuildbotSyncer);
726         });
727
728         it('should parse builder names correctly', () => {
729             let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
730             assert.equal(syncers[0].builderName(), 'ABTest-iPhone-RunBenchmark-Tests');
731             assert.equal(syncers[1].builderName(), 'ABTest-iPad-RunBenchmark-Tests');
732             assert.equal(syncers[2].builderName(), 'ABTest-iOS-Builder');
733         });
734
735         it('should parse test configurations with build configurations correctly', () => {
736             let syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
737
738             let configurations = syncers[0].testConfigurations();
739             assert(syncers[0].isTester());
740             assert.equal(configurations.length, 3);
741             assert.equal(configurations[0].platform, MockModels.iphone);
742             assert.equal(configurations[0].test, MockModels.speedometer);
743             assert.equal(configurations[1].platform, MockModels.iphone);
744             assert.equal(configurations[1].test, MockModels.jetstream);
745             assert.equal(configurations[2].platform, MockModels.iphone);
746             assert.equal(configurations[2].test, MockModels.domcore);
747             assert.deepEqual(syncers[0].buildConfigurations(), []);
748
749             configurations = syncers[1].testConfigurations();
750             assert(syncers[1].isTester());
751             assert.equal(configurations.length, 2);
752             assert.equal(configurations[0].platform, MockModels.ipad);
753             assert.equal(configurations[0].test, MockModels.speedometer);
754             assert.equal(configurations[1].platform, MockModels.ipad);
755             assert.equal(configurations[1].test, MockModels.jetstream);
756             assert.deepEqual(syncers[1].buildConfigurations(), []);
757
758             assert(!syncers[2].isTester());
759             assert.deepEqual(syncers[2].testConfigurations(), []);
760             configurations = syncers[2].buildConfigurations();
761             assert.equal(configurations.length, 2);
762             assert.equal(configurations[0].platform, MockModels.iphone);
763             assert.equal(configurations[0].test, null);
764             assert.equal(configurations[1].platform, MockModels.ipad);
765             assert.equal(configurations[1].test, null);
766         });
767
768         it('should throw when a build configuration use the same builder as a test configuration', () => {
769             assert.throws(() => {
770                 const config = sampleiOSConfig();
771                 config.buildConfigurations[0].builders = config.testConfigurations[0].builders;
772                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
773             });
774         });
775
776         it('should parse test configurations with types and platforms expansions correctly', () => {
777             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfigWithExpansions(), builderNameToIDMap());
778
779             assert.equal(syncers.length, 3);
780
781             let configurations = syncers[0].testConfigurations();
782             assert.equal(configurations.length, 4);
783             assert.equal(configurations[0].platform, MockModels.iphone);
784             assert.equal(configurations[0].test, MockModels.iPhonePLT);
785             assert.equal(configurations[1].platform, MockModels.iphone);
786             assert.equal(configurations[1].test, MockModels.speedometer);
787             assert.equal(configurations[2].platform, MockModels.iOS10iPhone);
788             assert.equal(configurations[2].test, MockModels.iPhonePLT);
789             assert.equal(configurations[3].platform, MockModels.iOS10iPhone);
790             assert.equal(configurations[3].test, MockModels.speedometer);
791             assert.deepEqual(syncers[0].buildConfigurations(), []);
792
793             configurations = syncers[1].testConfigurations();
794             assert.equal(configurations.length, 4);
795             assert.equal(configurations[0].platform, MockModels.iphone);
796             assert.equal(configurations[0].test, MockModels.iPhonePLT);
797             assert.equal(configurations[1].platform, MockModels.iphone);
798             assert.equal(configurations[1].test, MockModels.speedometer);
799             assert.equal(configurations[2].platform, MockModels.iOS10iPhone);
800             assert.equal(configurations[2].test, MockModels.iPhonePLT);
801             assert.equal(configurations[3].platform, MockModels.iOS10iPhone);
802             assert.equal(configurations[3].test, MockModels.speedometer);
803             assert.deepEqual(syncers[1].buildConfigurations(), []);
804
805             configurations = syncers[2].testConfigurations();
806             assert.equal(configurations.length, 2);
807             assert.equal(configurations[0].platform, MockModels.ipad);
808             assert.equal(configurations[0].test, MockModels.iPadPLT);
809             assert.equal(configurations[1].platform, MockModels.ipad);
810             assert.equal(configurations[1].test, MockModels.speedometer);
811             assert.deepEqual(syncers[2].buildConfigurations(), []);
812         });
813
814         it('should throw when repositoryGroups is not an object', () => {
815             assert.throws(() => {
816                 const config = smallConfiguration();
817                 config.repositoryGroups = 1;
818                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
819             }, /repositoryGroups must specify a dictionary from the name to its definition/);
820             assert.throws(() => {
821                 const config = smallConfiguration();
822                 config.repositoryGroups = 'hello';
823                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
824             }, /repositoryGroups must specify a dictionary from the name to its definition/);
825         });
826
827         it('should throw when a repository group does not specify a dictionary of repositories', () => {
828             assert.throws(() => {
829                 const config = smallConfiguration();
830                 config.repositoryGroups = {'some-group': {testProperties: {}}};
831                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
832             }, /Repository group "some-group" does not specify a dictionary of repositories/);
833             assert.throws(() => {
834                 const config = smallConfiguration();
835                 config.repositoryGroups = {'some-group': {repositories: 1}, testProperties: {}};
836                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
837             }, /Repository group "some-group" does not specify a dictionary of repositories/);
838         });
839
840         it('should throw when a repository group specifies an empty dictionary', () => {
841             assert.throws(() => {
842                 const config = smallConfiguration();
843                 config.repositoryGroups = {'some-group': {repositories: {}, testProperties: {}}};
844                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
845             }, /Repository group "some-group" does not specify any repository/);
846         });
847
848         it('should throw when a repository group specifies an invalid repository name', () => {
849             assert.throws(() => {
850                 const config = smallConfiguration();
851                 config.repositoryGroups = {'some-group': {repositories: {'InvalidRepositoryName': {}}}};
852                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
853             }, /"InvalidRepositoryName" is not a valid repository name/);
854         });
855
856         it('should throw when a repository group specifies a repository with a non-dictionary value', () => {
857             assert.throws(() => {
858                 const config = smallConfiguration();
859                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': 1}}};
860                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
861             }, /"WebKit" specifies a non-dictionary value/);
862         });
863
864         it('should throw when the description of a repository group is not a string', () => {
865             assert.throws(() => {
866                 const config = smallConfiguration();
867                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, description: 1}};
868                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
869             }, /Repository group "some-group" have an invalid description/);
870             assert.throws(() => {
871                 const config = smallConfiguration();
872                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, description: [1, 2]}};
873                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
874             }, /Repository group "some-group" have an invalid description/);
875         });
876
877         it('should throw when a repository group does not specify a dictionary of properties', () => {
878             assert.throws(() => {
879                 const config = smallConfiguration();
880                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, testProperties: 1}};
881                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
882             }, /Repository group "some-group" specifies the test configurations with an invalid type/);
883             assert.throws(() => {
884                 const config = smallConfiguration();
885                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, testProperties: 'hello'}};
886                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
887             }, /Repository group "some-group" specifies the test configurations with an invalid type/);
888         });
889
890         it('should throw when a repository group refers to a non-existent repository in the properties dictionary', () => {
891             assert.throws(() => {
892                 const config = smallConfiguration();
893                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, testProperties: {'wk': {revision: 'InvalidRepository'}}}};
894                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
895             }, /Repository group "some-group" an invalid repository "InvalidRepository"/);
896         });
897
898         it('should throw when a repository group refers to a repository which is not listed in the list of repositories', () => {
899             assert.throws(() => {
900                 const config = smallConfiguration();
901                 config.repositoryGroups = {'some-group': {repositories: {'WebKit': {}}, testProperties: {'os': {revision: 'iOS'}}}};
902                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
903             }, /Repository group "some-group" an invalid repository "iOS"/);
904             assert.throws(() => {
905                 const config = smallConfiguration();
906                 config.repositoryGroups = {'some-group': {
907                     repositories: {'WebKit': {acceptsPatch: true}},
908                     testProperties: {'wk': {revision: 'WebKit'}, 'install-roots': {'roots': {}}},
909                     buildProperties: {'os': {revision: 'iOS'}},
910                     acceptsRoots: true}};
911                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
912             }, /Repository group "some-group" an invalid repository "iOS"/);
913         });
914
915         it('should throw when a repository group refers to a repository in building a patch which does not accept a patch', () => {
916             assert.throws(() => {
917                 const config = smallConfiguration();
918                 config.repositoryGroups = {'some-group': {
919                     repositories: {'WebKit': {acceptsPatch: true}, 'iOS': {}},
920                     testProperties: {'wk': {revision: 'WebKit'}, 'ios': {revision: 'iOS'}, 'install-roots': {'roots': {}}},
921                     buildProperties: {'wk': {revision: 'WebKit'}, 'ios': {revision: 'iOS'}, 'wk-patch': {patch: 'iOS'}},
922                     acceptsRoots: true}};
923                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
924             }, /Repository group "some-group" specifies a patch for "iOS" but it does not accept a patch/);
925         });
926
927         it('should throw when a repository group specifies a patch without specifying a revision', () => {
928             assert.throws(() => {
929                 const config = smallConfiguration();
930                 config.repositoryGroups = {'some-group': {
931                     repositories: {'WebKit': {acceptsPatch: true}},
932                     testProperties: {'wk': {revision: 'WebKit'}, 'install-roots': {'roots': {}}},
933                     buildProperties: {'wk-patch': {patch: 'WebKit'}},
934                     acceptsRoots: true}};
935                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
936             }, /Repository group "some-group" specifies a patch for "WebKit" but does not specify a revision/);
937         });
938
939         it('should throw when a repository group does not use a listed repository', () => {
940             assert.throws(() => {
941                 const config = smallConfiguration();
942                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, testProperties: {}}};
943                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
944             }, /Repository group "some-group" does not use some of the repositories listed in testing/);
945             assert.throws(() => {
946                 const config = smallConfiguration();
947                 config.repositoryGroups = {'some-group': {
948                     repositories: {'WebKit': {acceptsPatch: true}},
949                     testProperties: {'wk': {revision: 'WebKit'}, 'install-roots': {'roots': {}}},
950                     buildProperties: {},
951                     acceptsRoots: true}};
952                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
953             }, /Repository group "some-group" does not use some of the repositories listed in building a patch/);
954         });
955
956         it('should throw when a repository group specifies non-boolean value to acceptsRoots', () => {
957             assert.throws(() => {
958                 const config = smallConfiguration();
959                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, 'testProperties': {'webkit': {'revision': 'WebKit'}}, acceptsRoots: 1}};
960                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
961             }, /Repository group "some-group" contains invalid acceptsRoots value:/);
962             assert.throws(() => {
963                 const config = smallConfiguration();
964                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {}}, 'testProperties': {'webkit': {'revision': 'WebKit'}}, acceptsRoots: []}};
965                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
966             }, /Repository group "some-group" contains invalid acceptsRoots value:/);
967         });
968
969         it('should throw when a repository group specifies non-boolean value to acceptsPatch', () => {
970             assert.throws(() => {
971                 const config = smallConfiguration();
972                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {acceptsPatch: 1}}, 'testProperties': {'webkit': {'revision': 'WebKit'}}}};
973                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
974             }, /"WebKit" contains invalid acceptsPatch value:/);
975             assert.throws(() => {
976                 const config = smallConfiguration();
977                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {acceptsPatch: []}}, 'testProperties': {'webkit': {'revision': 'WebKit'}}}};
978                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
979             }, /"WebKit" contains invalid acceptsPatch value:/);
980         });
981
982         it('should throw when a repository group specifies a patch in testProperties', () => {
983             assert.throws(() => {
984                 const config = smallConfiguration();
985                 config.repositoryGroups = {'some-group': {'repositories': {'WebKit': {acceptsPatch: true}},
986                     'testProperties': {'webkit': {'revision': 'WebKit'}, 'webkit-patch': {'patch': 'WebKit'}}}};
987                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
988             }, /Repository group "some-group" specifies a patch for "WebKit" in the properties for testing/);
989         });
990
991         it('should throw when a repository group specifies roots in buildProperties', () => {
992             assert.throws(() => {
993                 const config = smallConfiguration();
994                 config.repositoryGroups = {'some-group': {
995                     repositories: {'WebKit': {acceptsPatch: true}},
996                     testProperties: {'webkit': {revision: 'WebKit'}, 'install-roots': {'roots': {}}},
997                     buildProperties: {'webkit': {revision: 'WebKit'}, 'patch': {patch: 'WebKit'}, 'install-roots': {roots: {}}},
998                     acceptsRoots: true}};
999                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
1000             }, /Repository group "some-group" specifies roots in the properties for building/);
1001         });
1002
1003         it('should throw when a repository group that does not accept roots specifies roots in testProperties', () => {
1004             assert.throws(() => {
1005                 const config = smallConfiguration();
1006                 config.repositoryGroups = {'some-group': {
1007                     repositories: {'WebKit': {}},
1008                     testProperties: {'webkit': {'revision': 'WebKit'}, 'install-roots': {'roots': {}}}}};
1009                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
1010             }, /Repository group "some-group" specifies roots in a property but it does not accept roots/);
1011         });
1012
1013         it('should throw when a repository group specifies buildProperties but does not accept roots', () => {
1014             assert.throws(() => {
1015                 const config = smallConfiguration();
1016                 config.repositoryGroups = {'some-group': {
1017                     repositories: {'WebKit': {acceptsPatch: true}},
1018                     testProperties: {'webkit': {revision: 'WebKit'}},
1019                     buildProperties: {'webkit': {revision: 'WebKit'}, 'webkit-patch': {patch: 'WebKit'}}}};
1020                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
1021             }, /Repository group "some-group" specifies the properties for building but does not accept roots in testing/);
1022         });
1023
1024         it('should throw when a repository group specifies buildProperties but does not accept any patch', () => {
1025             assert.throws(() => {
1026                 const config = smallConfiguration();
1027                 config.repositoryGroups = {'some-group': {
1028                     repositories: {'WebKit': {}},
1029                     testProperties: {'webkit': {'revision': 'WebKit'}, 'install-roots': {'roots': {}}},
1030                     buildProperties: {'webkit': {'revision': 'WebKit'}},
1031                     acceptsRoots: true}};
1032                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
1033             }, /Repository group "some-group" specifies the properties for building but does not accept any patches/);
1034         });
1035
1036         it('should throw when a repository group accepts roots but does not specify roots in testProperties', () => {
1037             assert.throws(() => {
1038                 const config = smallConfiguration();
1039                 config.repositoryGroups = {'some-group': {
1040                     repositories: {'WebKit': {acceptsPatch: true}},
1041                     testProperties: {'webkit': {revision: 'WebKit'}},
1042                     buildProperties: {'webkit': {revision: 'WebKit'}, 'webkit-patch': {patch: 'WebKit'}},
1043                     acceptsRoots: true}};
1044                 BuildbotSyncer._loadConfig(MockRemoteAPI, config, builderNameToIDMap());
1045             }, /Repository group "some-group" accepts roots but does not specify roots in testProperties/);
1046         });
1047     });
1048
1049     describe('_propertiesForBuildRequest', () => {
1050         it('should include all properties specified in a given configuration', () => {
1051             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
1052             const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1053             const properties = syncers[0]._propertiesForBuildRequest(request, [request]);
1054             assert.deepEqual(Object.keys(properties).sort(), ['build_request_id', 'desired_image', 'forcescheduler', 'opensource', 'test_name']);
1055         });
1056
1057         it('should preserve non-parametric property values', () => {
1058             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
1059             let request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1060             let properties = syncers[0]._propertiesForBuildRequest(request, [request]);
1061             assert.equal(properties['test_name'], 'speedometer');
1062             assert.equal(properties['forcescheduler'], 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler');
1063
1064             request = createSampleBuildRequest(MockModels.ipad, MockModels.jetstream);
1065             properties = syncers[1]._propertiesForBuildRequest(request, [request]);
1066             assert.equal(properties['test_name'], 'jetstream');
1067             assert.equal(properties['forcescheduler'], 'ABTest-iPad-RunBenchmark-Tests-ForceScheduler');
1068         });
1069
1070         it('should resolve "root"', () => {
1071             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
1072             const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1073             const properties = syncers[0]._propertiesForBuildRequest(request, [request]);
1074             assert.equal(properties['desired_image'], '13A452');
1075         });
1076
1077         it('should resolve "revision"', () => {
1078             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
1079             const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1080             const properties = syncers[0]._propertiesForBuildRequest(request, [request]);
1081             assert.equal(properties['opensource'], '197463');
1082         });
1083
1084         it('should resolve "patch"', () => {
1085             const config = sampleiOSConfig();
1086             config.repositoryGroups['ios-svn-webkit'] = {
1087                 'repositories': {'WebKit': {'acceptsPatch': true}, 'Shared': {}, 'iOS': {}},
1088                 'testProperties': {
1089                     'os': {'revision': 'iOS'},
1090                     'webkit': {'revision': 'WebKit'},
1091                     'shared': {'revision': 'Shared'},
1092                     'roots': {'roots': {}},
1093                 },
1094                 'buildProperties': {
1095                     'webkit': {'revision': 'WebKit'},
1096                     'webkit-patch': {'patch': 'WebKit'},
1097                     'checkbox': {'ifRepositorySet': ['WebKit'], 'value': 'build-webkit'},
1098                     'shared': {'revision': 'Shared'},
1099                 },
1100                 'acceptsRoots': true,
1101             };
1102             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1103             const request = createSampleBuildRequestWithPatch(MockModels.iphone, null, -1);
1104             const properties = syncers[2]._propertiesForBuildRequest(request, [request]);
1105             assert.equal(properties['webkit'], '197463');
1106             assert.equal(properties['webkit-patch'], 'http://build.webkit.org/api/uploaded-file/453.dat');
1107             assert.equal(properties['checkbox'], 'build-webkit');
1108         });
1109
1110         it('should resolve "ifBuilt"', () => {
1111             const config = sampleiOSConfig();
1112             config.repositoryGroups['ios-svn-webkit'] = {
1113                 'repositories': {'WebKit': {}, 'Shared': {}, 'iOS': {}},
1114                 'testProperties': {
1115                     'os': {'revision': 'iOS'},
1116                     'webkit': {'revision': 'WebKit'},
1117                     'shared': {'revision': 'Shared'},
1118                     'roots': {'roots': {}},
1119                     'test-custom-build': {'ifBuilt': [], 'value': ''},
1120                     'has-built-patch': {'ifBuilt': [], 'value': 'true'},
1121                 },
1122                 'acceptsRoots': true,
1123             };
1124             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1125             const requestToBuild = createSampleBuildRequestWithPatch(MockModels.iphone, null, -1);
1126             const requestToTest = createSampleBuildRequestWithPatch(MockModels.iphone, MockModels.speedometer, 0);
1127             const otherRequestToTest = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1128
1129             let properties = syncers[0]._propertiesForBuildRequest(requestToTest, [requestToTest]);
1130             assert.equal(properties['webkit'], '197463');
1131             assert.equal(properties['roots'], '[{"url":"http://build.webkit.org/api/uploaded-file/456.dat"}]');
1132             assert.equal(properties['test-custom-build'], undefined);
1133             assert.equal(properties['has-built-patch'], undefined);
1134
1135             properties = syncers[0]._propertiesForBuildRequest(requestToTest, [requestToBuild, requestToTest]);
1136             assert.equal(properties['webkit'], '197463');
1137             assert.equal(properties['roots'], '[{"url":"http://build.webkit.org/api/uploaded-file/456.dat"}]');
1138             assert.equal(properties['test-custom-build'], '');
1139             assert.equal(properties['has-built-patch'], 'true');
1140
1141             properties = syncers[0]._propertiesForBuildRequest(otherRequestToTest, [requestToBuild, otherRequestToTest, requestToTest]);
1142             assert.equal(properties['webkit'], '197463');
1143             assert.equal(properties['roots'], undefined);
1144             assert.equal(properties['test-custom-build'], undefined);
1145             assert.equal(properties['has-built-patch'], undefined);
1146
1147         });
1148
1149         it('should set the value for "ifBuilt" if the repository in the list appears', () => {
1150             const config = sampleiOSConfig();
1151             config.repositoryGroups['ios-svn-webkit'] = {
1152                 'repositories': {'WebKit': {'acceptsPatch': true}, 'Shared': {}, 'iOS': {}},
1153                 'testProperties': {
1154                     'os': {'revision': 'iOS'},
1155                     'webkit': {'revision': 'WebKit'},
1156                     'shared': {'revision': 'Shared'},
1157                     'roots': {'roots': {}},
1158                     'checkbox': {'ifBuilt': ['WebKit'], 'value': 'test-webkit'}
1159                 },
1160                 'buildProperties': {
1161                     'webkit': {'revision': 'WebKit'},
1162                     'webkit-patch': {'patch': 'WebKit'},
1163                     'checkbox': {'ifRepositorySet': ['WebKit'], 'value': 'build-webkit'},
1164                     'shared': {'revision': 'Shared'},
1165                 },
1166                 'acceptsRoots': true,
1167             };
1168             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1169             const requestToBuild = createSampleBuildRequestWithPatch(MockModels.iphone, null, -1);
1170             const requestToTest = createSampleBuildRequestWithPatch(MockModels.iphone, MockModels.speedometer, 0);
1171             const properties = syncers[0]._propertiesForBuildRequest(requestToTest, [requestToBuild, requestToTest]);
1172             assert.equal(properties['webkit'], '197463');
1173             assert.equal(properties['roots'], '[{"url":"http://build.webkit.org/api/uploaded-file/456.dat"}]');
1174             assert.equal(properties['checkbox'], 'test-webkit');
1175         });
1176
1177         it('should not set the value for "ifBuilt" if no build for the repository in the list appears', () => {
1178             const config = sampleiOSConfig();
1179             config.repositoryGroups['ios-svn-webkit-with-owned-commit'] = {
1180                 'repositories': {'WebKit': {'acceptsPatch': true}, 'Owner Repository': {}, 'iOS': {}},
1181                 'testProperties': {
1182                     'os': {'revision': 'iOS'},
1183                     'webkit': {'revision': 'WebKit'},
1184                     'owner-repo': {'revision': 'Owner Repository'},
1185                     'roots': {'roots': {}},
1186                     'checkbox': {'ifBuilt': ['WebKit'], 'value': 'test-webkit'}
1187                 },
1188                 'buildProperties': {
1189                     'webkit': {'revision': 'WebKit'},
1190                     'webkit-patch': {'patch': 'WebKit'},
1191                     'owner-repo': {'revision': 'Owner Repository'},
1192                     'checkbox': {'ifRepositorySet': ['WebKit'], 'value': 'build-webkit'},
1193                     'owned-commits': {'ownedRevisions': 'Owner Repository'}
1194                 },
1195                 'acceptsRoots': true,
1196             };
1197             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1198             const requestToBuild =  createSampleBuildRequestWithOwnedCommit(MockModels.iphone, null, -1);
1199             const requestToTest = createSampleBuildRequestWithOwnedCommit(MockModels.iphone, MockModels.speedometer, 0);
1200             const properties = syncers[0]._propertiesForBuildRequest(requestToTest, [requestToBuild, requestToTest]);
1201
1202             assert.equal(properties['webkit'], '197463');
1203             assert.equal(properties['roots'], '[{"url":"http://build.webkit.org/api/uploaded-file/456.dat"}]');
1204             assert.equal(properties['checkbox'], undefined);
1205         });
1206
1207         it('should resolve "ifRepositorySet" and "requiresBuild"', () => {
1208             const config = sampleiOSConfig();
1209             config.repositoryGroups['ios-svn-webkit-with-owned-commit'] = {
1210                 'repositories': {'WebKit': {'acceptsPatch': true}, 'Owner Repository': {}, 'iOS': {}},
1211                 'testProperties': {
1212                     'os': {'revision': 'iOS'},
1213                     'webkit': {'revision': 'WebKit'},
1214                     'owner-repo': {'revision': 'Owner Repository'},
1215                     'roots': {'roots': {}},
1216                 },
1217                 'buildProperties': {
1218                     'webkit': {'revision': 'WebKit'},
1219                     'webkit-patch': {'patch': 'WebKit'},
1220                     'owner-repo': {'revision': 'Owner Repository'},
1221                     'checkbox': {'ifRepositorySet': ['WebKit'], 'value': 'build-webkit'},
1222                     'owned-commits': {'ownedRevisions': 'Owner Repository'}
1223                 },
1224                 'acceptsRoots': true,
1225             };
1226             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1227             const request = createSampleBuildRequestWithOwnedCommit(MockModels.iphone, null, -1);
1228             const properties = syncers[2]._propertiesForBuildRequest(request, [request]);
1229             assert.equal(properties['webkit'], '197463');
1230             assert.equal(properties['owner-repo'], 'owner-001');
1231             assert.equal(properties['checkbox'], undefined);
1232             assert.deepEqual(JSON.parse(properties['owned-commits']), {'Owner Repository': [{revision: 'owned-002', repository: 'Owned Repository', ownerRevision: 'owner-001'}]});
1233         });
1234
1235         it('should resolve "patch", "ifRepositorySet" and "requiresBuild"', () => {
1236
1237             const config = sampleiOSConfig();
1238             config.repositoryGroups['ios-svn-webkit-with-owned-commit'] = {
1239                 'repositories': {'WebKit': {'acceptsPatch': true}, 'Owner Repository': {}, 'iOS': {}},
1240                 'testProperties': {
1241                     'os': {'revision': 'iOS'},
1242                     'webkit': {'revision': 'WebKit'},
1243                     'owner-repo': {'revision': 'Owner Repository'},
1244                     'roots': {'roots': {}},
1245                 },
1246                 'buildProperties': {
1247                     'webkit': {'revision': 'WebKit'},
1248                     'webkit-patch': {'patch': 'WebKit'},
1249                     'owner-repo': {'revision': 'Owner Repository'},
1250                     'checkbox': {'ifRepositorySet': ['WebKit'], 'value': 'build-webkit'},
1251                     'owned-commits': {'ownedRevisions': 'Owner Repository'}
1252                 },
1253                 'acceptsRoots': true,
1254             };
1255             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, config, builderNameToIDMap());
1256             const request = createSampleBuildRequestWithOwnedCommitAndPatch(MockModels.iphone, null, -1);
1257             const properties = syncers[2]._propertiesForBuildRequest(request, [request]);
1258             assert.equal(properties['webkit'], '197463');
1259             assert.equal(properties['owner-repo'], 'owner-001');
1260             assert.equal(properties['checkbox'], 'build-webkit');
1261             assert.equal(properties['webkit-patch'], 'http://build.webkit.org/api/uploaded-file/453.dat');
1262             assert.deepEqual(JSON.parse(properties['owned-commits']), {'Owner Repository': [{revision: 'owned-002', repository: 'Owned Repository', ownerRevision: 'owner-001'}]});
1263         });
1264
1265         it('should set the property for the build request id', () => {
1266             const syncers = BuildbotSyncer._loadConfig(RemoteAPI, sampleiOSConfig(), builderNameToIDMap());
1267             const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1268             const properties = syncers[0]._propertiesForBuildRequest(request, [request]);
1269             assert.equal(properties['build_request_id'], request.id());
1270         });
1271     });
1272
1273
1274     describe('BuildbotBuildEntry', () => {
1275         it('should create BuildbotBuildEntry for pending build', () => {
1276             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1277             const buildbotData = samplePendingBuild();
1278             const pendingEntries = buildbotData.buildrequests.map((entry) => new BuildbotBuildEntry(syncer, entry));
1279
1280             assert.equal(pendingEntries.length, 1);
1281             const entry = pendingEntries[0];
1282             assert.ok(entry instanceof BuildbotBuildEntry);
1283             assert.ok(!entry.buildNumber());
1284             assert.ok(!entry.workerName());
1285             assert.equal(entry.buildRequestId(), 16733);
1286             assert.ok(entry.isPending());
1287             assert.ok(!entry.isInProgress());
1288             assert.ok(!entry.hasFinished());
1289             assert.equal(entry.url(), 'http://build.webkit.org/#/buildrequests/16733');
1290         });
1291
1292         it('should create BuildbotBuildEntry for in-progress build', () => {
1293             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1294             const buildbotData = sampleInProgressBuild();
1295             const entries = buildbotData.builds.map((entry) => new BuildbotBuildEntry(syncer, entry));
1296
1297             assert.equal(entries.length, 1);
1298             const entry = entries[0];
1299             assert.ok(entry instanceof BuildbotBuildEntry);
1300             assert.equal(entry.buildNumber(), 614);
1301             assert.equal(entry.workerName(), 'ABTest-iPad-0');
1302             assert.equal(entry.buildRequestId(), 16733);
1303             assert.ok(!entry.isPending());
1304             assert.ok(entry.isInProgress());
1305             assert.ok(!entry.hasFinished());
1306             assert.equal(entry.url(), 'http://build.webkit.org/#/builders/102/builds/614');
1307         });
1308
1309         it('should create BuildbotBuildEntry for finished build', () => {
1310             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1311             const buildbotData = sampleFinishedBuild();
1312             const entries = buildbotData.builds.map((entry) => new BuildbotBuildEntry(syncer, entry));
1313
1314             assert.deepEqual(entries.length, 1);
1315             const entry = entries[0];
1316             assert.ok(entry instanceof BuildbotBuildEntry);
1317             assert.equal(entry.buildNumber(), 1755);
1318             assert.equal(entry.workerName(), 'ABTest-iPad-0');
1319             assert.equal(entry.buildRequestId(), 18935);
1320             assert.ok(!entry.isPending());
1321             assert.ok(!entry.isInProgress());
1322             assert.ok(entry.hasFinished());
1323             assert.equal(entry.url(), 'http://build.webkit.org/#/builders/102/builds/1755');
1324         });
1325
1326         it('should create BuildbotBuildEntry for mix of in-progress and finished builds', () => {
1327             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1328             const buildbotData = {'builds': [sampleInProgressBuildData(), sampleFinishedBuildData()]};
1329             const entries = buildbotData.builds.map((entry) => new BuildbotBuildEntry(syncer, entry));
1330
1331             assert.deepEqual(entries.length, 2);
1332
1333             let entry = entries[0];
1334             assert.ok(entry instanceof BuildbotBuildEntry);
1335             assert.equal(entry.buildNumber(), 614);
1336             assert.equal(entry.workerName(), 'ABTest-iPad-0');
1337             assert.equal(entry.buildRequestId(), 16733);
1338             assert.ok(!entry.isPending());
1339             assert.ok(entry.isInProgress());
1340             assert.ok(!entry.hasFinished());
1341             assert.equal(entry.url(), 'http://build.webkit.org/#/builders/102/builds/614');
1342
1343             entry = entries[1];
1344             assert.ok(entry instanceof BuildbotBuildEntry);
1345             assert.equal(entry.buildNumber(), 1755);
1346             assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1347             assert.equal(entry.buildRequestId(), 18935);
1348             assert.ok(!entry.isPending());
1349             assert.ok(!entry.isInProgress());
1350             assert.ok(entry.hasFinished());
1351             assert.equal(entry.url(), 'http://build.webkit.org/#/builders/102/builds/1755');
1352         });
1353     });
1354
1355     describe('pullBuildbot', () => {
1356         it('should fetch pending builds from the right URL', () => {
1357             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1358             assert.equal(syncer.builderName(), 'ABTest-iPad-RunBenchmark-Tests');
1359             let expectedURL = '/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds';
1360             assert.equal(syncer.pathForPendingBuildsJSONDeprecated(), expectedURL);
1361             syncer.pullBuildbot();
1362             assert.equal(requests.length, 1);
1363             assert.equal(requests[0].url, expectedURL);
1364         });
1365
1366         it('should fetch recent builds once pending builds have been fetched', () => {
1367             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1368             assert.equal(syncer.builderName(), 'ABTest-iPad-RunBenchmark-Tests');
1369
1370             syncer.pullBuildbot(1);
1371             assert.equal(requests.length, 1);
1372             assert.equal(requests[0].url, '/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds');
1373             requests[0].resolve([]);
1374             return MockRemoteAPI.waitForRequest().then(() => {
1375                 assert.equal(requests.length, 2);
1376                 assert.equal(requests[1].url, '/json/builders/ABTest-iPad-RunBenchmark-Tests/builds/?select=-1');
1377             });
1378         });
1379
1380         it('should fetch the right number of recent builds', () => {
1381             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1382
1383             syncer.pullBuildbot(3);
1384             assert.equal(requests.length, 1);
1385             assert.equal(requests[0].url, '/json/builders/ABTest-iPad-RunBenchmark-Tests/pendingBuilds');
1386             requests[0].resolve([]);
1387             return MockRemoteAPI.waitForRequest().then(() => {
1388                 assert.equal(requests.length, 2);
1389                 assert.equal(requests[1].url, '/json/builders/ABTest-iPad-RunBenchmark-Tests/builds/?select=-1&select=-2&select=-3');
1390             });
1391         });
1392
1393         it('should create BuildbotBuildEntry for pending builds', () => {
1394             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1395             let promise = syncer.pullBuildbot();
1396             requests[0].resolve([samplePendingBuildDeprecated()]);
1397             return promise.then((entries) => {
1398                 assert.equal(entries.length, 1);
1399                 let entry = entries[0];
1400                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1401                 assert.ok(!entry.buildNumber());
1402                 assert.ok(!entry.slaveName());
1403                 assert.equal(entry.buildRequestId(), 16733);
1404                 assert.ok(entry.isPending());
1405                 assert.ok(!entry.isInProgress());
1406                 assert.ok(!entry.hasFinished());
1407                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
1408             });
1409         });
1410
1411         it('should create BuildbotBuildEntry for in-progress builds', () => {
1412             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1413
1414             let promise = syncer.pullBuildbot(1);
1415             assert.equal(requests.length, 1);
1416             requests[0].resolve([]);
1417             return MockRemoteAPI.waitForRequest().then(() => {
1418                 assert.equal(requests.length, 2);
1419                 requests[1].resolve({[-1]: sampleInProgressBuildDeprecated()});
1420                 return promise;
1421             }).then((entries) => {
1422                 assert.equal(entries.length, 1);
1423                 let entry = entries[0];
1424                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1425                 assert.equal(entry.buildNumber(), 614);
1426                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1427                 assert.equal(entry.buildRequestId(), 16733);
1428                 assert.ok(!entry.isPending());
1429                 assert.ok(entry.isInProgress());
1430                 assert.ok(!entry.hasFinished());
1431                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
1432             });
1433         });
1434
1435         it('should create BuildbotBuildEntry for finished builds', () => {
1436             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1437
1438             let promise = syncer.pullBuildbot(1);
1439             assert.equal(requests.length, 1);
1440             requests[0].resolve([]);
1441             return MockRemoteAPI.waitForRequest().then(() => {
1442                 assert.equal(requests.length, 2);
1443                 requests[1].resolve({[-1]: sampleFinishedBuildDeprecated()});
1444                 return promise;
1445             }).then((entries) => {
1446                 assert.deepEqual(entries.length, 1);
1447                 let entry = entries[0];
1448                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1449                 assert.equal(entry.buildNumber(), 1755);
1450                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1451                 assert.equal(entry.buildRequestId(), 18935);
1452                 assert.ok(!entry.isPending());
1453                 assert.ok(!entry.isInProgress());
1454                 assert.ok(entry.hasFinished());
1455                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
1456             });
1457         });
1458
1459         it('should create BuildbotBuildEntry for mixed pending, in-progress, finished, and missing builds', () => {
1460             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1461
1462             let promise = syncer.pullBuildbot(5);
1463             assert.equal(requests.length, 1);
1464
1465             requests[0].resolve([samplePendingBuildDeprecated(123)]);
1466
1467             return MockRemoteAPI.waitForRequest().then(() => {
1468                 assert.equal(requests.length, 2);
1469                 requests[1].resolve({[-1]: sampleFinishedBuildDeprecated(), [-2]: {'error': 'Not available'}, [-4]: sampleInProgressBuildDeprecated()});
1470                 return promise;
1471             }).then((entries) => {
1472                 assert.deepEqual(entries.length, 3);
1473
1474                 let entry = entries[0];
1475                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1476                 assert.equal(entry.buildNumber(), null);
1477                 assert.equal(entry.slaveName(), null);
1478                 assert.equal(entry.buildRequestId(), 123);
1479                 assert.ok(entry.isPending());
1480                 assert.ok(!entry.isInProgress());
1481                 assert.ok(!entry.hasFinished());
1482                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
1483
1484                 entry = entries[1];
1485                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1486                 assert.equal(entry.buildNumber(), 614);
1487                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1488                 assert.equal(entry.buildRequestId(), 16733);
1489                 assert.ok(!entry.isPending());
1490                 assert.ok(entry.isInProgress());
1491                 assert.ok(!entry.hasFinished());
1492                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
1493
1494                 entry = entries[2];
1495                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1496                 assert.equal(entry.buildNumber(), 1755);
1497                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1498                 assert.equal(entry.buildRequestId(), 18935);
1499                 assert.ok(!entry.isPending());
1500                 assert.ok(!entry.isInProgress());
1501                 assert.ok(entry.hasFinished());
1502                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
1503             });
1504         });
1505
1506         it('should sort BuildbotBuildEntry by order', () => {
1507             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1508
1509             let promise = syncer.pullBuildbot(5);
1510             assert.equal(requests.length, 1);
1511
1512             requests[0].resolve([samplePendingBuildDeprecated(456, 2), samplePendingBuildDeprecated(123, 1)]);
1513
1514             return MockRemoteAPI.waitForRequest().then(() => {
1515                 assert.equal(requests.length, 2);
1516                 requests[1].resolve({[-3]: sampleFinishedBuildDeprecated(), [-1]: {'error': 'Not available'}, [-2]: sampleInProgressBuildDeprecated()});
1517                 return promise;
1518             }).then((entries) => {
1519                 assert.deepEqual(entries.length, 4);
1520
1521                 let entry = entries[0];
1522                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1523                 assert.equal(entry.buildNumber(), null);
1524                 assert.equal(entry.slaveName(), null);
1525                 assert.equal(entry.buildRequestId(), 123);
1526                 assert.ok(entry.isPending());
1527                 assert.ok(!entry.isInProgress());
1528                 assert.ok(!entry.hasFinished());
1529                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
1530
1531                 entry = entries[1];
1532                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1533                 assert.equal(entry.buildNumber(), null);
1534                 assert.equal(entry.slaveName(), null);
1535                 assert.equal(entry.buildRequestId(), 456);
1536                 assert.ok(entry.isPending());
1537                 assert.ok(!entry.isInProgress());
1538                 assert.ok(!entry.hasFinished());
1539                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/');
1540
1541                 entry = entries[2];
1542                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1543                 assert.equal(entry.buildNumber(), 614);
1544                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1545                 assert.equal(entry.buildRequestId(), 16733);
1546                 assert.ok(!entry.isPending());
1547                 assert.ok(entry.isInProgress());
1548                 assert.ok(!entry.hasFinished());
1549                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
1550
1551                 entry = entries[3];
1552                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1553                 assert.equal(entry.buildNumber(), 1755);
1554                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1555                 assert.equal(entry.buildRequestId(), 18935);
1556                 assert.ok(!entry.isPending());
1557                 assert.ok(!entry.isInProgress());
1558                 assert.ok(entry.hasFinished());
1559                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
1560             });
1561         });
1562
1563         it('should override BuildbotBuildEntry for pending builds by in-progress builds', () => {
1564             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1565
1566             let promise = syncer.pullBuildbot(5);
1567             assert.equal(requests.length, 1);
1568
1569             requests[0].resolve([samplePendingBuildDeprecated()]);
1570
1571             return MockRemoteAPI.waitForRequest().then(() => {
1572                 assert.equal(requests.length, 2);
1573                 requests[1].resolve({[-1]: sampleInProgressBuildDeprecated()});
1574                 return promise;
1575             }).then((entries) => {
1576                 assert.equal(entries.length, 1);
1577
1578                 let entry = entries[0];
1579                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1580                 assert.equal(entry.buildNumber(), 614);
1581                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1582                 assert.equal(entry.buildRequestId(), 16733);
1583                 assert.ok(!entry.isPending());
1584                 assert.ok(entry.isInProgress());
1585                 assert.ok(!entry.hasFinished());
1586                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/614');
1587             });
1588         });
1589
1590         it('should override BuildbotBuildEntry for pending builds by finished builds', () => {
1591             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1592
1593             let promise = syncer.pullBuildbot(5);
1594             assert.equal(requests.length, 1);
1595
1596             requests[0].resolve([samplePendingBuildDeprecated()]);
1597
1598             return MockRemoteAPI.waitForRequest().then(() => {
1599                 assert.equal(requests.length, 2);
1600                 requests[1].resolve({[-1]: sampleFinishedBuildDeprecated(16733)});
1601                 return promise;
1602             }).then((entries) => {
1603                 assert.equal(entries.length, 1);
1604
1605                 let entry = entries[0];
1606                 assert.ok(entry instanceof BuildbotBuildEntryDeprecated);
1607                 assert.equal(entry.buildNumber(), 1755);
1608                 assert.equal(entry.slaveName(), 'ABTest-iPad-0');
1609                 assert.equal(entry.buildRequestId(), 16733);
1610                 assert.ok(!entry.isPending());
1611                 assert.ok(!entry.isInProgress());
1612                 assert.ok(entry.hasFinished());
1613                 assert.equal(entry.url(), 'http://build.webkit.org/builders/ABTest-iPad-RunBenchmark-Tests/builds/1755');
1614             });
1615         });
1616     });
1617
1618     describe('scheduleRequest', () => {
1619         it('should schedule a build request on a specified slave', () => {
1620             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[0];
1621
1622             const waitForRequest = MockRemoteAPI.waitForRequest();
1623             const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1624             syncer.scheduleRequest(request, [request], 'some-slave');
1625             return waitForRequest.then(() => {
1626                 assert.equal(requests.length, 1);
1627                 assert.equal(requests[0].url, '/builders/ABTest-iPhone-RunBenchmark-Tests/force');
1628                 assert.equal(requests[0].method, 'POST');
1629                 assert.deepEqual(requests[0].data, {
1630                     'build_request_id': '16733-' + MockModels.iphone.id(),
1631                     'desired_image': '13A452',
1632                     "opensource": "197463",
1633                     'forcescheduler': 'ABTest-iPhone-RunBenchmark-Tests-ForceScheduler',
1634                     'slavename': 'some-slave',
1635                     'test_name': 'speedometer'
1636                 });
1637             });
1638         });
1639     });
1640
1641     describe('scheduleRequestInGroupIfAvailable', () => {
1642
1643         function pullBuildbotWithAssertion(syncer, pendingBuilds, inProgressAndFinishedBuilds)
1644         {
1645             const promise = syncer.pullBuildbot(5);
1646             assert.equal(requests.length, 1);
1647             requests[0].resolve(pendingBuilds);
1648             return MockRemoteAPI.waitForRequest().then(() => {
1649                 assert.equal(requests.length, 2);
1650                 requests[1].resolve(inProgressAndFinishedBuilds);
1651                 requests.length = 0;
1652                 return promise;
1653             });
1654         }
1655
1656         it('should schedule a build if builder has no builds if slaveList is not specified', () => {
1657             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, smallConfiguration(), builderNameToIDMap())[0];
1658
1659             return pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1660                 const request = createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest);
1661                 syncer.scheduleRequestInGroupIfAvailable(request, [request]);
1662                 assert.equal(requests.length, 1);
1663                 assert.equal(requests[0].url, '/builders/some%20builder/force');
1664                 assert.equal(requests[0].method, 'POST');
1665                 assert.deepEqual(requests[0].data, {id: '16733-' + MockModels.somePlatform.id(), 'os': '13A452', 'wk': '197463'});
1666             });
1667         });
1668
1669         it('should schedule a build if builder only has finished builds if slaveList is not specified', () => {
1670             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, smallConfiguration(), builderNameToIDMap())[0];
1671
1672             return pullBuildbotWithAssertion(syncer, [], {[-1]: smallFinishedBuild()}).then(() => {
1673                 const request = createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest);
1674                 syncer.scheduleRequestInGroupIfAvailable(request, [request]);
1675                 assert.equal(requests.length, 1);
1676                 assert.equal(requests[0].url, '/builders/some%20builder/force');
1677                 assert.equal(requests[0].method, 'POST');
1678                 assert.deepEqual(requests[0].data, {id: '16733-' + MockModels.somePlatform.id(), 'os': '13A452', 'wk': '197463'});
1679             });
1680         });
1681
1682         it('should not schedule a build if builder has a pending build if slaveList is not specified', () => {
1683             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, smallConfiguration(), builderNameToIDMap())[0];
1684
1685             return pullBuildbotWithAssertion(syncer, [smallPendingBuild()], {}).then(() => {
1686                 syncer.scheduleRequestInGroupIfAvailable(createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest));
1687                 assert.equal(requests.length, 0);
1688             });
1689         });
1690
1691         it('should schedule a build if builder does not have pending or completed builds on the matching slave', () => {
1692             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[0];
1693
1694             return pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1695                 const request = createSampleBuildRequest(MockModels.iphone, MockModels.speedometer);
1696                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1697                 assert.equal(requests.length, 1);
1698                 assert.equal(requests[0].url, '/builders/ABTest-iPhone-RunBenchmark-Tests/force');
1699                 assert.equal(requests[0].method, 'POST');
1700             });
1701         });
1702
1703         it('should schedule a build if builder only has finished builds on the matching slave', () => {
1704             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1705
1706             pullBuildbotWithAssertion(syncer, [], {[-1]: sampleFinishedBuildDeprecated()}).then(() => {
1707                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1708                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1709                 assert.equal(requests.length, 1);
1710                 assert.equal(requests[0].url, '/builders/ABTest-iPad-RunBenchmark-Tests/force');
1711                 assert.equal(requests[0].method, 'POST');
1712             });
1713         });
1714
1715         it('should not schedule a build if builder has a pending build on the maching slave', () => {
1716             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1717
1718             pullBuildbotWithAssertion(syncer, [samplePendingBuildDeprecated()], {}).then(() => {
1719                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1720                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1721                 assert.equal(requests.length, 0);
1722             });
1723         });
1724
1725         it('should schedule a build if builder only has a pending build on a non-maching slave', () => {
1726             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1727
1728             return pullBuildbotWithAssertion(syncer, [samplePendingBuildDeprecated(1, 1, 'another-slave')], {}).then(() => {
1729                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1730                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1731                 assert.equal(requests.length, 1);
1732             });
1733         });
1734
1735         it('should schedule a build if builder only has an in-progress build on the matching slave', () => {
1736             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1737
1738             return pullBuildbotWithAssertion(syncer, [], {[-1]: sampleInProgressBuildDeprecated()}).then(() => {
1739                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1740                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1741                 assert.equal(requests.length, 1);
1742             });
1743         });
1744
1745         it('should schedule a build if builder has an in-progress build on another slave', () => {
1746             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1747
1748             return pullBuildbotWithAssertion(syncer, [], {[-1]: sampleInProgressBuildDeprecated('other-slave')}).then(() => {
1749                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1750                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1751                 assert.equal(requests.length, 1);
1752             });
1753         });
1754
1755         it('should not schedule a build if the request does not match any configuration', () => {
1756             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[0];
1757
1758             return pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1759                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1760                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1761                 assert.equal(requests.length, 0);
1762             });
1763         });
1764
1765         it('should not schedule a build if a new request had been submitted to the same slave', (done) => {
1766             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1767
1768             pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1769                 let request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1770                 syncer.scheduleRequest(request, [request], 'ABTest-iPad-0');
1771                 request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1772                 syncer.scheduleRequest(request, [request], 'ABTest-iPad-1');
1773             }).then(() => {
1774                 assert.equal(requests.length, 2);
1775                 const request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1776                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1777             }).then(() => {
1778                 assert.equal(requests.length, 2);
1779                 done();
1780             }).catch(done);
1781         });
1782
1783         it('should schedule a build if a new request had been submitted to another slave', () => {
1784             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, sampleiOSConfig(), builderNameToIDMap())[1];
1785
1786             return pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1787                 let request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer);
1788                 syncer.scheduleRequest(request, [request], 'ABTest-iPad-0');
1789                 assert.equal(requests.length, 1);
1790                 request = createSampleBuildRequest(MockModels.ipad, MockModels.speedometer)
1791                 syncer.scheduleRequestInGroupIfAvailable(request, [request], 'ABTest-iPad-1');
1792                 assert.equal(requests.length, 2);
1793             });
1794         });
1795
1796         it('should not schedule a build if a new request had been submitted to the same builder without slaveList', () => {
1797             let syncer = BuildbotSyncer._loadConfig(MockRemoteAPI, smallConfiguration(), builderNameToIDMap())[0];
1798
1799             return pullBuildbotWithAssertion(syncer, [], {}).then(() => {
1800                 let request = createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest);
1801                 syncer.scheduleRequest(request, [request], null);
1802                 assert.equal(requests.length, 1);
1803                 request = createSampleBuildRequest(MockModels.somePlatform, MockModels.someTest);
1804                 syncer.scheduleRequestInGroupIfAvailable(request, [request], null);
1805                 assert.equal(requests.length, 1);
1806             });
1807         });
1808     });
1809 });