concurrently 6.4.0 → 6.5.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 +6 -2
- package/bin/concurrently.js +9 -1
- package/bin/concurrently.spec.js +44 -1
- package/bin/epilogue.txt +6 -2
- package/index.js +8 -1
- package/package.json +1 -1
- package/src/command-parser/expand-npm-wildcard.spec.js +29 -0
- package/src/command.js +14 -0
- package/src/command.spec.js +93 -1
- package/src/completion-listener.js +1 -1
- package/src/completion-listener.spec.js +1 -0
- package/src/concurrently.js +6 -1
- package/src/defaults.js +3 -1
- package/src/flow-control/base-handler.spec.js +22 -0
- package/src/flow-control/log-timings.js +64 -0
- package/src/flow-control/log-timings.spec.js +137 -0
- package/src/flow-control/restart-process.spec.js +9 -1
- package/src/logger.js +57 -0
- package/src/logger.spec.js +95 -0
package/README.md
CHANGED
|
@@ -203,6 +203,10 @@ Examples:
|
|
|
203
203
|
$ concurrently --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold"
|
|
204
204
|
"http-server" "npm run watch"
|
|
205
205
|
|
|
206
|
+
- Configuring via environment variables with CONCURRENTLY_ prefix
|
|
207
|
+
|
|
208
|
+
$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true concurrently "echo hello" "echo world"
|
|
209
|
+
|
|
206
210
|
- Send input to default
|
|
207
211
|
|
|
208
212
|
$ concurrently --handle-input "nodemon" "npm run watch-js"
|
|
@@ -222,9 +226,9 @@ Examples:
|
|
|
222
226
|
|
|
223
227
|
$ concurrently npm:watch-node npm:watch-js npm:watch-css
|
|
224
228
|
|
|
225
|
-
- Shortened NPM run command with wildcard
|
|
229
|
+
- Shortened NPM run command with wildcard (make sure to wrap it in quotes!)
|
|
226
230
|
|
|
227
|
-
$ concurrently npm:watch-*
|
|
231
|
+
$ concurrently "npm:watch-*"
|
|
228
232
|
|
|
229
233
|
For more details, visit https://github.com/open-cli-tools/concurrently
|
|
230
234
|
```
|
package/bin/concurrently.js
CHANGED
|
@@ -11,6 +11,8 @@ const args = yargs
|
|
|
11
11
|
.version('v', require('../package.json').version)
|
|
12
12
|
.alias('v', 'V')
|
|
13
13
|
.alias('v', 'version')
|
|
14
|
+
// TODO: Add some tests for this.
|
|
15
|
+
.env('CONCURRENTLY')
|
|
14
16
|
.options({
|
|
15
17
|
// General
|
|
16
18
|
'm': {
|
|
@@ -62,6 +64,11 @@ const args = yargs
|
|
|
62
64
|
default: defaults.hide,
|
|
63
65
|
type: 'string'
|
|
64
66
|
},
|
|
67
|
+
'timings': {
|
|
68
|
+
describe: 'Show timing information for all processes',
|
|
69
|
+
type: 'boolean',
|
|
70
|
+
default: defaults.timings
|
|
71
|
+
},
|
|
65
72
|
|
|
66
73
|
// Kill others
|
|
67
74
|
'k': {
|
|
@@ -142,7 +149,7 @@ const args = yargs
|
|
|
142
149
|
'Can be either the index or the name of the process.'
|
|
143
150
|
}
|
|
144
151
|
})
|
|
145
|
-
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide'], 'General')
|
|
152
|
+
.group(['m', 'n', 'name-separator', 'raw', 's', 'no-color', 'hide', 'timings'], 'General')
|
|
146
153
|
.group(['p', 'c', 'l', 't'], 'Prefix styling')
|
|
147
154
|
.group(['i', 'default-input-target'], 'Input handling')
|
|
148
155
|
.group(['k', 'kill-others-on-fail'], 'Killing other processes')
|
|
@@ -172,6 +179,7 @@ concurrently(args._.map((command, index) => ({
|
|
|
172
179
|
restartTries: args.restartTries,
|
|
173
180
|
successCondition: args.success,
|
|
174
181
|
timestampFormat: args.timestampFormat,
|
|
182
|
+
timings: args.timings
|
|
175
183
|
}).then(
|
|
176
184
|
() => process.exit(0),
|
|
177
185
|
() => process.exit(1)
|
package/bin/concurrently.spec.js
CHANGED
|
@@ -346,7 +346,6 @@ describe('--handle-input', () => {
|
|
|
346
346
|
}, done);
|
|
347
347
|
});
|
|
348
348
|
|
|
349
|
-
|
|
350
349
|
it('forwards input to process --default-input-target', done => {
|
|
351
350
|
const lines = [];
|
|
352
351
|
const child = run('-ki --default-input-target 1 "node fixtures/read-echo.js" "node fixtures/read-echo.js"');
|
|
@@ -383,3 +382,47 @@ describe('--handle-input', () => {
|
|
|
383
382
|
}, done);
|
|
384
383
|
});
|
|
385
384
|
});
|
|
385
|
+
|
|
386
|
+
describe('--timings', () => {
|
|
387
|
+
const defaultTimestampFormatRegex = /\d{4}-\d{2}-\d{2} \d{2}:\d{2}:\d{2}.\d{3}/;
|
|
388
|
+
const processStartedMessageRegex = (index, command) => {
|
|
389
|
+
return new RegExp( `^\\[${ index }] ${ command } started at ${ defaultTimestampFormatRegex.source }$` );
|
|
390
|
+
};
|
|
391
|
+
const processStoppedMessageRegex = (index, command) => {
|
|
392
|
+
return new RegExp( `^\\[${ index }] ${ command } stopped at ${ defaultTimestampFormatRegex.source } after (\\d|,)+ms$` );
|
|
393
|
+
};
|
|
394
|
+
const expectLinesForProcessStartAndStop = (lines, index, command) => {
|
|
395
|
+
const escapedCommand = _.escapeRegExp(command);
|
|
396
|
+
expect(lines).toContainEqual(expect.stringMatching(processStartedMessageRegex(index, escapedCommand)));
|
|
397
|
+
expect(lines).toContainEqual(expect.stringMatching(processStoppedMessageRegex(index, escapedCommand)));
|
|
398
|
+
};
|
|
399
|
+
|
|
400
|
+
const expectLinesForTimingsTable = (lines) => {
|
|
401
|
+
const tableTopBorderRegex = /┌[─┬]+┐/g;
|
|
402
|
+
expect(lines).toContainEqual(expect.stringMatching(tableTopBorderRegex));
|
|
403
|
+
const tableHeaderRowRegex = /(\W+(name|duration|exit code|killed|command)\W+){5}/g;
|
|
404
|
+
expect(lines).toContainEqual(expect.stringMatching(tableHeaderRowRegex));
|
|
405
|
+
const tableBottomBorderRegex = /└[─┴]+┘/g;
|
|
406
|
+
expect(lines).toContainEqual(expect.stringMatching(tableBottomBorderRegex));
|
|
407
|
+
};
|
|
408
|
+
|
|
409
|
+
it('shows timings on success', done => {
|
|
410
|
+
const child = run('--timings "sleep 0.5" "exit 0"');
|
|
411
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
412
|
+
expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.5');
|
|
413
|
+
expectLinesForProcessStartAndStop(lines, 1, 'exit 0');
|
|
414
|
+
expectLinesForTimingsTable(lines);
|
|
415
|
+
done();
|
|
416
|
+
}, done);
|
|
417
|
+
});
|
|
418
|
+
|
|
419
|
+
it('shows timings on failure', done => {
|
|
420
|
+
const child = run('--timings "sleep 0.75" "exit 1"');
|
|
421
|
+
child.log.pipe(buffer(child.close)).subscribe(lines => {
|
|
422
|
+
expectLinesForProcessStartAndStop(lines, 0, 'sleep 0.75');
|
|
423
|
+
expectLinesForProcessStartAndStop(lines, 1, 'exit 1');
|
|
424
|
+
expectLinesForTimingsTable(lines);
|
|
425
|
+
done();
|
|
426
|
+
}, done);
|
|
427
|
+
});
|
|
428
|
+
});
|
package/bin/epilogue.txt
CHANGED
|
@@ -16,6 +16,10 @@ Examples:
|
|
|
16
16
|
|
|
17
17
|
$ $0 --names "HTTP,WATCH" -c "bgBlue.bold,bgMagenta.bold" "http-server" "npm run watch"
|
|
18
18
|
|
|
19
|
+
- Configuring via environment variables with CONCURRENTLY_ prefix
|
|
20
|
+
|
|
21
|
+
$ CONCURRENTLY_RAW=true CONCURRENTLY_KILL_OTHERS=true $0 "echo hello" "echo world"
|
|
22
|
+
|
|
19
23
|
- Send input to default
|
|
20
24
|
|
|
21
25
|
$ $0 --handle-input "nodemon" "npm run watch-js"
|
|
@@ -35,8 +39,8 @@ Examples:
|
|
|
35
39
|
|
|
36
40
|
$ $0 npm:watch-node npm:watch-js npm:watch-css
|
|
37
41
|
|
|
38
|
-
- Shortened NPM run command with wildcard
|
|
42
|
+
- Shortened NPM run command with wildcard (make sure to wrap it in quotes!)
|
|
39
43
|
|
|
40
|
-
$ $0 npm:watch-*
|
|
44
|
+
$ $0 "npm:watch-*"
|
|
41
45
|
|
|
42
46
|
For more details, visit https://github.com/open-cli-tools/concurrently
|
package/index.js
CHANGED
|
@@ -8,6 +8,7 @@ const RestartProcess = require('./src/flow-control/restart-process');
|
|
|
8
8
|
|
|
9
9
|
const concurrently = require('./src/concurrently');
|
|
10
10
|
const Logger = require('./src/logger');
|
|
11
|
+
const LogTimings = require( './src/flow-control/log-timings' );
|
|
11
12
|
|
|
12
13
|
module.exports = exports = (commands, options = {}) => {
|
|
13
14
|
const logger = new Logger({
|
|
@@ -43,9 +44,14 @@ module.exports = exports = (commands, options = {}) => {
|
|
|
43
44
|
new KillOthers({
|
|
44
45
|
logger,
|
|
45
46
|
conditions: options.killOthers
|
|
47
|
+
}),
|
|
48
|
+
new LogTimings({
|
|
49
|
+
logger: options.timings ? logger: null,
|
|
50
|
+
timestampFormat: options.timestampFormat,
|
|
46
51
|
})
|
|
47
52
|
],
|
|
48
|
-
prefixColors: options.prefixColors || []
|
|
53
|
+
prefixColors: options.prefixColors || [],
|
|
54
|
+
timings: options.timings
|
|
49
55
|
});
|
|
50
56
|
};
|
|
51
57
|
|
|
@@ -60,3 +66,4 @@ exports.LogError = LogError;
|
|
|
60
66
|
exports.LogExit = LogExit;
|
|
61
67
|
exports.LogOutput = LogOutput;
|
|
62
68
|
exports.RestartProcess = RestartProcess;
|
|
69
|
+
exports.LogTimings = LogTimings;
|
package/package.json
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const ExpandNpmWildcard = require('./expand-npm-wildcard');
|
|
2
|
+
const fs = require('fs');
|
|
2
3
|
|
|
3
4
|
let parser, readPkg;
|
|
4
5
|
|
|
@@ -7,6 +8,34 @@ beforeEach(() => {
|
|
|
7
8
|
parser = new ExpandNpmWildcard(readPkg);
|
|
8
9
|
});
|
|
9
10
|
|
|
11
|
+
describe('ExpandNpmWildcard#readPackage', () => {
|
|
12
|
+
it('can read package', () => {
|
|
13
|
+
const expectedPackage = {
|
|
14
|
+
'name': 'concurrently',
|
|
15
|
+
'version': '6.4.0',
|
|
16
|
+
};
|
|
17
|
+
jest.spyOn(fs, 'readFileSync').mockImplementation((path, options) => {
|
|
18
|
+
if (path === 'package.json') {
|
|
19
|
+
return JSON.stringify(expectedPackage);
|
|
20
|
+
}
|
|
21
|
+
return null;
|
|
22
|
+
});
|
|
23
|
+
|
|
24
|
+
const actualReadPackage = ExpandNpmWildcard.readPackage();
|
|
25
|
+
expect(actualReadPackage).toEqual(expectedPackage);
|
|
26
|
+
});
|
|
27
|
+
|
|
28
|
+
it('can handle errors reading package', () => {
|
|
29
|
+
jest.spyOn(fs, 'readFileSync').mockImplementation(() => {
|
|
30
|
+
throw new Error('Error reading package');
|
|
31
|
+
});
|
|
32
|
+
|
|
33
|
+
expect(() => ExpandNpmWildcard.readPackage()).not.toThrow();
|
|
34
|
+
expect(ExpandNpmWildcard.readPackage()).toEqual({});
|
|
35
|
+
});
|
|
36
|
+
|
|
37
|
+
});
|
|
38
|
+
|
|
10
39
|
it('returns same command if not an npm run command', () => {
|
|
11
40
|
const commandInfo = {
|
|
12
41
|
command: 'npm test'
|
package/src/command.js
CHANGED
|
@@ -17,6 +17,7 @@ module.exports = class Command {
|
|
|
17
17
|
this.killed = false;
|
|
18
18
|
|
|
19
19
|
this.error = new Rx.Subject();
|
|
20
|
+
this.timer = new Rx.Subject();
|
|
20
21
|
this.close = new Rx.Subject();
|
|
21
22
|
this.stdout = new Rx.Subject();
|
|
22
23
|
this.stderr = new Rx.Subject();
|
|
@@ -26,13 +27,21 @@ module.exports = class Command {
|
|
|
26
27
|
const child = this.spawn(this.command, this.spawnOpts);
|
|
27
28
|
this.process = child;
|
|
28
29
|
this.pid = child.pid;
|
|
30
|
+
const startDate = new Date(Date.now());
|
|
31
|
+
const highResStartTime = process.hrtime();
|
|
32
|
+
this.timer.next({startDate});
|
|
29
33
|
|
|
30
34
|
Rx.fromEvent(child, 'error').subscribe(event => {
|
|
31
35
|
this.process = undefined;
|
|
36
|
+
const endDate = new Date(Date.now());
|
|
37
|
+
this.timer.next({startDate, endDate});
|
|
32
38
|
this.error.next(event);
|
|
33
39
|
});
|
|
34
40
|
Rx.fromEvent(child, 'close').subscribe(([exitCode, signal]) => {
|
|
35
41
|
this.process = undefined;
|
|
42
|
+
const endDate = new Date(Date.now());
|
|
43
|
+
this.timer.next({startDate, endDate});
|
|
44
|
+
const [durationSeconds, durationNanoSeconds] = process.hrtime(highResStartTime);
|
|
36
45
|
this.close.next({
|
|
37
46
|
command: {
|
|
38
47
|
command: this.command,
|
|
@@ -43,6 +52,11 @@ module.exports = class Command {
|
|
|
43
52
|
index: this.index,
|
|
44
53
|
exitCode: exitCode === null ? signal : exitCode,
|
|
45
54
|
killed: this.killed,
|
|
55
|
+
timings: {
|
|
56
|
+
startDate,
|
|
57
|
+
endDate,
|
|
58
|
+
durationSeconds: durationSeconds + (durationNanoSeconds / 1e9),
|
|
59
|
+
}
|
|
46
60
|
});
|
|
47
61
|
});
|
|
48
62
|
child.stdout && pipeTo(Rx.fromEvent(child.stdout, 'data'), this.stdout);
|
package/src/command.spec.js
CHANGED
|
@@ -1,4 +1,5 @@
|
|
|
1
1
|
const EventEmitter = require('events');
|
|
2
|
+
const process = require('process');
|
|
2
3
|
const Command = require('./command');
|
|
3
4
|
|
|
4
5
|
const createProcess = () => {
|
|
@@ -54,6 +55,72 @@ describe('#start()', () => {
|
|
|
54
55
|
process.emit('error', 'foo');
|
|
55
56
|
});
|
|
56
57
|
|
|
58
|
+
it('shares start and close timing events to the timing stream', done => {
|
|
59
|
+
const process = createProcess();
|
|
60
|
+
const command = new Command({ spawn: () => process });
|
|
61
|
+
|
|
62
|
+
const startDate = new Date();
|
|
63
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
64
|
+
jest.spyOn(Date, 'now')
|
|
65
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
66
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
67
|
+
|
|
68
|
+
let callCount = 0;
|
|
69
|
+
command.timer.subscribe(({startDate: actualStartDate, endDate: actualEndDate}) => {
|
|
70
|
+
switch (callCount) {
|
|
71
|
+
case 0:
|
|
72
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
73
|
+
expect(actualEndDate).toBeUndefined();
|
|
74
|
+
break;
|
|
75
|
+
case 1:
|
|
76
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
77
|
+
expect(actualEndDate).toStrictEqual(endDate);
|
|
78
|
+
done();
|
|
79
|
+
break;
|
|
80
|
+
default:
|
|
81
|
+
throw new Error('Unexpected call count');
|
|
82
|
+
}
|
|
83
|
+
callCount++;
|
|
84
|
+
});
|
|
85
|
+
|
|
86
|
+
command.start();
|
|
87
|
+
process.emit('close', 0, null);
|
|
88
|
+
|
|
89
|
+
});
|
|
90
|
+
|
|
91
|
+
it('shares start and error timing events to the timing stream', done => {
|
|
92
|
+
const process = createProcess();
|
|
93
|
+
const command = new Command({ spawn: () => process });
|
|
94
|
+
|
|
95
|
+
const startDate = new Date();
|
|
96
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
97
|
+
jest.spyOn(Date, 'now')
|
|
98
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
99
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
100
|
+
|
|
101
|
+
let callCount = 0;
|
|
102
|
+
command.timer.subscribe(({startDate: actualStartDate, endDate: actualEndDate}) => {
|
|
103
|
+
switch (callCount) {
|
|
104
|
+
case 0:
|
|
105
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
106
|
+
expect(actualEndDate).toBeUndefined();
|
|
107
|
+
break;
|
|
108
|
+
case 1:
|
|
109
|
+
expect(actualStartDate).toStrictEqual(startDate);
|
|
110
|
+
expect(actualEndDate).toStrictEqual(endDate);
|
|
111
|
+
done();
|
|
112
|
+
break;
|
|
113
|
+
default:
|
|
114
|
+
throw new Error('Unexpected call count');
|
|
115
|
+
}
|
|
116
|
+
callCount++;
|
|
117
|
+
});
|
|
118
|
+
|
|
119
|
+
command.start();
|
|
120
|
+
process.emit('error', 0, null);
|
|
121
|
+
|
|
122
|
+
});
|
|
123
|
+
|
|
57
124
|
it('shares closes to the close stream with exit code', done => {
|
|
58
125
|
const process = createProcess();
|
|
59
126
|
const command = new Command({ spawn: () => process });
|
|
@@ -83,6 +150,31 @@ describe('#start()', () => {
|
|
|
83
150
|
process.emit('close', null, 'SIGKILL');
|
|
84
151
|
});
|
|
85
152
|
|
|
153
|
+
it('shares closes to the close stream with timing information', done => {
|
|
154
|
+
const process1 = createProcess();
|
|
155
|
+
const command = new Command({ spawn: () => process1 });
|
|
156
|
+
|
|
157
|
+
const startDate = new Date();
|
|
158
|
+
const endDate = new Date(startDate.getTime() + 1000);
|
|
159
|
+
jest.spyOn(Date, 'now')
|
|
160
|
+
.mockReturnValueOnce(startDate.getTime())
|
|
161
|
+
.mockReturnValueOnce(endDate.getTime());
|
|
162
|
+
|
|
163
|
+
jest.spyOn(process, 'hrtime')
|
|
164
|
+
.mockReturnValueOnce([0, 0])
|
|
165
|
+
.mockReturnValueOnce([1, 1e8]);
|
|
166
|
+
|
|
167
|
+
command.close.subscribe(data => {
|
|
168
|
+
expect(data.timings.startDate).toStrictEqual(startDate);
|
|
169
|
+
expect(data.timings.endDate).toStrictEqual(endDate);
|
|
170
|
+
expect(data.timings.durationSeconds).toBe(1.1);
|
|
171
|
+
done();
|
|
172
|
+
});
|
|
173
|
+
|
|
174
|
+
command.start();
|
|
175
|
+
process1.emit('close', null, 'SIGKILL');
|
|
176
|
+
});
|
|
177
|
+
|
|
86
178
|
it('shares closes to the close stream with command info and index', done => {
|
|
87
179
|
const process = createProcess();
|
|
88
180
|
const commandInfo = {
|
|
@@ -170,7 +262,7 @@ describe('#kill()', () => {
|
|
|
170
262
|
|
|
171
263
|
it('marks the command as killed', done => {
|
|
172
264
|
command.start();
|
|
173
|
-
|
|
265
|
+
|
|
174
266
|
command.close.subscribe(data => {
|
|
175
267
|
expect(data.exitCode).toBe(1);
|
|
176
268
|
expect(data.killed).toBe(true);
|
package/src/concurrently.js
CHANGED
|
@@ -18,6 +18,7 @@ const defaults = {
|
|
|
18
18
|
raw: false,
|
|
19
19
|
controllers: [],
|
|
20
20
|
cwd: undefined,
|
|
21
|
+
timings: false
|
|
21
22
|
};
|
|
22
23
|
|
|
23
24
|
module.exports = (commands, options) => {
|
|
@@ -50,6 +51,7 @@ module.exports = (commands, options) => {
|
|
|
50
51
|
prefixColor: lastColor,
|
|
51
52
|
killProcess: options.kill,
|
|
52
53
|
spawn: options.spawn,
|
|
54
|
+
timings: options.timings,
|
|
53
55
|
}, command)
|
|
54
56
|
);
|
|
55
57
|
})
|
|
@@ -73,7 +75,9 @@ module.exports = (commands, options) => {
|
|
|
73
75
|
maybeRunMore(commandsLeft);
|
|
74
76
|
}
|
|
75
77
|
|
|
76
|
-
return new CompletionListener({
|
|
78
|
+
return new CompletionListener({
|
|
79
|
+
successCondition: options.successCondition,
|
|
80
|
+
})
|
|
77
81
|
.listen(commands)
|
|
78
82
|
.finally(() => {
|
|
79
83
|
handleResult.onFinishCallbacks.forEach((onFinish) => onFinish());
|
|
@@ -86,6 +90,7 @@ function mapToCommandInfo(command) {
|
|
|
86
90
|
name: command.name || '',
|
|
87
91
|
env: command.env || {},
|
|
88
92
|
cwd: command.cwd || '',
|
|
93
|
+
|
|
89
94
|
}, command.prefixColor ? {
|
|
90
95
|
prefixColor: command.prefixColor,
|
|
91
96
|
} : {});
|
package/src/defaults.js
CHANGED
|
@@ -29,5 +29,7 @@ module.exports = {
|
|
|
29
29
|
// Refer to https://date-fns.org/v2.0.1/docs/format
|
|
30
30
|
timestampFormat: 'yyyy-MM-dd HH:mm:ss.SSS',
|
|
31
31
|
// Current working dir passed as option to spawn command. Default: process.cwd()
|
|
32
|
-
cwd: undefined
|
|
32
|
+
cwd: undefined,
|
|
33
|
+
// Whether to show timing information for processes in console output
|
|
34
|
+
timings: false,
|
|
33
35
|
};
|
|
@@ -0,0 +1,22 @@
|
|
|
1
|
+
const stream = require('stream');
|
|
2
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
3
|
+
|
|
4
|
+
const Logger = require('../logger');
|
|
5
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
6
|
+
const BaseHandler = require('./base-handler');
|
|
7
|
+
|
|
8
|
+
let commands, controller, inputStream, logger;
|
|
9
|
+
|
|
10
|
+
beforeEach(() => {
|
|
11
|
+
commands = [
|
|
12
|
+
createFakeCommand('foo', 'echo foo', 0),
|
|
13
|
+
createFakeCommand('bar', 'echo bar', 1),
|
|
14
|
+
];
|
|
15
|
+
inputStream = new stream.PassThrough();
|
|
16
|
+
logger = createMockInstance(Logger);
|
|
17
|
+
controller = new BaseHandler({ logger });
|
|
18
|
+
});
|
|
19
|
+
|
|
20
|
+
it('returns same commands and null onFinish callback by default', () => {
|
|
21
|
+
expect(controller.handle(commands)).toMatchObject({ commands, onFinish: null });
|
|
22
|
+
});
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const formatDate = require('date-fns/format');
|
|
2
|
+
const Rx = require('rxjs');
|
|
3
|
+
const { bufferCount, take } = require('rxjs/operators');
|
|
4
|
+
const _ = require('lodash');
|
|
5
|
+
const BaseHandler = require('./base-handler');
|
|
6
|
+
|
|
7
|
+
module.exports = class LogTimings extends BaseHandler {
|
|
8
|
+
constructor({ logger, timestampFormat }) {
|
|
9
|
+
super({ logger });
|
|
10
|
+
|
|
11
|
+
this.timestampFormat = timestampFormat;
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
printExitInfoTimingTable(exitInfos) {
|
|
15
|
+
const exitInfoTable = _(exitInfos)
|
|
16
|
+
.sortBy(({ timings }) => timings.durationSeconds)
|
|
17
|
+
.reverse()
|
|
18
|
+
.map(({ command, timings, killed, exitCode }) => {
|
|
19
|
+
const readableDurationMs = (timings.endDate - timings.startDate).toLocaleString();
|
|
20
|
+
return {
|
|
21
|
+
name: command.name,
|
|
22
|
+
duration: `${readableDurationMs}ms`,
|
|
23
|
+
'exit code': exitCode,
|
|
24
|
+
killed,
|
|
25
|
+
command: command.command,
|
|
26
|
+
};
|
|
27
|
+
})
|
|
28
|
+
.value();
|
|
29
|
+
|
|
30
|
+
this.logger.logGlobalEvent('Timings:');
|
|
31
|
+
this.logger.logTable(exitInfoTable);
|
|
32
|
+
return exitInfos;
|
|
33
|
+
};
|
|
34
|
+
|
|
35
|
+
handle(commands) {
|
|
36
|
+
if (!this.logger) {
|
|
37
|
+
return { commands };
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// individual process timings
|
|
41
|
+
commands.forEach(command => {
|
|
42
|
+
command.timer.subscribe(({ startDate, endDate }) => {
|
|
43
|
+
if (!endDate) {
|
|
44
|
+
const formattedStartDate = formatDate(startDate, this.timestampFormat);
|
|
45
|
+
this.logger.logCommandEvent(`${command.command} started at ${formattedStartDate}`, command);
|
|
46
|
+
} else {
|
|
47
|
+
const durationMs = endDate.getTime() - startDate.getTime();
|
|
48
|
+
const formattedEndDate = formatDate(endDate, this.timestampFormat);
|
|
49
|
+
this.logger.logCommandEvent(`${command.command} stopped at ${formattedEndDate} after ${durationMs.toLocaleString()}ms`, command);
|
|
50
|
+
}
|
|
51
|
+
});
|
|
52
|
+
});
|
|
53
|
+
|
|
54
|
+
// overall summary timings
|
|
55
|
+
const closeStreams = commands.map(command => command.close);
|
|
56
|
+
this.allProcessesClosed = Rx.merge(...closeStreams).pipe(
|
|
57
|
+
bufferCount(closeStreams.length),
|
|
58
|
+
take(1),
|
|
59
|
+
);
|
|
60
|
+
this.allProcessesClosed.subscribe((exitInfos) => this.printExitInfoTimingTable(exitInfos));
|
|
61
|
+
|
|
62
|
+
return { commands };
|
|
63
|
+
}
|
|
64
|
+
};
|
|
@@ -0,0 +1,137 @@
|
|
|
1
|
+
const { createMockInstance } = require('jest-create-mock-instance');
|
|
2
|
+
const formatDate = require('date-fns/format');
|
|
3
|
+
const Logger = require('../logger');
|
|
4
|
+
const LogTimings = require( './log-timings' );
|
|
5
|
+
const createFakeCommand = require('./fixtures/fake-command');
|
|
6
|
+
|
|
7
|
+
// shown in timing order
|
|
8
|
+
const startDate0 = new Date();
|
|
9
|
+
const startDate1 = new Date(startDate0.getTime() + 1000);
|
|
10
|
+
const endDate1 = new Date(startDate0.getTime() + 3000);
|
|
11
|
+
const endDate0 = new Date(startDate0.getTime() + 5000);
|
|
12
|
+
|
|
13
|
+
const timestampFormat = 'yyyy-MM-dd HH:mm:ss.SSS';
|
|
14
|
+
const getDurationText = (startDate, endDate) => `${(endDate.getTime() - startDate.getTime()).toLocaleString()}ms`;
|
|
15
|
+
const command0DurationTextMs = getDurationText(startDate0, endDate0);
|
|
16
|
+
const command1DurationTextMs = getDurationText(startDate1, endDate1);
|
|
17
|
+
|
|
18
|
+
const exitInfoToTimingInfo = ({ command, timings, killed, exitCode }) => {
|
|
19
|
+
const readableDurationMs = getDurationText(timings.startDate, timings.endDate);
|
|
20
|
+
return {
|
|
21
|
+
name: command.name,
|
|
22
|
+
duration: readableDurationMs,
|
|
23
|
+
'exit code': exitCode,
|
|
24
|
+
killed,
|
|
25
|
+
command: command.command,
|
|
26
|
+
};
|
|
27
|
+
};
|
|
28
|
+
|
|
29
|
+
let controller, logger, commands, command0ExitInfo, command1ExitInfo;
|
|
30
|
+
|
|
31
|
+
beforeEach(() => {
|
|
32
|
+
commands = [
|
|
33
|
+
createFakeCommand('foo', 'command 1', 0),
|
|
34
|
+
createFakeCommand('bar', 'command 2', 1),
|
|
35
|
+
];
|
|
36
|
+
|
|
37
|
+
command0ExitInfo = {
|
|
38
|
+
command: commands[0].command,
|
|
39
|
+
timings: {
|
|
40
|
+
startDate: startDate0,
|
|
41
|
+
endDate: endDate0,
|
|
42
|
+
},
|
|
43
|
+
index: commands[0].index,
|
|
44
|
+
killed: false,
|
|
45
|
+
exitCode: 0,
|
|
46
|
+
};
|
|
47
|
+
|
|
48
|
+
command1ExitInfo = {
|
|
49
|
+
command: commands[1].command,
|
|
50
|
+
timings: {
|
|
51
|
+
startDate: startDate1,
|
|
52
|
+
endDate: endDate1,
|
|
53
|
+
},
|
|
54
|
+
index: commands[1].index,
|
|
55
|
+
killed: false,
|
|
56
|
+
exitCode: 0,
|
|
57
|
+
};
|
|
58
|
+
|
|
59
|
+
logger = createMockInstance(Logger);
|
|
60
|
+
controller = new LogTimings({ logger, timestampFormat });
|
|
61
|
+
});
|
|
62
|
+
|
|
63
|
+
it('returns same commands', () => {
|
|
64
|
+
expect(controller.handle(commands)).toMatchObject({ commands });
|
|
65
|
+
});
|
|
66
|
+
|
|
67
|
+
it('does not log timings and doesn\'t throw if no logger is provided', () => {
|
|
68
|
+
controller = new LogTimings({ });
|
|
69
|
+
controller.handle(commands);
|
|
70
|
+
|
|
71
|
+
commands[0].timer.next({ startDate: startDate0 });
|
|
72
|
+
commands[1].timer.next({ startDate: startDate1 });
|
|
73
|
+
commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });
|
|
74
|
+
commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });
|
|
75
|
+
|
|
76
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(0);
|
|
77
|
+
});
|
|
78
|
+
|
|
79
|
+
it('logs the timings at the start and end (ie complete or error) event of each command', () => {
|
|
80
|
+
controller.handle(commands);
|
|
81
|
+
|
|
82
|
+
commands[0].timer.next({ startDate: startDate0 });
|
|
83
|
+
commands[1].timer.next({ startDate: startDate1 });
|
|
84
|
+
commands[1].timer.next({ startDate: startDate1, endDate: endDate1 });
|
|
85
|
+
commands[0].timer.next({ startDate: startDate0, endDate: endDate0 });
|
|
86
|
+
|
|
87
|
+
expect(logger.logCommandEvent).toHaveBeenCalledTimes(4);
|
|
88
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
89
|
+
`${commands[0].command} started at ${formatDate(startDate0, timestampFormat)}`,
|
|
90
|
+
commands[0]
|
|
91
|
+
);
|
|
92
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
93
|
+
`${commands[1].command} started at ${formatDate(startDate1, timestampFormat)}`,
|
|
94
|
+
commands[1]
|
|
95
|
+
);
|
|
96
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
97
|
+
`${commands[1].command} stopped at ${formatDate(endDate1, timestampFormat)} after ${command1DurationTextMs}`,
|
|
98
|
+
commands[1]
|
|
99
|
+
);
|
|
100
|
+
expect(logger.logCommandEvent).toHaveBeenCalledWith(
|
|
101
|
+
`${commands[0].command} stopped at ${formatDate(endDate0, timestampFormat)} after ${command0DurationTextMs}`,
|
|
102
|
+
commands[0]
|
|
103
|
+
);
|
|
104
|
+
});
|
|
105
|
+
|
|
106
|
+
it('does not log timings summary if there was an error', () => {
|
|
107
|
+
controller.handle(commands);
|
|
108
|
+
|
|
109
|
+
commands[0].close.next(command0ExitInfo);
|
|
110
|
+
commands[1].error.next();
|
|
111
|
+
|
|
112
|
+
expect(logger.logTable).toHaveBeenCalledTimes(0);
|
|
113
|
+
|
|
114
|
+
});
|
|
115
|
+
|
|
116
|
+
it('logs the sorted timings summary when all processes close successfully', () => {
|
|
117
|
+
jest.spyOn(controller, 'printExitInfoTimingTable');
|
|
118
|
+
controller.handle(commands);
|
|
119
|
+
|
|
120
|
+
commands[0].close.next(command0ExitInfo);
|
|
121
|
+
commands[1].close.next(command1ExitInfo);
|
|
122
|
+
|
|
123
|
+
expect(logger.logTable).toHaveBeenCalledTimes(1);
|
|
124
|
+
|
|
125
|
+
// un-sorted ie by finish order
|
|
126
|
+
expect(controller.printExitInfoTimingTable).toHaveBeenCalledWith([
|
|
127
|
+
command0ExitInfo,
|
|
128
|
+
command1ExitInfo
|
|
129
|
+
]);
|
|
130
|
+
|
|
131
|
+
// sorted by duration
|
|
132
|
+
expect(logger.logTable).toHaveBeenCalledWith([
|
|
133
|
+
exitInfoToTimingInfo(command1ExitInfo),
|
|
134
|
+
exitInfoToTimingInfo(command0ExitInfo)
|
|
135
|
+
]);
|
|
136
|
+
|
|
137
|
+
});
|
|
@@ -69,7 +69,15 @@ it('restarts processes up to tries', () => {
|
|
|
69
69
|
expect(commands[0].start).toHaveBeenCalledTimes(2);
|
|
70
70
|
});
|
|
71
71
|
|
|
72
|
-
it
|
|
72
|
+
it('restart processes forever, if tries is negative', () => {
|
|
73
|
+
controller = new RestartProcess({
|
|
74
|
+
logger,
|
|
75
|
+
scheduler,
|
|
76
|
+
delay: 100,
|
|
77
|
+
tries: -1
|
|
78
|
+
});
|
|
79
|
+
expect(controller.tries).toBe(Infinity);
|
|
80
|
+
});
|
|
73
81
|
|
|
74
82
|
it('restarts processes until they succeed', () => {
|
|
75
83
|
controller.handle(commands);
|
package/src/logger.js
CHANGED
|
@@ -96,6 +96,63 @@ module.exports = class Logger {
|
|
|
96
96
|
this.log(chalk.reset('-->') + ' ', chalk.reset(text) + '\n');
|
|
97
97
|
}
|
|
98
98
|
|
|
99
|
+
logTable(tableContents) {
|
|
100
|
+
// For now, can only print array tables with some content.
|
|
101
|
+
if (this.raw || !Array.isArray(tableContents) || !tableContents.length) {
|
|
102
|
+
return;
|
|
103
|
+
}
|
|
104
|
+
|
|
105
|
+
let nextColIndex = 0;
|
|
106
|
+
const headers = {};
|
|
107
|
+
const contentRows = tableContents.map(row => {
|
|
108
|
+
const rowContents = [];
|
|
109
|
+
Object.keys(row).forEach((col) => {
|
|
110
|
+
if (!headers[col]) {
|
|
111
|
+
headers[col] = {
|
|
112
|
+
index: nextColIndex++,
|
|
113
|
+
//
|
|
114
|
+
length: col.length,
|
|
115
|
+
};
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
const colIndex = headers[col].index;
|
|
119
|
+
const formattedValue = String(row[col] == null ? '' : row[col]);
|
|
120
|
+
// Update the column length in case this rows value is longer than the previous length for the column.
|
|
121
|
+
headers[col].length = Math.max(formattedValue.length, headers[col].length);
|
|
122
|
+
rowContents[colIndex] = formattedValue;
|
|
123
|
+
return rowContents;
|
|
124
|
+
});
|
|
125
|
+
return rowContents;
|
|
126
|
+
});
|
|
127
|
+
|
|
128
|
+
const headersFormatted = Object
|
|
129
|
+
.keys(headers)
|
|
130
|
+
.map(header => header.padEnd(headers[header].length, ' '));
|
|
131
|
+
|
|
132
|
+
if (!headersFormatted.length) {
|
|
133
|
+
// No columns exist.
|
|
134
|
+
return;
|
|
135
|
+
}
|
|
136
|
+
|
|
137
|
+
const borderRowFormatted = headersFormatted.map(header => '─'.padEnd(header.length, '─'));
|
|
138
|
+
|
|
139
|
+
this.logGlobalEvent(`┌─${borderRowFormatted.join('─┬─')}─┐`);
|
|
140
|
+
this.logGlobalEvent(`│ ${headersFormatted.join(' │ ')} │`);
|
|
141
|
+
this.logGlobalEvent(`├─${borderRowFormatted.join('─┼─')}─┤`);
|
|
142
|
+
|
|
143
|
+
contentRows.forEach(contentRow => {
|
|
144
|
+
const contentRowFormatted = headersFormatted.map((header, colIndex) => {
|
|
145
|
+
// If the table was expanded after this row was processed, it won't have this column.
|
|
146
|
+
// Use an empty string in this case.
|
|
147
|
+
const col = contentRow[colIndex] || '';
|
|
148
|
+
return col.padEnd(header.length, ' ');
|
|
149
|
+
});
|
|
150
|
+
this.logGlobalEvent(`│ ${contentRowFormatted.join(' │ ')} │`);
|
|
151
|
+
});
|
|
152
|
+
|
|
153
|
+
this.logGlobalEvent(`└─${borderRowFormatted.join('─┴─')}─┘`);
|
|
154
|
+
}
|
|
155
|
+
|
|
99
156
|
log(prefix, text) {
|
|
100
157
|
if (this.raw) {
|
|
101
158
|
return this.outputStream.write(text);
|
package/src/logger.spec.js
CHANGED
|
@@ -221,3 +221,98 @@ describe('#logCommandEvent()', () => {
|
|
|
221
221
|
expect(logger.log).toHaveBeenCalledWith(chalk.reset('[1]') + ' ', chalk.reset('foo') + '\n');
|
|
222
222
|
});
|
|
223
223
|
});
|
|
224
|
+
|
|
225
|
+
describe('#logTable()', () => {
|
|
226
|
+
it('does not log anything in raw mode', () => {
|
|
227
|
+
const logger = createLogger({ raw: true });
|
|
228
|
+
logger.logTable([{ foo: 1, bar: 2 }]);
|
|
229
|
+
|
|
230
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
231
|
+
});
|
|
232
|
+
|
|
233
|
+
it('does not log anything if value is not an array', () => {
|
|
234
|
+
const logger = createLogger();
|
|
235
|
+
logger.logTable({});
|
|
236
|
+
logger.logTable(null);
|
|
237
|
+
logger.logTable(0);
|
|
238
|
+
logger.logTable('');
|
|
239
|
+
|
|
240
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
241
|
+
});
|
|
242
|
+
|
|
243
|
+
it('does not log anything if array is empy', () => {
|
|
244
|
+
const logger = createLogger();
|
|
245
|
+
logger.logTable([]);
|
|
246
|
+
|
|
247
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
248
|
+
});
|
|
249
|
+
|
|
250
|
+
it('does not log anything if array items have no properties', () => {
|
|
251
|
+
const logger = createLogger();
|
|
252
|
+
logger.logTable([{}]);
|
|
253
|
+
|
|
254
|
+
expect(logger.log).not.toHaveBeenCalled();
|
|
255
|
+
});
|
|
256
|
+
|
|
257
|
+
it('logs a header for each item\'s properties', () => {
|
|
258
|
+
const logger = createLogger();
|
|
259
|
+
logger.logTable([{ foo: 1, bar: 2 }]);
|
|
260
|
+
|
|
261
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
262
|
+
chalk.reset('-->') + ' ',
|
|
263
|
+
chalk.reset('│ foo │ bar │') + '\n',
|
|
264
|
+
);
|
|
265
|
+
});
|
|
266
|
+
|
|
267
|
+
it('logs padded headers according to longest column\'s value', () => {
|
|
268
|
+
const logger = createLogger();
|
|
269
|
+
logger.logTable([{ a: 'foo', b: 'barbaz' }]);
|
|
270
|
+
|
|
271
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
272
|
+
chalk.reset('-->') + ' ',
|
|
273
|
+
chalk.reset('│ a │ b │') + '\n',
|
|
274
|
+
);
|
|
275
|
+
});
|
|
276
|
+
|
|
277
|
+
it('logs each items\'s values', () => {
|
|
278
|
+
const logger = createLogger();
|
|
279
|
+
logger.logTable([{ foo: 123 }, { foo: 456 }]);
|
|
280
|
+
|
|
281
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
282
|
+
chalk.reset('-->') + ' ',
|
|
283
|
+
chalk.reset('│ 123 │') + '\n',
|
|
284
|
+
);
|
|
285
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
286
|
+
chalk.reset('-->') + ' ',
|
|
287
|
+
chalk.reset('│ 456 │') + '\n',
|
|
288
|
+
);
|
|
289
|
+
});
|
|
290
|
+
|
|
291
|
+
it('logs each items\'s values padded according to longest column\'s value', () => {
|
|
292
|
+
const logger = createLogger();
|
|
293
|
+
logger.logTable([{ foo: 1 }, { foo: 123 }]);
|
|
294
|
+
|
|
295
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
296
|
+
chalk.reset('-->') + ' ',
|
|
297
|
+
chalk.reset('│ 1 │') + '\n',
|
|
298
|
+
);
|
|
299
|
+
});
|
|
300
|
+
|
|
301
|
+
it('logs items with different properties in each', () => {
|
|
302
|
+
const logger = createLogger();
|
|
303
|
+
logger.logTable([{ foo: 1 }, { bar: 2 }]);
|
|
304
|
+
|
|
305
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
306
|
+
chalk.reset('-->') + ' ',
|
|
307
|
+
chalk.reset('│ foo │ bar │') + '\n',
|
|
308
|
+
);
|
|
309
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
310
|
+
chalk.reset('-->') + ' ',
|
|
311
|
+
chalk.reset('│ 1 │ │') + '\n',
|
|
312
|
+
);
|
|
313
|
+
expect(logger.log).toHaveBeenCalledWith(
|
|
314
|
+
chalk.reset('-->') + ' ',
|
|
315
|
+
chalk.reset('│ │ 2 │') + '\n',
|
|
316
|
+
);
|
|
317
|
+
});
|
|
318
|
+
});
|