diff --git a/lib/linter.js b/lib/linter.js index f8c86b6..b5ff6c0 100644 --- a/lib/linter.js +++ b/lib/linter.js @@ -31,7 +31,7 @@ function translateOptions(cliOptions, cwd) { const eslintCache = new LRU(10); -function linter(cwd, args, text) { +exports.lint = function (cwd, args, text) { process.chdir(cwd); let cwdDeps = eslintCache.get(cwd); if (!cwdDeps) { @@ -116,9 +116,9 @@ function linter(cwd, args, text) { output += '\n# exit 1'; } return output; -} +}; -linter.getStatus = function () { +exports.getStatus = function () { const { keys } = eslintCache; if (keys.length === 0) { return 'Running, no instances cached'; @@ -128,5 +128,3 @@ linter.getStatus = function () { } return `Running, ${keys.length} instances cached`; }; - -module.exports = linter; diff --git a/lib/server.js b/lib/server.js index c8e2401..de49ea1 100644 --- a/lib/server.js +++ b/lib/server.js @@ -5,17 +5,19 @@ const net = require('net'); const portfile = require('./portfile'); const linter = require('./linter'); +function forceClose(con) { + try { + con.end('Server is stopping...\n# exit 1'); + } catch (ignore) { + // Nothing we can do + } +} + exports.start = function () { const token = crypto.randomBytes(8).toString('hex'); - let openConnections = []; - function forceClose(con) { - con.write('Server is stopping...\n# exit 1'); - con.end(); - } - const server = net.createServer({ allowHalfOpen: true }, (con) => { @@ -38,14 +40,13 @@ exports.start = function () { } data = data.substring(p + 1); if (data === 'stop') { + openConnections.forEach(forceClose); con.end(); server.close(); - openConnections.forEach(forceClose); return; } if (data === 'status') { - con.write(linter.getStatus()); - con.end(); + con.end(linter.getStatus()); return; } let cwd, args, text; @@ -65,7 +66,7 @@ exports.start = function () { args = parts.slice(1); } try { - con.write(linter(cwd, args, text)); + con.write(linter.lint(cwd, args, text)); } catch (e) { con.write(`${e.toString()}\n# exit 1`); } diff --git a/test/server-test.js b/test/server-test.js new file mode 100644 index 0000000..994c29f --- /dev/null +++ b/test/server-test.js @@ -0,0 +1,211 @@ +/*eslint-env mocha*/ +'use strict'; + +const net = require('net'); +const crypto = require('crypto'); +const EventEmitter = require('events'); +const { assert, refute, sinon } = require('@sinonjs/referee-sinon'); +const server = require('../lib/server'); +const linter = require('../lib/linter'); +const portfile = require('../lib/portfile'); + +function createConnection() { + const connection = new EventEmitter(); + connection.write = sinon.fake(); + connection.end = sinon.fake(); + return connection; +} + +describe('server', () => { + const token = 'c2d003e2b9de9e70'; + let net_server; + let connection; + + beforeEach(() => { + net_server = new EventEmitter(); + net_server.listen = sinon.fake(); + net_server.close = sinon.fake(); + net_server.address = sinon.fake.returns({ port: 8765 }); + connection = createConnection(); + sinon.replace(net, 'createServer', sinon.fake.returns(net_server)); + sinon.replace(portfile, 'write', sinon.fake()); + sinon.replace(crypto, 'randomBytes', + sinon.fake.returns(Buffer.from(token, 'hex'))); + }); + + afterEach(() => { + sinon.restore(); + }); + + function start() { + server.start(); + net_server.listen.callback(); + } + + function connect(connection) { + net_server.emit('connection', connection); + net.createServer.callback(connection); + } + + function request(connection, text) { + connect(connection); + if (text) { + connection.emit('data', text); + } + connection.emit('end'); + } + + it('starts server and listens on random port', () => { + const instance = server.start(); + + assert.equals(instance, net_server); + assert.calledOnceWith(net.createServer, { + allowHalfOpen: true + }, sinon.match.func); + assert.calledOnceWith(net_server.listen, 0, '127.0.0.1', sinon.match.func); + }); + + it('writes portfile when listen yields', () => { + server.start(); + + net_server.listen.callback(); + + assert.calledOnceWith(crypto.randomBytes, 8); + assert.calledOnceWith(portfile.write, 8765, token); + }); + + it('closes connection without writing anything for empty request', () => { + start(); + + request(connection); + + refute.called(connection.write); + assert.calledOnce(connection.end); + }); + + describe('stop', () => { + + it('closes connection and server for "stop" command', () => { + start(); + + request(connection, `${token} stop`); + + refute.called(connection.write); + assert.calledOnce(connection.end); + assert.calledOnce(net_server.close); + }); + + it('closes any other pending connection on "stop" command', () => { + start(); + const one = createConnection(); + const two = createConnection(); + connect(one); + connect(two); + + request(connection, `${token} stop`); + + assert.calledOnceWith(one.end, 'Server is stopping...\n# exit 1'); + assert.calledOnceWith(two.end, 'Server is stopping...\n# exit 1'); + assert.calledOnce(connection.end); + assert.calledWithExactly(connection.end); + }); + + it('ignores failures when attempting to close client connection', () => { + start(); + const one = createConnection(); + one.end = sinon.fake.throws(new Error()); + const two = createConnection(); + two.end = sinon.fake.throws(new Error()); + + connect(one); + connect(two); + + request(connection, `${token} stop`); + + assert.calledOnce(one.end); + assert.calledOnce(two.end); + assert.calledOnce(connection.end); + assert.calledOnce(net_server.close); + }); + + it('does not process "stop" if token is invalid', () => { + start(); + + request(connection, '123456789abcdef status'); + + assert.calledOnce(connection.end); + assert.calledWithExactly(connection.end); + refute.called(connection.write); + refute.called(net_server.close); + }); + + }); + + describe('status', () => { + + it('prints linter status and closes connection', () => { + sinon.replace(linter, 'getStatus', sinon.fake.returns('Oh, hi!\n')); + start(); + + request(connection, `${token} status`); + + assert.calledOnceWith(connection.end, 'Oh, hi!\n'); + }); + + it('does not process "status" if token is invalid', () => { + sinon.replace(linter, 'getStatus', sinon.fake()); + start(); + + request(connection, '123456789abcdef status'); + + assert.calledOnce(connection.end); + assert.calledWithExactly(connection.end); + refute.called(connection.write); + refute.called(linter.getStatus); + }); + + }); + + describe('lint', () => { + const json = { + cwd: '/some/path', + args: ['--some', '--args'], + text: '"Some text"' + }; + + it('invokes linter with JSON arguments', () => { + sinon.replace(linter, 'lint', sinon.fake.returns('Oh, hi!\n')); + start(); + + request(connection, `${token} ${JSON.stringify(json)}`); + + assert.calledOnceWith(linter.lint, json.cwd, json.args, json.text); + assert.calledOnceWith(connection.write, 'Oh, hi!\n'); + assert.calledOnce(connection.end); + }); + + it('invokes linter with plain text arguments', () => { + sinon.replace(linter, 'lint', sinon.fake.returns('Oh, hi!\n')); + start(); + + request(connection, + `${token} ${json.cwd} ${json.args.join(' ')}\n${json.text}`); + + assert.calledOnceWith(linter.lint, json.cwd, json.args, json.text); + assert.calledOnceWith(connection.write, 'Oh, hi!\n'); + assert.calledOnce(connection.end); + }); + + it('handles exception from linter', () => { + sinon.replace(linter, 'lint', sinon.fake.throws(new Error('Whatever'))); + start(); + + request(connection, `${token} ${JSON.stringify(json)}`); + + assert.calledOnceWith(connection.write, 'Error: Whatever\n# exit 1'); + assert.calledOnce(connection.end); + }); + + }); + +});