| #!/usr/local/bin/node |
| |
| var assert = require('assert'); |
| var crypto = require('crypto'); |
| var fs = require('fs'); |
| var http = require('http'); |
| var path = require('path'); |
| var vm = require('vm'); |
| |
| function connect(keepAlive) { |
| var pg = require('pg'); |
| var database = config('database'); |
| var connectionString = 'tcp://' + database.username + ':' + database.password + '@' + database.host + ':' + database.port |
| + '/' + database.name; |
| |
| var client = new pg.Client(connectionString); |
| if (!keepAlive) { |
| client.on('drain', function () { |
| client.end(); |
| client = undefined; |
| }); |
| } |
| client.connect(); |
| |
| return client; |
| } |
| |
| function pathToDatabseSQL(relativePath) { |
| return path.resolve(__dirname, 'init-database.sql'); |
| } |
| |
| function pathToTests(testName) { |
| return testName ? path.resolve(__dirname, 'tests', testName) : path.resolve(__dirname, 'tests'); |
| } |
| |
| var configurationJSON = require('./config.json'); |
| function config(key) { |
| return configurationJSON[key]; |
| } |
| |
| function TaskQueue() { |
| var queue = []; |
| var numberOfRemainingTasks = 0; |
| var emptyQueueCallback; |
| |
| function startTasksInQueue() { |
| if (!queue.length) |
| return emptyQueueCallback(); |
| |
| var swappedQueue = queue; |
| queue = []; |
| |
| // Increase the counter before the loop in the case taskCallback is called synchronously. |
| numberOfRemainingTasks += swappedQueue.length; |
| for (var i = 0; i < swappedQueue.length; ++i) |
| swappedQueue[i](null, taskCallback); |
| } |
| |
| function taskCallback(error) { |
| // FIXME: Handle error. |
| console.assert(numberOfRemainingTasks > 0); |
| numberOfRemainingTasks--; |
| if (!numberOfRemainingTasks) |
| setTimeout(startTasksInQueue, 0); |
| } |
| |
| this.addTask = function (task) { queue.push(task); } |
| this.start = function (callback) { |
| emptyQueueCallback = callback; |
| startTasksInQueue(); |
| } |
| } |
| |
| function SerializedTaskQueue() { |
| var queue = []; |
| |
| function executeNextTask(error) { |
| // FIXME: Handle error. |
| var callback = queue.pop(); |
| setTimeout(function () { callback(null, executeNextTask); }, 0); |
| } |
| |
| this.addTask = function (task) { queue.push(task); } |
| this.start = function (callback) { |
| queue.push(callback); |
| queue.reverse(); |
| executeNextTask(); |
| } |
| } |
| |
| function main(argv) { |
| var client = connect(true); |
| var filter = argv[2]; |
| confirmUserWantsDatabaseToBeInitializedIfNeeded(client, function (error, shouldContinue) { |
| if (error) |
| console.error(error); |
| |
| if (error || !shouldContinue) { |
| client.end(); |
| process.exit(1); |
| return; |
| } |
| |
| initializeDatabase(client, function (error) { |
| if (error) { |
| console.error('Failed to initialize the database', error); |
| client.end(); |
| process.exit(1); |
| } |
| |
| var testCaseQueue = new SerializedTaskQueue(); |
| var testFileQueue = new SerializedTaskQueue(); |
| fs.readdirSync(pathToTests()).map(function (testFile) { |
| if (!testFile.match(/.js$/) || (filter && testFile.indexOf(filter) != 0)) |
| return; |
| testFileQueue.addTask(function (error, callback) { |
| var testContent = fs.readFileSync(pathToTests(testFile), 'utf-8'); |
| var environment = new TestEnvironment(testCaseQueue); |
| vm.runInNewContext(testContent, environment, pathToTests(testFile)); |
| callback(); |
| }); |
| }); |
| testFileQueue.start(function () { |
| client.end(); |
| testCaseQueue.start(function () { |
| console.log('DONE'); |
| }); |
| }); |
| }); |
| }); |
| } |
| |
| function confirmUserWantsDatabaseToBeInitializedIfNeeded(client, callback) { |
| function fetchTableNames(error, callback) { |
| if (error) |
| return callback(error); |
| |
| client.query('SELECT table_name FROM information_schema.tables WHERE table_type = \'BASE TABLE\' and table_schema = \'public\'', function (error, result) { |
| if (error) |
| return callback(error); |
| callback(null, result.rows.map(function (row) { return row['table_name']; })); |
| }); |
| } |
| |
| function findNonEmptyTable(error, list, callback) { |
| if (error || !list.length) |
| return callback(error); |
| |
| var tableName = list.shift(); |
| client.query('SELECT COUNT(*) FROM ' + tableName + ' LIMIT 1', function (error, result) { |
| if (error) |
| return callback(error); |
| |
| if (result.rows[0]['count']) |
| return callback(null, tableName); |
| |
| findNonEmptyTable(null, list, callback); |
| }); |
| } |
| |
| fetchTableNames(null, function (error, tableNames) { |
| if (error) |
| return callback(error, false); |
| |
| findNonEmptyTable(null, tableNames, function (error, nonEmptyTable) { |
| if (error) |
| return callback(error, false); |
| |
| if (!nonEmptyTable) |
| return callback(null, true); |
| |
| console.warn('Table ' + nonEmptyTable + ' is not empty but running tests will drop all tables.'); |
| askYesOrNoQuestion(null, 'Do you really want to continue?', callback); |
| }); |
| }); |
| } |
| |
| function askYesOrNoQuestion(error, question, callback) { |
| if (error) |
| return callback(error); |
| |
| process.stdout.write(question + ' (y/n):'); |
| process.stdin.resume(); |
| process.stdin.setEncoding('utf-8'); |
| process.stdin.on('data', function (line) { |
| line = line.trim(); |
| if (line === 'y') { |
| process.stdin.pause(); |
| callback(null, true); |
| } else if (line === 'n') { |
| process.stdin.pause(); |
| callback(null, false); |
| } else |
| console.warn('Invalid input:', line); |
| }); |
| } |
| |
| function initializeDatabase(client, callback) { |
| var commaSeparatedSqlStatements = fs.readFileSync(pathToDatabseSQL(), "utf8"); |
| |
| var firstError; |
| var queue = new TaskQueue(); |
| commaSeparatedSqlStatements.split(/;\s*(?=CREATE|DROP)/).forEach(function (statement) { |
| queue.addTask(function (error, callback) { |
| client.query(statement, function (error) { |
| if (error && !firstError) |
| firstError = error; |
| callback(); |
| }); |
| }) |
| }); |
| |
| queue.start(function () { callback(firstError); }); |
| } |
| |
| var currentTestContext; |
| function TestEnvironment(testCaseQueue) { |
| var currentTestGroup; |
| |
| this.assert = assert; |
| this.console = console; |
| |
| // describe("~", function () { |
| // it("~", function () { assert(true); }); |
| // }); |
| this.describe = function (testGroup, callback) { |
| currentTestGroup = testGroup; |
| callback(); |
| } |
| |
| this.it = function (testCaseDescription, testCase) { |
| testCaseQueue.addTask(function (error, callback) { |
| currentTestContext = new TestContext(currentTestGroup, testCaseDescription, function () { |
| currentTestContext = null; |
| initializeDatabase(connect(), callback); |
| }); |
| testCase(); |
| }); |
| } |
| |
| this.postJSON = function (path, content, callback) { |
| sendHttpRequest(path, 'POST', 'application/json', JSON.stringify(content), function (error, response) { |
| assert.ifError(error); |
| callback(response); |
| }); |
| } |
| |
| this.httpGet = function (path, callback) { |
| sendHttpRequest(path, 'GET', null, '', function (error, response) { |
| assert.ifError(error); |
| callback(response); |
| }); |
| } |
| |
| this.httpPost= function (path, content, callback) { |
| var contentType = null; |
| if (typeof(content) != "string") { |
| contentType = 'application/x-www-form-urlencoded'; |
| var components = []; |
| for (var key in content) |
| components.push(key + '=' + escape(content[key])); |
| content = components.join('&'); |
| } |
| sendHttpRequest(path, 'POST', contentType, content, function (error, response) { |
| assert.ifError(error); |
| callback(response); |
| }); |
| } |
| |
| this.queryAndFetchAll = function (query, parameters, callback) { |
| var client = connect(); |
| client.query(query, parameters, function (error, result) { |
| setTimeout(function () { |
| assert.ifError(error); |
| callback(result.rows); |
| }, 0); |
| }); |
| } |
| |
| this.sha256 = function (data) { |
| var hash = crypto.createHash('sha256'); |
| hash.update(data); |
| return hash.digest('hex'); |
| } |
| |
| this.config = config; |
| |
| this.notifyDone = function () { currentTestContext.done(); } |
| } |
| |
| process.on('uncaughtException', function (error) { |
| if (!currentTestContext) |
| throw error; |
| currentTestContext.logError('Uncaught exception', error); |
| currentTestContext.done(); |
| }); |
| |
| function sendHttpRequest(path, method, contentType, content, callback) { |
| var options = config('testServer'); |
| options.path = path; |
| options.method = method; |
| |
| var request = http.request(options, function (response) { |
| var responseText = ''; |
| response.setEncoding('utf8'); |
| response.on('data', function (chunk) { responseText += chunk; }); |
| response.on('end', function () { |
| setTimeout(function () { |
| callback(null, {statusCode: response.statusCode, responseText: responseText}); |
| }, 0); |
| }); |
| }); |
| request.on('error', callback); |
| if (contentType) |
| request.setHeader('Content-Type', contentType); |
| if (content) |
| request.write(content); |
| request.end(); |
| } |
| |
| function TestContext(testGroup, testCase, callback) { |
| var failed = false; |
| |
| this.description = function () { |
| return testGroup + ' ' + testCase; |
| } |
| this.done = function () { |
| if (!failed) |
| console.log('PASS'); |
| callback(); |
| } |
| this.logError = function (error, details) { |
| failed = true; |
| console.error(error, details); |
| } |
| |
| process.stdout.write(this.description() + ': '); |
| } |
| |
| main(process.argv); |