Skip to content

Commit

Permalink
assert: implement assert.match() and assert.doesNotMatch()
Browse files Browse the repository at this point in the history
This adds a new functionality to the assertion module: a dedicated
check for regular expressions. So far it's possible to use
`assert.ok(regexp.test(string))`. This is not ideal though when it
comes to the error message, since it's not possible to know how
either of the input values look like. It's just known that the
assertion failed.
This allows to pass through the regular expression and the input
string. The string is then matched against the regular expression
and reports a expressive error message in case of a failure.

Backport-PR-URL: #31431
PR-URL: #30929
Reviewed-By: Michaël Zasso <targos@protonmail.com>
Reviewed-By: Rich Trott <rtrott@gmail.com>
Reviewed-By: Stephen Belanger <admin@stephenbelanger.com>
  • Loading branch information
BridgeAR authored and BethGriggs committed Feb 6, 2020
1 parent 9a6aff8 commit fc7b27e
Show file tree
Hide file tree
Showing 3 changed files with 220 additions and 4 deletions.
72 changes: 72 additions & 0 deletions doc/api/assert.md
Expand Up @@ -425,6 +425,42 @@ parameter is undefined, a default error message is assigned. If the `message`
parameter is an instance of an [`Error`][] then it will be thrown instead of the
`AssertionError`.

## `assert.doesNotMatch(string, regexp[, message])`
<!-- YAML
added: REPLACEME
-->

* `string` {string}
* `regexp` {RegExp}
* `message` {string|Error}

> Stability: 1 - Experimental
Expects the `string` input not to match the regular expression.

This feature is currently experimental and the name might change or it might be
completely removed again.

```js
const assert = require('assert').strict;

assert.doesNotMatch('I will fail', /fail/);
// AssertionError [ERR_ASSERTION]: The input was expected to not match the ...

assert.doesNotMatch(123, /pass/);
// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string.

assert.doesNotMatch('I will pass', /different/);
// OK
```

If the values do match, or if the `string` argument is of another type than
`string`, an [`AssertionError`][] is thrown with a `message` property set equal
to the value of the `message` parameter. If the `message` parameter is
undefined, a default error message is assigned. If the `message` parameter is an
instance of an [`Error`][] then it will be thrown instead of the
[`AssertionError`][].

## `assert.doesNotReject(asyncFn[, error][, message])`
<!-- YAML
added: v10.0.0
Expand Down Expand Up @@ -728,6 +764,42 @@ let err;
// at errorFrame
```

## `assert.match(string, regexp[, message])`
<!-- YAML
added: REPLACEME
-->

* `string` {string}
* `regexp` {RegExp}
* `message` {string|Error}

> Stability: 1 - Experimental
Expects the `string` input to match the regular expression.

This feature is currently experimental and the name might change or it might be
completely removed again.

```js
const assert = require('assert').strict;

assert.match('I will fail', /pass/);
// AssertionError [ERR_ASSERTION]: The input did not match the regular ...

assert.match(123, /pass/);
// AssertionError [ERR_ASSERTION]: The "string" argument must be of type string.

