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