Skip to content

Commit b194368

Browse files
authored
feat(cli): improve error logging (stoplightio#2071)
1 parent a3ba1a9 commit b194368

File tree

20 files changed

+409
-129
lines changed

20 files changed

+409
-129
lines changed

.eslintrc

Lines changed: 4 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -84,10 +84,12 @@
8484

8585
{
8686
"files": [
87-
"src/**/__tests__/**/*.jest.test.{ts,js}"
87+
"src/**/__tests__/**/*.jest.test.{ts,js}",
88+
"__mocks__/**/*.{ts,js}"
8889
],
8990
"env": {
90-
"jest": true
91+
"jest": true,
92+
"node": true
9193
}
9294
},
9395

__karma__/process.js

Lines changed: 0 additions & 1 deletion
This file was deleted.

__mocks__/nanoid/non-secure.ts

Lines changed: 0 additions & 7 deletions
This file was deleted.

__mocks__/process.js

Lines changed: 15 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,15 @@
1+
module.exports.exit = jest.fn();
2+
module.exports.cwd = jest.fn();
3+
module.exports.on = jest.fn();
4+
5+
module.exports.stdin = {
6+
fd: 0,
7+
isTTY: true,
8+
};
9+
module.exports.stdout = {
10+
write: jest.fn(),
11+
};
12+
13+
module.exports.stderr = {
14+
write: jest.fn(),
15+
};

karma.conf.ts

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -40,7 +40,7 @@ module.exports = (config: Config): void => {
4040
'nimma/legacy': require.resolve('./node_modules/nimma/dist/legacy/cjs/index.js'),
4141
'node-fetch': require.resolve('./__karma__/fetch'),
4242
fs: require.resolve('./__karma__/fs'),
43-
process: require.resolve('./__karma__/process'),
43+
process: require.resolve('./__mocks__/process'),
4444
perf_hooks: require.resolve('./__karma__/perf_hooks'),
4545
fsevents: require.resolve('./__karma__/fsevents'),
4646
},

packages/cli/package.json

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -51,15 +51,18 @@
5151
"lodash": "~4.17.21",
5252
"pony-cause": "^1.0.0",
5353
"proxy-agent": "5.0.0",
54+
"stacktracey": "^2.1.7",
5455
"strip-ansi": "6.0",
5556
"text-table": "0.2",
5657
"tslib": "^2.3.0",
5758
"yargs": "17.3.1"
5859
},
5960
"devDependencies": {
61+
"@types/es-aggregate-error": "^1.0.2",
6062
"@types/xml2js": "^0.4.9",
6163
"@types/yargs": "^17.0.8",
6264
"copyfiles": "^2.4.1",
65+
"es-aggregate-error": "^1.0.7",
6366
"jest-when": "^3.4.2",
6467
"nock": "^13.1.3",
6568
"node-html-parser": "^4.1.5",
@@ -73,7 +76,9 @@
7376
],
7477
"assets": [
7578
"./dist/**/*.json",
76-
"./dist/**/*.html"
79+
"./dist/**/*.html",
80+
"../*/dist/**/*.js.map",
81+
"../*/src/**/*.ts"
7782
]
7883
}
7984
}

packages/cli/src/commands/__tests__/lint.test.ts

Lines changed: 58 additions & 23 deletions
Original file line numberDiff line numberDiff line change
@@ -1,12 +1,16 @@
11
import * as yargs from 'yargs';
2-
import { noop } from 'lodash';
32
import { DiagnosticSeverity } from '@stoplight/types';
43
import { IRuleResult } from '@stoplight/spectral-core';
4+
import * as process from 'process';
5+
import { ErrorWithCause } from 'pony-cause';
6+
import AggregateError from 'es-aggregate-error';
57

68
import { lint } from '../../services/linter';
79
import { formatOutput, writeOutput } from '../../services/output';
810
import lintCommand from '../lint';
11+
import chalk from 'chalk';
912

13+
jest.mock('process');
1014
jest.mock('../../services/output');
1115
jest.mock('../../services/linter');
1216

@@ -24,9 +28,6 @@ function run(command: string) {
2428
}
2529

