Skip to content

Commit 95a5c97

Browse files
novemberbornjamestalmage
authored andcommitted
Improve watch logging (#737)
* display current time when mini & verbose reporters finish I'm using toLocaleTimeString() which works even back to Node.js 0.10. Forcing a 24-hour clock because we're geeks. * watcher: restart logger on subsequent runs The CLI starts the logger so the watcher shouldn't reset it on its first run. Restart the logger after resetting, this allows the mini reporter to render its spinner. * better api.run stub in watcher test Return an object for the runStatus, add assertions to verify this object is passed to logger.finish() * assert that r/rs reruns all tests * clear mini reporter in watch mode Clear the mini reporter unless the previous run had failures, or "r\n" was entered on stdin. * consistent empty lines in finish output Always print two empty lines before each error/rejection/exception when the mini and verbose reporters finish. Remove trailing whitespace from stack traces. Always print an empty line after the finish output. Add a test helper to more easily compare line output, with useful debug information. At the moment this new helper is only used for failing tests. * print line if reporter was not be cleared Watch mode won't clear the mini reporter if there were errors, or "r\n" was entered on stdin. The verbose reporter can't be cleared at all. To improve the separation between multiple test runs, write a horizontal line when starting a new test run and the reporter was not cleared. * remove debug output from cli test
1 parent a3565fd commit 95a5c97

File tree

11 files changed

+462
-154
lines changed

11 files changed

+462
-154
lines changed

lib/logger.js

Lines changed: 17 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -50,6 +50,23 @@ Logger.prototype.finish = function (runStatus) {
5050
this.write(this.reporter.finish(runStatus), runStatus);
5151
};
5252

53+
Logger.prototype.section = function () {
54+
if (!this.reporter.section) {
55+
return;
56+
}
57+
58+
this.write(this.reporter.section());
59+
};
60+
61+
Logger.prototype.clear = function () {
62+
if (!this.reporter.clear) {
63+
return false;
64+
}
65+
66+
this.write(this.reporter.clear());
67+
return true;
68+
};
69+
5370
Logger.prototype.write = function (str, runStatus) {
5471
if (typeof str === 'undefined') {
5572
return;

lib/reporters/mini.js

Lines changed: 27 additions & 28 deletions
Original file line numberDiff line numberDiff line change
@@ -7,6 +7,7 @@ var spinners = require('cli-spinners');
77
var chalk = require('chalk');
88
var cliTruncate = require('cli-truncate');
99
var cross = require('figures').cross;
10+
var repeating = require('repeating');
1011
var colors = require('../colors');
1112

1213
chalk.enabled = true;
@@ -113,32 +114,26 @@ MiniReporter.prototype.unhandledError = function (err) {
113114
}
114115
};
115116

116-
MiniReporter.prototype.reportCounts = function () {
117-
var status = '';
117+
MiniReporter.prototype.reportCounts = function (time) {
118+
var lines = [
119+
this.passCount > 0 ? '\n ' + colors.pass(this.passCount, 'passed') : '',
120+
this.failCount > 0 ? '\n ' + colors.error(this.failCount, 'failed') : '',
121+
this.skipCount > 0 ? '\n ' + colors.skip(this.skipCount, 'skipped') : '',
122+
this.todoCount > 0 ? '\n ' + colors.todo(this.todoCount, 'todo') : ''
123+
].filter(Boolean);
118124

119-
if (this.passCount > 0) {
120-
status += '\n ' + colors.pass(this.passCount, 'passed');
125+
if (time && lines.length > 0) {
126+
lines[0] += ' ' + time;
121127
}
122128

123-
if (this.failCount > 0) {
124-
status += '\n ' + colors.error(this.failCount, 'failed');
125-
}
126-
127-
if (this.skipCount > 0) {
128-
status += '\n ' + colors.skip(this.skipCount, 'skipped');
129-
}
130-
131-
if (this.todoCount > 0) {
132-
status += '\n ' + colors.todo(this.todoCount, 'todo');
133-
}
134-
135-
return status;
129+
return lines.join('');
136130
};
137131

138132
MiniReporter.prototype.finish = function (runStatus) {
139133
this.clearInterval();
140134

141-
var status = this.reportCounts();
135+
var time = chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
136+
var status = this.reportCounts(time);
142137

143138
if (this.rejectionCount > 0) {
144139
status += '\n ' + colors.error(this.rejectionCount, plur('rejection', this.rejectionCount));
@@ -162,12 +157,12 @@ MiniReporter.prototype.finish = function (runStatus) {
162157
var description;
163158

164159
if (test.error) {
165-
description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack);
160+
description = ' ' + test.error.message + '\n ' + stripFirstLine(test.error.stack).trimRight();
166161
} else {
167162
description = JSON.stringify(test);
168163
}
169164

170-
status += '\n\n ' + colors.error(i + '.', title) + '\n';
165+
status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
171166
status += colors.stack(description);
172167
});
173168
}
@@ -181,22 +176,26 @@ MiniReporter.prototype.finish = function (runStatus) {
181176
i++;
182177

183178
if (err.type === 'exception' && err.name === 'AvaError') {
184-
status += '\n\n ' + colors.error(cross + ' ' + err.message) + '\n';
179+
status += '\n\n\n ' + colors.error(cross + ' ' + err.message);
185180
} else {
186181
var title = err.type === 'rejection' ? 'Unhandled Rejection' : 'Uncaught Exception';
187-
var description = err.stack ? err.stack : JSON.stringify(err);
182+
var description = err.stack ? err.stack.trimRight() : JSON.stringify(err);
188183

189-
status += '\n\n ' + colors.error(i + '.', title) + '\n';
190-
status += ' ' + colors.stack(description);
184+
status += '\n\n\n ' + colors.error(i + '.', title) + '\n';
185+
status += ' ' + colors.stack(description);
191186
}
192187
});
193188
}
194189

195-
if (this.failCount === 0 && this.rejectionCount === 0 && this.exceptionCount === 0) {
196-
status += '\n';
197-
}
190+
return status + '\n';
191+
};
192+
193+
MiniReporter.prototype.section = function () {
194+
return '\n' + chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80));
195+
};
198196

