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