Skip to content

Commit 3201b1b

Browse files
committed
detect improper use of t.throws (#742)
* detect improper use of t.throws Protects against a common misuse of t.throws (Like that seen in #739). This required the creation of a custom babel plugin. https://github.com/jamestalmage/babel-plugin-ava-throws-helper * relative file path and colors * protect against null/undefined in _setAssertError * use babel-code-frame to do syntax highlighting on the Error * require `babel-code-frame` inline. It has a sizable dependency graph * remove middle section of message. It is redundant given code-frame * further tests and add documentation. * update readme.md
1 parent 195390e commit 3201b1b

14 files changed

+119
-2
lines changed

docs/recipes/babelrc.md

+1
Original file line numberDiff line numberDiff line change
@@ -122,5 +122,6 @@ AVA *always* adds a few custom Babel plugins when transpiling your plugins. They
122122

123123
* Enable `power-assert` support.
124124
* Rewrite require paths internal AVA dependencies like `babel-runtime` (important if you are still using `npm@2`).
125+
* [`ava-throws-helper`](https://github.com/jamestalmage/babel-plugin-ava-throws-helper) helps AVA [detect and report](https://github.com/sindresorhus/ava/pull/742) improper use of the `t.throws` assertion.
125126
* Generate test metadata to determine which files should be run first (*future*).
126127
* Static analysis of dependencies for precompilation (*future*).

lib/caching-precompiler.js

+2
Original file line numberDiff line numberDiff line change
@@ -52,11 +52,13 @@ CachingPrecompiler.prototype._init = function () {
5252
];
5353

5454
var transformRuntime = require('babel-plugin-transform-runtime');
55+
var throwsHelper = require('babel-plugin-ava-throws-helper');
5556
var rewriteBabelPaths = this._createRewritePlugin();
5657
var powerAssert = this._createEspowerPlugin();
5758

5859
this.defaultPlugins = [
5960
powerAssert,
61+
throwsHelper,
6062
rewriteBabelPaths,
6163
transformRuntime
6264
];

lib/fork.js

+1
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ if (env.NODE_PATH) {
2424
module.exports = function (file, opts) {
2525
opts = objectAssign({
2626
file: file,
27+
baseDir: process.cwd(),
2728
tty: process.stdout.isTTY ? {
2829
columns: process.stdout.columns,
2930
rows: process.stdout.rows

lib/test-worker.js

+4
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ sourceMapSupport.install({
6262
var loudRejection = require('loud-rejection/api')(process); // eslint-disable-line
6363
var serializeError = require('./serialize-error');
6464
var send = require('./send');
65+
var throwsHelper = require('./throws-helper');
6566
var installPrecompiler = require('require-precompiled'); // eslint-disable-line
6667
var cacheDir = opts.cacheDir;
6768

@@ -92,7 +93,10 @@ Object.keys(require.extensions).forEach(function (ext) {
9293

9394
require(testPath);
9495

96+
process.on('unhandledRejection', throwsHelper);
97+
9598
process.on('uncaughtException', function (exception) {
99+
throwsHelper(exception);
96100
send('uncaughtException', {exception: serializeError(exception)});
97101
});
98102

lib/test.js

+2
Original file line numberDiff line numberDiff line change
@@ -12,6 +12,7 @@ var plur = require('plur');
1212
var assert = require('./assert');
1313
var enhanceAssert = require('./enhance-assert');
1414
var globals = require('./globals');
15+
var throwsHelper = require('./throws-helper');
1516

1617
function Test(title, fn, contextRef, report) {
1718
if (!(this instanceof Test)) {
@@ -68,6 +69,7 @@ Test.prototype._assert = function (promise) {
6869
};
6970

7071
Test.prototype._setAssertError = function (err) {
72+
throwsHelper(err);
7173
if (this.assertError !== undefined) {
7274
return;
7375
}

lib/throws-helper.js

+35
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,35 @@
1+
'use strict';
2+
var fs = require('fs');
3+
var path = require('path');
4+
var chalk = require('chalk');
5+
var globals = require('./globals');
6+
7+
module.exports = function throwsHelper(error) {
8+
if (!error || !error._avaThrowsHelperData) {
9+
return;
10+
}
11+
var data = error._avaThrowsHelperData;
12+
var codeFrame = require('babel-code-frame');
13+
var frame = '';
14+
try {
15+
var rawLines = fs.readFileSync(data.filename, 'utf8');
16+
frame = codeFrame(rawLines, data.line, data.column, {highlightCode: true});
17+
} catch (e) {
18+
console.warn(e);
19+
console.warn(e);
20+
}
21+
console.error(
22+
[
23+
'Improper usage of t.throws detected at ' + chalk.bold.yellow('%s (%d:%d)') + ':',
24+
frame,
25+
'The first argument to t.throws should be wrapped in a function:',
26+
chalk.cyan(' t.throws(function() {\n %s\n })'),
27+
'Visit the following URL for more details:',
28+
' ' + chalk.blue.underline('https://github.com/sindresorhus/ava#throwsfunctionpromise-error-message')
29+
].join('\n\n'),
30+
path.relative(globals.options.baseDir, data.filename),
31+
data.line,
32+
data.column,
33+
data.source
34+
);
35+
};

package.json

+3
Original file line numberDiff line numberDiff line change
@@ -78,7 +78,9 @@
7878
"array-uniq": "^1.0.2",
7979
"arrify": "^1.0.0",
8080
"ava-init": "^0.1.0",
81+
"babel-code-frame": "^6.7.5",
8182
"babel-core": "^6.3.21",
83+
"babel-plugin-ava-throws-helper": "0.0.4",
8284
"babel-plugin-detective": "^1.0.2",
8385
"babel-plugin-espower": "^2.1.0",
8486
"babel-plugin-transform-runtime": "^6.3.13",
@@ -138,6 +140,7 @@
138140
"update-notifier": "^0.6.0"
139141
},
140142
"devDependencies": {
143+
"babel-code-frame": "^6.7.5",
141144
"cli-table2": "^0.2.0",
142145
"coveralls": "^2.11.4",
143146
"delay": "^1.3.0",

readme.md

+1-1
Original file line numberDiff line numberDiff line change
@@ -583,7 +583,7 @@ You can also use the special `"inherit"` keyword. This makes AVA defer to the Ba
583583

584584
See AVA's [`.babelrc` recipe](docs/recipes/babelrc.md) for further examples and a more detailed explanation of configuration options.
585585

586-
Note that AVA will *always* apply the [`espower`](https://github.com/power-assert-js/babel-plugin-espower) and [`transform-runtime`](https://babeljs.io/docs/plugins/transform-runtime/) plugins.
586+
Note that AVA will *always* apply [a few internal plugins](docs/recipes/babelrc.md#notes) regardless of configuration, but they should not impact the behavior of your code.
587587

588588
### TypeScript support
589589

test/caching-precompiler.js

+2-1
Original file line numberDiff line numberDiff line change
@@ -6,6 +6,7 @@ var uniqueTempDir = require('unique-temp-dir');
66
var sinon = require('sinon');
77
var babel = require('babel-core');
88
var transformRuntime = require('babel-plugin-transform-runtime');
9+
var throwsHelper = require('babel-plugin-ava-throws-helper');
910
var fromMapFileSource = require('convert-source-map').fromMapFileSource;
1011

1112
var CachingPrecompiler = require('../lib/caching-precompiler');
@@ -145,7 +146,7 @@ test('uses babelConfig for babel options when babelConfig is an object', functio
145146
t.true('inputSourceMap' in options);
146147
t.false(options.babelrc);
147148
t.same(options.presets, ['stage-2', 'es2015']);
148-
t.same(options.plugins, [customPlugin, powerAssert, rewrite, transformRuntime]);
149+
t.same(options.plugins, [customPlugin, powerAssert, throwsHelper, rewrite, transformRuntime]);
149150
t.end();
150151
});
151152

test/cli.js

+24
Original file line numberDiff line numberDiff line change
@@ -96,6 +96,30 @@ test('throwing a named function will report the to the console', function (t) {
9696
});
9797
});
9898

99+
test('improper use of t.throws will be reported to the console', function (t) {
100+
execCli('fixture/improper-t-throws.js', function (err, stdout, stderr) {
101+
t.ok(err);
102+
t.match(stderr, /Improper usage of t\.throws detected at .*improper-t-throws.js \(4:10\)/);
103+
t.end();
104+
});
105+
});
106+
107+
test('improper use of t.throws from within a Promise will be reported to the console', function (t) {
108+
execCli('fixture/improper-t-throws-promise.js', function (err, stdout, stderr) {
109+
t.ok(err);
110+
t.match(stderr, /Improper usage of t\.throws detected at .*improper-t-throws-promise.js \(5:11\)/);
111+
t.end();
112+
});
113+
});
114+
115+
test('improper use of t.throws from within an async callback will be reported to the console', function (t) {
116+
execCli('fixture/improper-t-throws-async-callback.js', function (err, stdout, stderr) {
117+
t.ok(err);
118+
t.match(stderr, /Improper usage of t\.throws detected at .*improper-t-throws-async-callback.js \(5:11\)/);
119+
t.end();
120+
});
121+
});
122+
99123
test('babel require hook only applies to the test file', function (t) {
100124
t.plan(3);
101125

Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import test from '../../';
2+
3+
test.cb(t => {
4+
setTimeout(() => {
5+
t.throws(throwSync());
6+
});
7+
});
8+
9+
function throwSync() {
10+
throw new Error('should be detected');
11+
}
+11
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,11 @@
1+
import test from '../../';
2+
3+
test(t => {
4+
return Promise.resolve().then(() => {
5+
t.throws(throwSync());
6+
});
7+
});
8+
9+
function throwSync() {
10+
throw new Error('should be detected');
11+
}
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,13 @@
1+
import test from '../../';
2+
3+
test.cb(t => {
4+
Promise.resolve().then(() => {
5+
t.throws(throwSync());
6+
});
7+
8+
setTimeout(t.end, 20);
9+
});
10+
11+
function throwSync() {
12+
throw new Error('should be detected');
13+
}

test/fixture/improper-t-throws.js

+9
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,9 @@
1+
import test from '../../';
2+
3+
test(t => {
4+
t.throws(throwSync());
5+
});
6+
7+
function throwSync() {
8+
throw new Error('should be detected');
9+
}

0 commit comments

Comments
 (0)