199-
return status;
197+
MiniReporter.prototype.clear = function () {
198+
return '';
200199
};
201200

202201
MiniReporter.prototype.write = function (str) {

lib/reporters/verbose.js

Lines changed: 23 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,9 @@
11
'use strict';
22
var prettyMs = require('pretty-ms');
33
var figures = require('figures');
4+
var chalk = require('chalk');
45
var plur = require('plur');
6+
var repeating = require('repeating');
57
var colors = require('../colors');
68

79
Object.keys(colors).forEach(function (key) {
@@ -68,31 +70,22 @@ VerboseReporter.prototype.unhandledError = function (err) {
6870
VerboseReporter.prototype.finish = function (runStatus) {
6971
var output = '\n';
7072

71-
if (runStatus.failCount > 0) {
72-
output += ' ' + colors.error(runStatus.failCount, plur('test', runStatus.failCount), 'failed') + '\n';
73-
} else {
74-
output += ' ' + colors.pass(runStatus.passCount, plur('test', runStatus.passCount), 'passed') + '\n';
75-
}
76-
77-
if (runStatus.skipCount > 0) {
78-
output += ' ' + colors.skip(runStatus.skipCount, plur('test', runStatus.skipCount), 'skipped') + '\n';
79-
}
80-
81-
if (runStatus.todoCount > 0) {
82-
output += ' ' + colors.todo(runStatus.todoCount, plur('test', runStatus.todoCount), 'todo') + '\n';
83-
}
84-
85-
if (runStatus.rejectionCount > 0) {
86-
output += ' ' + colors.error(runStatus.rejectionCount, 'unhandled', plur('rejection', runStatus.rejectionCount)) + '\n';
87-
}
88-
89-
if (runStatus.exceptionCount > 0) {
90-
output += ' ' + colors.error(runStatus.exceptionCount, 'uncaught', plur('exception', runStatus.exceptionCount)) + '\n';
73+
var lines = [
74+
runStatus.failCount > 0 ?
75+
' ' + colors.error(runStatus.failCount, plur('test', runStatus.failCount), 'failed') :
76+
' ' + colors.pass(runStatus.passCount, plur('test', runStatus.passCount), 'passed'),
77+
runStatus.skipCount > 0 ? ' ' + colors.skip(runStatus.skipCount, plur('test', runStatus.skipCount), 'skipped') : '',
78+
runStatus.todoCount > 0 ? ' ' + colors.todo(runStatus.todoCount, plur('test', runStatus.todoCount), 'todo') : '',
79+
runStatus.rejectionCount > 0 ? ' ' + colors.error(runStatus.rejectionCount, 'unhandled', plur('rejection', runStatus.rejectionCount)) : '',
80+
runStatus.exceptionCount > 0 ? ' ' + colors.error(runStatus.exceptionCount, 'uncaught', plur('exception', runStatus.exceptionCount)) : ''
81+
].filter(Boolean);
82+
83+
if (lines.length > 0) {
84+
lines[0] += ' ' + chalk.gray.dim('[' + new Date().toLocaleTimeString('en-US', {hour12: false}) + ']');
85+
output += lines.join('\n');
9186
}
9287

9388
if (runStatus.failCount > 0) {
94-
output += '\n';
95-
9689
var i = 0;
9790

9891
runStatus.tests.forEach(function (test) {
@@ -102,12 +95,17 @@ VerboseReporter.prototype.finish = function (runStatus) {
10295

10396
i++;
10497

105-
output += ' ' + colors.error(i + '.', test.title) + '\n';
106-
output += ' ' + colors.stack(test.error.stack) + '\n';
98+
output += '\n\n\n ' + colors.error(i + '.', test.title) + '\n';
99+
var stack = test.error.stack ? test.error.stack.trimRight() : '';
100+
output += ' ' + colors.stack(stack);
107101
});
108102
}
109103

110-
return output;
104+
return output + '\n';
105+
};
106+
107+
VerboseReporter.prototype.section = function () {
108+
return chalk.gray.dim(repeating('\u2500', process.stdout.columns || 80));
111109
};
112110

113111
VerboseReporter.prototype.write = function (str) {

lib/watcher.js

Lines changed: 19 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -43,8 +43,23 @@ function Watcher(logger, api, files, sources) {
4343
this.debouncer = new Debouncer(this);
4444

4545
this.isTest = makeTestMatcher(files, AvaFiles.defaultExcludePatterns());
46+
47+
var isFirstRun = true;
48+
this.clearLogOnNextRun = true;
4649
this.run = function (specificFiles) {
47-
logger.reset();
50+
if (isFirstRun) {
51+
isFirstRun = false;
52+
} else {
53+
var cleared = this.clearLogOnNextRun && logger.clear();
54+
if (!cleared) {
55+
logger.reset();
56+
logger.section();
57+
}
58+
this.clearLogOnNextRun = true;
59+
60+
logger.reset();
61+
logger.start();
62+
}
4863

4964
var runOnlyExclusive = false;
5065

@@ -63,10 +78,12 @@ function Watcher(logger, api, files, sources) {
6378
}
6479
}
6580

81+
var self = this;
6682
this.busy = api.run(specificFiles || files, {
6783
runOnlyExclusive: runOnlyExclusive
6884
}).then(function (runStatus) {
6985
logger.finish(runStatus);
86+
self.clearLogOnNextRun = self.clearLogOnNextRun && runStatus.failCount === 0;
7087
}, rethrowAsync);
7188
};
7289

@@ -182,6 +199,7 @@ Watcher.prototype.observeStdin = function (stdin) {
182199
// Cancel the debouncer again, it might have restarted while waiting for
183200
// the busy promise to fulfil.
184201
self.debouncer.cancel();
202+
self.clearLogOnNextRun = false;
185203
self.rerunAll();
186204
});
187205
});

package.json

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -129,6 +129,7 @@
129129
"power-assert-formatter": "^1.3.0",
130130
"power-assert-renderers": "^0.1.0",
131131
"pretty-ms": "^2.0.0",
132+
"repeating": "^2.0.0",
132133
"require-precompiled": "^0.1.0",
133134
"resolve-cwd": "^1.0.0",
134135
"set-immediate-shim": "^1.0.1",

test/cli.js

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -235,7 +235,7 @@ if (hasChokidar) {
235235
t.is(err.code, 1);
236236
t.match(stderr, 'The TAP reporter is not available when using watch mode.');
237237
t.end();
238-
}).stderr.pipe(process.stderr);
238+
});
239239
});
240240

