Perf dashboard should automatically add aggregators
[WebKit-https.git] / Websites / perf.webkit.org / run-tests.js
1 #!/usr/local/bin/node
2
3 var assert = require('assert');
4 var crypto = require('crypto');
5 var fs = require('fs');
6 var http = require('http');
7 var path = require('path');
8 var vm = require('vm');
9
10 function connect(keepAlive) {
11     var pg = require('pg');
12     var database = config('database');
13     var connectionString = 'tcp://' + database.username + ':' + database.password + '@' + database.host + ':' + database.port
14         + '/' + database.name;
15
16     var client = new pg.Client(connectionString);
17     if (!keepAlive) {
18         client.on('drain', function () {
19             client.end();
20             client = undefined;
21         });
22     }
23     client.connect();
24
25     return client;
26 }
27
28 function pathToDatabseSQL(relativePath) {
29     return path.resolve(__dirname, 'init-database.sql');
30 }
31
32 function pathToTests(testName) {
33     return testName ? path.resolve(__dirname, 'tests', testName) : path.resolve(__dirname, 'tests');
34 }
35
36 var configurationJSON = require('./config.json');
37 function config(key) {
38     return configurationJSON[key];
39 }
40
41 function TaskQueue() {
42     var queue = [];
43     var numberOfRemainingTasks = 0;
44     var emptyQueueCallback;
45
46     function startTasksInQueue() {
47         if (!queue.length)
48             return emptyQueueCallback();
49
50         var swappedQueue = queue;
51         queue = [];
52
53         // Increase the counter before the loop in the case taskCallback is called synchronously.
54         numberOfRemainingTasks += swappedQueue.length;
55         for (var i = 0; i < swappedQueue.length; ++i)
56             swappedQueue[i](null, taskCallback);
57     }
58
59     function taskCallback(error) {
60         // FIXME: Handle error.
61         console.assert(numberOfRemainingTasks > 0);
62         numberOfRemainingTasks--;
63         if (!numberOfRemainingTasks)
64             setTimeout(startTasksInQueue, 0);
65     }
66
67     this.addTask = function (task) { queue.push(task); }
68     this.start = function (callback) {
69         emptyQueueCallback = callback;
70         startTasksInQueue();
71     }
72 }
73
74 function SerializedTaskQueue() {
75     var queue = [];
76
77     function executeNextTask(error) {
78         // FIXME: Handle error.
79         var callback = queue.pop();
80         setTimeout(function () { callback(null, executeNextTask); }, 0);
81     }
82
83     this.addTask = function (task) { queue.push(task); }
84     this.start = function (callback) {
85         queue.push(callback);
86         queue.reverse();
87         executeNextTask();
88     }
89 }
90
91 function main(argv) {
92     var client = connect(true);
93     var filter = argv[2];
94     confirmUserWantsDatabaseToBeInitializedIfNeeded(client, function (error, shouldContinue) {
95         if (error)
96             console.error(error);
97
98         if (error || !shouldContinue) {
99             client.end();
100             process.exit(1);
101             return;
102         }
103
104         initializeDatabase(client, function (error) {
105             if (error) {
106                 console.error('Failed to initialize the database', error);
107                 client.end();
108                 process.exit(1);
109             }
110
111             var testCaseQueue = new SerializedTaskQueue();
112             var testFileQueue = new SerializedTaskQueue();
113             fs.readdirSync(pathToTests()).map(function (testFile) {
114                 if (!testFile.match(/.js$/) || (filter && testFile.indexOf(filter) != 0))
115                     return;
116                 testFileQueue.addTask(function (error, callback) {
117                     var testContent = fs.readFileSync(pathToTests(testFile), 'utf-8');
118                     var environment = new TestEnvironment(testCaseQueue);
119                     vm.runInNewContext(testContent, environment, pathToTests(testFile));
120                     callback();
121                 });
122             });
123             testFileQueue.start(function () {
124                 client.end();
125                 testCaseQueue.start(function () {
126                     console.log('DONE');
127                 });
128             });
129         });
130     });
131 }
132
133 function confirmUserWantsDatabaseToBeInitializedIfNeeded(client, callback) {
134     function fetchTableNames(error, callback) {
135         if (error)
136             return callback(error);
137
138         client.query('SELECT table_name FROM information_schema.tables WHERE table_type = \'BASE TABLE\' and table_schema = \'public\'', function (error, result) {
139             if (error)
140                 return callback(error);
141             callback(null, result.rows.map(function (row) { return row['table_name']; }));            
142         });
143     }
144
145     function findNonEmptyTable(error, list, callback) {
146         if (error || !list.length)
147             return callback(error);
148
149         var tableName = list.shift();
150         client.query('SELECT COUNT(*) FROM ' + tableName + ' LIMIT 1', function (error, result) {
151             if (error)
152                 return callback(error);
153
154             if (result.rows[0]['count'])
155                 return callback(null, tableName);
156
157             findNonEmptyTable(null, list, callback);
158         });
159     }
160
161     fetchTableNames(null, function (error, tableNames) {
162         if (error)
163             return callback(error, false);
164
165         findNonEmptyTable(null, tableNames, function (error, nonEmptyTable) {
166             if (error)
167                 return callback(error, false);
168
169             if (!nonEmptyTable)
170                 return callback(null, true);
171
172             console.warn('Table ' + nonEmptyTable + ' is not empty but running tests will drop all tables.');
173             askYesOrNoQuestion(null, 'Do you really want to continue?', callback);
174         });
175     });
176 }
177
178 function askYesOrNoQuestion(error, question, callback) {
179     if (error)
180         return callback(error);
181
182     process.stdout.write(question + ' (y/n):');
183     process.stdin.resume();
184     process.stdin.setEncoding('utf-8');
185     process.stdin.on('data', function (line) {
186         line = line.trim();
187         if (line === 'y') {
188             process.stdin.pause();
189             callback(null, true);
190         } else if (line === 'n') {
191             process.stdin.pause();
192             callback(null, false);
193         } else
194             console.warn('Invalid input:', line);
195     });
196 }
197
198 function initializeDatabase(client, callback) {
199     var commaSeparatedSqlStatements = fs.readFileSync(pathToDatabseSQL(), "utf8");
200
201     var firstError;
202     var queue = new TaskQueue();
203     commaSeparatedSqlStatements.split(/;\s*(?=CREATE|DROP)/).forEach(function (statement) {
204         queue.addTask(function (error, callback) {
205             client.query(statement, function (error) {
206                 if (error && !firstError)
207                     firstError = error;
208                 callback();
209             });
210         })
211     });
212
213     queue.start(function () { callback(firstError); });
214 }
215
216 var currentTestContext;
217 function TestEnvironment(testCaseQueue) {
218     var currentTestGroup;
219
220     this.assert = assert;
221     this.console = console;
222
223     // describe("~", function () {
224     //     it("~", function () { assert(true); });
225     // });
226     this.describe = function (testGroup, callback) {
227         currentTestGroup = testGroup;
228         callback();
229     }
230
231     this.it = function (testCaseDescription, testCase) {
232         testCaseQueue.addTask(function (error, callback) {
233             currentTestContext = new TestContext(currentTestGroup, testCaseDescription, function () {
234                 currentTestContext = null;
235                 initializeDatabase(connect(), callback);
236             });
237             testCase();
238         });
239     }
240
241     this.postJSON = function (path, content, callback) {
242         sendHttpRequest(path, 'POST', 'application/json', JSON.stringify(content), function (error, response) {
243             assert.ifError(error);
244             callback(response);
245         });
246     }
247
248     this.httpGet = function (path, callback) {
249         sendHttpRequest(path, 'GET', null, '', function (error, response) {
250             assert.ifError(error);
251             callback(response);
252         });
253     }
254
255     this.httpPost= function (path, content, callback) {
256         var contentType = null;
257         if (typeof(content) != "string") {
258             contentType = 'application/x-www-form-urlencoded';
259             var components = [];
260             for (var key in content)
261                 components.push(key + '=' + escape(content[key]));
262             content = components.join('&');
263         }
264         sendHttpRequest(path, 'POST', contentType, content, function (error, response) {
265             assert.ifError(error);
266             callback(response);
267         });
268     }
269
270     this.queryAndFetchAll = function (query, parameters, callback) {
271         var client = connect();
272         client.query(query, parameters, function (error, result) {
273             setTimeout(function () {
274                 assert.ifError(error);
275                 callback(result.rows);
276             }, 0);
277         });
278     }
279
280     this.sha256 = function (data) {
281         var hash = crypto.createHash('sha256');
282         hash.update(data);
283         return hash.digest('hex');
284     }
285
286     this.config = config;
287
288     this.notifyDone = function () { currentTestContext.done(); }
289 }
290
291 process.on('uncaughtException', function (error) {
292     if (!currentTestContext)
293         throw error;
294     currentTestContext.logError('Uncaught exception', error);
295     currentTestContext.done();
296 });
297
298 function sendHttpRequest(path, method, contentType, content, callback) {
299     var options = config('testServer');
300     options.path = path;
301     options.method = method;
302
303     var request = http.request(options, function (response) {
304         var responseText = '';
305         response.setEncoding('utf8');
306         response.on('data', function (chunk) { responseText += chunk; });
307         response.on('end', function () {
308             setTimeout(function () {
309                 callback(null, {statusCode: response.statusCode, responseText: responseText});
310             }, 0);
311         });
312     });
313     request.on('error', callback);
314     if (contentType)
315         request.setHeader('Content-Type', contentType);
316     if (content)
317         request.write(content);
318     request.end();
319 }
320
321 function TestContext(testGroup, testCase, callback) {
322     var failed = false;
323
324     this.description = function () {
325         return testGroup + ' ' + testCase;
326     }
327     this.done = function () {
328         if (!failed)
329             console.log('PASS');
330         callback();
331     }
332     this.logError = function (error, details) {
333         failed = true;
334         console.error(error, details);
335     }
336
337     process.stdout.write(this.description() + ': ');
338 }
339
340 main(process.argv);