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');
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;
16 var client = new pg.Client(connectionString);
18 client.on('drain', function () {
28 function pathToDatabseSQL(relativePath) {
29 return path.resolve(__dirname, 'init-database.sql');
32 function pathToTests(testName) {
33 return testName ? path.resolve(__dirname, 'tests', testName) : path.resolve(__dirname, 'tests');
36 var configurationJSON = require('./config.json');
37 function config(key) {
38 return configurationJSON[key];
41 function TaskQueue() {
43 var numberOfRemainingTasks = 0;
44 var emptyQueueCallback;
46 function startTasksInQueue() {
48 return emptyQueueCallback();
50 var swappedQueue = queue;
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);
59 function taskCallback(error) {
60 // FIXME: Handle error.
61 console.assert(numberOfRemainingTasks > 0);
62 numberOfRemainingTasks--;
63 if (!numberOfRemainingTasks)
64 setTimeout(startTasksInQueue, 0);
67 this.addTask = function (task) { queue.push(task); }
68 this.start = function (callback) {
69 emptyQueueCallback = callback;
74 function SerializedTaskQueue() {
77 function executeNextTask(error) {
78 // FIXME: Handle error.
79 var callback = queue.pop();
80 setTimeout(function () { callback(null, executeNextTask); }, 0);
83 this.addTask = function (task) { queue.push(task); }
84 this.start = function (callback) {
92 var client = connect(true);
94 confirmUserWantsDatabaseToBeInitializedIfNeeded(client, function (error, shouldContinue) {
98 if (error || !shouldContinue) {
104 initializeDatabase(client, function (error) {
106 console.error('Failed to initialize the database', error);
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))
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));
123 testFileQueue.start(function () {
125 testCaseQueue.start(function () {
133 function confirmUserWantsDatabaseToBeInitializedIfNeeded(client, callback) {
134 function fetchTableNames(error, callback) {
136 return callback(error);
138 client.query('SELECT table_name FROM information_schema.tables WHERE table_type = \'BASE TABLE\' and table_schema = \'public\'', function (error, result) {
140 return callback(error);
141 callback(null, result.rows.map(function (row) { return row['table_name']; }));
145 function findNonEmptyTable(error, list, callback) {
146 if (error || !list.length)
147 return callback(error);
149 var tableName = list.shift();
150 client.query('SELECT COUNT(*) FROM ' + tableName + ' LIMIT 1', function (error, result) {
152 return callback(error);
154 if (result.rows[0]['count'])
155 return callback(null, tableName);
157 findNonEmptyTable(null, list, callback);
161 fetchTableNames(null, function (error, tableNames) {
163 return callback(error, false);
165 findNonEmptyTable(null, tableNames, function (error, nonEmptyTable) {
167 return callback(error, false);
170 return callback(null, true);
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);
178 function askYesOrNoQuestion(error, question, callback) {
180 return callback(error);
182 process.stdout.write(question + ' (y/n):');
183 process.stdin.resume();
184 process.stdin.setEncoding('utf-8');
185 process.stdin.on('data', function (line) {
188 process.stdin.pause();
189 callback(null, true);
190 } else if (line === 'n') {
191 process.stdin.pause();
192 callback(null, false);
194 console.warn('Invalid input:', line);
198 function initializeDatabase(client, callback) {
199 var commaSeparatedSqlStatements = fs.readFileSync(pathToDatabseSQL(), "utf8");
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)
213 queue.start(function () { callback(firstError); });
216 var currentTestContext;
217 function TestEnvironment(testCaseQueue) {
218 var currentTestGroup;
220 this.assert = assert;
221 this.console = console;
223 // describe("~", function () {
224 // it("~", function () { assert(true); });
226 this.describe = function (testGroup, callback) {
227 currentTestGroup = testGroup;
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);
241 this.postJSON = function (path, content, callback) {
242 sendHttpRequest(path, 'POST', 'application/json', JSON.stringify(content), function (error, response) {
243 assert.ifError(error);
248 this.httpGet = function (path, callback) {
249 sendHttpRequest(path, 'GET', null, '', function (error, response) {
250 assert.ifError(error);
255 this.httpPost= function (path, content, callback) {
256 var contentType = null;
257 if (typeof(content) != "string") {
258 contentType = 'application/x-www-form-urlencoded';
260 for (var key in content)
261 components.push(key + '=' + escape(content[key]));
262 content = components.join('&');
264 sendHttpRequest(path, 'POST', contentType, content, function (error, response) {
265 assert.ifError(error);
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);
280 this.sha256 = function (data) {
281 var hash = crypto.createHash('sha256');
283 return hash.digest('hex');
286 this.config = config;
288 this.notifyDone = function () { currentTestContext.done(); }
291 process.on('uncaughtException', function (error) {
292 if (!currentTestContext)
294 currentTestContext.logError('Uncaught exception', error);
295 currentTestContext.done();
298 function sendHttpRequest(path, method, contentType, content, callback) {
299 var options = config('testServer');
301 options.method = method;
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});
313 request.on('error', callback);
315 request.setHeader('Content-Type', contentType);
317 request.write(content);
321 function TestContext(testGroup, testCase, callback) {
324 this.description = function () {
325 return testGroup + ' ' + testCase;
327 this.done = function () {
332 this.logError = function (error, details) {
334 console.error(error, details);
337 process.stdout.write(this.description() + ': ');