From e18cd87a07fd0a3f7a1bd3600e1b8bc3879a8ad3 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Ga=C3=ABl=20M=C3=A9tais?= Date: Tue, 2 Dec 2014 00:08:07 +0100 Subject: [PATCH] Writing the API (workin in progress) --- Gruntfile.js | 7 +- app/lib/testQueue.js | 66 ----------------- bin/server.js | 22 ++++++ lib/server/controllers/apiController.js | 86 +++++++++++++++++++++++ lib/server/controllers/uiController.js | 0 lib/server/datastores/resultsDatastore.js | 9 +++ lib/server/datastores/runsDatastore.js | 36 ++++++++++ lib/server/datastores/runsQueue.js | 73 +++++++++++++++++++ package.json | 2 +- test/api/phantomasWrapperTest.js | 12 ++-- test/api/yellowlabtoolsTest.js | 3 +- test/server/runsDatastoreTest.js | 41 +++++++++++ test/server/runsQueueTest.js | 64 +++++++++++++++++ test/server/testQueueTest.js | 23 ------ 14 files changed, 344 insertions(+), 100 deletions(-) delete mode 100644 app/lib/testQueue.js create mode 100644 bin/server.js create mode 100644 lib/server/controllers/apiController.js create mode 100644 lib/server/controllers/uiController.js create mode 100644 lib/server/datastores/resultsDatastore.js create mode 100644 lib/server/datastores/runsDatastore.js create mode 100644 lib/server/datastores/runsQueue.js create mode 100644 test/server/runsDatastoreTest.js create mode 100644 test/server/runsQueueTest.js delete mode 100644 test/server/testQueueTest.js diff --git a/Gruntfile.js b/Gruntfile.js index 1644902..41a5eef 100644 --- a/Gruntfile.js +++ b/Gruntfile.js @@ -36,7 +36,8 @@ module.exports = function(grunt) { 'lib/**/*.js', 'app/nodeControllers/*.js', 'app/public/scripts/*.js', - 'phantomas_custom/**/*.js' + 'phantomas_custom/**/*.js', + 'test/**/*.js' ] }, clean: { @@ -70,13 +71,13 @@ module.exports = function(grunt) { options: { reporter: 'spec', }, - src: ['coverage/test/api/*.js'] + src: ['coverage/test/api/*.js', 'coverage/test/server/*.js'] }, 'test-current-work': { options: { reporter: 'spec', }, - src: ['coverage/test/api/yellowlabtoolsTest.js'] + src: ['coverage/test/server/runsQueueTest.js'] }, coverage: { options: { diff --git a/app/lib/testQueue.js b/app/lib/testQueue.js deleted file mode 100644 index f271ccb..0000000 --- a/app/lib/testQueue.js +++ /dev/null @@ -1,66 +0,0 @@ -/** - * Creation of a queue and it's worker function - */ - -var util = require('util'); -var EventEmitter = require('events').EventEmitter; -var async = require('async'); -var phantomasWrapper = require('./phantomasWrapper'); - - -var testQueue = function() { - 'use strict'; - - var currentTask = null; - var self = this; - - var queue = async.queue(function(task, callback) { - currentTask = task; - - console.log('Starting test ' + task.testId); - - phantomasWrapper.execute(task, function(err, json, results) { - console.log('Test ' + task.testId + ' complete'); - currentTask = null; - callback(err, json, results); - self.emit('queueMoving'); - }); - }); - - - // Use this method to add a test to the queue - this.push = queue.push; - - - // Gives the position of a task in the queue - // Returns 0 if it is the current running task - // Returns -1 if not found - this.indexOf = function(testId) { - if (currentTask && currentTask.testId === testId) { - return 0; - } - - var position = -1; - if (queue.length() > 0) { - queue.tasks.forEach(function(task, index) { - if (task.data.testId === testId) { - position = index + 1; - } - }); - } - return position; - }; - - this.testComplete = function(testId) { - self.emit('testComplete', testId); - }; - - this.testFailed = function(testId) { - self.emit('testFailed', testId); - }; -}; - -// extend the EventEmitter class -util.inherits(testQueue, EventEmitter); - -module.exports = new testQueue(); \ No newline at end of file diff --git a/bin/server.js b/bin/server.js new file mode 100644 index 0000000..3f72983 --- /dev/null +++ b/bin/server.js @@ -0,0 +1,22 @@ +// Config file +var settings = require('../server_config/settings.json'); + +var express = require('express'); +var app = express(); +var server = require('http').createServer(app); +var bodyParser = require('body-parser'); +var compress = require('compression'); + +app.use(compress()); +app.use(bodyParser.urlencoded({ extended: false })); + + +// Initialize the controllers +var apiController = require('../lib/server/controllers/apiController')(app); +var uiController = require('../lib/server/controllers/uiController')(app); + + +// Launch the server +server.listen(settings.serverPort, function() { + console.log('Listening on port %d', server.address().port); +}); \ No newline at end of file diff --git a/lib/server/controllers/apiController.js b/lib/server/controllers/apiController.js new file mode 100644 index 0000000..fb9d7c6 --- /dev/null +++ b/lib/server/controllers/apiController.js @@ -0,0 +1,86 @@ +var debug = require('debug')('ylt:server'); + +var runsQueue = require('../datastores/runsQueue'); +var runsDatastore = require('../datastores/runsDatastore'); + + +function ApiController(app) { + 'use strict'; + + // Retrieve the list of all runs + /*app.get('/runs', function(req, res) { + // NOT YET + });*/ + + // Create a new run + app.post('/runs', function(req, res) { + + // Grab the test parameters + var run = { + // Generate a random run ID + _id: (Date.now()*1000 + Math.round(Math.random()*1000)).toString(36), + params: { + url: req.body.url, + waitForResponse: req.body.waitForResponse || true + } + }; + + // Add test to the testQueue + debug('Adding test %s to the queue', run._id); + var queuing = runsQueue.push(run._id); + + + // Save the run to the datastore + var position = runsQueue.getPosition(run._id); + run.status = { + statusCode: (position === 0) ? STATUS_RUNNING : STATUS_AWAITING, + position: position + }; + runsDatastore.add(run); + + + // Listening for position updates + queuing.progress(function(position) { + var savedRun = runsDatastore.get(run._id); + savedRun.status = { + statusCode: STATUS_AWAITING, + position: position + }; + runsDatastore.update(savedRun); + }); + + + queuing.then(function() { + + }); + + // The user doesn't not want to wait for the response + if (!params.waitForResponse) { + + // Sending just the test id + res.setHeader('Content-Type', 'application/javascript'); + res.send(JSON.stringify({ + testId: testId + })); + } + }); + + // Retrive one run by id + app.get('/run/:id', function(req, res) { + + }); + + // Delete one run by id + /*app.delete('/run/:id', function(req, res) { + // NOT YET + });*/ + + + var STATUS_AWAITING = 'awaiting'; + var STATUS_RUNNING = 'running'; + var STATUS_DONE = 'done'; + var STATUS_FAILED = 'failed'; + +} + +module.exports = ApiController; \ No newline at end of file diff --git a/lib/server/controllers/uiController.js b/lib/server/controllers/uiController.js new file mode 100644 index 0000000..e69de29 diff --git a/lib/server/datastores/resultsDatastore.js b/lib/server/datastores/resultsDatastore.js new file mode 100644 index 0000000..954f9b7 --- /dev/null +++ b/lib/server/datastores/resultsDatastore.js @@ -0,0 +1,9 @@ + + +function ResultsDatastore() { + 'use strict'; + + +} + +module.exports = ResultsDatastore; \ No newline at end of file diff --git a/lib/server/datastores/runsDatastore.js b/lib/server/datastores/runsDatastore.js new file mode 100644 index 0000000..c84ac4e --- /dev/null +++ b/lib/server/datastores/runsDatastore.js @@ -0,0 +1,36 @@ + + +function RunsDatastore() { + 'use strict'; + + // NOT PERSISTING RUNS + // For the moment, maybe one day + var runs = {}; + + + this.add = function(run) { + runs[run._id] = run; + }; + + this.get = function(runId) { + return runs[runId]; + }; + + this.update = function(run) { + runs[run._id] = run; + }; + + this.delete = function(runId) { + delete runs[runId]; + }; + + this.list = function() { + var runsArray = []; + Object.keys(runs).forEach(function(key) { + runsArray.push(runs[key]); + }); + return runsArray; + }; +} + +module.exports = RunsDatastore; \ No newline at end of file diff --git a/lib/server/datastores/runsQueue.js b/lib/server/datastores/runsQueue.js new file mode 100644 index 0000000..ad1a6b8 --- /dev/null +++ b/lib/server/datastores/runsQueue.js @@ -0,0 +1,73 @@ +var Q = require('q'); + + +function RunsQueue() { + 'use strict'; + + var queue = []; + + + this.push = function(runId) { + var deferred = Q.defer(); + + if (queue.length === 0) { + + // The queue is empty, let's run immediatly + queue.push({ + runId: runId + }); + + deferred.resolve(); + + } else { + + queue.push({ + runId: runId, + positionChangedCallback: function(position) { + deferred.notify(position); + }, + itIsTimeCallback: function() { + deferred.resolve(); + } + }); + } + + return deferred.promise; + }; + + + this.getPosition = function(runId) { + // Position 0 means it's a work in progress (a run is removed AFTER it is finished, not before) + var position = -1; + + queue.some(function(run, index) { + if (run.runId === runId) { + position = index; + return true; + } + return false; + }); + + return position; + }; + + + this.remove = function(runId) { + var position = this.getPosition(runId); + if (position >= 0) { + queue.splice(position, 1); + } + + // Update other runs' positions + queue.forEach(function(run, index) { + if (index === 0 && run.itIsTimeCallback) { + run.itIsTimeCallback(); + } else if (index > 0 && run.positionChangedCallback) { + run.positionChangedCallback(index); + } + }); + + }; +} + +module.exports = RunsQueue; \ No newline at end of file diff --git a/package.json b/package.json index 5bb8f24..29bca24 100644 --- a/package.json +++ b/package.json @@ -14,7 +14,7 @@ "body-parser": "~1.9.2", "compression": "~1.2.0", "debug": "^2.1.0", - "express": "~4.10.1", + "express": "~4.10.4", "phantomas": "1.7.0", "socket.io": "~1.2.0" }, diff --git a/test/api/phantomasWrapperTest.js b/test/api/phantomasWrapperTest.js index 785bdbe..904915e 100644 --- a/test/api/phantomasWrapperTest.js +++ b/test/api/phantomasWrapperTest.js @@ -23,9 +23,9 @@ describe('phantomasWrapper', function() { data.should.have.a.property('generator'); data.generator.should.contain('phantomas'); data.should.have.a.property('url').that.equals(url); - data.should.have.a.property('metrics').that.is.an('object').not.empty; - data.should.have.a.property('offenders').that.is.an('object').not.empty; - data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty; + data.should.have.a.property('metrics').that.is.an('object').not.empty(); + data.should.have.a.property('offenders').that.is.an('object').not.empty(); + data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty(); done(); }).fail(function(err) { @@ -73,9 +73,9 @@ describe('phantomasWrapper', function() { data.should.have.a.property('generator'); data.generator.should.contain('phantomas'); data.should.have.a.property('url').that.equals(url); - data.should.have.a.property('metrics').that.is.an('object').not.empty; - data.should.have.a.property('offenders').that.is.an('object').not.empty; - data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty; + data.should.have.a.property('metrics').that.is.an('object').not.empty(); + data.should.have.a.property('offenders').that.is.an('object').not.empty(); + data.offenders.should.have.a.property('javascriptExecutionTree').that.is.a('array').not.empty(); done(); }).fail(function(err) { diff --git a/test/api/yellowlabtoolsTest.js b/test/api/yellowlabtoolsTest.js index 89c36ab..b1dd85e 100644 --- a/test/api/yellowlabtoolsTest.js +++ b/test/api/yellowlabtoolsTest.js @@ -34,7 +34,7 @@ describe('yellowlabtools', function() { this.timeout(15000); // Check if console.log is called - sinon.spy(console, 'log') + sinon.spy(console, 'log'); var url = 'http://localhost:8388/simple-page.html'; @@ -75,6 +75,7 @@ describe('yellowlabtools', function() { "offenders": ["body > h1[1]"] }); + /*jshint expr: true*/ console.log.should.not.have.been.called; done(); diff --git a/test/server/runsDatastoreTest.js b/test/server/runsDatastoreTest.js new file mode 100644 index 0000000..750956d --- /dev/null +++ b/test/server/runsDatastoreTest.js @@ -0,0 +1,41 @@ +var should = require('chai').should(); +var runsDatastore = require('../../lib/server/datastores/runsDatastore'); + +describe('runsDatastore', function() { + + var datastore = new runsDatastore(); + + var randomId = Math.round(Math.random() * 100000); + + it('should accept a new run', function() { + datastore.should.have.a.property('add').that.is.a('function'); + + datastore.add({ + _id: randomId, + otherData: 123456789 + }); + }); + + it('should have stored the run', function() { + datastore.should.have.a.property('get').that.is.a('function'); + + var run = datastore.get(randomId); + + run.should.have.a.property('_id').that.equals(randomId); + }); + + it('should have exactly 1 run in the store', function() { + var runs = datastore.list(); + runs.should.be.a('array'); + runs.should.have.length(1); + runs[0].should.have.a.property('_id').that.equals(randomId); + }); + + it('should delete the run', function() { + datastore.delete(randomId); + + var runs = datastore.list(); + runs.should.be.a('array'); + runs.should.have.length(0); + }); +}); diff --git a/test/server/runsQueueTest.js b/test/server/runsQueueTest.js new file mode 100644 index 0000000..45a2a23 --- /dev/null +++ b/test/server/runsQueueTest.js @@ -0,0 +1,64 @@ +var should = require('chai').should(); +var runsQueue = require('../../lib/server/datastores/runsQueue.js'); + +describe('runsQueue', function() { + + var queue = new runsQueue(); + var cccRun = null; + + it('should accept a new runId', function(done) { + queue.should.have.a.property('push').that.is.a('function'); + + var aaaRun = queue.push('aaa'); + queue.push('bbb'); + + aaaRun.then(function() { + done(); + }); + }); + + it('should return the right positions', function() { + var aaaPosition = queue.getPosition('aaa'); + aaaPosition.should.equal(0); + + var bbbPosition = queue.getPosition('bbb'); + bbbPosition.should.equal(1); + + var cccPosition = queue.getPosition('ccc'); + cccPosition.should.equal(-1); + }); + + it('should refresh runs\' positions', function(done) { + cccRun = queue.push('ccc'); + + cccRun.progress(function(position) { + position.should.equal(1); + + var positionDoubleCheck = queue.getPosition('ccc'); + positionDoubleCheck.should.equal(1); + + done(); + }); + + queue.remove('aaa'); + }); + + it('should fulfill the promise when first in the line', function(done) { + cccRun.then(function() { + done(); + }); + + queue.remove('bbb'); + }); + + it('should not keep removed runs', function() { + var aaaPosition = queue.getPosition('aaa'); + aaaPosition.should.equal(-1); + + var bbbPosition = queue.getPosition('bbb'); + bbbPosition.should.equal(-1); + + var cccPosition = queue.getPosition('ccc'); + cccPosition.should.equal(0); + }); +}); \ No newline at end of file diff --git a/test/server/testQueueTest.js b/test/server/testQueueTest.js deleted file mode 100644 index 6ae1be6..0000000 --- a/test/server/testQueueTest.js +++ /dev/null @@ -1,23 +0,0 @@ -var should = require('chai').should(); -var testQueue = require('../../app/lib/testQueue.js'); - -describe('testQueue', function() { - - var url = 'http://www.not.existing'; - - it('should accept a new test with method push', function() { - testQueue.should.have.property('push').that.is.a('function'); - - var task = { - testId: 'aaaaa', - url: url, - options: {} - }; - - testQueue.push(task, function(err, json, results) { - done(); - }); - - }); - -}); \ No newline at end of file