241241
['--watch', '-w'].forEach(function (watchFlag) {
@@ -245,7 +245,7 @@ if (hasChokidar) {
245245
t.is(err.code, 1);
246246
t.match(stderr, 'The TAP reporter is not available when using watch mode.');
247247
t.end();
248-
}).stderr.pipe(process.stderr);
248+
});
249249
});
250250
});
251251
});

test/helper/compare-line-output.js

Lines changed: 27 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,27 @@
1+
'use strict';
2+
var SKIP_UNTIL_EMPTY_LINE = {};
3+
4+
function compareLineOutput(t, actual, lineExpectations) {
5+
var actualLines = actual.split('\n');
6+
7+
var expectationIndex = 0;
8+
var lineIndex = 0;
9+
while (lineIndex < actualLines.length && expectationIndex < lineExpectations.length) {
10+
var line = actualLines[lineIndex++];
11+
var expected = lineExpectations[expectationIndex++];
12+
if (expected === SKIP_UNTIL_EMPTY_LINE) {
13+
lineIndex = actualLines.indexOf('', lineIndex);
14+
continue;
15+
}
16+
17+
if (typeof expected === 'string') {
18+
// Assertion titles use 1-based line indexes
19+
t.is(line, expected, 'line ' + lineIndex + ' ≪' + line + '≫ is ≪' + expected + '≫');
20+
} else {
21+
t.match(line, expected, 'line ' + lineIndex + ' ≪' + line + '≫ matches ' + expected);
22+
}
23+
}
24+
}
25+
26+
module.exports = compareLineOutput;
27+
compareLineOutput.SKIP_UNTIL_EMPTY_LINE = SKIP_UNTIL_EMPTY_LINE;