2630
describe('lint', () => {
27-
let errorSpy: jest.SpyInstance;
28-
const { isTTY } = process.stdin;
29-
3031
const results: IRuleResult[] = [
3132
{
3233
code: 'parser',
@@ -47,29 +48,26 @@ describe('lint', () => {
4748
];
4849

4950
beforeEach(() => {
50-
(lint as jest.Mock).mockClear();
5151
(lint as jest.Mock).mockResolvedValueOnce(results);
52-
53-
(formatOutput as jest.Mock).mockClear();
5452
(formatOutput as jest.Mock).mockReturnValueOnce('<formatted output>');
55-
56-
(writeOutput as jest.Mock).mockClear();
5753
(writeOutput as jest.Mock).mockResolvedValueOnce(undefined);
58-
59-
errorSpy = jest.spyOn(console, 'error').mockImplementation(noop);
6054
});
6155

6256
afterEach(() => {
63-
errorSpy.mockRestore();
64-
process.stdin.isTTY = isTTY;
57+
process.stdin.isTTY = true;
58+
jest.clearAllMocks();
59+
jest.resetAllMocks();
6560
});
6661

6762
it('shows help when no document and no STDIN are present', () => {
68-
process.stdin.isTTY = true;
6963
return expect(run('lint')).rejects.toContain('documents Location of JSON/YAML documents');
7064
});
7165

7266
describe('when STDIN is present', () => {
67+
beforeEach(() => {
68+
process.stdin.isTTY = false;
69+
});
70+
7371
it('does not show help when documents are missing', async () => {
7472
const output = await run('lint');
7573
expect(output).not.toContain('documents Location of JSON/YAML documents');
@@ -150,35 +148,29 @@ describe('lint', () => {
150148

151149
it.each(['json', 'stylish'])('calls formatOutput with %s format', async format => {
152150
await run(`lint -f ${format} ./__fixtures__/empty-oas2-document.json`);
153-
await new Promise(resolve => void process.nextTick(resolve));
154151
expect(formatOutput).toBeCalledWith(results, format, { failSeverity: DiagnosticSeverity.Error });
155152
});
156153

157154
it('writes formatted output to a file', async () => {
158155
await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`);
159-
await new Promise(resolve => void process.nextTick(resolve));
160156
expect(writeOutput).toBeCalledWith('<formatted output>', 'foo.json');
161157
});
162158

163159
it('writes formatted output to multiple files when using format and output flags', async () => {
164-
(formatOutput as jest.Mock).mockClear();
165160
(formatOutput as jest.Mock).mockReturnValue('<formatted output>');
166161

167162
await run(
168163
`lint --format html --format json --output.json foo.json --output.html foo.html ./__fixtures__/empty-oas2-document.json`,
169164
);
170-
await new Promise(resolve => void process.nextTick(resolve));
171165
expect(writeOutput).toBeCalledTimes(2);
172166
expect(writeOutput).nthCalledWith(1, '<formatted output>', 'foo.html');
173167
expect(writeOutput).nthCalledWith(2, '<formatted output>', 'foo.json');
174168
});
175169

176170
it('writes formatted output to multiple files and stdout when using format and output flags', async () => {
177-
(formatOutput as jest.Mock).mockClear();
178171
(formatOutput as jest.Mock).mockReturnValue('<formatted output>');
179172

180173
await run(`lint --format html --format json --output.json foo.json ./__fixtures__/empty-oas2-document.json`);
181-
await new Promise(resolve => void process.nextTick(resolve));
182174
expect(writeOutput).toBeCalledTimes(2);
183175
expect(writeOutput).nthCalledWith(1, '<formatted output>', '<stdout>');
184176
expect(writeOutput).nthCalledWith(2, '<formatted output>', 'foo.json');
@@ -216,8 +208,51 @@ describe('lint', () => {
216208
const error = new Error('Failure');
217209
(lint as jest.Mock).mockReset();
218210
(lint as jest.Mock).mockRejectedValueOnce(error);
219-
await run(`lint -o foo.json ./__fixtures__/empty-oas2-document.json`);
220-
await new Promise(resolve => void process.nextTick(resolve));
221-
expect(errorSpy).toBeCalledWith('Failure');
211+
await run(`lint ./__fixtures__/empty-oas2-document.json`);
212+
expect(process.stderr.write).nthCalledWith(1, chalk.red('Error running Spectral!\n'));
213+
expect(process.stderr.write).nthCalledWith(2, chalk.red('Use --verbose flag to print the error stack.\n'));
214+
expect(process.stderr.write).nthCalledWith(3, `Error #1: ${chalk.red('Failure')}\n`);
215+
});
216+
217+
it('prints each error separately', async () => {
218+
(lint as jest.Mock).mockReset();
219+
(lint as jest.Mock).mockRejectedValueOnce(
220+
new AggregateError([
221+
new Error('some unhandled exception'),
222+
new TypeError('another one'),
223+
new ErrorWithCause('some error with cause', { cause: 'original exception' }),
224+
]),
225+
);
226+
await run(`lint ./__fixtures__/empty-oas2-document.json`);
227+
expect(process.stderr.write).nthCalledWith(3, `Error #1: ${chalk.red('some unhandled exception')}\n`);
228+
expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`);
229+
expect(process.stderr.write).nthCalledWith(5, `Error #3: ${chalk.red('original exception')}\n`);
230+
});
231+
232+
it('given verbose flag, prints each error together with their stacks', async () => {
233+
(lint as jest.Mock).mockReset();
234+
(lint as jest.Mock).mockRejectedValueOnce(
235+
new AggregateError([
236+
new Error('some unhandled exception'),
237+
new TypeError('another one'),
238+
new ErrorWithCause('some error with cause', { cause: 'original exception' }),
239+
]),
240+
);
241+
242+
await run(`lint --verbose ./__fixtures__/empty-oas2-document.json`);
243+
244+
expect(process.stderr.write).nthCalledWith(2, `Error #1: ${chalk.red('some unhandled exception')}\n`);
245+
expect(process.stderr.write).nthCalledWith(
246+
3,
247+
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:236`),
248+
);
249+
250+
expect(process.stderr.write).nthCalledWith(4, `Error #2: ${chalk.red('another one')}\n`);
251+
expect(process.stderr.write).nthCalledWith(
252+
5,
253+
expect.stringContaining(`packages/cli/src/commands/__tests__/lint.test.ts:237`),
254+
);
255+
256+
expect(process.stderr.write).nthCalledWith(6, `Error #3: ${chalk.red('original exception')}\n`);
222257
});
223258
});

0 commit comments

Comments
 (0)