From 07b8783d48047c200b5611e34ebef39542acbbce Mon Sep 17 00:00:00 2001 From: James Talmage Date: Thu, 31 Dec 2015 01:22:17 -0500 Subject: [PATCH 1/4] Allow a default message to be provided for a given pattern. See: https://github.com/sindresorhus/ava/pull/340#issuecomment-167479840 --- lib/decorate.js | 12 +++-------- lib/decorator.js | 45 ++++++++++++++++++++++++++++++------------ test/empower_test.js | 47 +++++++++++++++++++++++++++++++++++++++++++- 3 files changed, 81 insertions(+), 23 deletions(-) diff --git a/lib/decorate.js b/lib/decorate.js index f5d5d1b..1a1c0f3 100644 --- a/lib/decorate.js +++ b/lib/decorate.js @@ -5,10 +5,7 @@ var map = require('core-js/library/fn/array/map'); var slice = Array.prototype.slice; function decorate (callSpec, decorator) { - var func = callSpec.func; - var thisObj = callSpec.thisObj; var numArgsToCapture = callSpec.numArgsToCapture; - var enhanced = callSpec.enhanced; return function decoratedAssert () { var context, message, hasMessage = false, args = slice.apply(arguments); @@ -19,12 +16,9 @@ function decorate (callSpec, decorator) { } var invocation = { - thisObj: thisObj, - func: func, values: args, message: message, - hasMessage: hasMessage, - enhanced: enhanced + hasMessage: hasMessage }; if (some(args, isCaptured)) { @@ -45,9 +39,9 @@ function decorate (callSpec, decorator) { return arg.powerAssertContext.value; }); - return decorator.concreteAssert(invocation, context); + return decorator.concreteAssert(callSpec, invocation, context); } else { - return decorator.concreteAssert(invocation); + return decorator.concreteAssert(callSpec, invocation); } }; } diff --git a/lib/decorator.js b/lib/decorator.js index 8f9f1dd..6792264 100644 --- a/lib/decorator.js +++ b/lib/decorator.js @@ -12,8 +12,8 @@ function Decorator (receiver, config) { this.config = config; this.onError = config.onError; this.onSuccess = config.onSuccess; - this.signatures = map(config.patterns, signature.parse); - this.wrapOnlySignatures = map(config.wrapOnlyPatterns, signature.parse); + this.signatures = map(config.patterns, parse); + this.wrapOnlySignatures = map(config.wrapOnlyPatterns, parse); } Decorator.prototype.enhancement = function () { @@ -21,7 +21,8 @@ Decorator.prototype.enhancement = function () { var container = this.container(); var wrappedMethods = []; - function attach(matcher, enhanced) { + function attach(matcherSpec, enhanced) { + var matcher = matcherSpec.parsed; var methodName = detectMethodName(matcher.callee); if (typeof that.receiver[methodName] !== 'function' || wrappedMethods.indexOf(methodName) !== -1) { return; @@ -29,7 +30,8 @@ Decorator.prototype.enhancement = function () { var callSpec = { thisObj: that.receiver, func: that.receiver[methodName], - numArgsToCapture: numberOfArgumentsToCapture(matcher), + numArgsToCapture: numberOfArgumentsToCapture(matcherSpec), + matcherSpec: matcherSpec, enhanced: enhanced }; container[methodName] = decorate(callSpec, that); @@ -61,6 +63,7 @@ Decorator.prototype.container = function () { thisObj: null, func: this.receiver, numArgsToCapture: numberOfArgumentsToCapture(candidates[0]), + matcherSpec: candidates[0], enhanced: enhanced }; basement = decorate(callSpec, this); @@ -69,12 +72,12 @@ Decorator.prototype.container = function () { return basement; }; -Decorator.prototype.concreteAssert = function (invocation, context) { - var func = invocation.func; - var thisObj = invocation.thisObj; +Decorator.prototype.concreteAssert = function (callSpec, invocation, context) { + var func = callSpec.func; + var thisObj = callSpec.thisObj; + var enhanced = callSpec.enhanced; var args = invocation.values; var message = invocation.message; - var enhanced = invocation.enhanced; if (context && typeof this.config.modifyMessageBeforeAssert === 'function') { message = this.config.modifyMessageBeforeAssert({originalMessage: message, powerAssertContext: context}); } @@ -82,6 +85,7 @@ Decorator.prototype.concreteAssert = function (invocation, context) { var data = { originalMessage: message, + defaultMessage: callSpec.matcherSpec.defaultMessage, enhanced: enhanced, args: args }; @@ -103,7 +107,8 @@ Decorator.prototype.concreteAssert = function (invocation, context) { return this.onSuccess(data); }; -function numberOfArgumentsToCapture (matcher) { +function numberOfArgumentsToCapture (matcherSpec) { + var matcher = matcherSpec.parsed; var len = matcher.args.length; var lastArg; if (0 < len) { @@ -124,13 +129,27 @@ function detectMethodName (callee) { } -function functionCall (matcher) { - return matcher.callee.type === 'Identifier'; +function functionCall (matcherSpec) { + return matcherSpec.parsed.callee.type === 'Identifier'; } -function methodCall (matcher) { - return matcher.callee.type === 'MemberExpression'; +function methodCall (matcherSpec) { + return matcherSpec.parsed.callee.type === 'MemberExpression'; +} + +function parse(matcherSpec) { + if (typeof matcherSpec === 'string') { + return { + pattern: matcherSpec, + parsed: signature.parse(matcherSpec) + }; + } + return { + pattern: matcherSpec.pattern, + parsed: signature.parse(matcherSpec.pattern), + defaultMessage: matcherSpec.defaultMessage + } } diff --git a/test/empower_test.js b/test/empower_test.js index e7fa338..2039cd2 100644 --- a/test/empower_test.js +++ b/test/empower_test.js @@ -517,6 +517,46 @@ suite('onSuccess can throw', function () { }); }); +suite('enhanced methods can have default messages', function () { + var assert = empower( + { + fail: function (message) { + baseAssert.ok(false, message); + }, + pass: function (message) { + // noop + } + }, + { + patterns: [ + { + pattern: 'assert.fail([message])', + defaultMessage: 'User! You have failed this assertion!' + }, + 'assert.pass([message])' + ], + onError: function (event) { + baseAssert.equal(event.assertionThrew, true); + return event; + }, + onSuccess: function (event) { + baseAssert.equal(event.assertionThrew, false); + return event; + } + } + ); + + test('instrumented', function () { + var event = eval(weave("assert.fail('doh!');")); + baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); + }); + + test('noninstrumented', function () { + var event = assert.fail('doh!'); + baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); + }); +}); + suite('wrapOnlyPatterns', function () { var assert = empower( { @@ -529,7 +569,10 @@ suite('wrapOnlyPatterns', function () { }, { wrapOnlyPatterns: [ - 'assert.fail([message])', + { + pattern: 'assert.fail([message])', + defaultMessage: 'User! You have failed this assertion!' + }, 'assert.pass([message])' ], onError: function (event) { @@ -556,6 +599,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.equal(event.assertionThrew, true); baseAssert.strictEqual(event.enhanced, false); baseAssert.equal(event.originalMessage, 'Oh no!'); + baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); baseAssert.deepEqual(event.args, ['Oh no!']); }); @@ -572,6 +616,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.equal(event.assertionThrew, true); baseAssert.strictEqual(event.enhanced, false); baseAssert.equal(event.originalMessage, 'Oh no!'); + baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); baseAssert.deepEqual(event.args, ['Oh no!']); }); }); From 7e44c82059141406bb81868c0bb64144bf0c2d7a Mon Sep 17 00:00:00 2001 From: James Talmage Date: Fri, 1 Jan 2016 04:06:11 -0500 Subject: [PATCH 2/4] attach `assertionFunction` metada to event. This attaches the wrapper function to the assertion event. This can be used with Error.captureStackTrace(), to eliminate power-assert internals from the stack trace --- lib/decorator.js | 5 +++-- test/empower_test.js | 12 ++++++++++-- 2 files changed, 13 insertions(+), 4 deletions(-) diff --git a/lib/decorator.js b/lib/decorator.js index 6792264..e6fb869 100644 --- a/lib/decorator.js +++ b/lib/decorator.js @@ -34,7 +34,7 @@ Decorator.prototype.enhancement = function () { matcherSpec: matcherSpec, enhanced: enhanced }; - container[methodName] = decorate(callSpec, that); + container[methodName] = callSpec.enhancedFunc = decorate(callSpec, that); wrappedMethods.push(methodName); } @@ -66,7 +66,7 @@ Decorator.prototype.container = function () { matcherSpec: candidates[0], enhanced: enhanced }; - basement = decorate(callSpec, this); + basement = callSpec.enhancedFunc = decorate(callSpec, this); } } return basement; @@ -84,6 +84,7 @@ Decorator.prototype.concreteAssert = function (callSpec, invocation, context) { args = args.concat(message); var data = { + assertionFunction: callSpec.enhancedFunc, originalMessage: message, defaultMessage: callSpec.matcherSpec.defaultMessage, enhanced: enhanced, diff --git a/test/empower_test.js b/test/empower_test.js index 2039cd2..98069e0 100644 --- a/test/empower_test.js +++ b/test/empower_test.js @@ -517,7 +517,7 @@ suite('onSuccess can throw', function () { }); }); -suite('enhanced methods can have default messages', function () { +suite('metadata for enhanced methods', function () { var assert = empower( { fail: function (message) { @@ -549,11 +549,15 @@ suite('enhanced methods can have default messages', function () { test('instrumented', function () { var event = eval(weave("assert.fail('doh!');")); baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); + baseAssert.strictEqual(event.enhanced, true); + baseAssert.strictEqual(event.assertionFunction, assert.fail); }); - test('noninstrumented', function () { + test('non-instrumented', function () { var event = assert.fail('doh!'); baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); + baseAssert.strictEqual(event.enhanced, true); + baseAssert.strictEqual(event.assertionFunction, assert.fail); }); }); @@ -592,6 +596,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.strictEqual(event.enhanced, false); baseAssert.equal(event.originalMessage, 'woot!'); baseAssert.deepEqual(event.args, ['woot!']); + baseAssert.strictEqual(event.assertionFunction, assert.pass); }); test('instrumented code: error', function () { @@ -601,6 +606,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.equal(event.originalMessage, 'Oh no!'); baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); baseAssert.deepEqual(event.args, ['Oh no!']); + baseAssert.strictEqual(event.assertionFunction, assert.fail); }); test('non-instrumented code: success', function () { @@ -609,6 +615,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.strictEqual(event.enhanced, false); baseAssert.equal(event.originalMessage, 'woot!'); baseAssert.deepEqual(event.args, ['woot!']); + baseAssert.strictEqual(event.assertionFunction, assert.pass); }); test('non-instrumented code: error', function () { @@ -618,6 +625,7 @@ suite('wrapOnlyPatterns', function () { baseAssert.equal(event.originalMessage, 'Oh no!'); baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); baseAssert.deepEqual(event.args, ['Oh no!']); + baseAssert.strictEqual(event.assertionFunction, assert.fail); }); }); From a808b799fb9876a1575c308ac7672a5c7014ef82 Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 5 Jan 2016 01:59:10 -0500 Subject: [PATCH 3/4] expose the entire messageSpec in the event object --- lib/decorator.js | 22 ++++++++++++---------- test/empower_test.js | 8 ++++++++ 2 files changed, 20 insertions(+), 10 deletions(-) diff --git a/lib/decorator.js b/lib/decorator.js index e6fb869..4a58e1c 100644 --- a/lib/decorator.js +++ b/lib/decorator.js @@ -5,6 +5,7 @@ var filter = require('core-js/library/fn/array/filter'); var map = require('core-js/library/fn/array/map'); var signature = require('call-signature'); var decorate = require('./decorate'); +var keys = require('core-js/library/fn/object/keys'); function Decorator (receiver, config) { @@ -78,6 +79,8 @@ Decorator.prototype.concreteAssert = function (callSpec, invocation, context) { var enhanced = callSpec.enhanced; var args = invocation.values; var message = invocation.message; + var matcherSpec = callSpec.matcherSpec; + if (context && typeof this.config.modifyMessageBeforeAssert === 'function') { message = this.config.modifyMessageBeforeAssert({originalMessage: message, powerAssertContext: context}); } @@ -86,7 +89,8 @@ Decorator.prototype.concreteAssert = function (callSpec, invocation, context) { var data = { assertionFunction: callSpec.enhancedFunc, originalMessage: message, - defaultMessage: callSpec.matcherSpec.defaultMessage, + defaultMessage: matcherSpec.defaultMessage, + matcherSpec: matcherSpec, enhanced: enhanced, args: args }; @@ -141,16 +145,14 @@ function methodCall (matcherSpec) { function parse(matcherSpec) { if (typeof matcherSpec === 'string') { - return { - pattern: matcherSpec, - parsed: signature.parse(matcherSpec) - }; - } - return { - pattern: matcherSpec.pattern, - parsed: signature.parse(matcherSpec.pattern), - defaultMessage: matcherSpec.defaultMessage + matcherSpec = {pattern: matcherSpec}; } + var ret = {}; + keys(matcherSpec).forEach(function (key) { + ret[key] = matcherSpec[key]; + }); + ret.parsed = signature.parse(matcherSpec.pattern); + return ret; } diff --git a/test/empower_test.js b/test/empower_test.js index 98069e0..d10ef6f 100644 --- a/test/empower_test.js +++ b/test/empower_test.js @@ -551,6 +551,14 @@ suite('metadata for enhanced methods', function () { baseAssert.equal(event.defaultMessage, 'User! You have failed this assertion!'); baseAssert.strictEqual(event.enhanced, true); baseAssert.strictEqual(event.assertionFunction, assert.fail); + baseAssert.deepEqual(event.matcherSpec, { + pattern: 'assert.fail([message])', + defaultMessage: 'User! You have failed this assertion!', + parsed: { + args: [{name: 'message', optional: true}], + callee: {object: 'assert', member: 'fail', type: 'MemberExpression'} + } + }); }); test('non-instrumented', function () { From 8ae9d69cfa0bcb6cd130bb4a709db24ad535e1ce Mon Sep 17 00:00:00 2001 From: James Talmage Date: Tue, 5 Jan 2016 02:06:53 -0500 Subject: [PATCH 4/4] add documentation --- README.md | 21 +++++++++++++++++++-- 1 file changed, 19 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index 741165f..11bd25c 100644 --- a/README.md +++ b/README.md @@ -85,6 +85,10 @@ Both methods are called with a single `event` argument, it will have the followi - `event.originalMessage` - The actual value the user provided for optional `message` parameter. This will be `undefined` if the user did not provide a value, even if the underlying assertion provides a default message. +- `event.defaultMessage` - If you use objects instead of strings to specify patterns (see below), the `defaultMessage` metadata will be copied directly on the event object. + +- `event.matcherSpec` - This contains the complete parsed matcher spec as supplied, as well as any additional metadata you may have supplied (see patterns section below for details on how to supply additional metadata). + - `event.args` - An array of the actual arguments passed to the assertion. - `event.assertionThrew` - Whether or not the underlying assertion threw an error. This will always be `true` in an `onError` callback, and always `false` in an `onSuccess` callback. @@ -118,7 +122,7 @@ TBD | type | default value | |:--------------------|:--------------------| -| `Array` of `string` | objects shown below | +| `Array` of `string` or `objects`| objects shown below | ```javascript [ @@ -134,11 +138,23 @@ TBD 'assert.notDeepStrictEqual(actual, expected, [message])' ] ``` - Target patterns for power assert feature instrumentation. Pattern detection is done by [call-signature](https://github.com/jamestalmage/call-signature). Any arguments enclosed in bracket (for example, `[message]`) means optional parameters. Without bracket means mandatory parameters. +Instead of a string, you may alternatively specify an object with a `pattern` property, and any other arbitrary data. +Currently only `defaultMessage` is formally recommended, but you can attach any data here and it will be passed to the `onSuccess` and `onError` handlers. + +```javascript +[ + { + pattern: 'assert.fail([message])', + defaultMessage:'assert.fail() was called!!' + }, + ... +] +``` + #### options.wrapOnlyPatterns | type | default value | @@ -147,6 +163,7 @@ Pattern detection is done by [call-signature](https://github.com/jamestalmage/ca Methods matching these patterns will not be instrumented by the code transform, but they will be wrapped at runtime and trigger events in the `onSuccess` and `onError` callbacks. Note that "wrap only" events will never have a `powerAssertContext` property. +Similar to the `options.patterns`, you may supply objects with a `pattern` member, and the additional metadata will be passed to the assertion listeners. ### var options = empowerCore.defaultOptions();