diff --git a/test/versioned/q/package.json b/test/versioned/q/package.json index a6fe56e601..1fcbaf4c69 100644 --- a/test/versioned/q/package.json +++ b/test/versioned/q/package.json @@ -12,7 +12,7 @@ "q": ">=1.3.0 <2" }, "files": [ - "q.tap.js" + "q.test.js" ] } ] diff --git a/test/versioned/q/q.tap.js b/test/versioned/q/q.tap.js deleted file mode 100644 index b6df1c8379..0000000000 --- a/test/versioned/q/q.tap.js +++ /dev/null @@ -1,130 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const test = tap.test -const helper = require('../../lib/agent_helper') - -function QContext(t, agent) { - this.agent = agent - this.test = t -} - -QContext.prototype.assertTransaction = function assertTransaction(transaction) { - this.test.equal(this.agent.getTransaction(), transaction) - this.test.equal(this.agent.getTransaction().trace.root.children.length, 0) -} - -test('q.ninvoke', function testQNInvoke(t) { - const agent = setupAgent(t) - const q = require('q') - const qContext = new QContext(t, agent) - - const firstTest = q.defer() - const secondTest = q.defer() - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - q.ninvoke(function () { - qContext.assertTransaction(transaction) - firstTest.resolve() - }) - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - q.ninvoke(function () { - qContext.assertTransaction(transaction) - secondTest.resolve() - }) - }) - - q.all([firstTest, secondTest]).then(function done() { - t.end() - }) -}) - -test('q.then', function testQNInvoke(t) { - const agent = setupAgent(t) - const q = require('q') - const qContext = new QContext(t, agent) - - const firstTest = q.defer() - const secondTest = q.defer() - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - q(true).then(function () { - qContext.assertTransaction(transaction) - firstTest.resolve() - }) - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - q(true).then(function () { - qContext.assertTransaction(transaction) - secondTest.resolve() - }) - }) - - q.all([firstTest, secondTest]).then(function done() { - t.end() - }) -}) - -test('q.then rejections', function testQNInvoke(t) { - helper.temporarilyOverrideTapUncaughtBehavior(tap, t) - - t.plan(4) - - const agent = setupAgent(t) - const q = require('q') - const qContext = new QContext(t, agent) - - const firstTest = q.defer() - const secondTest = q.defer() - - helper.temporarilyRemoveListeners(t, process, 'unhandledRejection') - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - const thrownError = new Error('First unhandled error') - process.on('unhandledRejection', function rejectionHandler(error) { - if (error === thrownError) { - qContext.assertTransaction(transaction) - firstTest.resolve() - } - }) - - q(true).then(function () { - throw thrownError - }) - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - const thrownError = new Error('Second unhandled error') - process.on('unhandledRejection', function rejectionHandler(error) { - if (error === thrownError) { - qContext.assertTransaction(transaction) - secondTest.resolve() - } - }) - - q(true).then(function () { - throw thrownError - }) - }) - - q.all([firstTest.promise, secondTest.promise]).then(function done() { - t.end() - }) -}) - -function setupAgent(t) { - const agent = helper.instrumentMockedAgent() - t.teardown(function tearDown() { - helper.unloadAgent(agent) - }) - - return agent -} diff --git a/test/versioned/q/q.test.js b/test/versioned/q/q.test.js new file mode 100644 index 0000000000..daa7eaa416 --- /dev/null +++ b/test/versioned/q/q.test.js @@ -0,0 +1,113 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') +const tspl = require('@matteo.collina/tspl') + +const { removeModules } = require('../../lib/cache-buster') +const tempRemoveListeners = require('../../lib/temp-remove-listeners') +const helper = require('../../lib/agent_helper') + +function assertTransaction(agent, tx, expect = assert) { + expect.equal(agent.getTransaction(), tx) + expect.equal(agent.getTransaction().trace.root.children.length, 0) +} + +test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + ctx.nr.q = require('q') +}) + +test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['q']) +}) + +test('q.invoke', (t, end) => { + const { agent, q } = t.nr + const firstTest = q.defer() + const secondTest = q.defer() + + helper.runInTransaction(agent, (tx) => { + q.ninvoke(() => { + assertTransaction(agent, tx) + firstTest.resolve() + }) + }) + + helper.runInTransaction(agent, (tx) => { + q.ninvoke(() => { + assertTransaction(agent, tx) + secondTest.resolve() + }) + }) + + q.all([firstTest, secondTest]).then(() => end()) +}) + +test('q.then', (t, end) => { + const { agent, q } = t.nr + const firstTest = q.defer() + const secondTest = q.defer() + + helper.runInTransaction(agent, (tx) => { + q(true).then(function () { + assertTransaction(agent, tx) + firstTest.resolve() + }) + }) + + helper.runInTransaction(agent, (tx) => { + q(true).then(function () { + assertTransaction(agent, tx) + secondTest.resolve() + }) + }) + + q.all([firstTest, secondTest]).then(() => end()) +}) + +test('q.then rejections', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, q } = t.nr + + tempRemoveListeners({ t, emitter: process, event: 'unhandledRejection' }) + + const firstTest = q.defer() + const secondTest = q.defer() + + helper.runInTransaction(agent, (tx) => { + const thrownError = new Error('First unhandled error') + process.on('unhandledRejection', (error) => { + if (error === thrownError) { + assertTransaction(agent, tx, plan) + firstTest.resolve() + } + }) + q(true).then(() => { + throw thrownError + }) + }) + + helper.runInTransaction(agent, (tx) => { + const thrownError = new Error('Second unhandled error') + process.on('unhandledRejection', (error) => { + if (error === thrownError) { + assertTransaction(agent, tx, plan) + secondTest.resolve() + } + }) + q(true).then(() => { + throw thrownError + }) + }) + + q.all([firstTest.promise, secondTest.promise]) + await plan.completed +}) diff --git a/test/versioned/superagent/async-await.tap.js b/test/versioned/superagent/async-await.tap.js deleted file mode 100644 index a7811df08e..0000000000 --- a/test/versioned/superagent/async-await.tap.js +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const testServer = require('./test-server') -const { removeModules } = require('../../lib/cache-buster') -const EXTERNAL_NAME = /External\/127.0.0.1:\d+\// - -tap.test('SuperAgent instrumentation with async/await', (t) => { - t.beforeEach(async (t) => { - const { address, server, stopServer } = await testServer() - t.context.address = address - t.context.server = server - t.context.stopServer = stopServer - - t.context.agent = helper.instrumentMockedAgent() - t.context.request = require('superagent') - }) - t.afterEach(async (t) => { - helper.unloadAgent(t.context.agent) - removeModules(['superagent']) - - await t.context.stopServer() - }) - - t.test('should maintain transaction context with promises', (t) => { - const { address, agent } = t.context - helper.runInTransaction(agent, async function (tx) { - t.ok(tx) - - const { request } = t.context - await request.get(address) - - const mainSegment = tx.trace.root.children[0] - t.ok(mainSegment) - t.match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') - t.equal( - mainSegment.children.filter((c) => c.name === 'Callback: ').length, - 1, - 'CB created by superagent is present' - ) - - t.end() - }) - }) - - t.test('should not create segment if not in a transaction', async (t) => { - const { address, agent, request } = t.context - await request.get(address) - t.notOk(agent.getTransaction(), 'should not have a transaction') - t.end() - }) - - t.end() -}) diff --git a/test/versioned/superagent/async-await.test.js b/test/versioned/superagent/async-await.test.js new file mode 100644 index 0000000000..55f54e7a72 --- /dev/null +++ b/test/versioned/superagent/async-await.test.js @@ -0,0 +1,61 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const match = require('../../lib/custom-assertions/match') +const helper = require('../../lib/agent_helper') +const testServer = require('./test-server') + +const EXTERNAL_NAME = /External\/127.0.0.1:\d+\// + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const { address, server, stopServer } = await testServer() + ctx.nr.address = address + ctx.nr.server = server + ctx.nr.stopServer = stopServer + + ctx.nr.request = require('superagent') +}) + +test.afterEach(async (ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['superagent']) + await ctx.nr.stopServer() +}) + +test('should maintain transaction context with promises', (t, end) => { + const { address, agent } = t.nr + helper.runInTransaction(agent, async function (tx) { + assert.ok(tx) + + const { request } = t.nr + await request.get(address) + + const mainSegment = tx.trace.root.children[0] + assert.ok(mainSegment) + match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + assert.equal( + mainSegment.children.filter((c) => c.name === 'Callback: ').length, + 1, + 'CB created by superagent is present' + ) + + end() + }) +}) + +test('should not create segment if not in a transaction', async (t) => { + const { address, agent, request } = t.nr + await request.get(address) + assert.equal(agent.getTransaction(), undefined, 'should not have a transaction') +}) diff --git a/test/versioned/superagent/package.json b/test/versioned/superagent/package.json index 7645b61d0e..3bf864c102 100644 --- a/test/versioned/superagent/package.json +++ b/test/versioned/superagent/package.json @@ -14,8 +14,8 @@ } }, "files": [ - "async-await.tap.js", - "superagent.tap.js" + "async-await.test.js", + "superagent.test.js" ] }] } diff --git a/test/versioned/superagent/superagent.tap.js b/test/versioned/superagent/superagent.tap.js deleted file mode 100644 index d46df1abde..0000000000 --- a/test/versioned/superagent/superagent.tap.js +++ /dev/null @@ -1,89 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const tap = require('tap') -const helper = require('../../lib/agent_helper') -const testServer = require('./test-server') -const { removeModules } = require('../../lib/cache-buster') -const EXTERNAL_NAME = /External\/127.0.0.1:\d+\// - -tap.test('SuperAgent instrumentation', (t) => { - t.beforeEach(async (t) => { - const { address, server, stopServer } = await testServer() - t.context.address = address - t.context.server = server - t.context.stopServer = stopServer - - t.context.agent = helper.instrumentMockedAgent() - t.context.request = require('superagent') - }) - t.afterEach(async (t) => { - helper.unloadAgent(t.context.agent) - removeModules(['superagent']) - - await t.context.stopServer() - }) - - t.test('should maintain transaction context with callbacks', (t) => { - const { address, agent, request } = t.context - - helper.runInTransaction(agent, (tx) => { - request.get(address, function testCallback() { - t.ok(tx) - - const mainSegment = tx.trace.root.children[0] - t.ok(mainSegment) - t.match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') - t.equal( - mainSegment.children.filter((c) => c.name === 'Callback: testCallback').length, - 1, - 'has segment matching callback' - ) - - t.end() - }) - }) - }) - - t.test('should not create a segment for callback if not in transaction', (t) => { - const { address, agent, request } = t.context - request.get(address, function testCallback() { - t.notOk(agent.getTransaction(), 'should not have a transaction') - t.end() - }) - }) - - t.test('should maintain transaction context with promises', (t) => { - const { address, agent, request } = t.context - helper.runInTransaction(agent, (tx) => { - request.get(address).then(function testThen() { - t.ok(tx) - - const mainSegment = tx.trace.root.children[0] - t.ok(mainSegment) - t.match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') - t.equal( - mainSegment.children.filter((c) => c.name === 'Callback: ').length, - 1, - 'has segment matching callback' - ) - - t.end() - }) - }) - }) - - t.test('should not create segment for a promise if not in a transaction', (t) => { - const { address, agent, request } = t.context - request.get(address).then(function testThen() { - t.notOk(agent.getTransaction(), 'should not have a transaction') - t.end() - }) - }) - - t.end() -}) diff --git a/test/versioned/superagent/superagent.test.js b/test/versioned/superagent/superagent.test.js new file mode 100644 index 0000000000..481135cd9a --- /dev/null +++ b/test/versioned/superagent/superagent.test.js @@ -0,0 +1,91 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const { removeModules } = require('../../lib/cache-buster') +const match = require('../../lib/custom-assertions/match') +const helper = require('../../lib/agent_helper') +const testServer = require('./test-server') + +const EXTERNAL_NAME = /External\/127.0.0.1:\d+\// + +test.beforeEach(async (ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent() + + const { address, server, stopServer } = await testServer() + ctx.nr.address = address + ctx.nr.server = server + ctx.nr.stopServer = stopServer + + ctx.nr.request = require('superagent') +}) + +test.afterEach(async (ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['superagent']) + await ctx.nr.stopServer() +}) + +test('should maintain transaction context with callbacks', (t, end) => { + const { address, agent, request } = t.nr + + helper.runInTransaction(agent, (tx) => { + request.get(address, function testCallback() { + assert.ok(tx) + + const mainSegment = tx.trace.root.children[0] + assert.ok(mainSegment) + match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + assert.equal( + mainSegment.children.filter((c) => c.name === 'Callback: testCallback').length, + 1, + 'has segment matching callback' + ) + + end() + }) + }) +}) + +test('should not create a segment for callback if not in transaction', (t, end) => { + const { address, agent, request } = t.nr + request.get(address, function testCallback() { + assert.equal(agent.getTransaction(), undefined, 'should not have a transaction') + end() + }) +}) + +test('should maintain transaction context with promises', (t, end) => { + const { address, agent, request } = t.nr + helper.runInTransaction(agent, (tx) => { + request.get(address).then(function testThen() { + assert.ok(tx) + + const mainSegment = tx.trace.root.children[0] + assert.ok(mainSegment) + match(mainSegment.name, EXTERNAL_NAME, 'has segment matching request') + assert.equal( + mainSegment.children.filter((c) => c.name === 'Callback: ').length, + 1, + 'has segment matching callback' + ) + + end() + }) + }) +}) + +test('should not create segment for a promise if not in a transaction', (t, end) => { + const { address, agent, request } = t.nr + request.get(address).then(function testThen() { + assert.equal(agent.getTransaction(), undefined, 'should not have a transaction') + end() + }) +}) diff --git a/test/versioned/when/legacy-promise-segments.js b/test/versioned/when/legacy-promise-segments.js deleted file mode 100644 index 3edc160bc6..0000000000 --- a/test/versioned/when/legacy-promise-segments.js +++ /dev/null @@ -1,359 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -require('../../lib/metrics_helper') - -module.exports = runTests - -function runTests(t, agent, Promise) { - segmentsEnabledTests(t, agent, Promise, doSomeWork) - segmentsDisabledTests(t, agent, Promise, doSomeWork) - - // simulates a function that returns a promise and has a segment created for itself - function doSomeWork(segmentName, shouldReject) { - const tracer = agent.tracer - const segment = tracer.createSegment(segmentName) - return tracer.bindFunction(actualWork, segment)() - function actualWork() { - segment.touch() - return new Promise(function startSomeWork(resolve, reject) { - if (shouldReject) { - process.nextTick(function () { - reject('some reason') - }) - } else { - process.nextTick(function () { - resolve(123) - }) - } - }) - } - } -} - -function segmentsEnabledTests(t, agent, Promise, doSomeWork) { - const tracer = agent.tracer - - t.test('segments: child segment is created inside then handler', function (t) { - agent.config.feature_flag.promise_segments = true - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, [ - 'doSomeWork', - ['Promise startSomeWork', ['Promise#then ', ['someChildSegment']]] - ]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doSomeWork').then(function () { - const childSegment = tracer.createSegment('someChildSegment') - // touch the segment, so that it is not truncated - childSegment.touch() - tracer.bindFunction(function () {}, childSegment) - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: then handler that returns a new promise', function (t) { - // This test is prone to issues with implementation details of each library. - // To avoid that, we're manually constructing segments instead of - // piggy-backing on promise segments. - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - t.assertSegments(tx.trace.root, ['doWork1', ['doWork2', ['secondThen']]]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2') - }) - .then(function secondThen() { - const s = tracer.createSegment('secondThen') - s.start() - s.end() - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: then handler that returns a value', function (t) { - agent.config.feature_flag.promise_segments = true - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, [ - 'doWork1', - ['Promise startSomeWork', ['Promise#then firstThen', ['Promise#then secondThen']]] - ]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return 'some value' - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: catch handler with error from original promise', function (t) { - agent.config.feature_flag.promise_segments = true - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, [ - 'doWork1', - ['Promise startSomeWork', ['Promise#catch catchHandler']] - ]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1', true) - .then(function firstThen() { - return 'some value' - }) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: catch handler with error from subsequent promise', function (t) { - // This test is prone to issues with implementation details of each library. - // To avoid that, we're manually constructing segments instead of - // piggy-backing on promise segments. - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1', ['doWork2', ['catchHandler']]]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2', true) - }) - .then(function secondThen() { - const s = tracer.createSegment('secondThen') - s.start() - s.end() - }) - .catch(function catchHandler() { - const s = tracer.createSegment('catchHandler') - s.start() - s.end() - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('segments: when promise is created beforehand', function (t) { - agent.config.feature_flag.promise_segments = true - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 2) - - t.assertSegments( - tx.trace.root, - ['Promise startSomeWork', ['Promise#then myThen'], 'doSomeWork'], - true - ) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - let resolve - const p = new Promise(function startSomeWork(r) { - resolve = r - }) - - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) - - p.then(function myThen() { - segment.touch() - process.nextTick(transaction.end.bind(transaction)) - }) - - // Simulate call that resolves the promise, but its segment is created - // after the promise is created - resolve() - }) - }) -} - -function segmentsDisabledTests(t, agent, Promise, doSomeWork) { - const tracer = agent.tracer - - t.test('no segments: child segment is created inside then handler', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doSomeWork', ['someChildSegment']]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doSomeWork').then(function () { - const childSegment = tracer.createSegment('someChildSegment') - // touch the segment, so that it is not truncated - childSegment.touch() - tracer.bindFunction(function () {}, childSegment) - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: then handler that returns a new promise', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return new Promise(function secondChain(res) { - res() - }) - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: then handler that returns a value', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return 'some value' - }) - .then(function secondThen() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: catch handler with error from original promise', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1']) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1', true) - .then(function firstThen() { - return 'some value' - }) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: catch handler with error from subsequent promise', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doWork1', ['doWork2']]) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - doSomeWork('doWork1') - .then(function firstThen() { - return doSomeWork('doWork2', true) - }) - .then(function secondThen() {}) - .catch(function catchHandler() { - process.nextTick(transaction.end.bind(transaction)) - }) - }) - }) - - t.test('no segments: when promise is created beforehand', function (t) { - agent.config.feature_flag.promise_segments = false - - agent.once('transactionFinished', function (tx) { - t.equal(tx.trace.root.children.length, 1) - - t.assertSegments(tx.trace.root, ['doSomeWork'], true) - - t.end() - }) - - helper.runInTransaction(agent, function transactionWrapper(transaction) { - let resolve - const p = new Promise(function startSomeWork(r) { - resolve = r - }) - - const segment = tracer.createSegment('doSomeWork') - resolve = tracer.bindFunction(resolve, segment) - - p.then(function myThen() { - segment.touch() - process.nextTick(transaction.end.bind(transaction)) - }) - - // Simulate call that resolves the promise, but its segment is created - // after the promise is created. - resolve() - }) - }) -} diff --git a/test/versioned/when/package.json b/test/versioned/when/package.json index 5fd9ab2559..94160da9e6 100644 --- a/test/versioned/when/package.json +++ b/test/versioned/when/package.json @@ -12,8 +12,9 @@ "when": ">=3.7.0" }, "files": [ + "segments.test.js", "when.test.js", - "when.tap.js" + "when2.test.js" ] } ] diff --git a/test/versioned/when/segments.test.js b/test/versioned/when/segments.test.js new file mode 100644 index 0000000000..ae8d18fde8 --- /dev/null +++ b/test/versioned/when/segments.test.js @@ -0,0 +1,443 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const tspl = require('@matteo.collina/tspl') + +const { removeModules } = require('../../lib/cache-buster') +const assertSegments = require('../../lib/custom-assertions/assert-segments') +const helper = require('../../lib/agent_helper') + +// simulates a function that returns a promise and has a segment created for itself +function doSomeWork({ tracer, Promise = global.Promise, segmentName, shouldReject } = {}) { + const segment = tracer.createSegment(segmentName) + return tracer.bindFunction(actualWork, segment)() + + function actualWork() { + segment.touch() + + return new Promise(function startSomeWork(resolve, reject) { + if (shouldReject) { + process.nextTick(function () { + reject('some reason') + }) + } else { + process.nextTick(function () { + resolve(123) + }) + } + }) + } +} + +test('segments enabled', async (t) => { + test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + feature_flag: { promise_segments: true } + }) + ctx.nr.tracer = ctx.nr.agent.tracer + ctx.nr.when = require('when') + }) + + test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['when']) + }) + + await t.test('child segment is created inside then handler', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', (tx) => { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments( + tx.trace.root, + [ + 'doSomeWork', + ['Promise startSomeWork', ['Promise#then ', ['someChildSegment']]] + ], + {}, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, Promise: when.Promise, segmentName: 'doSomeWork' }).then(function () { + const childSegment = tracer.createSegment('someChildSegment') + // touch the segment, so that it is not truncated + childSegment.touch() + tracer.bindFunction(function () {}, childSegment) + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('then handler that returns a new promise', async (t) => { + const plan = tspl(t, { plan: 8 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + assertSegments( + tx.trace.root, + [ + 'doWork1', + [ + 'Promise startSomeWork', + [ + 'Promise#then firstThen', + [ + 'doWork2', + ['Promise startSomeWork', ['Promise#then ', 'Promise#then secondThen']] + ] + ] + ] + ], + { exact: false }, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return doSomeWork({ tracer, segmentName: 'doWork2', Promise: when.Promise }) + }) + .then(function secondThen() { + const s = tracer.createSegment('secondThen') + s.start() + s.end() + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('then handler that returns a value', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments( + tx.trace.root, + [ + 'doWork1', + ['Promise startSomeWork', ['Promise#then firstThen', ['Promise#then secondThen']]] + ], + {}, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return 'some value' + }) + .then(function secondThen() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('catch handler with error from original promise', async (t) => { + const plan = tspl(t, { plan: 8 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments( + tx.trace.root, + ['doWork1', ['Promise startSomeWork', ['Promise#catch catchHandler']]], + {}, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', shouldReject: true, Promise: when.Promise }) + .then(function firstThen() { + return 'some value' + }) + .catch(function catchHandler() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('catch handler with error from subsequent promise', async (t) => { + const plan = tspl(t, { plan: 7 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments( + tx.trace.root, + [ + 'doWork1', + [ + 'Promise startSomeWork', + [ + 'Promise#then firstThen', + ['doWork2', ['Promise startSomeWork', ['Promise#catch catchHandler']]] + ] + ] + ], + { exact: false }, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return doSomeWork({ + tracer, + segmentName: 'doWork2', + shouldReject: true, + Promise: when.Promise + }) + }) + .then(function secondThen() { + const s = tracer.createSegment('secondThen') + s.start() + s.end() + }) + .catch(function catchHandler() { + const s = tracer.createSegment('catchHandler') + s.start() + s.end() + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('when promise is created beforehand', async (t) => { + const plan = tspl(t, { plan: 8 }) + const { agent, tracer, when } = t.nr + const { Promise } = when + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 2) + + assertSegments( + tx.trace.root, + ['Promise startSomeWork', ['Promise#then myThen'], 'doSomeWork'], + { exact: true }, + { assert: plan } + ) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + let resolve + const p = new Promise(function startSomeWork(r) { + resolve = r + }) + + const segment = tracer.createSegment('doSomeWork') + resolve = tracer.bindFunction(resolve, segment) + + p.then(function myThen() { + segment.touch() + process.nextTick(transaction.end.bind(transaction)) + }) + + // Simulate call that resolves the promise, but its segment is created + // after the promise is created + resolve() + }) + + await plan.completed + }) +}) + +test('segments disabled', async (t) => { + test.beforeEach((ctx) => { + ctx.nr = {} + ctx.nr.agent = helper.instrumentMockedAgent({ + feature_flag: { promise_segments: false } + }) + ctx.nr.tracer = ctx.nr.agent.tracer + ctx.nr.when = require('when') + }) + + test.afterEach((ctx) => { + helper.unloadAgent(ctx.nr.agent) + removeModules(['when']) + }) + + await t.test('child segment is created inside then handler', async (t) => { + const plan = tspl(t, { plan: 6 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doSomeWork', ['someChildSegment']], {}, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doSomeWork', Promise: when.Promise }).then(function () { + const childSegment = tracer.createSegment('someChildSegment') + // touch the segment, so that it is not truncated + childSegment.touch() + tracer.bindFunction(function () {}, childSegment) + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('then handler that returns a new promise', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return new Promise(function secondChain(res) { + res() + }) + }) + .then(function secondThen() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('then handler that returns a value', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return 'some value' + }) + .then(function secondThen() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('catch handler with error from original promise', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doWork1'], {}, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', shouldReject: true, Promise: when.Promise }) + .then(function firstThen() { + return 'some value' + }) + .catch(function catchHandler() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('catch handler with error from subsequent promise', async (t) => { + const plan = tspl(t, { plan: 6 }) + const { agent, tracer, when } = t.nr + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doWork1', ['doWork2']], {}, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + doSomeWork({ tracer, segmentName: 'doWork1', Promise: when.Promise }) + .then(function firstThen() { + return doSomeWork({ + tracer, + segmentName: 'doWork2', + shouldReject: true, + Promise: when.Promise + }) + }) + .then(function secondThen() {}) + .catch(function catchHandler() { + process.nextTick(transaction.end.bind(transaction)) + }) + }) + + await plan.completed + }) + + await t.test('when promise is created beforehand', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, tracer, when } = t.nr + const { Promise } = when + + agent.once('transactionFinished', function (tx) { + plan.equal(tx.trace.root.children.length, 1) + + assertSegments(tx.trace.root, ['doSomeWork'], { exact: true }, { assert: plan }) + }) + + helper.runInTransaction(agent, function transactionWrapper(transaction) { + let resolve + const p = new Promise(function startSomeWork(r) { + resolve = r + }) + + const segment = tracer.createSegment('doSomeWork') + resolve = tracer.bindFunction(resolve, segment) + + p.then(function myThen() { + segment.touch() + process.nextTick(transaction.end.bind(transaction)) + }) + + // Simulate call that resolves the promise, but its segment is created + // after the promise is created. + resolve() + }) + + await plan.completed + }) +}) diff --git a/test/versioned/when/when.tap.js b/test/versioned/when/when.tap.js deleted file mode 100644 index 7c55645e5e..0000000000 --- a/test/versioned/when/when.tap.js +++ /dev/null @@ -1,1008 +0,0 @@ -/* - * Copyright 2020 New Relic Corporation. All rights reserved. - * SPDX-License-Identifier: Apache-2.0 - */ - -'use strict' - -const helper = require('../../lib/agent_helper') -const testPromiseSegments = require('./legacy-promise-segments') -const { runMultiple } = require('../../lib/promises/helpers') - -// grab process emit before tap / async-hooks-domain can mess with it -const originalEmit = process.emit - -const tap = require('tap') -const test = tap.test - -test('segments', function (t) { - const agent = setupAgent(t) - const when = require('when') - testPromiseSegments(t, agent, when.Promise) - t.autoend() -}) - -test('no transaction', function (t) { - setupAgent(t) - const when = require('when') - - when - .resolve(0) - .then(function step1() { - return 1 - }) - .then(function step2() { - return 2 - }) - .then(function finalHandler(res) { - t.equal(res, 2, 'should be the correct result') - }) - .finally(function finallyHandler() { - t.end() - }) -}) - -test('new Promise() throw', function (t) { - t.plan(2) - - const Promise = require('when').Promise - - try { - new Promise(function () { - throw new Error('test error') - }).then( - function () { - t.fail('Error should have been caught.') - }, - function (err) { - t.ok(err, 'Error should go to the reject handler') - t.equal(err.message, 'test error', 'Error should be as expected') - t.end() - } - ) - } catch (e) { - t.fail('Error should have passed to `reject`.') - } -}) - -test('new Promise() resolve then throw', function (t) { - t.plan(1) - - const Promise = require('when').Promise - - try { - new Promise(function (resolve) { - resolve('foo') - throw new Error('test error') - }).then( - function (res) { - t.equal(res, 'foo', 'promise should be resolved.') - t.end() - }, - function () { - t.fail('Error should have been swallowed by promise.') - } - ) - } catch (e) { - t.fail('Error should have passed to `reject`.') - } -}) - -test('when()', function (t) { - testPromiseLibraryMethod(t, 2, function (when, name) { - return when(name).then(function (x) { - t.equal(x, name, name + 'should pass the value') - - return when(when.reject(new Error(name + 'error message'))) - .then(function () { - t.fail(name + 'should not call resolve handler after throwing') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) - }) -}) - -test('when.defer', function (t) { - testPromiseLibraryMethod(t, 2, function (when, name) { - const defer = when.defer() - process.nextTick(function () { - defer.resolve(name + 'resolve value') - }) - - return defer.promise.then(function (x) { - t.equal(x, name + 'resolve value', name + 'should have correct value') - - const defer2 = when.defer() - defer2.reject(new Error(name + 'error message')) - return defer2.promise - .then(function () { - t.fail(name + 'should not call resolve handler after throwing') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) - }) -}) - -test('when debug API', function (t) { - helper.temporarilyOverrideTapUncaughtBehavior(tap, t) - - t.plan(2) - setupAgent(t) - const when = require('when') - - t.test('should not break onFatalRejection', function (t) { - helper.temporarilyRemoveListeners(t, process, 'unhandledRejection') - - const error = { val: 'test' } - when.Promise.onFatalRejection = function testFatal(e) { - t.equal(e.value, error) - t.end() - } - - const p = when.reject(error) - - p.done() - }) - - t.test('should not break onPotentiallyUnhandledRejectionHandled', function (t) { - t.plan(2) - helper.temporarilyRemoveListeners(t, process, 'unhandledRejection') - - // avoid async hook domain emit so `when` can behave normally. - // seems like *should not* have negative consequences but it may. - // https://github.com/isaacs/async-hook-domain/issues/3 - const asyncHookDomainEmit = process.emit - process.emit = (event, ...args) => { - return originalEmit.call(process, event, ...args) - } - - t.teardown(() => { - process.emit = asyncHookDomainEmit - }) - - const error = { val: 'test' } - when.Promise.onPotentiallyUnhandledRejectionHandled = function testOPURH(e) { - t.equal(e.value, error, 'should have passed error through') - t.end() - } - - when.Promise.onPotentiallyUnhandledRejection = function testOPUR(e) { - t.equal(e.value, error, 'should pass error though') - } - - const p = when.reject(error) - - setTimeout(function () { - p.catch(function () {}) - }, 10) - }) -}) - -test('when.iterate', function (t) { - const COUNT = 10 - testPromiseLibraryMethod(t, COUNT * 6 + 2, function (when, name) { - const agent = helper.getAgent() - const transaction = agent.getTransaction() - - let incrementerCount = 0 - let predicateCount = 0 - let bodyCount = 0 - return when.iterate( - function (x) { - t.equal( - agent.getTransaction(), - transaction, - name + 'iterator has correct transaction state' - ) - - t.equal(incrementerCount++, x++, name + 'should iterate as expected') - return x - }, - function (x) { - t.equal( - agent.getTransaction(), - transaction, - name + 'predicate has correct transaction state' - ) - - t.equal(predicateCount++, x, name + 'should execute predicate each time') - return x >= COUNT // true to stop!? - }, - function (x) { - t.equal(agent.getTransaction(), transaction, name + 'body has correct transaction state') - - t.equal(bodyCount++, x, name + 'should execute body each time') - }, - 0 - ) - }) -}) - -test('when.join', function (t) { - testPromiseLibraryMethod(t, 2, function (when, name) { - return when.join(2, when.resolve(name)).then(function (x) { - t.same(x, [2, name], name + 'should resolve with correct value') - - return when - .join(2, when.reject(new Error(name + 'error message'))) - .then(function () { - t.fail(name + 'should not call resolve handler after throwing') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) - }) -}) - -test('when.lift', function (t) { - testPromiseLibraryMethod(t, 2, function (when, name) { - const func = when.lift(function (x) { - if (x instanceof Error) { - throw x - } - return x - }) - - return func(name + 'return value').then(function (x) { - t.equal(x, name + 'return value', name + 'should pass return value') - - return func(new Error(name + 'error message')) - .then(function () { - t.fail(name + 'should not call resolve handler after throwing') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) - }) -}) - -test('when.promise', function (t) { - testPromiseLibraryMethod(t, 2, function (when, name) { - return when - .promise(function (resolve) { - resolve(name + 'resolve value') - }) - .then(function (x) { - t.equal(x, name + 'resolve value', name + 'should pass the value') - - return when.promise(function (resolve, reject) { - reject(name + 'reject value') - }) - }) - .then( - function () { - t.fail(name + 'should not call resolve handler after rejection') - }, - function (x) { - t.equal(x, name + 'reject value', name + 'should pass the value') - } - ) - }) -}) - -test('when.resolve', function (t) { - testPromiseLibraryMethod(t, 1, function (when, name) { - return when.resolve(name + 'resolve value').then(function (res) { - t.equal(res, name + 'resolve value', name + 'should pass the value') - }) - }) -}) - -test('when.reject', function (t) { - testPromiseLibraryMethod(t, 1, function (when, name) { - return when - .reject(name + 'reject value') - .then(function () { - t.fail(name + 'should not resolve after a rejection') - }) - .catch(function (err) { - t.equal(err, name + 'reject value', name + 'should reject with the err') - }) - }) -}) -;['try', 'attempt'].forEach(function (method) { - test('when.' + method, function (t) { - testPromiseLibraryMethod(t, 3, function (when, name) { - return when[method](function (x) { - t.equal(x, name + '' + method, name + 'should receive values') - return name + 'return value' - }, name + '' + method).then(function (x) { - t.equal(x, name + 'return value', name + 'should pass result through') - - return when[method](function () { - throw new Error(name + 'error message') - }) - .then(function () { - t.fail(name + 'should not call resolve handler after throwing') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) - }) - }) -}) - -test('Promise.resolve', function (t) { - testPromiseClassMethod(t, 1, function resolveTest(Promise, name) { - return Promise.resolve(name + 'resolve value').then(function (res) { - t.equal(res, name + 'resolve value', name + 'should pass the value') - }) - }) -}) - -test('Promise.reject', function (t) { - testPromiseClassMethod(t, 1, function rejectTest(Promise, name) { - return Promise.reject(name + 'reject value') - .then(function () { - t.fail(name + 'should not resolve after a rejection') - }) - .catch(function (err) { - t.equal(err, name + 'reject value', name + 'should reject with the err') - }) - }) -}) - -test('Promise#done', function (t) { - testPromiseClassMethod(t, 3, function (Promise, name) { - return new Promise(function (resolve, reject) { - const ret = Promise.resolve(name + 'resolve value').done(resolve, reject) - t.equal(ret, undefined, name + 'should not return a promise from #done') - }) - .then(function (x) { - t.equal(x, name + 'resolve value', name + 'should resolve correctly') - }) - .then(function () { - return new Promise(function (resolve, reject) { - Promise.reject(new Error(name + 'error message')).done(resolve, reject) - }) - }) - .then(function () { - t.fail(name + 'should not resolve after rejection') - }) - .catch(function (err) { - t.equal(err.message, name + 'error message', name + 'should have correct error') - }) - }) -}) - -test('Promise#then', function (t) { - testPromiseInstanceMethod(t, 3, function thenTest(p, name) { - return p - .then(function (res) { - t.same(res, [1, 2, 3, name], name + 'should have the correct result value') - throw new Error('Promise#then test error') - }) - .then( - function () { - t.fail(name + 'should not go into resolve handler from rejected promise') - }, - function (err) { - t.ok(err, name + 'should pass error into thenned rejection handler') - if (err) { - t.equal(err.message, 'Promise#then test error', name + 'should be correct error') - } - } - ) - }) -}) - -test('Promise#catch', function (t) { - testPromiseInstanceMethod(t, 2, function catchTest(p, name) { - return p - .catch(function () { - t.fail(name + 'should not go into catch from a resolved promise') - }) - .then(function () { - throw new Error('Promise#catch test error') - }) - .catch(function (err) { - t.ok(err, name + 'should pass error into rejection handler') - if (err) { - t.equal(err.message, 'Promise#catch test error', name + 'should be correct error') - } - }) - }) -}) - -test('Promise#otherwise', function (t) { - testPromiseInstanceMethod(t, 2, function otherwiseTest(p, name) { - return p - .otherwise(function () { - t.fail(name + 'should not go into otherwise from a resolved promise') - }) - .then(function () { - throw new Error(name + ' test error') - }) - .otherwise(function (err) { - t.ok(err, name + 'should pass error into rejection handler') - if (err) { - t.equal(err.message, name + ' test error', name + 'should be correct error') - } - }) - }) -}) - -test('Promise#finally', function (t) { - testPromiseInstanceMethod(t, 6, function finallyTest(p, name) { - return p - .finally(function () { - t.equal(arguments.length, 0, name + 'should not receive any parameters') - }) - .then(function (res) { - t.same(res, [1, 2, 3, name], name + 'should pass values beyond finally handler') - throw new Error('Promise#finally test error') - }) - .finally(function () { - t.equal(arguments.length, 0, name + 'should not receive any parameters') - t.pass(name + 'should go into finally handler from rejected promise') - }) - .catch(function (err) { - t.ok(err, name + 'should pass error beyond finally handler') - if (err) { - t.equal(err.message, 'Promise#finally test error', name + 'should be correct error') - } - }) - }) -}) - -test('Promise#ensure', function (t) { - testPromiseInstanceMethod(t, 6, function ensureTest(p, name) { - return p - .ensure(function () { - t.equal(arguments.length, 0, name + 'should not receive any parameters') - }) - .then(function (res) { - t.same(res, [1, 2, 3, name], name + 'should pass values beyond ensure handler') - throw new Error('Promise#ensure test error') - }) - .ensure(function () { - t.equal(arguments.length, 0, name + 'should not receive any parameters') - t.pass(name + 'should go into ensure handler from rejected promise') - }) - .catch(function (err) { - t.ok(err, name + 'should pass error beyond ensure handler') - if (err) { - t.equal(err.message, 'Promise#ensure test error', name + 'should be correct error') - } - }) - }) -}) - -test('Promise#tap', function (t) { - testPromiseInstanceMethod(t, 4, function tapTest(p, name) { - return p - .tap(function (res) { - t.same(res, [1, 2, 3, name], name + 'should pass values into tap handler') - }) - .then(function (res) { - t.same(res, [1, 2, 3, name], name + 'should pass values beyond tap handler') - throw new Error('Promise#tap test error') - }) - .tap(function () { - t.fail(name + 'should not call tap after rejected promises') - }) - .catch(function (err) { - t.ok(err, name + 'should pass error beyond tap handler') - if (err) { - t.equal(err.message, 'Promise#tap test error', name + 'should be correct error') - } - }) - }) -}) - -test('Promise#spread', function (t) { - testPromiseInstanceMethod(t, 1, function spreadTest(p, name) { - return p.spread(function (a, b, c, d) { - t.same([a, b, c, d], [1, 2, 3, name], name + 'parameters should be correct') - }) - }) -}) - -test('Promise#fold', function (t) { - testPromiseInstanceMethod(t, 3, function (p, name) { - return p - .fold( - function (a, b) { - t.equal(a, name, name + 'first parameter should be second promise') - t.same(b, [1, 2, 3, name], name + 'second parameter should be first promise') - - return [a, b] - }, - p.then(function () { - return name - }) - ) - .then(function (x) { - t.same(x, [name, [1, 2, 3, name]], name + 'should have correct parameters') - }) - }) -}) - -test('Promise#yield', function (t) { - testPromiseInstanceMethod(t, 1, function (p, name) { - return p.yield(name + 'yield value').then(function (x) { - t.equal(x, name + 'yield value', name + 'should have correct value') - }) - }) -}) -;['else', 'orElse'].forEach(function (method) { - test('Promise#' + method, function (t) { - testPromiseInstanceMethod(t, 2, function (p, name) { - return p[method](new Error(name + 'skipped else message')) - .then( - function (x) { - t.same(x, [1, 2, 3, name], name + 'should pass value through the else') - }, - function () { - t.fail(name + 'should not have rejected first promise') - } - ) - .then(function () { - throw new Error(name + 'original error') - }) - [method](name + 'elsed value') - .then(function (x) { - t.equal(x, name + 'elsed value', name + 'should resolve with else value') - }) - }) - }) -}) - -test('Promise#delay', function (t) { - testPromiseInstanceMethod(t, 2, function (p, name) { - const start = Date.now() - return p.delay(100).then(function (x) { - const end = Date.now() - t.same(x, [1, 2, 3, name], name + 'should resolve with original promise') - t.ok(end - start >= 100, name + 'should delay at least the specified duration') - }) - }) -}) - -test('Promise#timeout', function (t) { - testPromiseInstanceMethod(t, 3, function (p, name) { - const start = Date.now() - return p - .delay(100) - .timeout(50, new Error(name + 'timeout message')) - .then(function () { - t.fail(name + 'should not have resolved') - }) - .catch(function (err) { - const end = Date.now() - t.equal(err.message, name + 'timeout message', name + 'should have correct message') - t.ok(end - start > 48, name + 'should wait close to correct time') - t.ok(end - start < 75, name + 'should wait close to correct time') - }) - }) -}) - -test('Promise#with', function (t) { - testPromiseInstanceMethod(t, 2, function (p, name) { - const obj = {} - return p.with(obj).then(function (x) { - t.same(x, [1, 2, 3, name], name + 'should resolve with correct value') - t.equal(this, obj, name + 'should have correct context') - }) - }) -}) - -test('all', function (t) { - t.autoend() - let agent - let when - let Promise - let p1 - let p2 - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - - p1 = new Promise(function (resolve) { - resolve(1) - }) - p2 = new Promise(function (resolve) { - resolve(2) - }) - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when.all([p1, p2]).then(function () { - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.all([p1, p2]).then(function () { - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('any', function (t) { - t.autoend() - let agent - let when - let Promise - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when.any([when.resolve(1), when.resolve(2)]).then(function (result) { - t.equal(result, 1) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.any([when.resolve(1), when.resolve(2)]).then(function (result) { - t.equal(result, 1) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('some', function (t) { - t.autoend() - let agent - let when - let Promise - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when.some([when.resolve(1), when.resolve(2), when.resolve(3)], 2).then(function (result) { - t.equal(result.length, 2) - t.equal(result[0], 1) - t.equal(result[1], 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.some([when.resolve(1), when.resolve(2), when.resolve(3)], 2).then(function (result) { - t.equal(result.length, 2) - t.equal(result[0], 1) - t.equal(result[1], 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('map', function (t) { - t.autoend() - let agent - let when - let Promise - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when - .map([1, 2], function (item) { - return when.resolve(item) - }) - .then(function (result) { - t.equal(result.length, 2) - t.equal(result[0], 1) - t.equal(result[1], 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.map([1, 2], function (item) { - return when.resolve(item) - }).then(function (result) { - t.equal(result.length, 2) - t.equal(result[0], 1) - t.equal(result[1], 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('reduce', function (t) { - t.autoend() - let agent - let when - let Promise - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when - .reduce( - [1, 2], - function (total, item) { - return when.resolve(item).then(function (result) { - return total + result - }) - }, - 0 - ) - .then(function (total) { - t.equal(total, 3) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.reduce( - [1, 2], - function (total, item) { - return when.resolve(item).then(function (result) { - return total + result - }) - }, - 0 - ).then(function (total) { - t.equal(total, 3) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('filter', function (t) { - t.autoend() - let agent - let when - let Promise - - t.beforeEach(() => { - agent = helper.instrumentMockedAgent({ feature_flag: { promise_segments: false } }) - when = require('when') - Promise = when.Promise - }) - - t.afterEach(() => { - helper.unloadAgent(agent) - }) - - t.test('on library', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - when - .filter([1, 2, 3, 4], function (value) { - // filter out even numbers - return value % 2 - }) - .then(function (result) { - t.equal(result.length, 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) - - t.test('on Promise', function (t) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - Promise.filter([1, 2, 3, 4], function (value) { - // filter out even numbers - return value % 2 - }).then(function (result) { - t.equal(result.length, 2) - t.equal(agent.getTransaction(), transaction, 'has the right transaction') - t.end() - }) - }) - }) -}) - -test('fn.apply', function (t) { - setupAgent(t) - - require('when') - const fn = require('when/function') - - function noop() {} - - const args = [1, 2, 3] - fn.apply(noop, args).then(function () { - t.end() - }) -}) - -test('node.apply', function (t) { - setupAgent(t) - - require('when') - const nodefn = require('when/node') - - function nodeStyleFunction(arg1, cb) { - process.nextTick(cb) - } - - const args = [1] - nodefn - .apply(nodeStyleFunction, args) - .then(function () { - t.end() - }) - .catch(function (err) { - t.fail(err) - }) -}) - -function setupAgent(t, enableSegments) { - const agent = helper.instrumentMockedAgent({ - feature_flag: { promise_segments: enableSegments } - }) - t.teardown(function tearDown() { - helper.unloadAgent(agent) - }) - return agent -} - -function testPromiseInstanceMethod(t, plan, testFunc) { - const agent = setupAgent(t) - const Promise = require('when').Promise - - _testPromiseMethod(t, plan, agent, function (name) { - const p = new Promise(function (resolve) { - resolve([1, 2, 3, name]) - }) - return testFunc(p, name, agent) - }) -} - -function testPromiseClassMethod(t, plan, testFunc) { - const agent = setupAgent(t) - const when = require('when') - const Promise = when.Promise - - _testPromiseMethod(t, plan, agent, function (name) { - return testFunc(Promise, name) - }) -} - -function testPromiseLibraryMethod(t, plan, testFunc) { - const agent = setupAgent(t) - const when = require('when') - - _testPromiseMethod(t, plan, agent, function (name) { - return testFunc(when, name) - }) -} - -function _testPromiseMethod(t, plan, agent, testFunc) { - const COUNT = 2 - t.plan(plan * 3 + (COUNT + 1) * 3) - - t.doesNotThrow(function outTXPromiseThrowTest() { - const name = '[no tx] ' - let isAsync = false - testFunc(name) - .finally(function () { - t.ok(isAsync, name + 'should have executed asynchronously') - }) - .then( - function () { - t.notOk(agent.getTransaction(), name + 'has no transaction') - testInTransaction() - }, - function (err) { - if (err) { - /* eslint-disable no-console */ - console.log(err) - console.log(err.stack) - /* eslint-enable no-console */ - } - t.notOk(err, name + 'should not result in error') - t.end() - } - ) - isAsync = true - }, '[no tx] should not throw out of a transaction') - - function testInTransaction() { - runMultiple( - COUNT, - function (i, cb) { - helper.runInTransaction(agent, function transactionWrapper(transaction) { - const name = '[tx ' + i + '] ' - t.doesNotThrow(function inTXPromiseThrowTest() { - let isAsync = false - testFunc(name) - .finally(function () { - t.ok(isAsync, name + 'should have executed asynchronously') - }) - .then( - function () { - t.equal(agent.getTransaction(), transaction, name + 'has the right transaction') - }, - function (err) { - if (err) { - /* eslint-disable no-console */ - console.log(err) - console.log(err.stack) - /* eslint-enable no-console */ - } - t.notOk(err, name + 'should not result in error') - } - ) - .finally(cb) - isAsync = true - }, name + 'should not throw in a transaction') - }) - }, - function () { - t.end() - } - ) - } -} diff --git a/test/versioned/when/when.test.js b/test/versioned/when/when.test.js index 2ce0d34d43..bfb5151fd7 100644 --- a/test/versioned/when/when.test.js +++ b/test/versioned/when/when.test.js @@ -4,41 +4,985 @@ */ 'use strict' -const assert = require('node:assert') + const test = require('node:test') +const assert = require('node:assert') +const tspl = require('@matteo.collina/tspl') + +const { removeModules } = require('../../lib/cache-buster') +const tempOverrideUncaught = require('../../lib/temp-override-uncaught') const helper = require('../../lib/agent_helper') -const testTransactionState = require(`../../lib/promises/transaction-state`) -function setupAgent(t) { - const agent = helper.instrumentMockedAgent() - t.after(() => { - helper.unloadAgent(agent) +function setupTest(t, enableSegments) { + t.nr.agent = helper.instrumentMockedAgent({ + feature_flag: { promise_segments: enableSegments } + }) + t.nr.when = require('when') + + return { agent: t.nr.agent, when: t.nr.when } +} + +test.beforeEach((ctx) => { + ctx.nr = {} +}) + +test.afterEach((ctx) => { + if (ctx.nr.agent) { + helper.unloadAgent(ctx.nr.agent) + } + removeModules(['when']) +}) + +test('no transaction', (t, end) => { + const { when } = setupTest(t) + + when + .resolve(0) + .then(function step1() { + return 1 + }) + .then(function step2() { + return 2 + }) + .then(function finalHandler(res) { + assert.equal(res, 2, 'should be the correct result') + }) + .finally(function finallyHandler() { + end() + }) +}) + +test('new Promise() throw', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { when } = setupTest(t) + const { Promise } = when + + try { + new Promise(function () { + throw new Error('test error') + }).then( + function resolved() { + plan.fail('Error should have been caught.') + }, + function rejected(err) { + plan.ok(err, 'Error should go to the reject handler') + plan.equal(err.message, 'test error', 'Error should be as expected') + } + ) + /* eslint-disable-next-line node/no-unsupported-features/es-syntax */ + } catch { + plan.fail('Error should have passed to `reject`.') + } + + await plan.completed +}) + +test('new Promise() resolve then throw', async (t) => { + const plan = tspl(t, { plan: 1 }) + const { when } = setupTest(t) + const { Promise } = when + + try { + new Promise(function (resolve) { + resolve('foo') + throw new Error('test error') + }).then( + function resolved(res) { + plan.equal(res, 'foo', 'promise should be resolved.') + }, + function rejected() { + plan.fail('Error should have been swallowed by promise.') + } + ) + /* eslint-disable-next-line node/no-unsupported-features/es-syntax */ + } catch { + plan.fail('Error should have passed to `reject`.') + } + + await plan.completed +}) + +test('when()', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when(name).then(function resolved(value) { + plan.equal(value, name, `${name} should pass the value`) + + return when(when.reject(Error(`${name} error message`))) + .then(() => plan.fail(`${name} should not call resolve handler after throwing`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.defer', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => { + const defer = when.defer() + process.nextTick(() => defer.resolve(`${name} resolve value`)) + + return defer.promise.then((value) => { + plan.equal(value, `${name} resolve value`, `${name} should have correct value`) + + const defer2 = when.defer() + defer2.reject(Error(`${name} error message`)) + return defer2.promise + .then(() => plan.fail(`${name} should not call resolve handler after throwing`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + }) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when debug API', async (t) => { + await t.test('should not break onFatalRejection', async (t) => { + const plan = tspl(t, { plan: 1 }) + const { when } = setupTest(t) + tempOverrideUncaught({ t, handler() {}, type: tempOverrideUncaught.REJECTION }) + + const error = { val: 'test' } + when.Promise.onFatalRejection = (e) => { + plan.equal(e.value, error) + } + + const p = when.reject(error) + p.done() + + await plan.completed + }) + + await t.test('should not break onPotentiallyUnhandledRejectionHandled', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { when } = setupTest(t) + tempOverrideUncaught({ t, handler() {}, type: tempOverrideUncaught.REJECTION }) + + let p = null + const error = { val: 'test' } + when.Promise.onPotentiallyUnhandledRejectionHandled = (e) => + plan.equal(e.value, error, 'should have passed error through') + when.Promise.onPotentiallyUnhandledRejection = (e) => { + plan.equal(e.value, error, 'should pass error through') + // Trigger the `onPotentiallyUnhandledRejectionHandled` callback. + p.catch(() => {}) + } + + when.Promise.reject(error) + p = when.reject(error) + + await plan.completed + }) +}) + +test('when.iterate', async (t) => { + const plan = tspl(t, { plan: 130 }) + const { agent, when } = setupTest(t) + const COUNT = 10 + const testFunc = (name) => { + const tx = agent.getTransaction() + let incrementerCount = 0 + let predicateCount = 0 + let bodyCount = 0 + + return when.iterate(iterator, predicate, handler, 0) + + function iterator(seed) { + plan.equal(agent.getTransaction(), tx, `${name} iterator has correct transaction state`) + plan.equal(incrementerCount++, seed++, `${name} should iterate as expected`) + return seed + } + + function predicate(iteration) { + plan.equal(agent.getTransaction(), tx, `${name} predicate has correct transaction state`) + plan.equal(predicateCount++, iteration, `${name} should execute predicate each time`) + return iteration >= COUNT + } + + function handler(value) { + plan.equal(agent.getTransaction(), tx, `${name} body has correct transaction state`) + plan.equal(bodyCount++, value, `${name} should execute each time`) + } + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.join', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when.join(2, when.resolve(name)).then((value) => { + plan.deepStrictEqual(value, [2, name], `${name} should resolve with correct value`) + return when + .join(2, when.reject(Error(`${name} error message`))) + .then(() => plan.fail(`${name} should not call resolve handler after throwing`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.lift', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => { + const func = when.lift((value) => { + if (value instanceof Error) { + throw value + } + return value + }) + + return func(`${name} return value`).then((value) => { + plan.equal(value, `${name} return value`, `${name} should pass return value`) + return func(Error(`${name} error message`)) + .then(() => plan.fail(`${name} should not call resolve handler after throwing`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + }) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.promise', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when + .promise((resolve) => resolve(`${name} resolve value`)) + .then((value) => { + plan.equal(value, `${name} resolve value`, `${name} should pass the value`) + return when.promise((_, reject) => reject(`${name} reject value`)) + }) + .then( + () => plan.fail(`${name} should not call resolve handler after rejection`), + (error) => plan.equal(error, `${name} reject value`, `${name} should pass the value`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.resolve', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when + .resolve(`${name} resolve value`) + .then((value) => plan.equal(value, `${name} resolve value`, `${name} should pass the value`)) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('when.reject', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when + .reject(`${name} reject value`) + .then(() => plan.fail(`${name} should not resolve after a rejection`)) + .catch((error) => + plan.equal(error, `${name} reject value`, `${name} should reject with the error`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +for (const method of ['try', 'attempt']) { + test(`when.${method}`, async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => { + return when[method](handler, `${name}${method}`).then((value) => { + plan.equal(value, `${name} return value`, `${name} should pass result through`) + return when[method](() => { + throw Error(`${name} error message`) + }) + .then(() => plan.fail(`${name} should not call resolve handler after throwing`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + }) + + function handler(value) { + plan.equal(value, `${name}${method}`, `${name} should receive values`) + return `${name} return value` + } + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed + }) +} + +test('Promise.resolve', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when.Promise.resolve(`${name} resolve value`).then((value) => + plan.equal(value, `${name} resolve value`, `${name} should pass the value`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise.reject', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const testFunc = (name) => + when.Promise.reject(`${name} reject value`) + .then(() => plan.fail(`${name} should not resolve after a rejection`)) + .catch((error) => + plan.equal(error, `${name} reject value`, `${name} should reject with the error`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#done', async (t) => { + const plan = tspl(t, { plan: 4 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve, reject) => { + const ret = Promise.resolve(`${name} resolve value`).done(resolve, reject) + plan.equal(ret, undefined, `${name} should not return a promise from #done`) + }) + .then((value) => + plan.equal(value, `${name} resolve value`, `${name} should resolve correctly`) + ) + .then( + () => + new Promise((resolve, reject) => + Promise.reject(Error(`${name} error message`)).done(resolve, reject) + ) + ) + .then(() => plan.fail(`${name} should not resolve after rejection`)) + .catch((error) => + plan.equal(error.message, `${name} error message`, `${name} should have correct error`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#then', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .then((value) => { + plan.deepStrictEqual(value, [1, 2, 3, name], `${name} should have the correct result value`) + throw Error('Promise#then test error') + }) + .then( + () => plan.fail(`${name} should not go into resolve handler from rejected promise`), + (error) => { + plan.ok(error, `${name} should pass error into then-ed rejection handler`) + plan.equal(error.message, 'Promise#then test error', `${name} should be correct error`) + } + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#catch', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .catch(() => plan.fail(`${name} should not go into catch from a resolved promise`)) + .then(() => { + throw Error('Promise#catch test error') + }) + .catch((error) => { + plan.ok(error, `${name} should pass error into then-ed rejection handler`) + plan.equal(error.message, 'Promise#catch test error', `${name} should be correct error`) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#otherwise', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .otherwise(() => plan.fail(`${name} should not go into otherwise from a resolved promise`)) + .then(() => { + throw Error('Promise#otherwise test error') + }) + .otherwise((error) => { + plan.ok(error, `${name} should pass error into then-ed rejection handler`) + plan.equal(error.message, 'Promise#otherwise test error', `${name} should be correct error`) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#finally', async (t) => { + const plan = tspl(t, { plan: 18 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .finally((...args) => { + plan.equal(args.length, 0, `${name} should not receive any parameters`) + }) + .then((value) => { + plan.deepStrictEqual( + value, + [1, 2, 3, name], + `${name} should pass values beyond finally handler` + ) + throw Error('Promise#finally test error') + }) + .finally((...args) => { + plan.equal(args.length, 0, `${name} should not receive any parameters`) + plan.ok(true, `${name} should go into finally handler from rejected promise`) + }) + .catch((error) => { + plan.ok(error, `${name} should pass error beyond finally handler`) + plan.equal(error.message, 'Promise#finally test error', `${name} should be correct error`) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#ensure', async (t) => { + const plan = tspl(t, { plan: 18 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .ensure((...args) => { + plan.equal(args.length, 0, `${name} should not receive any parameters`) + }) + .then((value) => { + plan.deepStrictEqual( + value, + [1, 2, 3, name], + `${name} should pass values beyond ensure handler` + ) + throw Error('Promise#ensure test error') + }) + .ensure((...args) => { + plan.equal(args.length, 0, `${name} should not receive any parameters`) + plan.ok(true, `${name} should go into ensure handler from rejected promise`) + }) + .catch((error) => { + plan.ok(error, `${name} should pass error beyond ensure handler`) + plan.equal(error.message, 'Promise#ensure test error', `${name} should be correct error`) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#tap', async (t) => { + const plan = tspl(t, { plan: 14 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .tap((value) => { + plan.deepStrictEqual(value, [1, 2, 3, name], `${name} should pass values into tap handler`) + }) + .then((value) => { + plan.deepStrictEqual( + value, + [1, 2, 3, name], + `${name} should pass values beyond tap handler` + ) + throw Error('Promise#tap test error') + }) + .tap(() => plan.fail(`${name} should not call tap after rejected promises`)) + .catch((error) => { + plan.ok(error, `${name} should pass error beyond tap handler`) + plan.equal(error.message, 'Promise#tap test error', `${name} should be correct error`) + }) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#spread', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])).spread((a, b, c, d) => + plan.deepStrictEqual([a, b, c, d], [1, 2, 3, name], `${name} parameters should be correct`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#fold', async (t) => { + const plan = tspl(t, { plan: 12 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => { + const p = new Promise((resolve) => resolve([1, 2, 3, name])) + return p + .fold( + (a, b) => { + plan.equal(a, name, `${name} first parameter should be second promise`) + plan.deepStrictEqual( + b, + [1, 2, 3, name], + `${name} second parameter should be first promise` + ) + return [a, b] + }, + p.then(() => name) + ) + .then((value) => + plan.deepStrictEqual( + value, + [name, [1, 2, 3, name]], + `${name} should have correct parameters` + ) + ) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('Promise#yield', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => + new Promise((resolve) => resolve([1, 2, 3, name])) + .yield(`${name} yield value`) + .then((value) => + plan.equal(value, `${name} yield value`, `${name} should have correct value`) + ) + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +for (const method of ['else', 'orElse']) { + test(`Promise#${method}`, async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => { + const p = new Promise((resolve) => resolve([1, 2, 3, name])) + return p[method](Error(`${name} skipped else message`)) + .then( + (value) => + plan.deepStrictEqual( + value, + [1, 2, 3, name], + `${name} should pass value through the else` + ), + () => plan.fail(`${name} should not have rejected first promise`) + ) + .then(() => { + throw Error(`${name} original error`) + }) + [method](`${name} elsed value`) + .then((value) => + plan.deepStrictEqual( + value, + `${name} elsed value`, + `${name} should resolve with else value` + ) + ) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed }) } -test('Promise constructor retains all properties', function (t) { - let Promise = require('when').Promise - const originalKeys = Object.keys(Promise) +test('Promise#delay', async (t) => { + const plan = tspl(t, { plan: 2 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => { + const start = Date.now() + return new Promise((resolve) => resolve([1, 2, 3, name])).delay(100).then((value) => { + const end = Date.now() + plan.deepStrictEqual(value, [1, 2, 3, name], `${name} should resolve with original promise`) + plan.ok(end - start >= 100, `${name} should delay at least the specified duration`) + }) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) - setupAgent(t) - Promise = require('when').Promise - const wrappedKeys = Object.keys(Promise) +test('Promise#timeout', async (t) => { + const plan = tspl(t, { plan: 12 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => { + const start = Date.now() + return new Promise((resolve) => resolve([1, 2, 3, name])) + .delay(100) + .timeout(50, Error(`${name} timeout message`)) + .then(() => plan.fail(`${name} should not have resolved`)) + .catch((error) => { + const end = Date.now() + plan.equal(error.message, `${name} timeout message`, `${name} should have correct message`) + plan.ok(end - start > 48, `${name} should wait close to the correct time`) + plan.ok(end - start < 75, `${name} should wait close to the correct time`) + }) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) - originalKeys.forEach(function (key) { - if (wrappedKeys.indexOf(key) === -1) { - assert.ok(0, 'Property ' + key + ' is not present on wrapped Promise') +test('Promise#with', async (t) => { + const plan = tspl(t, { plan: 10 }) + const { agent, when } = setupTest(t) + const { Promise } = when + const testFunc = (name) => { + const obj = { + [Symbol.toStringTag]: 'test-obj' } + return new Promise((resolve) => resolve([1, 2, 3, name])).with(obj).then(function (value) { + plan.deepStrictEqual(value, [1, 2, 3, name], `${name} should resolve with original promise`) + plan.strictEqual(this === obj, true, `${name} should have correct context`) + }) + } + + await testThrowOutsideTransaction({ plan, agent, testFunc }) + await testInsideTransaction({ plan, agent, testFunc }) + await plan.completed +}) + +test('all', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + const p1 = new Promise((resolve) => resolve(1)) + const p2 = new Promise((resolve) => resolve(2)) + + helper.runInTransaction(agent, (tx) => { + when.all([p1, p2]).then(() => { + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) + + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + const p1 = new Promise((resolve) => resolve(1)) + const p2 = new Promise((resolve) => resolve(2)) + + helper.runInTransaction(agent, (tx) => { + Promise.all([p1, p2]).then(() => { + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) +}) + +test('any', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) + + helper.runInTransaction(agent, (tx) => { + when.any([when.resolve(1), when.resolve(2)]).then((value) => { + assert.equal(value, 1) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) + + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + + helper.runInTransaction(agent, (tx) => { + Promise.any([when.resolve(1), when.resolve(2)]).then((value) => { + assert.equal(value, 1) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) +}) + +test('some', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) + + helper.runInTransaction(agent, (tx) => { + when.some([when.resolve(1), when.resolve(2), when.resolve(3)], 2).then((value) => { + assert.equal(value.length, 2) + assert.equal(value[0], 1) + assert.equal(value[1], 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) + + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + + helper.runInTransaction(agent, (tx) => { + Promise.some([when.resolve(1), when.resolve(2), when.resolve(3)], 2).then((value) => { + assert.equal(value.length, 2) + assert.equal(value[0], 1) + assert.equal(value[1], 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) }) }) -test('transaction state', async function (t) { - const agent = helper.instrumentMockedAgent() - const when = require('when') - const Promise = when.Promise +test('map', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) - t.after(() => { - helper.unloadAgent(agent) + helper.runInTransaction(agent, (tx) => { + when + .map([1, 2], (item) => when.resolve(item)) + .then((value) => { + assert.equal(value.length, 2) + assert.equal(value[0], 1) + assert.equal(value[1], 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) }) - await testTransactionState({ t, agent, Promise, library: when }) + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + + helper.runInTransaction(agent, (tx) => { + Promise.map([1, 2], (item) => when.resolve(item)).then((value) => { + assert.equal(value.length, 2) + assert.equal(value[0], 1) + assert.equal(value[1], 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) }) + +test('reduce', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) + + helper.runInTransaction(agent, (tx) => { + when + .reduce( + [1, 2], + (total, item) => { + return when.resolve(item).then((r) => total + r) + }, + 0 + ) + .then((total) => { + assert.equal(total, 3) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) + + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + + helper.runInTransaction(agent, (tx) => { + Promise.reduce( + [1, 2], + (total, item) => { + return when.resolve(item).then((r) => total + r) + }, + 0 + ).then((total) => { + assert.equal(total, 3) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) +}) + +test('filter', async (t) => { + await t.test('on library', (t, end) => { + const { agent, when } = setupTest(t, false) + + helper.runInTransaction(agent, (tx) => { + when + .filter([1, 2, 3, 4], (v) => v % 2) + .then((value) => { + assert.equal(value.length, 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) + + await t.test('on Promise', (t, end) => { + const { agent, when } = setupTest(t, false) + const { Promise } = when + + helper.runInTransaction(agent, (tx) => { + Promise.filter([1, 2, 3, 4], (v) => v % 2).then((value) => { + assert.equal(value.length, 2) + assert.equal(agent.getTransaction(), tx, 'has the right transaction') + end() + }) + }) + }) +}) + +test('fn.apply', (t, end) => { + setupTest(t) + const fn = require('when/function') + + function noop() {} + + const args = [1, 2, 3] + fn.apply(noop, args).then(end) +}) + +test('node.apply', (t, end) => { + setupTest(t) + const nodefn = require('when/node') + + function nodeStyleFunction(arg1, cb) { + process.nextTick(cb) + } + + const args = [1] + nodefn.apply(nodeStyleFunction, args).then(end).catch(end) +}) + +/** + * Tests a `when` library method outside of an agent transaction. + * + * @param {object} params + * @param {object} plan The assertion library that expects a set number of + * assertions to be completed during the test. + * @param {object} agent A mocked agent instance. + * @param {function} testFunc A function that accepts a "name" parameter and + * returns a promise. The parameter is a string for identifying the test and + * values used within the test. + * @returns {Promise} + */ +async function testThrowOutsideTransaction({ plan, agent, testFunc }) { + plan.doesNotThrow(() => { + const name = '[no tx]' + let isAsync = false + testFunc(name) + .finally(() => { + plan.ok(isAsync, `${name} should have executed asynchronously`) + }) + .then( + function resolved() { + plan.equal(agent.getTransaction(), undefined, `${name} has no transaction`) + }, + function rejected(error) { + assert.ifError(error, `${name} should not result in error`) + } + ) + isAsync = true + }) +} + +/** + * Tests a `when` library method inside of an agent transaction. + * + * @param {object} params + * @param {object} plan The assertion library that expects a set number of + * assertions to be completed during the test. + * @param {object} agent A mocked agent instance. + * @param {function} testFunc A function that accepts a "name" parameter and + * returns a promise. The parameter is a string for identifying the test and + * values used within the test. + * @returns {Promise} + */ +async function testInsideTransaction({ plan, agent, testFunc }) { + helper.runInTransaction(agent, (tx) => { + const name = '[in tx]' + plan.doesNotThrow(() => { + let isAsync = false + testFunc(name) + .finally(() => plan.ok(isAsync, `${name} should have executed asynchronously`)) + .then( + function resolved() { + plan.equal(agent.getTransaction(), tx, `${name} has the right transaction`) + }, + function rejected(error) { + plan.ifError(error, `${name} should not result in error`) + } + ) + isAsync = true + }) + }) +} diff --git a/test/versioned/when/when2.test.js b/test/versioned/when/when2.test.js new file mode 100644 index 0000000000..474aa53949 --- /dev/null +++ b/test/versioned/when/when2.test.js @@ -0,0 +1,52 @@ +/* + * Copyright 2024 New Relic Corporation. All rights reserved. + * SPDX-License-Identifier: Apache-2.0 + */ + +'use strict' + +const test = require('node:test') +const assert = require('node:assert') + +const helper = require('../../lib/agent_helper') +const testTransactionState = require(`../../lib/promises/transaction-state`) + +// We cannot use `test.beforeEach` and `test.afterEach` with this suite because +// of the `testTransactionState` waterfall tests. Those setup nested subtests +// which will each execute the `beforeEach`. Due to the singleton nature of +// the mocked agent, this causes cascading failures that would be too difficult +// to resolve. + +function setupAgent(t) { + const agent = helper.instrumentMockedAgent() + t.after(() => { + helper.unloadAgent(agent) + }) +} + +test('Promise constructor retains all properties', function (t) { + let Promise = require('when').Promise + const originalKeys = Object.keys(Promise) + + setupAgent(t) + Promise = require('when').Promise + const wrappedKeys = Object.keys(Promise) + + originalKeys.forEach(function (key) { + if (wrappedKeys.indexOf(key) === -1) { + assert.ok(0, 'Property ' + key + ' is not present on wrapped Promise') + } + }) +}) + +test('transaction state', async function (t) { + const agent = helper.instrumentMockedAgent() + const when = require('when') + const Promise = when.Promise + + t.after(() => { + helper.unloadAgent(agent) + }) + + await testTransactionState({ t, agent, Promise, library: when }) +})