test/logger.js

Lines changed: 50 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -45,6 +45,56 @@ test('only write if reset is supported by reporter', function (t) {
4545
t.end();
4646
});
4747

48+
test('only call section if supported by reporter', function (t) {
49+
var tapReporter = tap();
50+
var logger = new Logger(tapReporter);
51+
tapReporter.section = undefined;
52+
logger.section();
53+
t.end();
54+
});
55+
56+
test('only write if section is supported by reporter', function (t) {
57+
var tapReporter = tap();
58+
var logger = new Logger(tapReporter);
59+
tapReporter.section = undefined;
60+
logger.write = t.fail;
61+
logger.section();
62+
t.end();
63+
});
64+
65+
test('only call clear if supported by reporter', function (t) {
66+
var tapReporter = tap();
67+
var logger = new Logger(tapReporter);
68+
tapReporter.clear = undefined;
69+
logger.clear();
70+
t.end();
71+
});
72+
73+
test('only write if clear is supported by reporter', function (t) {
74+
var tapReporter = tap();
75+
var logger = new Logger(tapReporter);
76+
tapReporter.clear = undefined;
77+
logger.write = t.fail;
78+
logger.clear();
79+
t.end();
80+
});
81+
82+
test('return false if clear is not supported by reporter', function (t) {
83+
var tapReporter = tap();
84+
var logger = new Logger(tapReporter);
85+
tapReporter.clear = undefined;
86+
t.false(logger.clear());
87+
t.end();
88+
});
89+
90+
test('return true if clear is supported by reporter', function (t) {
91+
var tapReporter = tap();
92+
var logger = new Logger(tapReporter);
93+
tapReporter.clear = function () {};
94+
t.true(logger.clear());
95+
t.end();
96+
});
97+
4898
test('writes the reporter reset result', function (t) {
4999
var tapReporter = tap();
50100
var logger = new Logger(tapReporter);

0 commit comments

Comments
 (0)