assert.match('I will pass', /pass/);
// OK
```

If the values do not match, or if the `string` argument is of another type than
`string`, an [`AssertionError`][] is thrown with a `message` property set equal
to the value of the `message` parameter. If the `message` parameter is
undefined, a default error message is assigned. If the `message` parameter is an
instance of an [`Error`][] then it will be thrown instead of the
[`AssertionError`][].

## `assert.notDeepEqual(actual, expected[, message])`
<!-- YAML
added: v0.1.21
Expand Down
52 changes: 48 additions & 4 deletions lib/assert.js
Expand Up @@ -27,6 +27,7 @@ const {
ObjectKeys,
ObjectPrototypeIsPrototypeOf,
Map,
RegExpPrototypeTest,
} = primordials;

const { Buffer } = require('buffer');
Expand Down Expand Up @@ -533,7 +534,7 @@ class Comparison {
if (actual !== undefined &&
typeof actual[key] === 'string' &&
isRegExp(obj[key]) &&
obj[key].test(actual[key])) {
RegExpPrototypeTest(obj[key], actual[key])) {
this[key] = actual[key];
} else {
this[key] = obj[key];
Expand Down Expand Up @@ -579,7 +580,7 @@ function expectedException(actual, expected, message, fn) {
// Handle regular expressions.
if (isRegExp(expected)) {
const str = String(actual);
if (expected.test(str))
if (RegExpPrototypeTest(expected, str))
return;

if (!message) {
Expand Down Expand Up @@ -614,7 +615,7 @@ function expectedException(actual, expected, message, fn) {
for (const key of keys) {
if (typeof actual[key] === 'string' &&
isRegExp(expected[key]) &&
expected[key].test(actual[key])) {
RegExpPrototypeTest(expected[key], actual[key])) {
continue;
}
compareExceptionKey(actual, expected, key, message, keys, fn);
Expand Down Expand Up @@ -751,7 +752,7 @@ function hasMatchingError(actual, expected) {
if (typeof expected !== 'function') {
if (isRegExp(expected)) {
const str = String(actual);
return expected.test(str);
return RegExpPrototypeTest(expected, str);
}
throw new ERR_INVALID_ARG_TYPE(
'expected', ['Function', 'RegExp'], expected
Expand Down Expand Up @@ -856,6 +857,49 @@ assert.ifError = function ifError(err) {
}
};

function internalMatch(string, regexp, message, fn) {
if (!isRegExp(regexp)) {
throw new ERR_INVALID_ARG_TYPE(
'regexp', 'RegExp', regexp
);
}
const match = fn.name === 'match';
if (typeof string !== 'string' ||
RegExpPrototypeTest(regexp, string) !== match) {
if (message instanceof Error) {
throw message;
}

const generatedMessage = !message;

// 'The input was expected to not match the regular expression ' +
message = message || (typeof string !== 'string' ?
'The "string" argument must be of type string. Received type ' +
`${typeof string} (${inspect(string)})` :
(match ?
'The input did not match the regular expression ' :
'The input was expected to not match the regular expression ') +
`${inspect(regexp)}. Input:\n\n${inspect(string)}\n`);
const err = new AssertionError({
actual: string,
expected: regexp,
message,
operator: fn.name,
stackStartFn: fn
});
err.generatedMessage = generatedMessage;
throw err;
}
}

assert.match = function match(string, regexp, message) {
internalMatch(string, regexp, message, match);
};

assert.doesNotMatch = function doesNotMatch(string, regexp, message) {
internalMatch(string, regexp, message, doesNotMatch);
};

// Expose a strict only variant of assert
function strict(...args) {
innerOk(strict, args.length, ...args);
Expand Down
100 changes: 100 additions & 0 deletions test/parallel/test-assert.js
Expand Up @@ -1301,3 +1301,103 @@ assert.throws(
assert(!err2.stack.includes('hidden'));
})();
}

// Multiple assert.match() tests.
{
assert.throws(
() => assert.match(/abc/, 'string'),
{
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "regexp" argument must be an instance of RegExp. ' +
"Received type string ('string')"
}
);
assert.throws(
() => assert.match('string', /abc/),
{
actual: 'string',
expected: /abc/,
operator: 'match',
message: 'The input did not match the regular expression /abc/. ' +
"Input:\n\n'string'\n",
generatedMessage: true
}
);
assert.throws(
() => assert.match('string', /abc/, 'foobar'),
{
actual: 'string',
expected: /abc/,
operator: 'match',
message: 'foobar',
generatedMessage: false
}
);
const errorMessage = new RangeError('foobar');
assert.throws(
() => assert.match('string', /abc/, errorMessage),
errorMessage
);
assert.throws(
() => assert.match({ abc: 123 }, /abc/),
{
actual: { abc: 123 },
expected: /abc/,
operator: 'match',
message: 'The "string" argument must be of type string. ' +
'Received type object ({ abc: 123 })',
generatedMessage: true
}
);
assert.match('I will pass', /pass$/);
}

// Multiple assert.doesNotMatch() tests.
{
assert.throws(
() => assert.doesNotMatch(/abc/, 'string'),
{
code: 'ERR_INVALID_ARG_TYPE',
message: 'The "regexp" argument must be an instance of RegExp. ' +
"Received type string ('string')"
}
);
assert.throws(
() => assert.doesNotMatch('string', /string/),
{
actual: 'string',
expected: /string/,
operator: 'doesNotMatch',
message: 'The input was expected to not match the regular expression ' +
"/string/. Input:\n\n'string'\n",
generatedMessage: true
}
);
assert.throws(
() => assert.doesNotMatch('string', /string/, 'foobar'),
{
actual: 'string',
expected: /string/,
operator: 'doesNotMatch',
message: 'foobar',
generatedMessage: false
}
);
const errorMessage = new RangeError('foobar');
assert.throws(
() => assert.doesNotMatch('string', /string/, errorMessage),
errorMessage
);
assert.throws(
() => assert.doesNotMatch({ abc: 123 }, /abc/),
{
actual: { abc: 123 },
expected: /abc/,
operator: 'doesNotMatch',
message: 'The "string" argument must be of type string. ' +
'Received type object ({ abc: 123 })',
generatedMessage: true
}
);
assert.doesNotMatch('I will pass', /different$/);
}

0 comments on commit fc7b27e

Please sign in to comment.