diff --git a/docs/config/01-configuration-file.md b/docs/config/01-configuration-file.md index 6198f6ea5..280615607 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 520dd43d3..3c23f0596 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.') @@ -187,6 +189,17 @@ var describeRun = function () { .describe('help', 'Print usage.') } +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' + @@ -196,6 +209,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 = { @@ -212,6 +240,10 @@ exports.process = function () { options.clientArgs = parseClientArgs(process.argv) break + case 'stop': + describeStop() + break + case 'init': describeInit() break @@ -240,11 +272,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 7c90eb459..6f5f0d8f9 100644 --- a/lib/config.js +++ b/lib/config.js @@ -267,6 +267,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 377b1e39d..06334fc44 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 f84570cfa..dc200fda6 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 999214f20..0adcdd6a8 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,12 @@ 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) { + setTimeout((function (_this) { + 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')) + })(this), 1000) + }) } diff --git a/test/e2e/stop.feature b/test/e2e/stop.feature new file mode 100644 index 000000000..d9eb0e4c9 --- /dev/null +++ b/test/e2e/stop.feature @@ -0,0 +1,44 @@ +Feature: Stop karma + In order to use Karma + As a person who wants to write great tests + I want to be able to stop Karma. + + 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. + """