Skip to content

Commit

Permalink
feat: Add possibility to stop a karma server
Browse files Browse the repository at this point in the history
Add detached mode using the `karma start --detached` command.
Add middleware for stopping a server (detached or not).
Described the detached option.
  • Loading branch information
budde377 committed Feb 8, 2016
1 parent 0d3bee4 commit db79170
Show file tree
Hide file tree
Showing 9 changed files with 193 additions and 19 deletions.
12 changes: 12 additions & 0 deletions docs/config/01-configuration-file.md
Original file line number Diff line number Diff line change
Expand Up @@ -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

Expand Down
37 changes: 36 additions & 1 deletion lib/cli.js
Original file line number Diff line number Diff line change
@@ -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')
Expand Down Expand Up @@ -159,6 +160,7 @@ var describeStart = function () {
' $0 start [<configFile>] [<options>]')
.describe('port', '<integer> 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', '<disable | error | warn | info | debug> Level of logging.')
.describe('colors', 'Use colors when reporting and printing logs.')
Expand Down Expand Up @@ -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 [<configFile>] [<options>]')
.describe('port', '<integer> Port where the server is listening.')
.describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
.describe('help', 'Print usage.')
}

var describeCompletion = function () {
optimist
.usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
Expand All @@ -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 = {
Expand All @@ -215,6 +243,10 @@ exports.process = function () {
options.clientArgs = parseClientArgs(process.argv)
break

case 'stop':
describeStop()
break

case 'init':
describeInit()
break
Expand Down Expand Up @@ -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
Expand Down
1 change: 1 addition & 0 deletions lib/config.js
Original file line number Diff line number Diff line change
Expand Up @@ -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' +
Expand Down
2 changes: 1 addition & 1 deletion lib/middleware/runner.js
Original file line number Diff line number Diff line change
@@ -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.
*/
Expand Down
18 changes: 18 additions & 0 deletions lib/middleware/stopper.js
Original file line number Diff line number Diff line change
@@ -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
33 changes: 33 additions & 0 deletions lib/stopper.js
Original file line number Diff line number Diff line change
@@ -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()
}
2 changes: 2 additions & 0 deletions lib/web-server.js
Original file line number Diff line number Diff line change
Expand Up @@ -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')
Expand Down Expand Up @@ -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))
Expand Down
65 changes: 48 additions & 17 deletions test/e2e/steps/core_steps.js
Original file line number Diff line number Diff line change
Expand Up @@ -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()
Expand All @@ -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
Expand Down Expand Up @@ -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) {
Expand All @@ -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))
}

Expand Down Expand Up @@ -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)
})
}
42 changes: 42 additions & 0 deletions test/e2e/stop.feature
Original file line number Diff line number Diff line change
@@ -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.
"""

0 comments on commit db79170

Please sign in to comment.