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