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