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