testem 3.19.0 → 3.20.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +32 -9
- package/lib/api.js +1 -1
- package/lib/app.js +204 -141
- package/lib/browser_launcher.js +5 -6
- package/lib/config.js +246 -133
- package/lib/file_watcher.js +111 -37
- package/lib/file_watcher_impl.js +295 -0
- package/lib/launcher.js +14 -23
- package/lib/log.js +92 -0
- package/lib/process-ctl.js +4 -4
- package/lib/reporters/dev/index.js +2 -1
- package/lib/reporters/dev/split_log_panel.js +2 -2
- package/lib/runners/browser_test_runner.js +15 -8
- package/lib/runners/hook_runner.js +13 -12
- package/lib/runners/process_test_runner.js +4 -5
- package/lib/runners/tap_process_test_runner.js +5 -6
- package/lib/runners/to-result.js +1 -1
- package/lib/server/index.js +62 -38
- package/lib/tap_consumer.js +74 -12
- package/lib/utils/browser-args.js +1 -1
- package/lib/utils/esbuild-browser-polyfills.js +46 -0
- package/lib/utils/esbuild-buffer-inject.js +6 -0
- package/lib/utils/expand_glob_pattern.js +19 -0
- package/lib/utils/file_watch_glob_policy.js +119 -0
- package/lib/utils/fileutils.js +4 -4
- package/lib/utils/is_emfile_error.js +16 -0
- package/lib/utils/known-browsers.js +161 -123
- package/lib/utils/path_pattern_match.js +39 -0
- package/lib/utils/process.js +45 -4
- package/lib/utils/promises.js +149 -0
- package/lib/utils/report-file.js +1 -3
- package/lib/utils/reporter.js +5 -5
- package/lib/utils/run-timeout.js +4 -4
- package/lib/utils/signal-listeners.js +9 -7
- package/lib/utils/tmp-cleanup.js +26 -0
- package/package.json +42 -32
- package/testem.js +170 -79
package/README.md
CHANGED
|
@@ -3,17 +3,19 @@ Got Scripts? Test’em!
|
|
|
3
3
|
|
|
4
4
|
[](https://github.com/testem/testem/actions/workflows/ci.yml?query=branch%3Amaster) [](http://badge.fury.io/js/testem)
|
|
5
5
|
|
|
6
|
+
Testem is a **JavaScript test runner** that runs your tests in **real desktop browsers**—Chrome, Firefox, Safari, Edge, and others you launch—so your specs execute in the same browser engines and DOM your users get, not a pretend environment. It also runs tests in **[Node](http://nodejs.org/)**, [PhantomJS](http://phantomjs.org/), or any launcher you configure. It is **framework-agnostic** and aimed at **any kind of tests** you want to run: unit, integration, end-to-end style suites, or custom setups—you pick the style; Testem wires it to the browser or process.
|
|
7
|
+
|
|
6
8
|
Unit testing in Javascript can be tedious and painful, but Testem makes it so easy that you will actually *want* to write tests.
|
|
7
9
|
|
|
8
10
|
Features
|
|
9
11
|
--------
|
|
10
12
|
|
|
11
|
-
* Test-framework agnostic. Support for
|
|
13
|
+
* Test-framework agnostic — designed for **any kind of tests** that fit your project (unit, integration, custom runners, etc.), not a single prescribed style. Support for
|
|
12
14
|
- [Jasmine](http://jasmine.github.io/)
|
|
13
15
|
- [QUnit](http://qunitjs.com/)
|
|
14
16
|
- [Mocha](http://mochajs.org/)
|
|
15
17
|
- Others, through custom test framework adapters.
|
|
16
|
-
* Run tests in all major browsers as well as [Node](http://nodejs.org) and [PhantomJS](http://phantomjs.org/)
|
|
18
|
+
* Run tests in **all** major **real** browsers (your tests load and run in the actual browser) as well as [Node](http://nodejs.org) and [PhantomJS](http://phantomjs.org/)
|
|
17
19
|
* Two distinct use-cases:
|
|
18
20
|
- Test-Driven-Development(TDD) — designed to streamline the TDD workflow
|
|
19
21
|
- Continuous Integration(CI) — designed to work well with popular CI servers like Jenkins or Teamcity
|
|
@@ -27,6 +29,8 @@ Features
|
|
|
27
29
|
- JSHint/JSLint
|
|
28
30
|
- everything else
|
|
29
31
|
|
|
32
|
+
**Internet Explorer** and **PhantomJS** are still supported as launchers, but we document them as **deprecated** targets: keeping them viable through transpilation and polyfills is likely to get more difficult over time, so prefer evergreen browsers or Node for new projects. See the [configuration reference](docs/config_file.md) for how we categorize browsers.
|
|
33
|
+
|
|
30
34
|
Screencasts
|
|
31
35
|
-----------
|
|
32
36
|
|
|
@@ -61,7 +65,7 @@ You will see a terminal-based interface which looks like this
|
|
|
61
65
|
|
|
62
66
|

|
|
63
67
|
|
|
64
|
-
Now open
|
|
68
|
+
Now open a **real browser** (the URL Testem prints is a normal page in Chrome, Firefox, Safari, etc.) and go to the specified URL. You should now see
|
|
65
69
|
|
|
66
70
|

|
|
67
71
|
|
|
@@ -107,6 +111,28 @@ In development mode, Testem has a text-based graphical user interface which uses
|
|
|
107
111
|
* d : half a page down target text panel
|
|
108
112
|
* u : half a page up target text panel
|
|
109
113
|
|
|
114
|
+
### File watching
|
|
115
|
+
|
|
116
|
+
In development mode, Testem watches your project directory for changes and re-runs tests when a
|
|
117
|
+
relevant file is added, edited, or removed. Watching is implemented with
|
|
118
|
+
[chokidar](https://github.com/paulmillr/chokidar) (v5).
|
|
119
|
+
|
|
120
|
+
* **`src_files`** — Glob patterns for source files whose changes should trigger a run (defaults to
|
|
121
|
+
`*.js` when unset). This is the main *watch list*.
|
|
122
|
+
* **`watch_files`** — Optional; if set, these patterns are watched instead of defaulting to
|
|
123
|
+
`src_files` (see `docs/config_file.md`).
|
|
124
|
+
* **`src_files_ignore`** — Patterns to exclude from the watch policy (e.g. `node_modules`).
|
|
125
|
+
* **`disable_watching`** — Set to `true` to turn off the file watcher entirely.
|
|
126
|
+
|
|
127
|
+
Testem watches the **current working directory** and applies your include/ignore patterns to
|
|
128
|
+
events from the watcher. You do not need to list every file explicitly; globs and ignores follow
|
|
129
|
+
the same policy as in the config reference.
|
|
130
|
+
|
|
131
|
+
**Troubleshooting:** On some setups (Docker, network filesystems, VMs), native `fs.watch` can be
|
|
132
|
+
flaky. Chokidar supports environment variables such as `CHOKIDAR_USE_POLLING=1` (force polling)
|
|
133
|
+
and `CHOKIDAR_INTERVAL` (polling interval in ms). See the
|
|
134
|
+
[chokidar readme](https://github.com/paulmillr/chokidar) for details.
|
|
135
|
+
|
|
110
136
|
### Command line options
|
|
111
137
|
|
|
112
138
|
To see all command line options
|
|
@@ -134,9 +160,7 @@ Will print them out. The output might look like
|
|
|
134
160
|
|
|
135
161
|
$ testem launchers
|
|
136
162
|
Browsers available on this system:
|
|
137
|
-
|
|
138
|
-
IE8
|
|
139
|
-
IE9
|
|
163
|
+
IE11
|
|
140
164
|
Chrome
|
|
141
165
|
Firefox
|
|
142
166
|
Safari
|
|
@@ -144,8 +168,6 @@ Will print them out. The output might look like
|
|
|
144
168
|
Opera
|
|
145
169
|
PhantomJS
|
|
146
170
|
|
|
147
|
-
Did you notice that this system has IE versions 7-9? Yes, actually it has only IE9 installed, but Testem uses IE's compatibility mode feature to emulate IE 7 and 8.
|
|
148
|
-
|
|
149
171
|
When you run `testem ci` to run tests, it outputs the results in the [TAP](http://testanything.org/) format by default, which looks like
|
|
150
172
|
|
|
151
173
|
ok 1 Chrome 16.0 - hello should say hello.
|
|
@@ -455,7 +477,8 @@ If you need to run a preprocessor (or indeed any shell command before the start
|
|
|
455
477
|
|
|
456
478
|
"before_tests": "coffee -c *.coffee"
|
|
457
479
|
|
|
458
|
-
And Testem will run it before each test run.
|
|
480
|
+
And Testem will run it before each test run. Point **`src_files`** at the sources you want
|
|
481
|
+
watched (see **File watching** under Development Mode above).
|
|
459
482
|
|
|
460
483
|
```javascript
|
|
461
484
|
"src_files": [
|
package/lib/api.js
CHANGED
package/lib/app.js
CHANGED
|
@@ -1,9 +1,7 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
1
|
const EventEmitter = require('events').EventEmitter;
|
|
4
|
-
const
|
|
2
|
+
const { fromCallback, using, disposer, mapLimit } = require('./utils/promises');
|
|
5
3
|
const Path = require('path');
|
|
6
|
-
const log = require('
|
|
4
|
+
const log = require('./log');
|
|
7
5
|
const StyledString = require('styled_string');
|
|
8
6
|
const _ = require('lodash');
|
|
9
7
|
|
|
@@ -40,7 +38,7 @@ module.exports = class App extends EventEmitter {
|
|
|
40
38
|
|
|
41
39
|
let alreadyExit = false;
|
|
42
40
|
|
|
43
|
-
this.cleanExit = err => {
|
|
41
|
+
this.cleanExit = (err) => {
|
|
44
42
|
if (!alreadyExit) {
|
|
45
43
|
alreadyExit = true;
|
|
46
44
|
|
|
@@ -52,7 +50,9 @@ module.exports = class App extends EventEmitter {
|
|
|
52
50
|
|
|
53
51
|
if (this.testSuiteTimedOut === true) {
|
|
54
52
|
let timeoutSeconds = this.testSuiteTimeout.timeout;
|
|
55
|
-
err = new Error(
|
|
53
|
+
err = new Error(
|
|
54
|
+
`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminated all test runners.`,
|
|
55
|
+
);
|
|
56
56
|
exitCode = 1;
|
|
57
57
|
}
|
|
58
58
|
|
|
@@ -64,53 +64,63 @@ module.exports = class App extends EventEmitter {
|
|
|
64
64
|
start(cb) {
|
|
65
65
|
log.info('Starting ' + this.config.appMode);
|
|
66
66
|
|
|
67
|
-
return
|
|
68
|
-
signalListeners.on('signal', err => this.exit(err));
|
|
67
|
+
return using(SignalListeners.with(), (signalListeners) => {
|
|
68
|
+
signalListeners.on('signal', (err) => this.exit(err));
|
|
69
69
|
|
|
70
|
-
return
|
|
71
|
-
this.
|
|
70
|
+
return using(
|
|
71
|
+
Reporter.with(this, this.stdoutStream, this.reportFileName),
|
|
72
|
+
(reporter) => {
|
|
73
|
+
this.reporter = reporter;
|
|
72
74
|
|
|
73
|
-
|
|
74
|
-
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
|
|
75
|
+
return using(this.fileWatch(), () => {
|
|
76
|
+
return using(this.getServer(), () => {
|
|
77
|
+
return using(this.getRunners(), () => {
|
|
78
|
+
return using(this.runHook('on_start'), () => {
|
|
79
|
+
let w = this.waitForTests();
|
|
78
80
|
|
|
79
|
-
|
|
80
|
-
|
|
81
|
-
|
|
81
|
+
if (cb) {
|
|
82
|
+
cb();
|
|
83
|
+
}
|
|
82
84
|
|
|
83
|
-
|
|
84
|
-
|
|
85
|
-
|
|
85
|
+
return w;
|
|
86
|
+
})
|
|
87
|
+
.then(() => {
|
|
88
|
+
log.info('Stopping ' + this.config.appMode);
|
|
86
89
|
|
|
87
|
-
|
|
90
|
+
this.emit('tests-finish');
|
|
88
91
|
|
|
89
|
-
|
|
90
|
-
|
|
91
|
-
|
|
92
|
-
|
|
92
|
+
return using(this.runHook('on_exit'), () => {});
|
|
93
|
+
})
|
|
94
|
+
.catch((error) => {
|
|
95
|
+
log.error(error);
|
|
96
|
+
log.info('Stopping ' + this.config.appMode);
|
|
93
97
|
|
|
94
|
-
|
|
98
|
+
this.emit('tests-error');
|
|
95
99
|
|
|
96
|
-
|
|
97
|
-
|
|
98
|
-
|
|
100
|
+
return new Promise((resolve, reject) => {
|
|
101
|
+
using(this.runHook('on_exit'), () => {}).then(() => {
|
|
102
|
+
reject(error);
|
|
103
|
+
});
|
|
104
|
+
});
|
|
99
105
|
});
|
|
100
|
-
});
|
|
101
106
|
});
|
|
102
107
|
});
|
|
103
108
|
});
|
|
104
|
-
}
|
|
105
|
-
|
|
106
|
-
}).
|
|
109
|
+
},
|
|
110
|
+
);
|
|
111
|
+
}).then(
|
|
112
|
+
() => this.cleanExit(),
|
|
113
|
+
(err) => this.cleanExit(err),
|
|
114
|
+
);
|
|
107
115
|
}
|
|
108
116
|
|
|
109
117
|
waitForTests() {
|
|
110
118
|
log.info('Waiting for tests.');
|
|
111
119
|
|
|
112
120
|
if (this.exited) {
|
|
113
|
-
return
|
|
121
|
+
return Promise.reject(
|
|
122
|
+
this.exitErr || new Error('Testem exited before running any tests.'),
|
|
123
|
+
);
|
|
114
124
|
}
|
|
115
125
|
|
|
116
126
|
let run = this.triggerRun('Start');
|
|
@@ -119,7 +129,7 @@ module.exports = class App extends EventEmitter {
|
|
|
119
129
|
run.then(() => this.exit());
|
|
120
130
|
}
|
|
121
131
|
|
|
122
|
-
return new
|
|
132
|
+
return new Promise((resolve, reject) => {
|
|
123
133
|
this.on('testFinish', resolve);
|
|
124
134
|
this.on('testError', reject);
|
|
125
135
|
});
|
|
@@ -133,38 +143,42 @@ module.exports = class App extends EventEmitter {
|
|
|
133
143
|
}
|
|
134
144
|
this.restarting = true;
|
|
135
145
|
|
|
136
|
-
return this.stopCurrentRun()
|
|
137
|
-
this.
|
|
146
|
+
return this.stopCurrentRun()
|
|
147
|
+
.catch(this.exit.bind(this))
|
|
148
|
+
.then(() => {
|
|
149
|
+
this.restarting = false;
|
|
138
150
|
|
|
139
|
-
|
|
140
|
-
|
|
151
|
+
return this.runTests();
|
|
152
|
+
});
|
|
141
153
|
}
|
|
142
154
|
|
|
143
155
|
stopCurrentRun() {
|
|
144
156
|
if (!this.currentRun) {
|
|
145
|
-
return
|
|
157
|
+
return Promise.resolve();
|
|
146
158
|
}
|
|
147
159
|
|
|
148
|
-
return
|
|
160
|
+
return Promise.all([this.stopRunners(), this.currentRun]);
|
|
149
161
|
}
|
|
150
162
|
|
|
151
163
|
runTests() {
|
|
152
164
|
if (this.paused) {
|
|
153
|
-
return
|
|
165
|
+
return Promise.resolve();
|
|
154
166
|
}
|
|
155
167
|
|
|
156
168
|
log.info('Running tests...');
|
|
157
169
|
|
|
158
170
|
this.reporter.onStart('testem', { launcherId: 0 });
|
|
159
171
|
|
|
160
|
-
return
|
|
161
|
-
return
|
|
172
|
+
return using(this.runHook('before_tests'), () => {
|
|
173
|
+
return using(RunTimeout.with(this.config.get('timeout')), (timeout) => {
|
|
162
174
|
this.testSuiteTimeout = timeout;
|
|
163
175
|
|
|
164
176
|
timeout.on('timeout', () => {
|
|
165
177
|
let timeoutSeconds = timeout.timeout;
|
|
166
178
|
|
|
167
|
-
log.info(
|
|
179
|
+
log.info(
|
|
180
|
+
`Test suite execution has timed out (config.timeout = ${timeoutSeconds} seconds). Terminating all test runners`,
|
|
181
|
+
);
|
|
168
182
|
this.testSuiteTimedOut = true;
|
|
169
183
|
this.killRunners();
|
|
170
184
|
});
|
|
@@ -176,25 +190,27 @@ module.exports = class App extends EventEmitter {
|
|
|
176
190
|
|
|
177
191
|
return this.currentRun;
|
|
178
192
|
}).then(() => {
|
|
179
|
-
return
|
|
193
|
+
return using(this.runHook('after_tests'), () => {});
|
|
180
194
|
});
|
|
181
|
-
})
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
let result = {
|
|
187
|
-
failed: 1,
|
|
188
|
-
passed: 0,
|
|
189
|
-
name: 'testem',
|
|
190
|
-
launcherId: 0,
|
|
191
|
-
error: {
|
|
192
|
-
message: err.toString()
|
|
195
|
+
})
|
|
196
|
+
.catch((err) => {
|
|
197
|
+
if (err.hideFromReporter) {
|
|
198
|
+
return;
|
|
193
199
|
}
|
|
194
|
-
};
|
|
195
200
|
|
|
196
|
-
|
|
197
|
-
|
|
201
|
+
let result = {
|
|
202
|
+
failed: 1,
|
|
203
|
+
passed: 0,
|
|
204
|
+
name: 'testem',
|
|
205
|
+
launcherId: 0,
|
|
206
|
+
error: {
|
|
207
|
+
message: err.toString(),
|
|
208
|
+
},
|
|
209
|
+
};
|
|
210
|
+
|
|
211
|
+
this.reporter.report('testem', result);
|
|
212
|
+
})
|
|
213
|
+
.finally(() => this.reporter.onEnd('testem', { launcherId: 0 }));
|
|
198
214
|
}
|
|
199
215
|
|
|
200
216
|
exit(err, cb) {
|
|
@@ -221,7 +237,7 @@ module.exports = class App extends EventEmitter {
|
|
|
221
237
|
return;
|
|
222
238
|
}
|
|
223
239
|
|
|
224
|
-
startServer(
|
|
240
|
+
startServer() {
|
|
225
241
|
log.info('Starting server');
|
|
226
242
|
this.server = new Server(this.config);
|
|
227
243
|
this.server.on('file-requested', this.onFileRequested.bind(this));
|
|
@@ -229,16 +245,29 @@ module.exports = class App extends EventEmitter {
|
|
|
229
245
|
this.server.on('browser-relogin', this.onBrowserRelogin.bind(this));
|
|
230
246
|
this.server.on('server-error', this.onServerError.bind(this));
|
|
231
247
|
|
|
232
|
-
return this.server.start()
|
|
248
|
+
return this.server.start();
|
|
233
249
|
}
|
|
234
250
|
|
|
235
251
|
getServer() {
|
|
236
|
-
return this.startServer()
|
|
252
|
+
return disposer(Promise.resolve(this.startServer()), () =>
|
|
253
|
+
this.stopServer(),
|
|
254
|
+
);
|
|
237
255
|
}
|
|
238
256
|
|
|
239
|
-
|
|
257
|
+
/**
|
|
258
|
+
* When the server requests a file, add it to the file watcher if it is not already being watched.
|
|
259
|
+
* If the file watcher is disabled, do nothing.
|
|
260
|
+
*
|
|
261
|
+
* @param {string} filepath
|
|
262
|
+
* @returns {Promise<void>}
|
|
263
|
+
*/
|
|
264
|
+
async onFileRequested(filepath) {
|
|
240
265
|
if (this.fileWatcher && !this.config.get('serve_files')) {
|
|
241
|
-
|
|
266
|
+
try {
|
|
267
|
+
await this.fileWatcher.add(filepath);
|
|
268
|
+
} catch (err) {
|
|
269
|
+
log.error(err);
|
|
270
|
+
}
|
|
242
271
|
}
|
|
243
272
|
}
|
|
244
273
|
|
|
@@ -251,18 +280,30 @@ module.exports = class App extends EventEmitter {
|
|
|
251
280
|
}
|
|
252
281
|
|
|
253
282
|
onBrowserLogin(browserName, id, socket) {
|
|
254
|
-
let browser = _.find(this.runners, runner => {
|
|
255
|
-
return
|
|
283
|
+
let browser = _.find(this.runners, (runner) => {
|
|
284
|
+
return (
|
|
285
|
+
runner.launcherId === id && (!runner.socket || !runner.socket.connected)
|
|
286
|
+
);
|
|
256
287
|
});
|
|
257
288
|
|
|
258
289
|
if (!browser) {
|
|
259
|
-
let launcher = new LauncherFactory(
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
290
|
+
let launcher = new LauncherFactory(
|
|
291
|
+
browserName,
|
|
292
|
+
{
|
|
293
|
+
id: id,
|
|
294
|
+
protocol: 'browser',
|
|
295
|
+
},
|
|
296
|
+
this.config,
|
|
297
|
+
).create();
|
|
263
298
|
const singleRun = this.config.get('single_run');
|
|
264
299
|
|
|
265
|
-
browser = new BrowserTestRunner(
|
|
300
|
+
browser = new BrowserTestRunner(
|
|
301
|
+
launcher,
|
|
302
|
+
this.reporter,
|
|
303
|
+
this.runnerIndex++,
|
|
304
|
+
singleRun,
|
|
305
|
+
this.config,
|
|
306
|
+
);
|
|
266
307
|
this.addRunner(browser);
|
|
267
308
|
}
|
|
268
309
|
|
|
@@ -270,10 +311,12 @@ module.exports = class App extends EventEmitter {
|
|
|
270
311
|
}
|
|
271
312
|
|
|
272
313
|
onBrowserRelogin(browserName, id, socket) {
|
|
273
|
-
let browser = _.find(this.runners, runner => {
|
|
314
|
+
let browser = _.find(this.runners, (runner) => {
|
|
274
315
|
// a browser relogin can happen if a client socket was disconnected, which may not be reflected in runner.socket's connected state
|
|
275
316
|
// or if the socket was nulled by 'onDisconnect'
|
|
276
|
-
return
|
|
317
|
+
return (
|
|
318
|
+
runner.launcherId === id && (runner.socket || runner.socket === null)
|
|
319
|
+
);
|
|
277
320
|
});
|
|
278
321
|
|
|
279
322
|
if (!browser) {
|
|
@@ -293,55 +336,74 @@ module.exports = class App extends EventEmitter {
|
|
|
293
336
|
this.emit('runnerAdded', runner);
|
|
294
337
|
}
|
|
295
338
|
|
|
339
|
+
// Watcher teardown is this disposer's cleanup (using() awaits it on success and on setup failure).
|
|
296
340
|
fileWatch() {
|
|
297
|
-
return this.configureFileWatch()
|
|
341
|
+
return disposer(Promise.resolve(this.configureFileWatch()), () => {
|
|
342
|
+
if (this.fileWatcher) {
|
|
343
|
+
const w = this.fileWatcher;
|
|
344
|
+
this.fileWatcher = undefined;
|
|
345
|
+
return w.close();
|
|
346
|
+
}
|
|
347
|
+
});
|
|
298
348
|
}
|
|
299
349
|
|
|
300
|
-
configureFileWatch(
|
|
350
|
+
configureFileWatch() {
|
|
301
351
|
if (this.config.get('disable_watching')) {
|
|
302
|
-
return
|
|
352
|
+
return Promise.resolve();
|
|
303
353
|
}
|
|
304
354
|
|
|
305
|
-
|
|
306
|
-
|
|
307
|
-
|
|
308
|
-
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
|
|
313
|
-
|
|
314
|
-
|
|
315
|
-
|
|
316
|
-
|
|
317
|
-
|
|
318
|
-
|
|
319
|
-
|
|
320
|
-
this.
|
|
321
|
-
|
|
322
|
-
|
|
323
|
-
|
|
324
|
-
|
|
325
|
-
|
|
326
|
-
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
355
|
+
return FileWatcher.create(this.config).then((fw) => {
|
|
356
|
+
this.fileWatcher = fw;
|
|
357
|
+
this.fileWatcher.on('fileChanged', (filepath) => {
|
|
358
|
+
log.info(
|
|
359
|
+
filepath +
|
|
360
|
+
' changed (' +
|
|
361
|
+
(this.disableFileWatch ? 'disabled' : 'enabled') +
|
|
362
|
+
').',
|
|
363
|
+
);
|
|
364
|
+
if (this.disableFileWatch || this.paused) {
|
|
365
|
+
return;
|
|
366
|
+
}
|
|
367
|
+
let configFile = this.config.get('file');
|
|
368
|
+
if (
|
|
369
|
+
(configFile && filepath === Path.resolve(configFile)) ||
|
|
370
|
+
(this.config.isCwdMode() && filepath === process.cwd())
|
|
371
|
+
) {
|
|
372
|
+
// config changed
|
|
373
|
+
this.configure(() => {
|
|
374
|
+
this.triggerRun('Config changed');
|
|
375
|
+
});
|
|
376
|
+
} else {
|
|
377
|
+
using(this.runHook('on_change', { file: filepath }), () => {
|
|
378
|
+
this.triggerRun('File changed: ' + filepath);
|
|
379
|
+
});
|
|
380
|
+
}
|
|
381
|
+
});
|
|
382
|
+
this.fileWatcher.on('EMFILE', () => {
|
|
383
|
+
let view = this.view;
|
|
384
|
+
let text = [
|
|
385
|
+
'The file watcher received a EMFILE system error, which means that ',
|
|
386
|
+
'it has hit the maximum number of files that can be open at a time. ',
|
|
387
|
+
'Luckily, you can increase this limit as a workaround. See the directions below \n \n',
|
|
388
|
+
'Linux: http://stackoverflow.com/a/34645/5304\n',
|
|
389
|
+
'Mac OS: http://serverfault.com/a/15575/47234',
|
|
390
|
+
].join('');
|
|
391
|
+
view.setErrorPopupMessage(
|
|
392
|
+
new StyledString(text + '\n ').foreground('megenta'),
|
|
393
|
+
);
|
|
394
|
+
});
|
|
334
395
|
});
|
|
335
|
-
|
|
336
|
-
return Bluebird.resolve().asCallback(cb);
|
|
337
396
|
}
|
|
338
397
|
|
|
339
398
|
getRunners() {
|
|
340
|
-
return
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
399
|
+
return disposer(
|
|
400
|
+
fromCallback((callback) => {
|
|
401
|
+
this.createRunners(callback);
|
|
402
|
+
}),
|
|
403
|
+
() => {
|
|
404
|
+
return this.killRunners();
|
|
405
|
+
},
|
|
406
|
+
);
|
|
345
407
|
}
|
|
346
408
|
|
|
347
409
|
createRunners(callback) {
|
|
@@ -374,56 +436,55 @@ module.exports = class App extends EventEmitter {
|
|
|
374
436
|
case 'tap':
|
|
375
437
|
return TapProcessTestRunner;
|
|
376
438
|
default:
|
|
377
|
-
throw new Error('
|
|
439
|
+
throw new Error('Unknown protocol: ' + protocol);
|
|
378
440
|
}
|
|
379
441
|
}
|
|
380
442
|
|
|
381
443
|
createTestRunner(launcher, reporter) {
|
|
382
444
|
let singleRun = this.config.get('single_run');
|
|
383
445
|
|
|
384
|
-
return new (this.getRunnerFactory(launcher))(
|
|
446
|
+
return new (this.getRunnerFactory(launcher))(
|
|
447
|
+
launcher,
|
|
448
|
+
reporter,
|
|
449
|
+
this.runnerIndex++,
|
|
450
|
+
singleRun,
|
|
451
|
+
this.config,
|
|
452
|
+
);
|
|
385
453
|
}
|
|
386
454
|
|
|
387
455
|
withTestTimeout() {
|
|
388
|
-
return this.startClock()
|
|
456
|
+
return disposer(this.startClock(), () => {
|
|
389
457
|
return this.cancelExistingTimeout();
|
|
390
458
|
});
|
|
391
459
|
}
|
|
392
460
|
|
|
393
461
|
singleRun(timeout) {
|
|
394
462
|
let limit = this.config.get('parallel');
|
|
463
|
+
let concurrency = limit && limit >= 1 ? parseInt(limit) : Infinity;
|
|
395
464
|
|
|
396
|
-
|
|
397
|
-
|
|
398
|
-
if (limit && limit >= 1) {
|
|
399
|
-
options.concurrency = parseInt(limit);
|
|
400
|
-
} else {
|
|
401
|
-
options.concurrency = Infinity;
|
|
402
|
-
}
|
|
403
|
-
|
|
404
|
-
return Bluebird.map(this.runners, (runner) => {
|
|
465
|
+
return mapLimit(this.runners, concurrency, (runner) => {
|
|
405
466
|
if (this.exited) {
|
|
406
467
|
let e = new Error('Run canceled.');
|
|
407
468
|
e.hideFromReporter = true;
|
|
408
|
-
return
|
|
469
|
+
return Promise.reject(e);
|
|
409
470
|
}
|
|
410
471
|
if (this.restarting) {
|
|
411
|
-
return
|
|
472
|
+
return Promise.resolve();
|
|
412
473
|
}
|
|
413
474
|
return timeout.try(() => runner.start());
|
|
414
|
-
}
|
|
475
|
+
});
|
|
415
476
|
}
|
|
416
477
|
|
|
417
478
|
wrapUp(err) {
|
|
418
479
|
this.exit(err);
|
|
419
480
|
}
|
|
420
481
|
|
|
421
|
-
stopServer(
|
|
482
|
+
stopServer() {
|
|
422
483
|
if (!this.server) {
|
|
423
|
-
return
|
|
484
|
+
return Promise.resolve();
|
|
424
485
|
}
|
|
425
486
|
|
|
426
|
-
return this.server.stop()
|
|
487
|
+
return this.server.stop();
|
|
427
488
|
}
|
|
428
489
|
|
|
429
490
|
getExitCode() {
|
|
@@ -442,20 +503,22 @@ module.exports = class App extends EventEmitter {
|
|
|
442
503
|
}
|
|
443
504
|
|
|
444
505
|
stopRunners() {
|
|
445
|
-
return
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
506
|
+
return Promise.all(
|
|
507
|
+
this.runners.map((runner) => {
|
|
508
|
+
if (typeof runner.stop === 'function') {
|
|
509
|
+
return runner.stop();
|
|
510
|
+
}
|
|
449
511
|
|
|
450
|
-
|
|
451
|
-
|
|
512
|
+
return runner.exit();
|
|
513
|
+
}),
|
|
514
|
+
);
|
|
452
515
|
}
|
|
453
516
|
|
|
454
517
|
killRunners() {
|
|
455
|
-
return
|
|
518
|
+
return Promise.all(this.runners.map((runner) => runner.exit()));
|
|
456
519
|
}
|
|
457
520
|
|
|
458
521
|
launchers() {
|
|
459
|
-
return this.runners.map(runner => runner.launcher);
|
|
522
|
+
return this.runners.map((runner) => runner.launcher);
|
|
460
523
|
}
|
|
461
524
|
};
|