Add the support for scheduling a A/B testing with a patch.
[WebKit-https.git] / Websites / perf.webkit.org / server-tests / api-update-triggerable.js
1 'use strict';
2
3 const assert = require('assert');
4
5 require('../tools/js/v3-models.js');
6
7 const TestServer = require('./resources/test-server.js');
8 const MockData = require('./resources/mock-data.js');
9 const addSlaveForReport = require('./resources/common-operations.js').addSlaveForReport;
10 const prepareServerTest = require('./resources/common-operations.js').prepareServerTest;
11
12 describe('/api/update-triggerable/', function () {
13     prepareServerTest(this);
14
15     const emptyUpdate = {
16         'slaveName': 'someSlave',
17         'slavePassword': 'somePassword',
18         'triggerable': 'build-webkit',
19         'configurations': [],
20     };
21
22     const smallUpdate = {
23         'slaveName': 'someSlave',
24         'slavePassword': 'somePassword',
25         'triggerable': 'build-webkit',
26         'configurations': [
27             {test: MockData.someTestId(), platform: MockData.somePlatformId()}
28         ],
29     };
30
31     it('should reject when slave name is missing', () => {
32         return TestServer.remoteAPI().postJSON('/api/update-triggerable/', {}).then((response) => {
33             assert.equal(response['status'], 'MissingSlaveName');
34         });
35     });
36
37     it('should reject when there are no slaves', () => {
38         const update = {slaveName: emptyUpdate.slaveName, slavePassword: emptyUpdate.slavePassword};
39         return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update).then((response) => {
40             assert.equal(response['status'], 'SlaveNotFound');
41         });
42     });
43
44     it('should reject when the slave password doesn\'t match', () => {
45         return MockData.addMockData(TestServer.database()).then(() => {
46             return addSlaveForReport(emptyUpdate);
47         }).then(() => {
48             const report = {slaveName: emptyUpdate.slaveName, slavePassword: 'badPassword'};
49             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', emptyUpdate);
50         }).then((response) => {
51             assert.equal(response['status'], 'OK');
52         });
53     });
54
55     it('should accept an empty report', () => {
56         return MockData.addMockData(TestServer.database()).then(() => {
57             return addSlaveForReport(emptyUpdate);
58         }).then(() => {
59             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', emptyUpdate);
60         }).then((response) => {
61             assert.equal(response['status'], 'OK');
62         });
63     });
64
65     it('delete existing configurations when accepting an empty report', () => {
66         const db = TestServer.database();
67         return MockData.addMockData(db).then(() => {
68             return Promise.all([
69                 addSlaveForReport(emptyUpdate),
70                 db.insert('triggerable_configurations', {'triggerable': 1000 // build-webkit
71                     , 'test': MockData.someTestId(), 'platform': MockData.somePlatformId()})
72             ]);
73         }).then(() => {
74             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', emptyUpdate);
75         }).then((response) => {
76             assert.equal(response['status'], 'OK');
77             return db.selectAll('triggerable_configurations', 'test');
78         }).then((rows) => {
79             assert.equal(rows.length, 0);
80         });
81     });
82
83     it('should add configurations in the update', () => {
84         const db = TestServer.database();
85         return MockData.addMockData(db).then(() => {
86             return addSlaveForReport(smallUpdate);
87         }).then(() => {
88             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', smallUpdate);
89         }).then((response) => {
90             assert.equal(response['status'], 'OK');
91             return db.selectAll('triggerable_configurations', 'test');
92         }).then((rows) => {
93             assert.equal(rows.length, 1);
94             assert.equal(rows[0]['test'], smallUpdate.configurations[0]['test']);
95             assert.equal(rows[0]['platform'], smallUpdate.configurations[0]['platform']);
96         });
97     });
98
99     it('should reject when a configuration is malformed', () => {
100         return MockData.addMockData(TestServer.database()).then(() => {
101             return addSlaveForReport(smallUpdate);
102         }).then(() => {
103             const update = {
104                 'slaveName': 'someSlave',
105                 'slavePassword': 'somePassword',
106                 'triggerable': 'build-webkit',
107                 'configurations': [{}],
108             };
109             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
110         }).then((response) => {
111             assert.equal(response['status'], 'InvalidConfigurationEntry');
112         });
113     });
114
115     function updateWithOSXRepositoryGroup()
116     {
117         return {
118             'slaveName': 'someSlave',
119             'slavePassword': 'somePassword',
120             'triggerable': 'empty-triggerable',
121             'configurations': [
122                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
123             ],
124             'repositoryGroups': [
125                 {name: 'system-only', repositories: [
126                     {repository: MockData.macosRepositoryId(), acceptsPatch: false},
127                 ]},
128             ]
129         };
130     }
131
132     it('should reject when repositoryGroups is not an array', () => {
133         const update = updateWithOSXRepositoryGroup();
134         update.repositoryGroups = 1;
135         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
136             return addSlaveForReport(update);
137         }).then(() => {
138             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
139         }).then((response) => {
140             assert.equal(response['status'], 'InvalidRepositoryGroups');
141         });
142     });
143
144     it('should reject when the name of a repository group is not specified', () => {
145         const update = updateWithOSXRepositoryGroup();
146         delete update.repositoryGroups[0].name;
147         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
148             return addSlaveForReport(update);
149         }).then(() => {
150             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
151         }).then((response) => {
152             assert.equal(response['status'], 'InvalidRepositoryGroup');
153         });
154     });
155
156     it('should reject when the repository list is not specified for a repository group', () => {
157         const update = updateWithOSXRepositoryGroup();
158         delete update.repositoryGroups[0].repositories;
159         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
160             return addSlaveForReport(update);
161         }).then(() => {
162             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
163         }).then((response) => {
164             assert.equal(response['status'], 'InvalidRepositoryGroup');
165         });
166     });
167
168     it('should reject when the repository list of a repository group is not an array', () => {
169         const update = updateWithOSXRepositoryGroup();
170         update.repositoryGroups[0].repositories = 'hi';
171         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
172             return addSlaveForReport(update);
173         }).then(() => {
174             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
175         }).then((response) => {
176             assert.equal(response['status'], 'InvalidRepositoryGroup');
177         });
178     });
179
180     it('should reject when a repository group contains a repository data that is not an array', () => {
181         const update = updateWithOSXRepositoryGroup();
182         update.repositoryGroups[0].repositories[0] = 999;
183         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
184             return addSlaveForReport(update);
185         }).then(() => {
186             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
187         }).then((response) => {
188             assert.equal(response['status'], 'InvalidRepositoryData');
189         });
190     });
191
192     it('should reject when a repository group contains an invalid repository id', () => {
193         const update = updateWithOSXRepositoryGroup();
194         update.repositoryGroups[0].repositories[0] = {repository: 999};
195         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
196             return addSlaveForReport(update);
197         }).then(() => {
198             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
199         }).then((response) => {
200             assert.equal(response['status'], 'InvalidRepository');
201         });
202     });
203
204     it('should reject when a repository group contains a duplicate repository id', () => {
205         const update = updateWithOSXRepositoryGroup();
206         const group = update.repositoryGroups[0];
207         group.repositories.push(group.repositories[0]);
208         return MockData.addEmptyTriggerable(TestServer.database()).then(() => {
209             return addSlaveForReport(update);
210         }).then(() => {
211             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', update);
212         }).then((response) => {
213             assert.equal(response['status'], 'DuplicateRepository');
214         });
215     });
216
217     it('should add a new repository group when there are none', () => {
218         const db = TestServer.database();
219         return MockData.addEmptyTriggerable(db).then(() => {
220             return addSlaveForReport(updateWithOSXRepositoryGroup());
221         }).then(() => {
222             return TestServer.remoteAPI().postJSON('/api/update-triggerable/', updateWithOSXRepositoryGroup());
223         }).then((response) => {
224             assert.equal(response['status'], 'OK');
225             return Promise.all([db.selectAll('triggerable_configurations', 'test'), db.selectAll('triggerable_repository_groups')]);
226         }).then((result) => {
227             const [configurations, repositoryGroups] = result;
228
229             assert.equal(configurations.length, 1);
230             assert.equal(configurations[0]['test'], MockData.someTestId());
231             assert.equal(configurations[0]['platform'], MockData.somePlatformId());
232
233             assert.equal(repositoryGroups.length, 1);
234             assert.equal(repositoryGroups[0]['name'], 'system-only');
235             assert.equal(repositoryGroups[0]['triggerable'], MockData.emptyTriggeragbleId());
236         });
237     });
238
239     it('should not add a duplicate repository group when there is a group of the same name', () => {
240         const db = TestServer.database();
241         let initialResult;
242         return MockData.addEmptyTriggerable(db).then(() => {
243             return addSlaveForReport(updateWithOSXRepositoryGroup());
244         }).then(() => {
245             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', updateWithOSXRepositoryGroup());
246         }).then((response) => {
247             return Promise.all([db.selectAll('triggerable_configurations', 'test'), db.selectAll('triggerable_repository_groups')]);
248         }).then((result) => {
249             initialResult = result;
250             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', updateWithOSXRepositoryGroup());
251         }).then(() => {
252             return Promise.all([db.selectAll('triggerable_configurations', 'test'), db.selectAll('triggerable_repository_groups')]);
253         }).then((result) => {
254             const [initialConfigurations, initialRepositoryGroups] = initialResult;
255             const [configurations, repositoryGroups] = result;
256             assert.deepEqual(configurations, initialConfigurations);
257             assert.deepEqual(repositoryGroups, initialRepositoryGroups);
258         })
259     });
260
261     it('should not add a duplicate repository group when there is a group of the same name', () => {
262         const db = TestServer.database();
263         let initialResult;
264         return MockData.addEmptyTriggerable(db).then(() => {
265             return addSlaveForReport(updateWithOSXRepositoryGroup());
266         }).then(() => {
267             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', updateWithOSXRepositoryGroup());
268         }).then((response) => {
269             return Promise.all([db.selectAll('triggerable_configurations', 'test'), db.selectAll('triggerable_repository_groups')]);
270         }).then((result) => {
271             initialResult = result;
272             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', updateWithOSXRepositoryGroup());
273         }).then(() => {
274             return Promise.all([db.selectAll('triggerable_configurations', 'test'), db.selectAll('triggerable_repository_groups')]);
275         }).then((result) => {
276             const [initialConfigurations, initialRepositoryGroups] = initialResult;
277             const [configurations, repositoryGroups] = result;
278             assert.deepEqual(configurations, initialConfigurations);
279             assert.deepEqual(repositoryGroups, initialRepositoryGroups);
280         })
281     });
282
283     it('should update the description of a repository group when the name matches', () => {
284         const db = TestServer.database();
285         const initialUpdate = updateWithOSXRepositoryGroup();
286         const secondUpdate = updateWithOSXRepositoryGroup();
287         secondUpdate.repositoryGroups[0].description = 'this group is awesome';
288         return MockData.addEmptyTriggerable(db).then(() => {
289             return addSlaveForReport(initialUpdate);
290         }).then(() => {
291             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
292         }).then((response) => db.selectAll('triggerable_repository_groups')).then((repositoryGroups) => {
293             assert.equal(repositoryGroups.length, 1);
294             assert.equal(repositoryGroups[0]['name'], 'system-only');
295             assert.equal(repositoryGroups[0]['description'], null);
296             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', secondUpdate);
297         }).then(() => db.selectAll('triggerable_repository_groups')).then((repositoryGroups) => {
298             assert.equal(repositoryGroups.length, 1);
299             assert.equal(repositoryGroups[0]['name'], 'system-only');
300             assert.equal(repositoryGroups[0]['description'], 'this group is awesome');
301         });
302     });
303
304     function updateWithMacWebKitRepositoryGroups()
305     {
306         return {
307             'slaveName': 'someSlave',
308             'slavePassword': 'somePassword',
309             'triggerable': 'empty-triggerable',
310             'configurations': [
311                 {test: MockData.someTestId(), platform: MockData.somePlatformId()}
312             ],
313             'repositoryGroups': [
314                 {name: 'system-only', repositories: [{repository: MockData.macosRepositoryId()}]},
315                 {name: 'system-and-webkit', repositories:
316                     [{repository: MockData.webkitRepositoryId()}, {repository: MockData.macosRepositoryId()}]},
317             ]
318         };
319     }
320
321     function mapRepositoriesByGroup(repositories)
322     {
323         const map = {};
324         for (const row of repositories) {
325             const groupId = row['group'];
326             if (!(groupId in map))
327                 map[groupId] = [];
328             map[groupId].push(row['repository']);
329         }
330         return map;
331     }
332
333     function refetchManifest()
334     {
335         MockData.resetV3Models();
336         return TestServer.remoteAPI().getJSON('/api/manifest').then((content) => Manifest._didFetchManifest(content));
337     }
338
339     it('should update the acceptable of custom roots and patches', () => {
340         const db = TestServer.database();
341         const initialUpdate = updateWithMacWebKitRepositoryGroups();
342         const secondUpdate = updateWithMacWebKitRepositoryGroups();
343         secondUpdate.repositoryGroups[0].acceptsRoots = true;
344         secondUpdate.repositoryGroups[1].repositories[0].acceptsPatch = true;
345         return MockData.addEmptyTriggerable(db).then(() => {
346             return addSlaveForReport(initialUpdate);
347         }).then(() => {
348             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
349         }).then(() => refetchManifest()).then(() => {
350             const repositoryGroups = TriggerableRepositoryGroup.sortByName(TriggerableRepositoryGroup.all());
351             const webkit = Repository.findTopLevelByName('WebKit');
352             const macos = Repository.findTopLevelByName('macOS');
353             assert.equal(repositoryGroups.length, 2);
354             assert.equal(repositoryGroups[0].name(), 'system-and-webkit');
355             assert.equal(repositoryGroups[0].description(), 'system-and-webkit');
356             assert.equal(repositoryGroups[0].acceptsCustomRoots(), false);
357             assert.deepEqual(repositoryGroups[0].repositories(), [webkit, macos]);
358             assert.equal(repositoryGroups[0].acceptsPatchForRepository(webkit), false);
359             assert.equal(repositoryGroups[0].acceptsPatchForRepository(macos), false);
360
361             assert.equal(repositoryGroups[1].name(), 'system-only');
362             assert.equal(repositoryGroups[1].description(), 'system-only');
363             assert.equal(repositoryGroups[1].acceptsCustomRoots(), false);
364             assert.deepEqual(repositoryGroups[1].repositories(), [macos]);
365             assert.equal(repositoryGroups[1].acceptsPatchForRepository(webkit), false);
366             assert.equal(repositoryGroups[1].acceptsPatchForRepository(macos), false);
367             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', secondUpdate);
368         }).then(() => refetchManifest()).then(() => {
369             const repositoryGroups = TriggerableRepositoryGroup.sortByName(TriggerableRepositoryGroup.all());
370             const webkit = Repository.findTopLevelByName('WebKit');
371             const macos = Repository.findTopLevelByName('macOS');
372             assert.equal(repositoryGroups.length, 2);
373             assert.equal(repositoryGroups[0].name(), 'system-and-webkit');
374             assert.equal(repositoryGroups[0].description(), 'system-and-webkit');
375             assert.equal(repositoryGroups[0].acceptsCustomRoots(), false);
376             assert.deepEqual(repositoryGroups[0].repositories(), [webkit, macos]);
377             assert.equal(repositoryGroups[0].acceptsPatchForRepository(webkit), true);
378             assert.equal(repositoryGroups[0].acceptsPatchForRepository(macos), false);
379
380             assert.equal(repositoryGroups[1].name(), 'system-only');
381             assert.equal(repositoryGroups[1].description(), 'system-only');
382             assert.equal(repositoryGroups[1].acceptsCustomRoots(), true);
383             assert.deepEqual(repositoryGroups[1].repositories(), [macos]);
384             assert.equal(repositoryGroups[1].acceptsPatchForRepository(webkit), false);
385             assert.equal(repositoryGroups[1].acceptsPatchForRepository(macos), false);
386             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
387         });
388     });
389
390     it('should replace a repository when the repository group name matches', () => {
391         const db = TestServer.database();
392         const initialUpdate = updateWithMacWebKitRepositoryGroups();
393         const secondUpdate = updateWithMacWebKitRepositoryGroups();
394         let initialGroups;
395         secondUpdate.repositoryGroups[1].repositories[0] = {repository: MockData.gitWebkitRepositoryId()}
396         return MockData.addEmptyTriggerable(db).then(() => {
397             return addSlaveForReport(initialUpdate);
398         }).then(() => {
399             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
400         }).then((response) => {
401             return Promise.all([db.selectAll('triggerable_repository_groups', 'name'), db.selectAll('triggerable_repositories', 'repository')]);
402         }).then((result) => {
403             const [repositoryGroups, repositories] = result;
404             assert.equal(repositoryGroups.length, 2);
405             assert.equal(repositoryGroups[0]['name'], 'system-and-webkit');
406             assert.equal(repositoryGroups[0]['triggerable'], MockData.emptyTriggeragbleId());
407             assert.equal(repositoryGroups[1]['name'], 'system-only');
408             assert.equal(repositoryGroups[1]['triggerable'], MockData.emptyTriggeragbleId());
409             initialGroups = repositoryGroups;
410
411             const repositoriesByGroup = mapRepositoriesByGroup(repositories);
412             assert.equal(Object.keys(repositoriesByGroup).length, 2);
413             assert.deepEqual(repositoriesByGroup[repositoryGroups[0]['id']], [MockData.macosRepositoryId(), MockData.webkitRepositoryId()]);
414             assert.deepEqual(repositoriesByGroup[repositoryGroups[1]['id']], [MockData.macosRepositoryId()]);
415
416             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', secondUpdate);
417         }).then(() => {
418             return Promise.all([db.selectAll('triggerable_repository_groups', 'name'), db.selectAll('triggerable_repositories', 'repository')]);
419         }).then((result) => {
420             const [repositoryGroups, repositories] = result;
421             assert.deepEqual(repositoryGroups, initialGroups);
422
423             const repositoriesByGroup = mapRepositoriesByGroup(repositories);
424             assert.equal(Object.keys(repositoriesByGroup).length, 2);
425             assert.deepEqual(repositoriesByGroup[initialGroups[0]['id']], [MockData.macosRepositoryId(), MockData.gitWebkitRepositoryId()]);
426             assert.deepEqual(repositoriesByGroup[initialGroups[1]['id']], [MockData.macosRepositoryId()]);
427         });
428     });
429
430     it('should replace a repository when the list of repositories matches', () => {
431         const db = TestServer.database();
432         const initialUpdate = updateWithMacWebKitRepositoryGroups();
433         const secondUpdate = updateWithMacWebKitRepositoryGroups();
434         let initialGroups;
435         let initialRepositories;
436         secondUpdate.repositoryGroups[0].name = 'mac-only';
437         return MockData.addEmptyTriggerable(db).then(() => {
438             return addSlaveForReport(initialUpdate);
439         }).then(() => {
440             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', initialUpdate);
441         }).then((response) => {
442             return Promise.all([db.selectAll('triggerable_repository_groups', 'name'), db.selectAll('triggerable_repositories', 'repository')]);
443         }).then((result) => {
444             const [repositoryGroups, repositories] = result;
445             assert.equal(repositoryGroups.length, 2);
446             assert.equal(repositoryGroups[0]['name'], 'system-and-webkit');
447             assert.equal(repositoryGroups[0]['triggerable'], MockData.emptyTriggeragbleId());
448             assert.equal(repositoryGroups[1]['name'], 'system-only');
449             assert.equal(repositoryGroups[1]['triggerable'], MockData.emptyTriggeragbleId());
450             initialGroups = repositoryGroups;
451
452             const repositoriesByGroup = mapRepositoriesByGroup(repositories);
453             assert.equal(Object.keys(repositoriesByGroup).length, 2);
454             assert.deepEqual(repositoriesByGroup[repositoryGroups[0]['id']], [MockData.macosRepositoryId(), MockData.webkitRepositoryId()]);
455             assert.deepEqual(repositoriesByGroup[repositoryGroups[1]['id']], [MockData.macosRepositoryId()]);
456             initialRepositories = repositories;
457
458             return TestServer.remoteAPI().postJSONWithStatus('/api/update-triggerable/', secondUpdate);
459         }).then(() => {
460             return Promise.all([db.selectAll('triggerable_repository_groups', 'name'), db.selectAll('triggerable_repositories', 'repository')]);
461         }).then((result) => {
462             const [repositoryGroups, repositories] = result;
463
464             assert.equal(repositoryGroups.length, 2);
465             assert.equal(repositoryGroups[0]['name'], 'mac-only');
466             assert.equal(repositoryGroups[0]['triggerable'], initialGroups[1]['triggerable']);
467             assert.equal(repositoryGroups[1]['name'], 'system-and-webkit');
468             assert.equal(repositoryGroups[1]['triggerable'], initialGroups[0]['triggerable']);
469
470             assert.deepEqual(repositories, initialRepositories);
471         });
472     });
473
474 });