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