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/lib/file_watcher.js
CHANGED
|
@@ -1,51 +1,125 @@
|
|
|
1
|
+
const EventEmitter = require('events').EventEmitter;
|
|
1
2
|
|
|
3
|
+
const Impl = require('./file_watcher_impl');
|
|
2
4
|
|
|
3
|
-
const
|
|
4
|
-
const EventEmitter = require('events').EventEmitter;
|
|
5
|
+
const kCompose = Symbol('FileWatcher.compose');
|
|
5
6
|
|
|
6
|
-
|
|
7
|
-
|
|
7
|
+
/**
|
|
8
|
+
* Coordinates file watching for Testem: it observes the current working directory (and any
|
|
9
|
+
* non-glob paths from config that live outside it), applies the same include / ignore rules as
|
|
10
|
+
* the rest of the app (`src_files`, `src_files_ignore`, `watch_files`, etc.), and reports when
|
|
11
|
+
* paths that matter for the test run change.
|
|
12
|
+
*
|
|
13
|
+
* Listen for {@link FileWatcher#event:fileChanged} to react to changes, and optionally for
|
|
14
|
+
* {@link FileWatcher#event:EMFILE} when the process hits open-file limits.
|
|
15
|
+
*
|
|
16
|
+
* ### Construction
|
|
17
|
+
*
|
|
18
|
+
* **Do not call `new FileWatcher(...)`.** The constructor is not part of the public API; it
|
|
19
|
+
* exists only for internal wiring and will always throw if you invoke it from outside this
|
|
20
|
+
* module. Obtain instances exclusively via {@link FileWatcher.create} with a config object that
|
|
21
|
+
* exposes `get(key)` and `isCwdMode()` (the same shape as Testem’s `Config`).
|
|
22
|
+
*
|
|
23
|
+
* ### Async vs synchronous
|
|
24
|
+
*
|
|
25
|
+
* **Return a `Promise` — use `await`, `.then()`, or `.catch()`:**
|
|
26
|
+
* {@link FileWatcher.create}, {@link FileWatcher#add}, {@link FileWatcher#close}.
|
|
27
|
+
*
|
|
28
|
+
* **Synchronous:** {@link FileWatcher#onFileChanged}, {@link FileWatcher#onEMFILE}, and
|
|
29
|
+
* inherited `EventEmitter` methods (`on`, `once`, `emit`, `removeListener`, …).
|
|
30
|
+
*
|
|
31
|
+
* @hideconstructor
|
|
32
|
+
* @fires FileWatcher#event:fileChanged
|
|
33
|
+
* @fires FileWatcher#event:EMFILE
|
|
34
|
+
*/
|
|
35
|
+
class FileWatcher extends EventEmitter {
|
|
36
|
+
/**
|
|
37
|
+
* Not for public use. Application code must use {@link FileWatcher.create} instead; calling
|
|
38
|
+
* `new FileWatcher` directly always throws.
|
|
39
|
+
*
|
|
40
|
+
* @private
|
|
41
|
+
* @param {*} impl Internal implementation instance.
|
|
42
|
+
* @param {symbol} token Internal composition token (not exposed).
|
|
43
|
+
*/
|
|
44
|
+
constructor(impl, token) {
|
|
8
45
|
super();
|
|
46
|
+
if (token !== kCompose) {
|
|
47
|
+
throw new TypeError(
|
|
48
|
+
'FileWatcher is not constructable; use FileWatcher.create(config)',
|
|
49
|
+
);
|
|
50
|
+
}
|
|
51
|
+
this._impl = impl;
|
|
52
|
+
impl.on('fileChanged', (filePath) => this.emit('fileChanged', filePath));
|
|
53
|
+
impl.on('EMFILE', () => this.emit('EMFILE'));
|
|
54
|
+
}
|
|
9
55
|
|
|
10
|
-
|
|
11
|
-
|
|
12
|
-
|
|
56
|
+
/**
|
|
57
|
+
* Create a watcher, wait until the underlying engine has finished its initial setup, and
|
|
58
|
+
* return a ready-to-use `FileWatcher`. This is the **only** supported way to obtain an
|
|
59
|
+
* instance.
|
|
60
|
+
*
|
|
61
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config Testem-style
|
|
62
|
+
* config (typically a `Config` instance): `get` reads keys such as `src_files`,
|
|
63
|
+
* `src_files_ignore`, `watch_files`, `file`, `disable_watching`; `isCwdMode` reflects cwd
|
|
64
|
+
* mode.
|
|
65
|
+
* @returns {Promise<FileWatcher>} Resolves with a connected watcher once startup is complete.
|
|
66
|
+
* **Await** this promise; if the engine fails during startup, the promise rejects.
|
|
67
|
+
*/
|
|
68
|
+
static async create(config) {
|
|
69
|
+
const impl = await Impl.create(config);
|
|
70
|
+
return new FileWatcher(impl, kCompose);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
/**
|
|
74
|
+
* Request that a **concrete** filesystem path be watched in addition to the configured
|
|
75
|
+
* patterns (for example, a file the dev server started serving after startup). Glob or
|
|
76
|
+
* minimatch strings are not accepted here — only real paths; pattern-based includes stay in
|
|
77
|
+
* config.
|
|
78
|
+
*
|
|
79
|
+
* @param {string} file Path relative to the current working directory or absolute, as
|
|
80
|
+
* accepted by the engine.
|
|
81
|
+
* @returns {Promise<void>} Settles when the engine has finished the add request. **Await**
|
|
82
|
+
* this promise (or attach `.catch()`) so validation errors, engine errors, and internal
|
|
83
|
+
* teardown on failure are observed.
|
|
84
|
+
*/
|
|
85
|
+
add(file) {
|
|
86
|
+
return this._impl.add(file);
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Tear down the watcher: close the engine, release OS watch handles, and remove listeners on
|
|
91
|
+
* this `FileWatcher`. Safe to call more than once; subsequent calls resolve promptly.
|
|
92
|
+
*
|
|
93
|
+
* @returns {Promise<void>} Resolves when shutdown is complete. **Await** if later code must
|
|
94
|
+
* run only after handles are released.
|
|
95
|
+
*/
|
|
96
|
+
close() {
|
|
97
|
+
return Promise.resolve(this._impl.close()).then(() => {
|
|
98
|
+
this.removeAllListeners();
|
|
13
99
|
});
|
|
14
|
-
let onFileChanged = this.onFileChanged.bind(this);
|
|
15
|
-
this.fileWatcher.on('change', onFileChanged);
|
|
16
|
-
this.fileWatcher.on('add', onFileChanged);
|
|
17
|
-
this.fileWatcher.on('remove', onFileChanged);
|
|
18
|
-
this.fileWatcher.on('emfile', this.onEMFILE.bind(this));
|
|
19
|
-
|
|
20
|
-
let watchFiles = config.get('watch_files');
|
|
21
|
-
this.fileWatcher.clear();
|
|
22
|
-
let confFile = config.get('file');
|
|
23
|
-
if (confFile) {
|
|
24
|
-
this.fileWatcher.add(confFile);
|
|
25
|
-
}
|
|
26
|
-
if (config.isCwdMode()) {
|
|
27
|
-
this.fileWatcher.add('*.js');
|
|
28
|
-
}
|
|
29
|
-
if (watchFiles) {
|
|
30
|
-
this.fileWatcher.add(watchFiles);
|
|
31
|
-
}
|
|
32
|
-
let srcFiles = config.get('src_files') || '*.js';
|
|
33
|
-
this.fileWatcher.add(srcFiles);
|
|
34
|
-
let ignoreFiles = config.get('src_files_ignore');
|
|
35
|
-
if (ignoreFiles) {
|
|
36
|
-
this.fileWatcher.ignore(ignoreFiles);
|
|
37
|
-
}
|
|
38
100
|
}
|
|
39
101
|
|
|
102
|
+
/**
|
|
103
|
+
* Manually signal that a path changed, for the same listeners as a real `fileChanged` event.
|
|
104
|
+
* Used in tests and narrow control flows; normal file edits are reported automatically by the
|
|
105
|
+
* engine.
|
|
106
|
+
*
|
|
107
|
+
* @param {string} filePath Path that should be treated as changed.
|
|
108
|
+
* @returns {void}
|
|
109
|
+
*/
|
|
40
110
|
onFileChanged(filePath) {
|
|
41
|
-
this.
|
|
111
|
+
return this._impl.onFileChanged(filePath);
|
|
42
112
|
}
|
|
43
113
|
|
|
114
|
+
/**
|
|
115
|
+
* Manually emit the `EMFILE` condition (too many open files) for listeners that show user
|
|
116
|
+
* guidance. The real engine also surfaces this when appropriate.
|
|
117
|
+
*
|
|
118
|
+
* @returns {void}
|
|
119
|
+
*/
|
|
44
120
|
onEMFILE() {
|
|
45
|
-
this.
|
|
121
|
+
return this._impl.onEMFILE();
|
|
46
122
|
}
|
|
123
|
+
}
|
|
47
124
|
|
|
48
|
-
|
|
49
|
-
this.fileWatcher.add(file);
|
|
50
|
-
}
|
|
51
|
-
};
|
|
125
|
+
module.exports = FileWatcher;
|
|
@@ -0,0 +1,295 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Internal `FileWatcher` implementation (see {@link module:lib/file_watcher} for the public API).
|
|
3
|
+
*
|
|
4
|
+
* **Third-party pieces in use today:** `chokidar` (filesystem watch engine), `glob` (`hasMagic`
|
|
5
|
+
* for validating concrete `add` paths), and `minimatch` (include-pattern magic checks). Policy
|
|
6
|
+
* lists and matching live largely in {@link module:lib/utils/file_watch_glob_policy}.
|
|
7
|
+
*
|
|
8
|
+
* Nothing here assumes a single vendor forever: the engine is created only through
|
|
9
|
+
* {@link FileWatcherImpl.createWatcher} (with `chokidar` loaded lazily there), and glob/minimatch
|
|
10
|
+
* are confined to small helpers—so swapping the watcher, glob detection, or minimatch usage is
|
|
11
|
+
* meant to be localized work rather than a rewrite.
|
|
12
|
+
*
|
|
13
|
+
* @module lib/file_watcher_impl
|
|
14
|
+
*/
|
|
15
|
+
const path = require('path');
|
|
16
|
+
const EventEmitter = require('events').EventEmitter;
|
|
17
|
+
const { hasMagic } = require('glob');
|
|
18
|
+
const { Minimatch } = require('minimatch');
|
|
19
|
+
|
|
20
|
+
const isEmfileError = require('./utils/is_emfile_error');
|
|
21
|
+
const { convertToPosix } = require('./utils/posix');
|
|
22
|
+
const {
|
|
23
|
+
buildWatchGlobPolicy,
|
|
24
|
+
pathMatchesWatchTarget,
|
|
25
|
+
} = require('./utils/file_watch_glob_policy');
|
|
26
|
+
|
|
27
|
+
/** @private Only {@link FileWatcherImpl.create} may construct instances. */
|
|
28
|
+
const kInternal = Symbol('FileWatcherImpl.internal');
|
|
29
|
+
|
|
30
|
+
function includePatternHasGlobMagic(pattern) {
|
|
31
|
+
return new Minimatch(pattern, { dot: true }).hasMagic();
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
function resolvedPathIsUnderCwd(resolved) {
|
|
35
|
+
const rel = path.relative(process.cwd(), resolved);
|
|
36
|
+
return rel !== '' && !rel.startsWith('..') && !path.isAbsolute(rel);
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
/**
|
|
40
|
+
* Whether the watch engine's `all` event (add/addDir) refers to the same path as `file` /
|
|
41
|
+
* `absTarget`.
|
|
42
|
+
*/
|
|
43
|
+
function dynamicAddEventMatchesPath(watchedPath, file, absTarget) {
|
|
44
|
+
const absWatched = path.resolve(watchedPath);
|
|
45
|
+
const relWatched = convertToPosix(
|
|
46
|
+
path.relative(process.cwd(), absWatched),
|
|
47
|
+
);
|
|
48
|
+
const relTarget = convertToPosix(
|
|
49
|
+
path.relative(process.cwd(), absTarget),
|
|
50
|
+
);
|
|
51
|
+
return (
|
|
52
|
+
absWatched === absTarget ||
|
|
53
|
+
relWatched === relTarget ||
|
|
54
|
+
watchedPath === file
|
|
55
|
+
);
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
class FileWatcherImpl extends EventEmitter {
|
|
59
|
+
/**
|
|
60
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
|
|
61
|
+
* @param {symbol} internalToken
|
|
62
|
+
*/
|
|
63
|
+
constructor(config, internalToken) {
|
|
64
|
+
super();
|
|
65
|
+
|
|
66
|
+
if (internalToken !== kInternal) {
|
|
67
|
+
throw new TypeError(
|
|
68
|
+
'FileWatcherImpl is not constructable; use FileWatcherImpl.create(config)',
|
|
69
|
+
);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
const policy = buildWatchGlobPolicy(config);
|
|
73
|
+
this._watchPolicy = policy;
|
|
74
|
+
this._explicitWatchRelPaths = new Set();
|
|
75
|
+
const opts = { ignoreInitial: true };
|
|
76
|
+
if (policy.ignorePatterns.length) {
|
|
77
|
+
opts.ignored = policy.ignorePatterns;
|
|
78
|
+
}
|
|
79
|
+
|
|
80
|
+
this.fileWatcher = FileWatcherImpl.createWatcher('.', opts);
|
|
81
|
+
|
|
82
|
+
let readySettled = false;
|
|
83
|
+
this._readyPromise = new Promise((resolve, reject) => {
|
|
84
|
+
this.fileWatcher.once('ready', () => {
|
|
85
|
+
readySettled = true;
|
|
86
|
+
resolve();
|
|
87
|
+
});
|
|
88
|
+
this.fileWatcher.once('error', (err) => {
|
|
89
|
+
if (readySettled || isEmfileError(err)) {
|
|
90
|
+
return;
|
|
91
|
+
}
|
|
92
|
+
readySettled = true;
|
|
93
|
+
reject(err);
|
|
94
|
+
});
|
|
95
|
+
});
|
|
96
|
+
|
|
97
|
+
for (let i = 0; i < policy.includePatterns.length; i++) {
|
|
98
|
+
const pattern = policy.includePatterns[i];
|
|
99
|
+
if (includePatternHasGlobMagic(pattern)) {
|
|
100
|
+
continue;
|
|
101
|
+
}
|
|
102
|
+
const resolved = path.resolve(process.cwd(), pattern);
|
|
103
|
+
if (!resolvedPathIsUnderCwd(resolved)) {
|
|
104
|
+
this.fileWatcher.add(path.normalize(pattern));
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
|
|
108
|
+
const onEngineFsEvent = this.onEngineFsEvent.bind(this);
|
|
109
|
+
this.fileWatcher.on('change', onEngineFsEvent);
|
|
110
|
+
this.fileWatcher.on('add', onEngineFsEvent);
|
|
111
|
+
this.fileWatcher.on('unlink', onEngineFsEvent);
|
|
112
|
+
this.fileWatcher.on('unlinkDir', onEngineFsEvent);
|
|
113
|
+
this.fileWatcher.on('error', (err) => {
|
|
114
|
+
if (isEmfileError(err)) {
|
|
115
|
+
this.onEMFILE();
|
|
116
|
+
}
|
|
117
|
+
});
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* @param {{ get: function(string): *, isCwdMode: function(): boolean }} config
|
|
122
|
+
* @returns {Promise<FileWatcherImpl>}
|
|
123
|
+
*/
|
|
124
|
+
static async create(config) {
|
|
125
|
+
const fw = new FileWatcherImpl(config, kInternal);
|
|
126
|
+
await fw._readyPromise;
|
|
127
|
+
return fw;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
onEngineFsEvent(filePath) {
|
|
131
|
+
const resolved = path.resolve(filePath);
|
|
132
|
+
const posixRel = convertToPosix(
|
|
133
|
+
path.relative(process.cwd(), resolved),
|
|
134
|
+
);
|
|
135
|
+
if (
|
|
136
|
+
pathMatchesWatchTarget(posixRel, this._watchPolicy) ||
|
|
137
|
+
this._explicitWatchRelPaths.has(posixRel)
|
|
138
|
+
) {
|
|
139
|
+
this.onFileChanged(filePath);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
onFileChanged(filePath) {
|
|
144
|
+
this.emit('fileChanged', filePath);
|
|
145
|
+
}
|
|
146
|
+
|
|
147
|
+
onEMFILE() {
|
|
148
|
+
this.emit('EMFILE');
|
|
149
|
+
}
|
|
150
|
+
|
|
151
|
+
/**
|
|
152
|
+
* @param {Error} err
|
|
153
|
+
* @returns {Promise<never>}
|
|
154
|
+
*/
|
|
155
|
+
async _closeAfterFailedAdd(err) {
|
|
156
|
+
try {
|
|
157
|
+
await this.close();
|
|
158
|
+
} catch {
|
|
159
|
+
// ignore close errors; primary error is `err`
|
|
160
|
+
}
|
|
161
|
+
throw err;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
/**
|
|
165
|
+
* @param {string} file
|
|
166
|
+
* @returns {Promise<void>}
|
|
167
|
+
*/
|
|
168
|
+
async _validateConcreteAddPath(file) {
|
|
169
|
+
if (typeof file !== 'string') {
|
|
170
|
+
await this._closeAfterFailedAdd(
|
|
171
|
+
new TypeError('FileWatcher.add expects a string path'),
|
|
172
|
+
);
|
|
173
|
+
}
|
|
174
|
+
if (file.length === 0) {
|
|
175
|
+
await this._closeAfterFailedAdd(
|
|
176
|
+
new TypeError('FileWatcher.add expects a non-empty path'),
|
|
177
|
+
);
|
|
178
|
+
}
|
|
179
|
+
if (hasMagic(file, { magicalBraces: true })) {
|
|
180
|
+
await this._closeAfterFailedAdd(
|
|
181
|
+
new TypeError(
|
|
182
|
+
'FileWatcher.add does not accept glob patterns; pass a concrete file path: ' +
|
|
183
|
+
file,
|
|
184
|
+
),
|
|
185
|
+
);
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
/**
|
|
190
|
+
* Wait for dynamic `add` on the engine to settle (`all` add/addDir, `error`, or timeout).
|
|
191
|
+
*
|
|
192
|
+
* @param {string} file
|
|
193
|
+
* @returns {Promise<void>}
|
|
194
|
+
*/
|
|
195
|
+
async _awaitEngineDynamicAdd(file) {
|
|
196
|
+
const engine = this.fileWatcher;
|
|
197
|
+
const absTarget = path.resolve(process.cwd(), file);
|
|
198
|
+
let onError;
|
|
199
|
+
let onAll;
|
|
200
|
+
let timeoutId;
|
|
201
|
+
|
|
202
|
+
const cleanup = () => {
|
|
203
|
+
if (onError) {
|
|
204
|
+
engine.removeListener('error', onError);
|
|
205
|
+
}
|
|
206
|
+
if (onAll) {
|
|
207
|
+
engine.removeListener('all', onAll);
|
|
208
|
+
}
|
|
209
|
+
if (timeoutId !== undefined) {
|
|
210
|
+
clearTimeout(timeoutId);
|
|
211
|
+
}
|
|
212
|
+
};
|
|
213
|
+
|
|
214
|
+
await new Promise((resolve, reject) => {
|
|
215
|
+
let settled = false;
|
|
216
|
+
|
|
217
|
+
const finish = (err) => {
|
|
218
|
+
if (settled) {
|
|
219
|
+
return;
|
|
220
|
+
}
|
|
221
|
+
settled = true;
|
|
222
|
+
cleanup();
|
|
223
|
+
if (err) {
|
|
224
|
+
reject(err);
|
|
225
|
+
} else {
|
|
226
|
+
resolve();
|
|
227
|
+
}
|
|
228
|
+
};
|
|
229
|
+
|
|
230
|
+
onError = (err) => {
|
|
231
|
+
finish(err);
|
|
232
|
+
};
|
|
233
|
+
engine.prependOnceListener('error', onError);
|
|
234
|
+
|
|
235
|
+
onAll = (event, watchedPath) => {
|
|
236
|
+
if (event === 'add' || event === 'addDir') {
|
|
237
|
+
if (dynamicAddEventMatchesPath(watchedPath, file, absTarget)) {
|
|
238
|
+
finish();
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
};
|
|
242
|
+
engine.on('all', onAll);
|
|
243
|
+
|
|
244
|
+
timeoutId = setTimeout(() => finish(), 1000);
|
|
245
|
+
|
|
246
|
+
try {
|
|
247
|
+
engine.add(file);
|
|
248
|
+
} catch (err) {
|
|
249
|
+
finish(err);
|
|
250
|
+
}
|
|
251
|
+
});
|
|
252
|
+
}
|
|
253
|
+
|
|
254
|
+
/**
|
|
255
|
+
* @param {string} file
|
|
256
|
+
* @returns {Promise<void>}
|
|
257
|
+
*/
|
|
258
|
+
async add(file) {
|
|
259
|
+
await this._validateConcreteAddPath(file);
|
|
260
|
+
const rel = convertToPosix(
|
|
261
|
+
path.relative(
|
|
262
|
+
process.cwd(),
|
|
263
|
+
path.resolve(process.cwd(), file),
|
|
264
|
+
),
|
|
265
|
+
);
|
|
266
|
+
this._explicitWatchRelPaths.add(rel);
|
|
267
|
+
try {
|
|
268
|
+
await this._awaitEngineDynamicAdd(file);
|
|
269
|
+
} catch (err) {
|
|
270
|
+
await this._closeAfterFailedAdd(err);
|
|
271
|
+
}
|
|
272
|
+
}
|
|
273
|
+
|
|
274
|
+
/**
|
|
275
|
+
* @returns {Promise<void>}
|
|
276
|
+
*/
|
|
277
|
+
close() {
|
|
278
|
+
if (this.fileWatcher) {
|
|
279
|
+
const engine = this.fileWatcher;
|
|
280
|
+
this.fileWatcher = null;
|
|
281
|
+
return Promise.resolve(engine.close()).then(() => {
|
|
282
|
+
this.removeAllListeners();
|
|
283
|
+
});
|
|
284
|
+
}
|
|
285
|
+
this.removeAllListeners();
|
|
286
|
+
return Promise.resolve();
|
|
287
|
+
}
|
|
288
|
+
}
|
|
289
|
+
|
|
290
|
+
/** Lazy `require` keeps chokidar off the load path until a real watcher is built (e.g. tests stub this). */
|
|
291
|
+
FileWatcherImpl.createWatcher = function createWatcher(rootPath, opts) {
|
|
292
|
+
return require('chokidar').watch(rootPath, opts);
|
|
293
|
+
};
|
|
294
|
+
|
|
295
|
+
module.exports = FileWatcherImpl;
|
package/lib/launcher.js
CHANGED
|
@@ -1,16 +1,12 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
3
|
-
const log = require('npmlog');
|
|
4
|
-
const tmp = require('tmp');
|
|
1
|
+
const log = require('./log');
|
|
2
|
+
const fs = require('fs');
|
|
5
3
|
const path = require('path');
|
|
6
|
-
const
|
|
4
|
+
const { fromCallback } = require('./utils/promises');
|
|
5
|
+
const { registerCleanup } = require('./utils/tmp-cleanup');
|
|
7
6
|
|
|
8
7
|
const template = require('./utils/strutils').template;
|
|
9
8
|
const ProcessCtl = require('./process-ctl');
|
|
10
9
|
|
|
11
|
-
// setup graceful cleanup: removes the created directories when an uncaught exception occurs.
|
|
12
|
-
tmp.setGracefulCleanup();
|
|
13
|
-
|
|
14
10
|
module.exports = class Launcher {
|
|
15
11
|
constructor(name, settings, config) {
|
|
16
12
|
this.name = name;
|
|
@@ -54,15 +50,11 @@ module.exports = class Launcher {
|
|
|
54
50
|
const settings = this.settings;
|
|
55
51
|
this.setupBrowserTmpDir();
|
|
56
52
|
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
settings.setup.call(this, this.config, setupCallback);
|
|
61
|
-
});
|
|
62
|
-
}
|
|
53
|
+
const setupPromise = settings.setup
|
|
54
|
+
? fromCallback(setupCallback => settings.setup.call(this, this.config, setupCallback))
|
|
55
|
+
: Promise.resolve();
|
|
63
56
|
|
|
64
|
-
|
|
65
|
-
}).then(() => this.doLaunch());
|
|
57
|
+
return setupPromise.then(() => this.doLaunch());
|
|
66
58
|
}
|
|
67
59
|
|
|
68
60
|
doLaunch() {
|
|
@@ -84,7 +76,7 @@ module.exports = class Launcher {
|
|
|
84
76
|
|
|
85
77
|
return this.processCtl.exec(cmd, options);
|
|
86
78
|
} else {
|
|
87
|
-
return
|
|
79
|
+
return Promise.reject(new Error('No command or exe/args specified for launcher ' + this.name));
|
|
88
80
|
}
|
|
89
81
|
}
|
|
90
82
|
|
|
@@ -130,12 +122,11 @@ module.exports = class Launcher {
|
|
|
130
122
|
|
|
131
123
|
setupBrowserTmpDir() {
|
|
132
124
|
const userDataDir = this.config.getUserDataDir();
|
|
133
|
-
const tmpPath = path.join(userDataDir, 'testem-' + this.id);
|
|
125
|
+
const tmpPath = path.join(userDataDir, 'testem-' + this.id + '-');
|
|
134
126
|
|
|
135
|
-
|
|
136
|
-
|
|
137
|
-
|
|
138
|
-
});
|
|
127
|
+
const dir = fs.mkdtempSync(tmpPath);
|
|
128
|
+
registerCleanup(dir);
|
|
129
|
+
this.browserTmpDirectory = dir;
|
|
139
130
|
}
|
|
140
131
|
|
|
141
132
|
browserTmpDir() {
|
|
@@ -143,6 +134,6 @@ module.exports = class Launcher {
|
|
|
143
134
|
this.setupBrowserTmpDir();
|
|
144
135
|
}
|
|
145
136
|
|
|
146
|
-
return this.browserTmpDirectory
|
|
137
|
+
return this.browserTmpDirectory;
|
|
147
138
|
}
|
|
148
139
|
};
|
package/lib/log.js
ADDED
|
@@ -0,0 +1,92 @@
|
|
|
1
|
+
const EventEmitter = require('events');
|
|
2
|
+
const procLog = require('proc-log').log;
|
|
3
|
+
|
|
4
|
+
class Log extends EventEmitter {
|
|
5
|
+
constructor() {
|
|
6
|
+
super();
|
|
7
|
+
this.stream = process.stderr;
|
|
8
|
+
}
|
|
9
|
+
|
|
10
|
+
formatRecord(level, args) {
|
|
11
|
+
let prefix = '';
|
|
12
|
+
let message = '';
|
|
13
|
+
|
|
14
|
+
if (args.length === 1) {
|
|
15
|
+
message = args[0];
|
|
16
|
+
} else if (args.length >= 2) {
|
|
17
|
+
prefix = args[0] || '';
|
|
18
|
+
message = args.slice(1).join(' ');
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
return {
|
|
22
|
+
level,
|
|
23
|
+
prefix,
|
|
24
|
+
message: String(message)
|
|
25
|
+
};
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
write(record) {
|
|
29
|
+
if (this.stream && typeof this.stream.write === 'function') {
|
|
30
|
+
this.stream.write(record.message + '\n');
|
|
31
|
+
}
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
emitLog(level, args) {
|
|
35
|
+
const record = this.formatRecord(level, args);
|
|
36
|
+
|
|
37
|
+
this.emit(`log.${level}`, record);
|
|
38
|
+
this.write(record);
|
|
39
|
+
|
|
40
|
+
if (procLog && typeof procLog[level] === 'function') {
|
|
41
|
+
procLog[level](...args);
|
|
42
|
+
}
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
error(...args) {
|
|
46
|
+
this.emitLog('error', args);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
notice(...args) {
|
|
50
|
+
this.emitLog('notice', args);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
warn(...args) {
|
|
54
|
+
this.emitLog('warn', args);
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
info(...args) {
|
|
58
|
+
this.emitLog('info', args);
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
verbose(...args) {
|
|
62
|
+
this.emitLog('verbose', args);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
http(...args) {
|
|
66
|
+
this.emitLog('http', args);
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
silly(...args) {
|
|
70
|
+
this.emitLog('silly', args);
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
timing(...args) {
|
|
74
|
+
this.emitLog('timing', args);
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
pause() {
|
|
78
|
+
this.emit('log.pause');
|
|
79
|
+
if (procLog && typeof procLog.pause === 'function') {
|
|
80
|
+
procLog.pause();
|
|
81
|
+
}
|
|
82
|
+
}
|
|
83
|
+
|
|
84
|
+
resume() {
|
|
85
|
+
this.emit('log.resume');
|
|
86
|
+
if (procLog && typeof procLog.resume === 'function') {
|
|
87
|
+
procLog.resume();
|
|
88
|
+
}
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
module.exports = new Log();
|
package/lib/process-ctl.js
CHANGED
|
@@ -1,8 +1,7 @@
|
|
|
1
1
|
|
|
2
2
|
|
|
3
|
-
const log = require('
|
|
3
|
+
const log = require('./log');
|
|
4
4
|
const execa = require('execa').execa;
|
|
5
|
-
const Bluebird = require('bluebird');
|
|
6
5
|
const util = require('util');
|
|
7
6
|
const EventEmitter = require('events').EventEmitter;
|
|
8
7
|
const spawnargs = require('spawn-args');
|
|
@@ -12,6 +11,7 @@ const envWithLocalPath = require('./utils/env-with-local-path');
|
|
|
12
11
|
const fileutils = require('./utils/fileutils');
|
|
13
12
|
const Process = require('./utils/process');
|
|
14
13
|
const isWin = require('./utils/is-win')();
|
|
14
|
+
const { reduce } = require('./utils/promises');
|
|
15
15
|
|
|
16
16
|
const fileExists = fileutils.fileExists;
|
|
17
17
|
const executableExists = fileutils.executableExists;
|
|
@@ -53,14 +53,14 @@ module.exports = class ProcessCtl extends EventEmitter {
|
|
|
53
53
|
|
|
54
54
|
let p = new Process(this.name, { killTimeout: this.killTimeout }, process);
|
|
55
55
|
this.emit('processStarted', p);
|
|
56
|
-
return
|
|
56
|
+
return Promise.resolve(p);
|
|
57
57
|
}
|
|
58
58
|
|
|
59
59
|
spawn(exe, args, options) {
|
|
60
60
|
let _options = this.prepareOptions(options);
|
|
61
61
|
|
|
62
62
|
if (Array.isArray(exe)) {
|
|
63
|
-
return
|
|
63
|
+
return reduce(exe, (found, exe) => {
|
|
64
64
|
if (found) {
|
|
65
65
|
return found;
|
|
66
66
|
}
|
|
@@ -10,7 +10,7 @@ The actual AppView. This encapsulates the entire UI.
|
|
|
10
10
|
*/
|
|
11
11
|
|
|
12
12
|
const StyledString = require('styled_string');
|
|
13
|
-
const log = require('
|
|
13
|
+
const log = require('../../log');
|
|
14
14
|
const Backbone = require('backbone');
|
|
15
15
|
|
|
16
16
|
const View = require('./view');
|
|
@@ -60,6 +60,7 @@ module.exports = View.extend({
|
|
|
60
60
|
if (atLeastOne && this.get('currentTab') < 0) {
|
|
61
61
|
this.set('currentTab', 0);
|
|
62
62
|
}
|
|
63
|
+
this.renderTop();
|
|
63
64
|
this.renderMiddle();
|
|
64
65
|
this.renderBottom();
|
|
65
66
|
});
|
|
@@ -205,7 +205,6 @@ module.exports = View.extend({
|
|
|
205
205
|
var runner = this.get('runner');
|
|
206
206
|
var results = runner.get('results');
|
|
207
207
|
var topLevelError = results ? results.get('topLevelError') : null;
|
|
208
|
-
var tests = null;
|
|
209
208
|
var out = '';
|
|
210
209
|
var pendingOut = '';
|
|
211
210
|
|
|
@@ -213,7 +212,8 @@ module.exports = View.extend({
|
|
|
213
212
|
out += 'Top Level:\n' + indent(topLevelError) + '\n\n';
|
|
214
213
|
}
|
|
215
214
|
|
|
216
|
-
|
|
215
|
+
var tests;
|
|
216
|
+
if (results && (tests = results.get('tests')) && !topLevelError) {
|
|
217
217
|
var total = results.get('total');
|
|
218
218
|
var pending = results.get('pending');
|
|
219
219
|
var allDone = results.get('all');
|