Add a model for parsing buildbot JSON with unit tests
[WebKit-https.git] / Websites / perf.webkit.org / tools / js / buildbot-syncer.js
1 'use strict';
2
3 let assert = require('assert');
4
5 require('./v3-models.js');
6
7 class BuildbotBuildEntry {
8     constructor(syncer, type, rawData)
9     {
10         assert.equal(syncer.builderName(), rawData['builderName']);
11
12         this._slaveName = null;
13         this._buildRequestId = null;
14         this._isInProgress = 'currentStep' in rawData;
15         this._buildNumber = rawData['number'];
16
17         for (var propertyTuple of (rawData['properties'] || [])) {
18             // e.g. ['build_request_id', '16733', 'Force Build Form']
19             var name = propertyTuple[0];
20             var value = propertyTuple[1];
21             if (name == syncer._slavePropertyName)
22                 this._slaveName = value;
23             else if (name == syncer._buildRequestPropertyName)
24                 this._buildRequestId = value;
25         }
26     }
27
28     slaveName() { return this._slaveName; }
29     buildRequestId() { return this._buildRequestId; }
30     isInProgress() { return this._isInProgress; }
31 }
32
33 class BuildbotSyncer {
34
35     constructor(url, object)
36     {
37         this._url = url;
38         this._builderName = object.builder;
39         this._platformName = object.platform;
40         this._testPath = object.test;
41         this._propertiesTemplate = object.properties;
42         this._slavePropertyName = object.slaveArgument;
43         this._buildRequestPropertyName = object.buildRequestArgument;
44     }
45
46     testPath() { return this._testPath }
47     builderName() { return this._builderName; }
48     platformName() { return this._platformName; }
49
50     fetchPendingRequests()
51     {
52         return RemoteAPI.fetchJSON(`${this._url}/json/builders/${this._name}/pendingBuilds`).then(function (content) {
53             var requests = [];
54             for (var entry of content) {
55                 var properties = entry['properties'];
56                 if (!properties)
57                     continue;
58                 for (var propertyTuple of properties) {
59                     // e.g. ['build_request_id', '16733', 'Force Build Form']
60                     if (propertyTuple[0] == this._buildRequestPropertyName)
61                         requests.push(propertyTuple[1]);
62                 }
63             }
64             return requests;
65         });
66     }
67
68     _propertiesForBuildRequest(buildRequest)
69     {
70         console.assert(buildRequest instanceof BuildRequest);
71
72         let rootSet = buildRequest.rootSet();
73         console.assert(rootSet instanceof RootSet);
74
75         let repositoryByName = {};
76         for (let repository of rootSet.repositories())
77             repositoryByName[repository.name()] = repository;
78
79         let properties = {};
80         for (let key in this._propertiesTemplate) {
81             let value = this._propertiesTemplate[key];
82             if (typeof(value) != 'object')
83                 properties[key] = value;
84             else if ('root' in value) {
85                 let repositoryName = value['root'];
86                 let repository = repositoryByName[repositoryName];
87                 assert(repository, '"${repositoryName}" must be specified');
88                 properties[key] = rootSet.revisionForRepository(repository);
89             } else if ('rootsExcluding' in value) {
90                 let revisionSet = this._revisionSetFromRootSetWithExclusionList(rootSet, value['rootsExcluding']);
91                 properties[key] = JSON.stringify(revisionSet);
92             }
93         }
94
95         properties[this._buildRequestPropertyName] = buildRequest.id();
96
97         return properties;
98     }
99
100     _revisionSetFromRootSetWithExclusionList(rootSet, exclusionList)
101     {
102         let revisionSet = {};
103         for (let repository of rootSet.repositories()) {
104             if (exclusionList.indexOf(repository.name()) >= 0)
105                 continue;
106             let commit = rootSet.commitForRepository(repository);
107             revisionSet[repository.name()] = {
108                 id: commit.id(),
109                 time: +commit.time(),
110                 repository: repository.name(),
111                 revision: commit.revision(),
112             };
113         }
114         return revisionSet;
115     }
116
117     static _loadConfig(url, config)
118     {
119         let shared = config['shared'] || {};
120         let types = config['types'] || {};
121         let builders = config['builders'] || {};
122
123         let syncers = [];
124         for (let entry of config['configurations']) {
125             let newConfig = {};
126             this._validateAndMergeConfig(newConfig, shared);
127
128             this._validateAndMergeConfig(newConfig, entry);
129
130             let type = entry['type'];
131             if (type) {
132                 assert(types[type]);
133                 this._validateAndMergeConfig(newConfig, types[type]);
134             }
135
136             let builder = entry['builder'];
137             if (builders[builder])
138                 this._validateAndMergeConfig(newConfig, builders[builder]);
139
140             assert('platform' in newConfig, 'configuration must specify a platform');
141             assert('test' in newConfig, 'configuration must specify a test');
142             assert('builder' in newConfig, 'configuration must specify a builder');
143             assert('properties' in newConfig, 'configuration must specify arguments to post on a builder');
144             assert('buildRequestArgument' in newConfig, 'configuration must specify buildRequestArgument');
145             syncers.push(new BuildbotSyncer(url, newConfig));
146         }
147
148         return syncers;
149     }
150
151     static _validateAndMergeConfig(config, valuesToMerge)
152     {
153         for (let name in valuesToMerge) {
154             let value = valuesToMerge[name];
155             switch (name) {
156             case 'arguments':
157                 assert.equal(typeof(value), 'object', 'arguments should be a dictionary');
158                 if (!config['properties'])
159                     config['properties'] = {};
160                 this._validateAndMergeProperties(config['properties'], value);
161                 break;
162             case 'test':
163                 assert(value instanceof Array, 'test should be an array');
164                 assert(value.every(function (part) { return typeof part == 'string'; }), 'test should be an array of strings');
165                 config[name] = value.slice();
166                 break;
167             case 'type': // fallthrough
168             case 'builder': // fallthrough
169             case 'platform': // fallthrough
170             case 'slaveArgument': // fallthrough
171             case 'buildRequestArgument':
172                 assert.equal(typeof(value), 'string', `${name} should be of string type`);
173                 config[name] = value;
174                 break;
175             default:
176                 assert(false, `Unrecognized parameter ${name}`);
177             }
178         }
179     }
180
181     static _validateAndMergeProperties(properties, configArguments)
182     {
183         for (let name in configArguments) {
184             let value = configArguments[name];
185             if (typeof(value) == 'string') {
186                 properties[name] = value;
187                 continue;
188             }
189             assert.equal(typeof(value), 'object', 'A argument value must be either a string or a dictionary');
190                 
191             let keys = Object.keys(value);
192             assert.equal(keys.length, 1, 'arguments value cannot contain more than one key');
193             let namedValue = value[keys[0]];
194             switch (keys[0]) {
195             case 'root':
196                 assert.equal(typeof(namedValue), 'string', 'root name must be a string');
197                 break;
198             case 'rootsExcluding':
199                 assert(namedValue instanceof Array, 'rootsExcluding must specify an array');
200                 for (let excludedRootName of namedValue)
201                     assert.equal(typeof(excludedRootName), 'string', 'rootsExcluding must specify an array of strings');
202                 namedValue = namedValue.slice();
203                 break;
204             default:
205                 assert(false, `Unrecognized named argument ${keys[0]}`);
206             }
207             properties[name] = {[keys[0]]: namedValue};
208         }
209     }
210
211 }
212
213 if (typeof module != 'undefined') {
214     module.exports.BuildbotSyncer = BuildbotSyncer;
215     module.exports.BuildbotBuildEntry = BuildbotBuildEntry;
216 }