From 4c8f6e0fec61baf88b89a2512b444e88e43fc061 Mon Sep 17 00:00:00 2001 From: Christian Budde Christensen Date: Thu, 28 Jan 2016 20:46:01 +0100 Subject: [PATCH] feat: Add possibility to stop a karma server Add detached mode using the `karma start --detached` command. Add middleware for stopping a server (detached or not). Described the detached option. --- docs/config/01-configuration-file.md | 12 ++++++ lib/cli.js | 37 +++++++++++++++- lib/config.js | 1 + lib/middleware/runner.js | 2 +- lib/middleware/stopper.js | 18 ++++++++ lib/stopper.js | 33 +++++++++++++++ lib/web-server.js | 2 + test/e2e/steps/core_steps.js | 63 ++++++++++++++++++++-------- test/e2e/stop.feature | 42 +++++++++++++++++++ 9 files changed, 191 insertions(+), 19 deletions(-) create mode 100644 lib/middleware/stopper.js create mode 100644 lib/stopper.js create mode 100644 test/e2e/stop.feature diff --git a/docs/config/01-configuration-file.md b/docs/config/01-configuration-file.md index ea58e3c0e..6f4a73348 100644 --- a/docs/config/01-configuration-file.md +++ b/docs/config/01-configuration-file.md @@ -249,6 +249,18 @@ customHeaders: [{ }] ``` + +## detached +**Type:** Boolean + +**Default:** `false` + +**CLI:** `--detached` + +**Description:** When true, this will start the karma server in another process, writing no output to the console. +The server can be stopped using the `karma stop` command. + + ## exclude **Type:** Array diff --git a/lib/cli.js b/lib/cli.js index d8198abcf..edeb56df3 100644 --- a/lib/cli.js +++ b/lib/cli.js @@ -1,6 +1,7 @@ var path = require('path') var optimist = require('optimist') var fs = require('graceful-fs') +var spawn = require('child_process').spawn var Server = require('./server') var helper = require('./helper') @@ -159,6 +160,7 @@ var describeStart = function () { ' $0 start [] []') .describe('port', ' Port where the server is running.') .describe('auto-watch', 'Auto watch source files and run on change.') + .describe('detached', 'Detach the server.') .describe('no-auto-watch', 'Do not watch source files.') .describe('log-level', ' Level of logging.') .describe('colors', 'Use colors when reporting and printing logs.') @@ -190,6 +192,17 @@ var describeRun = function () { .describe('no-colors', 'Do not use colors when reporting or printing logs.') } +var describeStop = function () { + optimist + .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + + 'STOP - Stop the server (requires running server).\n\n' + + 'Usage:\n' + + ' $0 run [] []') + .describe('port', ' Port where the server is listening.') + .describe('log-level', ' Level of logging.') + .describe('help', 'Print usage.') +} + var describeCompletion = function () { optimist .usage('Karma - Spectacular Test Runner for JavaScript.\n\n' + @@ -199,6 +212,21 @@ var describeCompletion = function () { .describe('help', 'Print usage.') } +var startServer = function (config) { + var args = process.argv + var detachedIndex = args.indexOf('--detached') + if (detachedIndex === -1) { + new Server(config).start() + return + } + args.splice(detachedIndex, 1) + var child = spawn(args[0], args.slice(1), { + detached: true, + stdio: ['ignore', 'ignore', 'ignore'] + }) + child.unref() +} + exports.process = function () { var argv = optimist.parse(argsBeforeDoubleDash(process.argv.slice(2))) var options = { @@ -215,6 +243,10 @@ exports.process = function () { options.clientArgs = parseClientArgs(process.argv) break + case 'stop': + describeStop() + break + case 'init': describeInit() break @@ -243,11 +275,14 @@ exports.run = function () { switch (config.cmd) { case 'start': - new Server(config).start() + startServer(config) break case 'run': require('./runner').run(config) break + case 'stop': + require('./stopper').stop(config) + break case 'init': require('./init').init(config) break diff --git a/lib/config.js b/lib/config.js index 40621b402..b4a3accdc 100644 --- a/lib/config.js +++ b/lib/config.js @@ -266,6 +266,7 @@ var Config = function () { this.concurrency = Infinity this.failOnEmptyTestSuite = true this.retryLimit = 2 + this.detached = false } var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' + diff --git a/lib/middleware/runner.js b/lib/middleware/runner.js index 00f03e201..7c0d13dbc 100644 --- a/lib/middleware/runner.js +++ b/lib/middleware/runner.js @@ -1,5 +1,5 @@ /** - * Runner middleware is reponsible for communication with `karma run`. + * Runner middleware is responsible for communication with `karma run`. * * It basically triggers a test run and streams stdout back. */ diff --git a/lib/middleware/stopper.js b/lib/middleware/stopper.js new file mode 100644 index 000000000..ecd2b0b90 --- /dev/null +++ b/lib/middleware/stopper.js @@ -0,0 +1,18 @@ +/** + * Stopper middleware is responsible for communicating with `karma stop`. + */ + +var log = require('../logger').create('middleware:stopper') + +var createStopperMiddleware = function (urlRoot) { + return function (request, response, next) { + if (request.url !== urlRoot + 'stop') return next() + response.writeHead(200) + log.info('Stopping server') + response.end('OK') + process.exit(0) + } +} + +createStopperMiddleware.$inject = ['config.urlRoot'] +exports.create = createStopperMiddleware diff --git a/lib/stopper.js b/lib/stopper.js new file mode 100644 index 000000000..e67480792 --- /dev/null +++ b/lib/stopper.js @@ -0,0 +1,33 @@ +var http = require('http') + +var cfg = require('./config') +var logger = require('./logger') + +exports.stop = function (config) { + logger.setupFromConfig(config) + var log = logger.create('stopper') + config = cfg.parseConfig(config.configFile, config) + var options = { + hostname: config.hostname, + path: config.urlRoot + 'stop', + port: config.port, + method: 'GET' + } + + var request = http.request(options) + + request.on('response', function (response) { + log.info('Server stopped.') + process.exit(response.statusCode === 200 ? 0 : 1) + }) + + request.on('error', function (e) { + if (e.code === 'ECONNREFUSED') { + log.error('There is no server listening on port %d', options.port) + process.exit(1, e.code) + } else { + throw e + } + }) + request.end() +} diff --git a/lib/web-server.js b/lib/web-server.js index 4dfc248c7..fc5e7eb99 100644 --- a/lib/web-server.js +++ b/lib/web-server.js @@ -7,6 +7,7 @@ var Promise = require('bluebird') var common = require('./middleware/common') var runnerMiddleware = require('./middleware/runner') +var stopperMiddleware = require('./middleware/stopper') var stripHostMiddleware = require('./middleware/strip_host') var karmaMiddleware = require('./middleware/karma') var sourceFilesMiddleware = require('./middleware/source_files') @@ -56,6 +57,7 @@ var createWebServer = function (injector, emitter, fileList) { var handler = connect() .use(injector.invoke(runnerMiddleware.create)) + .use(injector.invoke(stopperMiddleware.create)) .use(injector.invoke(stripHostMiddleware.create)) .use(injector.invoke(karmaMiddleware.create)) .use(injector.invoke(sourceFilesMiddleware.create)) diff --git a/test/e2e/steps/core_steps.js b/test/e2e/steps/core_steps.js index 6416a7a8c..79345abdc 100644 --- a/test/e2e/steps/core_steps.js +++ b/test/e2e/steps/core_steps.js @@ -15,19 +15,18 @@ module.exports = function coreSteps () { var cleansingNeeded = true var additionalArgs = [] - var cleanseIfNeeded = (function (_this) { - return function () { - if (cleansingNeeded) { - try { - rimraf.sync(tmpDir) - } catch (e) {} + var cleanseIfNeeded = function () { + if (cleansingNeeded) { + try { + rimraf.sync(tmpDir) + } catch (e) { + } - cleansingNeeded = false + cleansingNeeded = false - return cleansingNeeded - } + return cleansingNeeded } - })(this) + } this.Given(/^a configuration with:$/, function (fileContent, callback) { cleanseIfNeeded() @@ -40,17 +39,39 @@ module.exports = function coreSteps () { return callback() }) - this.When(/^I (run|runOut|start|init) Karma$/, function (command, callback) { + this.When(/^I start a server in background/, function (callback) { this.writeConfigFile(tmpDir, tmpConfigFile, (function (_this) { return function (err, hash) { if (err) { return callback.fail(new Error(err)) } + var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile) + var runtimePath = path.join(baseDir, 'bin', 'karma') + _this.child = spawn('' + runtimePath, ['start', '--log-level', 'debug', configFile]) + _this.child.stdout.on('data', function () { + callback() + callback = function () { + } + }) + _this.child.on('exit', function (exitCode) { + _this.childExitCode = exitCode + }) + } + })(this)) + }) + + this.When(/^I (run|runOut|start|init|stop) Karma( with log-level ([a-z]+))?$/, function (command, withLogLevel, level, callback) { + this.writeConfigFile(tmpDir, tmpConfigFile, (function (_this) { + return function (err, hash) { + if (err) { + return callback.fail(new Error(err)) + } + level = withLogLevel === undefined ? 'warn' : level var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile) var runtimePath = path.join(baseDir, 'bin', 'karma') var execKarma = function (done) { - var cmd = runtimePath + ' ' + command + ' --log-level warn ' + configFile + ' ' + additionalArgs + var cmd = runtimePath + ' ' + command + ' --log-level ' + level + ' ' + configFile + ' ' + additionalArgs return exec(cmd, { cwd: baseDir @@ -107,11 +128,10 @@ module.exports = function coreSteps () { })(this)) }) - this.Then(/^it passes with( no debug)?:$/, {timeout: 10 * 1000}, function (noDebug, expectedOutput, callback) { - noDebug = noDebug === ' no debug' + this.Then(/^it passes with( no debug| like)?:$/, {timeout: 10 * 1000}, function (mode, expectedOutput, callback) { + var noDebug = mode === ' no debug' + var like = mode === ' like' var actualOutput = this.lastRun.stdout.toString() - var actualError = this.lastRun.error - var actualStderr = this.lastRun.stderr.toString() var lines if (noDebug) { @@ -120,12 +140,15 @@ module.exports = function coreSteps () { }) actualOutput = lines.join('\n') } + if (like && actualOutput.indexOf(expectedOutput) >= 0) { + return callback() + } if (actualOutput.indexOf(expectedOutput) === 0) { return callback() } - if (actualError || actualStderr) { + if (actualOutput) { return callback(new Error('Expected output to match the following:\n ' + expectedOutput + '\nGot:\n ' + actualOutput)) } @@ -159,4 +182,10 @@ module.exports = function coreSteps () { callback(new Error('Expected output to match the following:\n ' + expectedOutput + '\nGot:\n ' + actualOutput)) } }) + + this.Then(/^The server is dead( with exit code ([0-9]+))?$/, function (withExitCode, code, callback) { + if (this.childExitCode === undefined) return callback(new Error('Server has not exited.')) + if (code === undefined || parseInt(code, 10) === this.childExitCode) return callback() + callback(new Error('Exit-code mismatch')) + }) } diff --git a/test/e2e/stop.feature b/test/e2e/stop.feature new file mode 100644 index 000000000..75f2f3785 --- /dev/null +++ b/test/e2e/stop.feature @@ -0,0 +1,42 @@ +Feature: Stop karma + TODO write this + + Scenario: A server can't be stopped if it isn't running + When I stop Karma + Then it fails with like: + """ + ERROR \[stopper\]: There is no server listening on port [0-9]+ + """ + + Scenario: A server can be stopped + Given a configuration with: + """ + files = ['basic/plus.js', 'basic/test.js']; + browsers = ['PhantomJS']; + plugins = [ + 'karma-jasmine', + 'karma-phantomjs-launcher' + ]; + singleRun = false; + """ + When I start a server in background + And I stop Karma + Then The server is dead with exit code 0 + + Scenario: A server can be stopped and give informative output + Given a configuration with: + """ + files = ['basic/plus.js', 'basic/test.js']; + browsers = ['PhantomJS']; + plugins = [ + 'karma-jasmine', + 'karma-phantomjs-launcher' + ]; + singleRun = false; + """ + When I start a server in background + And I stop Karma with log-level info + Then it passes with like: + """ + Server stopped. + """