Skip to content

Commit 66ae80b

Browse files
committed
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.
1 parent 0f1b1ec commit 66ae80b

9 files changed

Lines changed: 195 additions & 19 deletions

File tree

docs/config/01-configuration-file.md

Lines changed: 12 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -249,6 +249,18 @@ customHeaders: [{
249249
}]
250250
```
251251

252+
253+
## detached
254+
**Type:** Boolean
255+
256+
**Default:** `false`
257+
258+
**CLI:** `--detached`
259+
260+
**Description:** When true, this will start the karma server in another process, writing no output to the console.
261+
The server can be stopped using the `karma stop` command.
262+
263+
252264
## exclude
253265
**Type:** Array
254266

lib/cli.js

Lines changed: 36 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,6 +1,7 @@
11
var path = require('path')
22
var optimist = require('optimist')
33
var fs = require('graceful-fs')
4+
var spawn = require('child_process').spawn
45

56
var Server = require('./server')
67
var helper = require('./helper')
@@ -159,6 +160,7 @@ var describeStart = function () {
159160
' $0 start [<configFile>] [<options>]')
160161
.describe('port', '<integer> Port where the server is running.')
161162
.describe('auto-watch', 'Auto watch source files and run on change.')
163+
.describe('detached', 'Detach the server.')
162164
.describe('no-auto-watch', 'Do not watch source files.')
163165
.describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
164166
.describe('colors', 'Use colors when reporting and printing logs.')
@@ -187,6 +189,17 @@ var describeRun = function () {
187189
.describe('help', 'Print usage.')
188190
}
189191

192+
var describeStop = function () {
193+
optimist
194+
.usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
195+
'STOP - Stop the server (requires running server).\n\n' +
196+
'Usage:\n' +
197+
' $0 run [<configFile>] [<options>]')
198+
.describe('port', '<integer> Port where the server is listening.')
199+
.describe('log-level', '<disable | error | warn | info | debug> Level of logging.')
200+
.describe('help', 'Print usage.')
201+
}
202+
190203
var describeCompletion = function () {
191204
optimist
192205
.usage('Karma - Spectacular Test Runner for JavaScript.\n\n' +
@@ -196,6 +209,21 @@ var describeCompletion = function () {
196209
.describe('help', 'Print usage.')
197210
}
198211

212+
var startServer = function (config) {
213+
var args = process.argv
214+
var detachedIndex = args.indexOf('--detached')
215+
if (detachedIndex === -1) {
216+
new Server(config).start()
217+
return
218+
}
219+
args.splice(detachedIndex, 1)
220+
var child = spawn(args[0], args.slice(1), {
221+
detached: true,
222+
stdio: ['ignore', 'ignore', 'ignore']
223+
})
224+
child.unref()
225+
}
226+
199227
exports.process = function () {
200228
var argv = optimist.parse(argsBeforeDoubleDash(process.argv.slice(2)))
201229
var options = {
@@ -212,6 +240,10 @@ exports.process = function () {
212240
options.clientArgs = parseClientArgs(process.argv)
213241
break
214242

243+
case 'stop':
244+
describeStop()
245+
break
246+
215247
case 'init':
216248
describeInit()
217249
break
@@ -240,11 +272,14 @@ exports.run = function () {
240272

241273
switch (config.cmd) {
242274
case 'start':
243-
new Server(config).start()
275+
startServer(config)
244276
break
245277
case 'run':
246278
require('./runner').run(config)
247279
break
280+
case 'stop':
281+
require('./stopper').stop(config)
282+
break
248283
case 'init':
249284
require('./init').init(config)
250285
break

lib/config.js

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -267,6 +267,7 @@ var Config = function () {
267267
this.concurrency = Infinity
268268
this.failOnEmptyTestSuite = true
269269
this.retryLimit = 2
270+
this.detached = false
270271
}
271272

272273
var CONFIG_SYNTAX_HELP = ' module.exports = function(config) {\n' +

lib/middleware/runner.js

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,5 @@
11
/**
2-
* Runner middleware is reponsible for communication with `karma run`.
2+
* Runner middleware is responsible for communication with `karma run`.
33
*
44
* It basically triggers a test run and streams stdout back.
55
*/

lib/middleware/stopper.js

Lines changed: 18 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,18 @@
1+
/**
2+
* Stopper middleware is responsible for communicating with `karma stop`.
3+
*/
4+
5+
var log = require('../logger').create('middleware:stopper')
6+
7+
var createStopperMiddleware = function (urlRoot) {
8+
return function (request, response, next) {
9+
if (request.url !== urlRoot + 'stop') return next()
10+
response.writeHead(200)
11+
log.info('Stopping server')
12+
response.end('OK')
13+
process.exit(0)
14+
}
15+
}
16+
17+
createStopperMiddleware.$inject = ['config.urlRoot']
18+
exports.create = createStopperMiddleware

lib/stopper.js

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
var http = require('http')
2+
3+
var cfg = require('./config')
4+
var logger = require('./logger')
5+
6+
exports.stop = function (config) {
7+
logger.setupFromConfig(config)
8+
var log = logger.create('stopper')
9+
config = cfg.parseConfig(config.configFile, config)
10+
var options = {
11+
hostname: config.hostname,
12+
path: config.urlRoot + 'stop',
13+
port: config.port,
14+
method: 'GET'
15+
}
16+
17+
var request = http.request(options)
18+
19+
request.on('response', function (response) {
20+
log.info('Server stopped.')
21+
process.exit(response.statusCode === 200 ? 0 : 1)
22+
})
23+
24+
request.on('error', function (e) {
25+
if (e.code === 'ECONNREFUSED') {
26+
log.error('There is no server listening on port %d', options.port)
27+
process.exit(1, e.code)
28+
} else {
29+
throw e
30+
}
31+
})
32+
request.end()
33+
}

lib/web-server.js

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var Promise = require('bluebird')
77

88
var common = require('./middleware/common')
99
var runnerMiddleware = require('./middleware/runner')
10+
var stopperMiddleware = require('./middleware/stopper')
1011
var stripHostMiddleware = require('./middleware/strip_host')
1112
var karmaMiddleware = require('./middleware/karma')
1213
var sourceFilesMiddleware = require('./middleware/source_files')
@@ -56,6 +57,7 @@ var createWebServer = function (injector, emitter, fileList) {
5657

5758
var handler = connect()
5859
.use(injector.invoke(runnerMiddleware.create))
60+
.use(injector.invoke(stopperMiddleware.create))
5961
.use(injector.invoke(stripHostMiddleware.create))
6062
.use(injector.invoke(karmaMiddleware.create))
6163
.use(injector.invoke(sourceFilesMiddleware.create))

test/e2e/steps/core_steps.js

Lines changed: 48 additions & 17 deletions
Original file line numberDiff line numberDiff line change
@@ -15,19 +15,18 @@ module.exports = function coreSteps () {
1515
var cleansingNeeded = true
1616
var additionalArgs = []
1717

18-
var cleanseIfNeeded = (function (_this) {
19-
return function () {
20-
if (cleansingNeeded) {
21-
try {
22-
rimraf.sync(tmpDir)
23-
} catch (e) {}
18+
var cleanseIfNeeded = function () {
19+
if (cleansingNeeded) {
20+
try {
21+
rimraf.sync(tmpDir)
22+
} catch (e) {
23+
}
2424

25-
cleansingNeeded = false
25+
cleansingNeeded = false
2626

27-
return cleansingNeeded
28-
}
27+
return cleansingNeeded
2928
}
30-
})(this)
29+
}
3130

3231
this.Given(/^a configuration with:$/, function (fileContent, callback) {
3332
cleanseIfNeeded()
@@ -40,17 +39,39 @@ module.exports = function coreSteps () {
4039
return callback()
4140
})
4241

43-
this.When(/^I (run|runOut|start|init) Karma$/, function (command, callback) {
42+
this.When(/^I start a server in background/, function (callback) {
4443
this.writeConfigFile(tmpDir, tmpConfigFile, (function (_this) {
4544
return function (err, hash) {
4645
if (err) {
4746
return callback.fail(new Error(err))
4847
}
4948

49+
var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile)
50+
var runtimePath = path.join(baseDir, 'bin', 'karma')
51+
_this.child = spawn('' + runtimePath, ['start', '--log-level', 'debug', configFile])
52+
_this.child.stdout.on('data', function () {
53+
callback()
54+
callback = function () {
55+
}
56+
})
57+
_this.child.on('exit', function (exitCode) {
58+
_this.childExitCode = exitCode
59+
})
60+
}
61+
})(this))
62+
})
63+
64+
this.When(/^I (run|runOut|start|init|stop) Karma( with log-level ([a-z]+))?$/, function (command, withLogLevel, level, callback) {
65+
this.writeConfigFile(tmpDir, tmpConfigFile, (function (_this) {
66+
return function (err, hash) {
67+
if (err) {
68+
return callback.fail(new Error(err))
69+
}
70+
level = withLogLevel === undefined ? 'warn' : level
5071
var configFile = path.join(tmpDir, hash + '.' + tmpConfigFile)
5172
var runtimePath = path.join(baseDir, 'bin', 'karma')
5273
var execKarma = function (done) {
53-
var cmd = runtimePath + ' ' + command + ' --log-level warn ' + configFile + ' ' + additionalArgs
74+
var cmd = runtimePath + ' ' + command + ' --log-level ' + level + ' ' + configFile + ' ' + additionalArgs
5475

5576
return exec(cmd, {
5677
cwd: baseDir
@@ -107,11 +128,10 @@ module.exports = function coreSteps () {
107128
})(this))
108129
})
109130

110-
this.Then(/^it passes with( no debug)?:$/, {timeout: 10 * 1000}, function (noDebug, expectedOutput, callback) {
111-
noDebug = noDebug === ' no debug'
131+
this.Then(/^it passes with( no debug| like)?:$/, {timeout: 10 * 1000}, function (mode, expectedOutput, callback) {
132+
var noDebug = mode === ' no debug'
133+
var like = mode === ' like'
112134
var actualOutput = this.lastRun.stdout.toString()
113-
var actualError = this.lastRun.error
114-
var actualStderr = this.lastRun.stderr.toString()
115135
var lines
116136

117137
if (noDebug) {
@@ -120,12 +140,15 @@ module.exports = function coreSteps () {
120140
})
121141
actualOutput = lines.join('\n')
122142
}
143+
if (like && actualOutput.indexOf(expectedOutput) >= 0) {
144+
return callback()
145+
}
123146

124147
if (actualOutput.indexOf(expectedOutput) === 0) {
125148
return callback()
126149
}
127150

128-
if (actualError || actualStderr) {
151+
if (actualOutput) {
129152
return callback(new Error('Expected output to match the following:\n ' + expectedOutput + '\nGot:\n ' + actualOutput))
130153
}
131154

@@ -159,4 +182,12 @@ module.exports = function coreSteps () {
159182
callback(new Error('Expected output to match the following:\n ' + expectedOutput + '\nGot:\n ' + actualOutput))
160183
}
161184
})
185+
186+
this.Then(/^The server is dead( with exit code ([0-9]+))?$/, function (withExitCode, code, callback) {
187+
setTimeout((function (_this) {
188+
if (_this.childExitCode === undefined) return callback(new Error('Server has not exited.'))
189+
if (code === undefined || parseInt(code, 10) === _this.childExitCode) return callback()
190+
callback(new Error('Exit-code mismatch'))
191+
})(this), 1000)
192+
})
162193
}

test/e2e/stop.feature

Lines changed: 44 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,44 @@
1+
Feature: Stop karma
2+
In order to use Karma
3+
As a person who wants to write great tests
4+
I want to be able to stop Karma.
5+
6+
Scenario: A server can't be stopped if it isn't running
7+
When I stop Karma
8+
Then it fails with like:
9+
"""
10+
ERROR \[stopper\]: There is no server listening on port [0-9]+
11+
"""
12+
13+
Scenario: A server can be stopped
14+
Given a configuration with:
15+
"""
16+
files = ['basic/plus.js', 'basic/test.js'];
17+
browsers = ['PhantomJS'];
18+
plugins = [
19+
'karma-jasmine',
20+
'karma-phantomjs-launcher'
21+
];
22+
singleRun = false;
23+
"""
24+
When I start a server in background
25+
And I stop Karma
26+
Then The server is dead with exit code 0
27+
28+
Scenario: A server can be stopped and give informative output
29+
Given a configuration with:
30+
"""
31+
files = ['basic/plus.js', 'basic/test.js'];
32+
browsers = ['PhantomJS'];
33+
plugins = [
34+
'karma-jasmine',
35+
'karma-phantomjs-launcher'
36+
];
37+
singleRun = false;
38+
"""
39+
When I start a server in background
40+
And I stop Karma with log-level info
41+
Then it passes with like:
42+
"""
43+
Server stopped.
44+
"""

0 commit comments

Comments
 (0)