first commit
This commit is contained in:
425
libraries/o-error/test/o-error-util.test.js
Normal file
425
libraries/o-error/test/o-error-util.test.js
Normal file
@@ -0,0 +1,425 @@
|
||||
const { expect } = require('chai')
|
||||
const { promisify } = require('node:util')
|
||||
|
||||
const OError = require('..')
|
||||
|
||||
const {
|
||||
expectError,
|
||||
expectFullStackWithoutStackFramesToEqual,
|
||||
} = require('./support')
|
||||
|
||||
describe('utils', function () {
|
||||
describe('OError.tag', function () {
|
||||
it('tags errors thrown from an async function', async function () {
|
||||
const delay = promisify(setTimeout)
|
||||
|
||||
async function foo() {
|
||||
await delay(10)
|
||||
throw new Error('foo error')
|
||||
}
|
||||
|
||||
async function bar() {
|
||||
try {
|
||||
await foo()
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to bar', { bar: 'baz' })
|
||||
}
|
||||
}
|
||||
|
||||
async function baz() {
|
||||
try {
|
||||
await bar()
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to baz', { baz: 'bat' })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await baz()
|
||||
expect.fail('should have thrown')
|
||||
} catch (error) {
|
||||
expectError(error, {
|
||||
name: 'Error',
|
||||
klass: Error,
|
||||
message: 'Error: foo error',
|
||||
firstFrameRx: /at foo/,
|
||||
})
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'Error: foo error',
|
||||
'TaggedError: failed to bar',
|
||||
'TaggedError: failed to baz',
|
||||
])
|
||||
expect(OError.getFullInfo(error)).to.eql({
|
||||
bar: 'baz',
|
||||
baz: 'bat',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('tags errors thrown from a promise rejection', async function () {
|
||||
function foo() {
|
||||
return new Promise((resolve, reject) => {
|
||||
setTimeout(() => {
|
||||
reject(new Error('foo error'))
|
||||
}, 10)
|
||||
})
|
||||
}
|
||||
|
||||
async function bar() {
|
||||
try {
|
||||
await foo()
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to bar', { bar: 'baz' })
|
||||
}
|
||||
}
|
||||
|
||||
async function baz() {
|
||||
try {
|
||||
await bar()
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to baz', { baz: 'bat' })
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await baz()
|
||||
expect.fail('should have thrown')
|
||||
} catch (error) {
|
||||
expectError(error, {
|
||||
name: 'Error',
|
||||
klass: Error,
|
||||
message: 'Error: foo error',
|
||||
firstFrameRx: /_onTimeout/,
|
||||
})
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'Error: foo error',
|
||||
'TaggedError: failed to bar',
|
||||
'TaggedError: failed to baz',
|
||||
])
|
||||
expect(OError.getFullInfo(error)).to.eql({
|
||||
bar: 'baz',
|
||||
baz: 'bat',
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
it('tags errors yielded through callbacks', function (done) {
|
||||
function foo(cb) {
|
||||
setTimeout(() => {
|
||||
cb(new Error('foo error'))
|
||||
}, 10)
|
||||
}
|
||||
|
||||
function bar(cb) {
|
||||
foo(err => {
|
||||
if (err) {
|
||||
return cb(OError.tag(err, 'failed to bar', { bar: 'baz' }))
|
||||
}
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
function baz(cb) {
|
||||
bar(err => {
|
||||
if (err) {
|
||||
return cb(OError.tag(err, 'failed to baz', { baz: 'bat' }))
|
||||
}
|
||||
cb()
|
||||
})
|
||||
}
|
||||
|
||||
baz(err => {
|
||||
if (err) {
|
||||
expectError(err, {
|
||||
name: 'Error',
|
||||
klass: Error,
|
||||
message: 'Error: foo error',
|
||||
firstFrameRx: /_onTimeout/,
|
||||
})
|
||||
expectFullStackWithoutStackFramesToEqual(err, [
|
||||
'Error: foo error',
|
||||
'TaggedError: failed to bar',
|
||||
'TaggedError: failed to baz',
|
||||
])
|
||||
expect(OError.getFullInfo(err)).to.eql({
|
||||
bar: 'baz',
|
||||
baz: 'bat',
|
||||
})
|
||||
return done()
|
||||
}
|
||||
expect.fail('should have yielded an error')
|
||||
})
|
||||
})
|
||||
|
||||
it('is not included in the stack trace if using capture', function () {
|
||||
if (!Error.captureStackTrace) return this.skip()
|
||||
const err = new Error('test error')
|
||||
OError.tag(err, 'test message')
|
||||
const stack = OError.getFullStack(err)
|
||||
expect(stack).to.match(/TaggedError: test message\n\s+at/)
|
||||
expect(stack).to.not.match(/TaggedError: test message\n\s+at [\w.]*tag/)
|
||||
})
|
||||
|
||||
describe('without Error.captureStackTrace', function () {
|
||||
/* eslint-disable mocha/no-hooks-for-single-case */
|
||||
before(function () {
|
||||
this.originalCaptureStackTrace = Error.captureStackTrace
|
||||
Error.captureStackTrace = null
|
||||
})
|
||||
after(function () {
|
||||
Error.captureStackTrace = this.originalCaptureStackTrace
|
||||
})
|
||||
|
||||
it('still captures a stack trace, albeit including itself', function () {
|
||||
const err = new Error('test error')
|
||||
OError.tag(err, 'test message')
|
||||
expectFullStackWithoutStackFramesToEqual(err, [
|
||||
'Error: test error',
|
||||
'TaggedError: test message',
|
||||
])
|
||||
const stack = OError.getFullStack(err)
|
||||
expect(stack).to.match(/TaggedError: test message\n\s+at [\w.]*tag/)
|
||||
})
|
||||
})
|
||||
|
||||
describe('with limit on the number of tags', function () {
|
||||
before(function () {
|
||||
this.originalMaxTags = OError.maxTags
|
||||
OError.maxTags = 3
|
||||
})
|
||||
after(function () {
|
||||
OError.maxTags = this.originalMaxTags
|
||||
})
|
||||
|
||||
it('should not tag more than that', function () {
|
||||
const err = new Error('test error')
|
||||
OError.tag(err, 'test message 1')
|
||||
OError.tag(err, 'test message 2')
|
||||
OError.tag(err, 'test message 3')
|
||||
OError.tag(err, 'test message 4')
|
||||
OError.tag(err, 'test message 5')
|
||||
expectFullStackWithoutStackFramesToEqual(err, [
|
||||
'Error: test error',
|
||||
'TaggedError: test message 1',
|
||||
'TaggedError: ... dropped tags',
|
||||
'TaggedError: test message 4',
|
||||
'TaggedError: test message 5',
|
||||
])
|
||||
})
|
||||
|
||||
it('should handle deep recursion', async function () {
|
||||
async function recursiveAdd(n) {
|
||||
try {
|
||||
if (n === 0) throw new Error('deep error')
|
||||
const result = await recursiveAdd(n - 1)
|
||||
return result + 1
|
||||
} catch (err) {
|
||||
throw OError.tag(err, `at level ${n}`)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
await recursiveAdd(10)
|
||||
} catch (err) {
|
||||
expectFullStackWithoutStackFramesToEqual(err, [
|
||||
'Error: deep error',
|
||||
'TaggedError: at level 0',
|
||||
'TaggedError: ... dropped tags',
|
||||
'TaggedError: at level 9',
|
||||
'TaggedError: at level 10',
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('should handle a singleton error', function (done) {
|
||||
const err = new Error('singleton error')
|
||||
|
||||
function endpoint(callback) {
|
||||
helper(err => callback(err && OError.tag(err, 'in endpoint')))
|
||||
}
|
||||
|
||||
function helper(callback) {
|
||||
libraryFunction(err => callback(err && OError.tag(err, 'in helper')))
|
||||
}
|
||||
|
||||
function libraryFunction(callback) {
|
||||
callback(err)
|
||||
}
|
||||
|
||||
endpoint(() => {
|
||||
endpoint(err => {
|
||||
expect(err).to.exist
|
||||
expectFullStackWithoutStackFramesToEqual(err, [
|
||||
'Error: singleton error',
|
||||
'TaggedError: in helper',
|
||||
'TaggedError: ... dropped tags',
|
||||
'TaggedError: in helper',
|
||||
'TaggedError: in endpoint',
|
||||
])
|
||||
done()
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
})
|
||||
|
||||
describe('OError.getFullInfo', function () {
|
||||
it('works when given null', function () {
|
||||
expect(OError.getFullInfo(null)).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('works on a normal error', function () {
|
||||
const err = new Error('foo')
|
||||
expect(OError.getFullInfo(err)).to.deep.equal({})
|
||||
})
|
||||
|
||||
it('works on an error with tags', function () {
|
||||
const err = OError.tag(new Error('foo'), 'bar', { userId: 123 })
|
||||
expect(OError.getFullInfo(err)).to.deep.equal({ userId: 123 })
|
||||
})
|
||||
|
||||
it('merges info from an error and its tags', function () {
|
||||
const err = new OError('foo').withInfo({ projectId: 456 })
|
||||
OError.tag(err, 'failed to foo', { userId: 123 })
|
||||
expect(OError.getFullInfo(err)).to.deep.equal({
|
||||
projectId: 456,
|
||||
userId: 123,
|
||||
})
|
||||
})
|
||||
|
||||
it('merges info from a cause', function () {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
err2.info = { userId: 123 }
|
||||
expect(OError.getFullInfo(err1)).to.deep.equal({ userId: 123 })
|
||||
})
|
||||
|
||||
it('merges info from a nested cause', function () {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
const err3 = new Error('baz')
|
||||
err1.cause = err2
|
||||
err2.info = { userId: 123 }
|
||||
err2.cause = err3
|
||||
err3.info = { foo: 42 }
|
||||
expect(OError.getFullInfo(err1)).to.deep.equal({
|
||||
userId: 123,
|
||||
foo: 42,
|
||||
})
|
||||
})
|
||||
|
||||
it('merges info from cause with duplicate keys', function () {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.info = { userId: 42, foo: 1337 }
|
||||
err1.cause = err2
|
||||
err2.info = { userId: 1 }
|
||||
expect(OError.getFullInfo(err1)).to.deep.equal({
|
||||
userId: 42,
|
||||
foo: 1337,
|
||||
})
|
||||
})
|
||||
|
||||
it('merges info from tags with duplicate keys', function () {
|
||||
const err1 = OError.tag(new Error('foo'), 'bar', { userId: 123 })
|
||||
const err2 = OError.tag(err1, 'bat', { userId: 456 })
|
||||
expect(OError.getFullInfo(err2)).to.deep.equal({ userId: 456 })
|
||||
})
|
||||
|
||||
it('works on an error with .info set to a string', function () {
|
||||
const err = new Error('foo')
|
||||
err.info = 'test'
|
||||
expect(OError.getFullInfo(err)).to.deep.equal({})
|
||||
})
|
||||
})
|
||||
|
||||
describe('OError.getFullStack', function () {
|
||||
it('works when given null', function () {
|
||||
expect(OError.getFullStack(null)).to.equal('')
|
||||
})
|
||||
|
||||
it('works on a normal error', function () {
|
||||
const err = new Error('foo')
|
||||
const fullStack = OError.getFullStack(err)
|
||||
expect(fullStack).to.match(/^Error: foo$/m)
|
||||
expect(fullStack).to.match(/^\s+at /m)
|
||||
})
|
||||
|
||||
it('works on an error with a cause', function () {
|
||||
const err1 = new Error('foo')
|
||||
const err2 = new Error('bar')
|
||||
err1.cause = err2
|
||||
|
||||
const fullStack = OError.getFullStack(err1)
|
||||
expect(fullStack).to.match(/^Error: foo$/m)
|
||||
expect(fullStack).to.match(/^\s+at /m)
|
||||
expect(fullStack).to.match(/^caused by:\n\s+Error: bar$/m)
|
||||
})
|
||||
|
||||
it('works on both tags and causes', async function () {
|
||||
// Here's the actual error.
|
||||
function tryToFoo() {
|
||||
try {
|
||||
throw Error('foo')
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to foo', { foo: 1 })
|
||||
}
|
||||
}
|
||||
|
||||
// Inside another function that wraps it.
|
||||
function tryToBar() {
|
||||
try {
|
||||
tryToFoo()
|
||||
} catch (error) {
|
||||
throw new OError('failed to bar').withCause(error)
|
||||
}
|
||||
}
|
||||
|
||||
// And it is in another try.
|
||||
try {
|
||||
try {
|
||||
tryToBar()
|
||||
expect.fail('should have thrown')
|
||||
} catch (error) {
|
||||
throw OError.tag(error, 'failed to bat', { bat: 1 })
|
||||
}
|
||||
} catch (error) {
|
||||
// We catch the wrapping error.
|
||||
expectError(error, {
|
||||
name: 'OError',
|
||||
klass: OError,
|
||||
message: 'OError: failed to bar',
|
||||
firstFrameRx: /tryToBar/,
|
||||
})
|
||||
|
||||
// But the stack contains all of the errors and tags.
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'OError: failed to bar',
|
||||
'TaggedError: failed to bat',
|
||||
'caused by:',
|
||||
' Error: foo',
|
||||
' TaggedError: failed to foo',
|
||||
])
|
||||
|
||||
// The info from the wrapped cause should be picked up for logging.
|
||||
expect(OError.getFullInfo(error)).to.eql({ bat: 1, foo: 1 })
|
||||
|
||||
// But it should still be recorded.
|
||||
expect(OError.getFullInfo(error.cause)).to.eql({ foo: 1 })
|
||||
}
|
||||
})
|
||||
|
||||
it('works when given non Error', function () {
|
||||
expect(OError.getFullStack({ message: 'Foo' })).to.equal('Foo')
|
||||
})
|
||||
|
||||
it('works when given non Error with tags', function () {
|
||||
const error = OError.tag({ message: 'Foo: bar' }, 'baz')
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'Foo: bar',
|
||||
'TaggedError: baz',
|
||||
])
|
||||
})
|
||||
})
|
||||
})
|
111
libraries/o-error/test/o-error.test.js
Normal file
111
libraries/o-error/test/o-error.test.js
Normal file
@@ -0,0 +1,111 @@
|
||||
const { expect } = require('chai')
|
||||
|
||||
const OError = require('..')
|
||||
const {
|
||||
expectError,
|
||||
expectFullStackWithoutStackFramesToEqual,
|
||||
} = require('./support')
|
||||
|
||||
class CustomError1 extends OError {
|
||||
constructor() {
|
||||
super('failed to foo')
|
||||
}
|
||||
}
|
||||
|
||||
class CustomError2 extends OError {
|
||||
constructor(customMessage) {
|
||||
super(customMessage || 'failed to bar')
|
||||
}
|
||||
}
|
||||
|
||||
describe('OError', function () {
|
||||
it('can have an info object', function () {
|
||||
const err1 = new OError('foo', { foo: 1 })
|
||||
expect(err1.info).to.eql({ foo: 1 })
|
||||
|
||||
const err2 = new OError('foo').withInfo({ foo: 2 })
|
||||
expect(err2.info).to.eql({ foo: 2 })
|
||||
})
|
||||
|
||||
it('can have a cause', function () {
|
||||
const err1 = new OError('foo', { foo: 1 }, new Error('cause 1'))
|
||||
expect(err1.cause.message).to.equal('cause 1')
|
||||
|
||||
const err2 = new OError('foo').withCause(new Error('cause 2'))
|
||||
expect(err2.cause.message).to.equal('cause 2')
|
||||
})
|
||||
|
||||
it('handles a custom error type with a cause', function () {
|
||||
function doSomethingBadInternally() {
|
||||
throw new Error('internal error')
|
||||
}
|
||||
|
||||
function doSomethingBad() {
|
||||
try {
|
||||
doSomethingBadInternally()
|
||||
} catch (error) {
|
||||
throw new CustomError1().withCause(error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
doSomethingBad()
|
||||
expect.fail('should have thrown')
|
||||
} catch (error) {
|
||||
expectError(error, {
|
||||
name: 'CustomError1',
|
||||
klass: CustomError1,
|
||||
message: 'CustomError1: failed to foo',
|
||||
firstFrameRx: /doSomethingBad/,
|
||||
})
|
||||
expect(OError.getFullInfo(error)).to.deep.equal({})
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'CustomError1: failed to foo',
|
||||
'caused by:',
|
||||
' Error: internal error',
|
||||
])
|
||||
}
|
||||
})
|
||||
|
||||
it('handles a custom error type with nested causes', function () {
|
||||
function doSomethingBadInternally() {
|
||||
throw new Error('internal error')
|
||||
}
|
||||
|
||||
function doBar() {
|
||||
try {
|
||||
doSomethingBadInternally()
|
||||
} catch (error) {
|
||||
throw new CustomError2('failed to bar!').withCause(error)
|
||||
}
|
||||
}
|
||||
|
||||
function doFoo() {
|
||||
try {
|
||||
doBar()
|
||||
} catch (error) {
|
||||
throw new CustomError1().withCause(error)
|
||||
}
|
||||
}
|
||||
|
||||
try {
|
||||
doFoo()
|
||||
expect.fail('should have thrown')
|
||||
} catch (error) {
|
||||
expectError(error, {
|
||||
name: 'CustomError1',
|
||||
klass: CustomError1,
|
||||
message: 'CustomError1: failed to foo',
|
||||
firstFrameRx: /doFoo/,
|
||||
})
|
||||
expectFullStackWithoutStackFramesToEqual(error, [
|
||||
'CustomError1: failed to foo',
|
||||
'caused by:',
|
||||
' CustomError2: failed to bar!',
|
||||
' caused by:',
|
||||
' Error: internal error',
|
||||
])
|
||||
expect(OError.getFullInfo(error)).to.deep.equal({})
|
||||
}
|
||||
})
|
||||
})
|
61
libraries/o-error/test/support/index.js
Normal file
61
libraries/o-error/test/support/index.js
Normal file
@@ -0,0 +1,61 @@
|
||||
const { expect } = require('chai')
|
||||
|
||||
const OError = require('../..')
|
||||
|
||||
/**
|
||||
* @param {Error} e
|
||||
* @param {any} expected
|
||||
*/
|
||||
exports.expectError = function OErrorExpectError(e, expected) {
|
||||
expect(
|
||||
e.name,
|
||||
"error should set the name property to the error's name"
|
||||
).to.equal(expected.name)
|
||||
|
||||
expect(
|
||||
e instanceof expected.klass,
|
||||
'error should be an instance of the error type'
|
||||
).to.be.true
|
||||
|
||||
expect(
|
||||
e instanceof Error,
|
||||
'error should be an instance of the built-in Error type'
|
||||
).to.be.true
|
||||
|
||||
expect(
|
||||
require('node:util').types.isNativeError(e),
|
||||
'error should be recognised by util.types.isNativeError'
|
||||
).to.be.true
|
||||
|
||||
expect(
|
||||
e.toString(),
|
||||
'toString should return the default error message formatting'
|
||||
).to.equal(expected.message)
|
||||
|
||||
expect(e.stack, 'error should have a stack trace').to.not.be.empty
|
||||
|
||||
expect(
|
||||
/** @type {string} */ (e.stack).split('\n')[0],
|
||||
'stack should start with the default error message formatting'
|
||||
).to.match(new RegExp(`^${expected.name}:`))
|
||||
|
||||
expect(
|
||||
/** @type {string} */ (e.stack).split('\n')[1],
|
||||
'first stack frame should be the function where the error was thrown'
|
||||
).to.match(expected.firstFrameRx)
|
||||
}
|
||||
|
||||
/**
|
||||
* @param {Error} error
|
||||
* @param {String[]} expected
|
||||
*/
|
||||
exports.expectFullStackWithoutStackFramesToEqual = function (error, expected) {
|
||||
const fullStack = OError.getFullStack(error)
|
||||
const fullStackWithoutFrames = fullStack
|
||||
.split('\n')
|
||||
.filter(line => !/^\s+at\s/.test(line))
|
||||
expect(
|
||||
fullStackWithoutFrames,
|
||||
'full stack without frames should equal'
|
||||
).to.deep.equal(expected)
|
||||
}
|
Reference in New Issue
Block a user