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