diff --git a/doc/contributing/dependencies.md b/doc/contributing/dependencies.md index 6675f25231f..b21af2b86e4 100644 --- a/doc/contributing/dependencies.md +++ b/doc/contributing/dependencies.md @@ -23,7 +23,7 @@ An exception is granted for dependencies on `@opentelemetry/api`, which, if used ## Third-Party Library Dependencies -Packages categorized as third-party and listed under the `"dependencies"` section (e.g., @grpc/grpc-js, @grpc/proto-loader, shimmer, etc.) should remain unpinned and utilize the caret (`^`) symbol. This approach offers several advantages: +Packages categorized as third-party and listed under the `"dependencies"` section (e.g., @grpc/grpc-js, @grpc/proto-loader, etc.) should remain unpinned and utilize the caret (`^`) symbol. This approach offers several advantages: - Our users could get bug fixes of those 3rd-party packages easily, without waiting for us to update our library. - In cases where multiple packages have dependencies on different versions of the same package, npm will opt for the most recent version, saving space and preventing potential disruptions. diff --git a/experimental/CHANGELOG.md b/experimental/CHANGELOG.md index c53c4178997..9f94622f4a4 100644 --- a/experimental/CHANGELOG.md +++ b/experimental/CHANGELOG.md @@ -14,6 +14,8 @@ For notes on migrating to 2.x / 0.200.x see [the upgrade guide](doc/upgrade-to-2 ### :bug: Bug Fixes +* fix(instrumentation): remove dependency on the shimmer module [#5652](https://github.com/open-telemetry/opentelemetry-js/pull/5652) @cjihrig + ### :books: Documentation ### :house: Internal diff --git a/experimental/packages/opentelemetry-instrumentation/LICENSES/shimmer/LICENSE b/experimental/packages/opentelemetry-instrumentation/LICENSES/shimmer/LICENSE new file mode 100644 index 00000000000..a87c6555432 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/LICENSES/shimmer/LICENSE @@ -0,0 +1,25 @@ +BSD 2-Clause License + +Copyright (c) 2013-2019, Forrest L Norvell +All rights reserved. + +Redistribution and use in source and binary forms, with or without +modification, are permitted provided that the following conditions are met: + +* Redistributions of source code must retain the above copyright notice, this + list of conditions and the following disclaimer. + +* Redistributions in binary form must reproduce the above copyright notice, + this list of conditions and the following disclaimer in the documentation + and/or other materials provided with the distribution. + +THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" +AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE +IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE +DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE +FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL +DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR +SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER +CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, +OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE +OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. diff --git a/experimental/packages/opentelemetry-instrumentation/README.md b/experimental/packages/opentelemetry-instrumentation/README.md index 865592e282b..92ee119c395 100644 --- a/experimental/packages/opentelemetry-instrumentation/README.md +++ b/experimental/packages/opentelemetry-instrumentation/README.md @@ -245,6 +245,8 @@ Instrumentations for external modules (e.g. express, mongodb,...) hooks the `req Apache 2.0 - See [LICENSE][license-url] for more information. +Third-party licenses and copyright notices can be found in the [LICENSES directory](./LICENSES). + ## Useful links - For more information on OpenTelemetry, visit: diff --git a/experimental/packages/opentelemetry-instrumentation/package.json b/experimental/packages/opentelemetry-instrumentation/package.json index a73128671c7..cf927d37332 100644 --- a/experimental/packages/opentelemetry-instrumentation/package.json +++ b/experimental/packages/opentelemetry-instrumentation/package.json @@ -35,6 +35,7 @@ "hook.mjs", "doc", "LICENSE", + "LICENSES/**/*", "README.md" ], "scripts": { @@ -71,10 +72,8 @@ }, "dependencies": { "@opentelemetry/api-logs": "0.201.1", - "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" + "require-in-the-middle": "^7.1.1" }, "peerDependencies": { "@opentelemetry/api": "^1.3.0" diff --git a/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts index 114f25d67bb..572de28e18b 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/instrumentation.ts @@ -26,7 +26,7 @@ import { Span, } from '@opentelemetry/api'; import { Logger, LoggerProvider, logs } from '@opentelemetry/api-logs'; -import * as shimmer from 'shimmer'; +import * as shimmer from './shimmer'; import { InstrumentationModuleDefinition, Instrumentation, diff --git a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts index a74b687f4ad..8c63389cd95 100644 --- a/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts +++ b/experimental/packages/opentelemetry-instrumentation/src/platform/node/instrumentation.ts @@ -18,7 +18,7 @@ import * as types from '../../types'; import * as path from 'path'; import { types as utilTypes } from 'util'; import { satisfies } from '../../semver'; -import { wrap, unwrap, massWrap, massUnwrap } from 'shimmer'; +import { wrap, unwrap, massWrap, massUnwrap } from '../../shimmer'; import { InstrumentationAbstract } from '../../instrumentation'; import { RequireInTheMiddleSingleton, diff --git a/experimental/packages/opentelemetry-instrumentation/src/shimmer.ts b/experimental/packages/opentelemetry-instrumentation/src/shimmer.ts new file mode 100644 index 00000000000..8e633b134a2 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/src/shimmer.ts @@ -0,0 +1,197 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * BSD 2-Clause License + * + * Copyright (c) 2013-2019, Forrest L Norvell + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Modified by OpenTelemetry Authors + * - converted to TypeScript + * - aligned with style-guide + */ + +import { ShimWrapped } from './types'; + +// Default to complaining loudly when things don't go according to plan. +// eslint-disable-next-line no-console +let logger: typeof console.error = console.error.bind(console); + +// Sets a property on an object, preserving its enumerability. +// This function assumes that the property is already writable. +function defineProperty(obj: object, name: PropertyKey, value: unknown): void { + const enumerable = + !!obj[name as keyof typeof obj] && + Object.prototype.propertyIsEnumerable.call(obj, name); + + Object.defineProperty(obj, name, { + configurable: true, + enumerable, + writable: true, + value, + }); +} + +export const wrap = ( + nodule: Nodule, + name: FieldName, + wrapper: (original: Nodule[FieldName], name: FieldName) => Nodule[FieldName] +): ShimWrapped | undefined => { + if (!nodule || !nodule[name]) { + logger('no original function ' + String(name) + ' to wrap'); + return; + } + + if (!wrapper) { + logger('no wrapper function'); + logger(new Error().stack); + return; + } + + const original = nodule[name]; + + if (typeof original !== 'function' || typeof wrapper !== 'function') { + logger('original object and wrapper must be functions'); + return; + } + + const wrapped = wrapper(original, name) as object; + + defineProperty(wrapped, '__original', original); + defineProperty(wrapped, '__unwrap', () => { + if (nodule[name] === wrapped) { + defineProperty(nodule, name, original); + } + }); + defineProperty(wrapped, '__wrapped', true); + defineProperty(nodule, name, wrapped); + return wrapped as ShimWrapped; +}; + +export const massWrap = ( + nodules: Nodule[], + names: FieldName[], + wrapper: (original: Nodule[FieldName]) => Nodule[FieldName] +): void => { + if (!nodules) { + logger('must provide one or more modules to patch'); + logger(new Error().stack); + return; + } else if (!Array.isArray(nodules)) { + nodules = [nodules]; + } + + if (!(names && Array.isArray(names))) { + logger('must provide one or more functions to wrap on modules'); + return; + } + + nodules.forEach(nodule => { + names.forEach(name => { + wrap(nodule, name, wrapper); + }); + }); +}; + +export const unwrap = ( + nodule: Nodule, + name: keyof Nodule +): void => { + if (!nodule || !nodule[name]) { + logger('no function to unwrap.'); + logger(new Error().stack); + return; + } + + const wrapped = nodule[name] as unknown as ShimWrapped; + + if (!wrapped.__unwrap) { + logger( + 'no original to unwrap to -- has ' + + String(name) + + ' already been unwrapped?' + ); + } else { + wrapped.__unwrap(); + return; + } +}; + +export const massUnwrap = ( + nodules: Nodule[], + names: Array +): void => { + if (!nodules) { + logger('must provide one or more modules to patch'); + logger(new Error().stack); + return; + } else if (!Array.isArray(nodules)) { + nodules = [nodules]; + } + + if (!(names && Array.isArray(names))) { + logger('must provide one or more functions to unwrap on modules'); + return; + } + + nodules.forEach(nodule => { + names.forEach(name => { + unwrap(nodule, name); + }); + }); +}; + +export interface ShimmerOptions { + logger?: typeof console.error; +} + +export default function shimmer(options: ShimmerOptions): void { + if (options && options.logger) { + if (typeof options.logger !== 'function') { + logger("new logger isn't a function, not replacing"); + } else { + logger = options.logger; + } + } +} + +shimmer.wrap = wrap; +shimmer.massWrap = massWrap; +shimmer.unwrap = unwrap; +shimmer.massUnwrap = massUnwrap; diff --git a/experimental/packages/opentelemetry-instrumentation/test/common/shimmer.test.ts b/experimental/packages/opentelemetry-instrumentation/test/common/shimmer.test.ts new file mode 100644 index 00000000000..b2a11dcf582 --- /dev/null +++ b/experimental/packages/opentelemetry-instrumentation/test/common/shimmer.test.ts @@ -0,0 +1,750 @@ +/* + * Copyright The OpenTelemetry Authors + * + * Licensed under the Apache License, Version 2.0 (the "License"); + * you may not use this file except in compliance with the License. + * You may obtain a copy of the License at + * + * https://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ + +/* + * BSD 2-Clause License + * + * Copyright (c) 2013-2019, Forrest L Norvell + * All rights reserved. + * + * Redistribution and use in source and binary forms, with or without + * modification, are permitted provided that the following conditions are met: + * + * * Redistributions of source code must retain the above copyright notice, this + * list of conditions and the following disclaimer. + * + * * Redistributions in binary form must reproduce the above copyright notice, + * this list of conditions and the following disclaimer in the documentation + * and/or other materials provided with the distribution. + * + * THIS SOFTWARE IS PROVIDED BY THE COPYRIGHT HOLDERS AND CONTRIBUTORS "AS IS" + * AND ANY EXPRESS OR IMPLIED WARRANTIES, INCLUDING, BUT NOT LIMITED TO, THE + * IMPLIED WARRANTIES OF MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE ARE + * DISCLAIMED. IN NO EVENT SHALL THE COPYRIGHT HOLDER OR CONTRIBUTORS BE LIABLE + * FOR ANY DIRECT, INDIRECT, INCIDENTAL, SPECIAL, EXEMPLARY, OR CONSEQUENTIAL + * DAMAGES (INCLUDING, BUT NOT LIMITED TO, PROCUREMENT OF SUBSTITUTE GOODS OR + * SERVICES; LOSS OF USE, DATA, OR PROFITS; OR BUSINESS INTERRUPTION) HOWEVER + * CAUSED AND ON ANY THEORY OF LIABILITY, WHETHER IN CONTRACT, STRICT LIABILITY, + * OR TORT (INCLUDING NEGLIGENCE OR OTHERWISE) ARISING IN ANY WAY OUT OF THE USE + * OF THIS SOFTWARE, EVEN IF ADVISED OF THE POSSIBILITY OF SUCH DAMAGE. + */ + +/* Modified by OpenTelemetry Authors + * - converted to TypeScript + * - adapted tests to use `node:assert` and `mocha` + * - aligned with style-guide + */ + +import * as assert from 'assert'; +import * as sinon from 'sinon'; +import shimmer from '../../src/shimmer'; +import { ShimWrapped } from '../../src'; + +describe('Shimmer', function () { + describe('init', function () { + it('shimmer initialization', function () { + assert.doesNotThrow(function () { + (shimmer as any)(); + }); + const mock = sinon.expectation + .create('logger') + .withArgs('no original function undefined to wrap') + .once(); + + assert.doesNotThrow(function () { + shimmer({ logger: mock }); + }, "initializer doesn't throw"); + + assert.doesNotThrow(function () { + (shimmer as any).wrap(); + }, "invoking the wrap method with no params doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger method was called with the expected message'); + }); + + it('shimmer initialized with non-function logger', function () { + const mock = sinon.expectation + .create('logger') + .withArgs("new logger isn't a function, not replacing") + .once(); + + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + shimmer({ logger: { ham: 'chunx' } } as any); + }, "even bad initialization doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger initialization failed in the expected way'); + }); + }); + + describe('wrap', function () { + let outsider = 0; + + function counter() { + return ++outsider; + } + function anticounter() { + return --outsider; + } + + const generator: any = { + inc: counter, + }; + Object.defineProperty(generator, 'dec', { + value: anticounter, + writable: true, + configurable: true, + enumerable: false, + }); + + it('should wrap safely', function () { + assert.equal(counter, generator.inc, 'method is mapped to function'); + assert.doesNotThrow(function () { + generator.inc(); + }, 'original function works'); + assert.equal(1, outsider, 'calls have side effects'); + + let count = 0; + function wrapper(original: any) { + return function (this: any) { + count++; + const returned = original.apply(this, arguments); + count++; + return returned; + }; + } + shimmer.wrap(generator, 'inc', wrapper); + + assert.ok( + (generator.inc as unknown as ShimWrapped).__wrapped, + "function tells us it's wrapped" + ); + assert.equal( + (generator.inc as unknown as ShimWrapped).__original, + counter, + 'original function is available' + ); + assert.doesNotThrow(function () { + generator.inc(); + }, 'wrapping works'); + assert.equal( + 2, + count, + 'both pre and post increments should have happened' + ); + assert.equal(2, outsider, 'original function has still been called'); + assert.ok( + Object.prototype.propertyIsEnumerable.call(generator, 'inc'), + 'wrapped enumerable property is still enumerable' + ); + assert.equal( + Object.keys(generator.inc).length, + 0, + 'wrapped object has no additional properties' + ); + + shimmer.wrap(generator, 'dec', function (original) { + return function (this: any) { + return original.apply(this, arguments); + }; + }); + + assert.ok( + !Object.prototype.propertyIsEnumerable.call(generator, 'dec'), + 'wrapped unenumerable property is still unenumerable' + ); + }); + + it('wrap called with no arguments', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs('no original function undefined to wrap') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).wrap(); + }, "wrapping with no arguments doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with module but nothing else', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs('no original function undefined to wrap') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).wrap(generator); + }, "wrapping with only 1 argument doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with original but no wrapper', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).wrap(generator, 'inc'); + }, "wrapping with only original method doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with non-function original', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs('original object and wrapper must be functions') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + shimmer.wrap({ orange: 'slices' }, 'orange', function () {} as any); + }, "wrapping non-function original doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with non-function wrapper', function () { + const mock = sinon.expectation + .create('logger') + .withArgs('original object and wrapper must be functions') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).wrap({ orange: function () {} }, 'orange', 'hamchunx'); + }, "wrapping with non-function wrapper doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + }); + + describe('unwrap', function () { + let outsider = 0; + + function counter() { + return ++outsider; + } + + const generator = { + inc: counter, + }; + + it('should unwrap safely', function () { + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(1, outsider, 'calls have side effects'); + + function wrapper(original: any) { + return function (this: any) { + return original.apply(this, arguments); + }; + } + shimmer.wrap(generator, 'inc', wrapper); + + assert.notEqual(counter, generator.inc, 'function should be wrapped'); + + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(2, outsider, 'original function has still been called'); + + shimmer.unwrap(generator, 'inc'); + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(3, outsider, 'original function has still been called'); + }); + + it("shouldn't throw on double unwrapping", function () { + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + + const mock = sinon.expectation + .create('logger') + .withArgs( + 'no original to unwrap to -- ' + 'has inc already been unwrapped?' + ) + .once(); + shimmer({ logger: mock }); + + function wrapper(original: any) { + return function (this: any) { + return original.apply(this, arguments); + }; + } + shimmer.wrap(generator, 'inc', wrapper); + + assert.notEqual(counter, generator.inc, 'function should be wrapped'); + + shimmer.unwrap(generator, 'inc'); + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + + assert.doesNotThrow(function () { + shimmer.unwrap(generator, 'inc'); + }, 'should double unwrap without issue'); + assert.equal( + counter, + generator.inc, + 'function is unchanged after unwrapping' + ); + + mock.verify(); + }); + + it('unwrap called with no arguments', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).unwrap(); + }, 'should log instead of throwing'); + + mock.verify(); + }); + + it('unwrap called with module but no name', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).unwrap({}); + }, 'should log instead of throwing'); + + mock.verify(); + }); + }); + + describe('massWrap', function () { + let outsider = 0; + function counter() { + return ++outsider; + } + function anticounter() { + return --outsider; + } + + const generator: any = { + inc: counter, + dec: anticounter, + }; + + const arrow = { + in: counter, + out: anticounter, + }; + + const nester = { + in: counter, + out: anticounter, + }; + + it('should wrap multiple functions safely', function () { + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + generator.dec, + 'basic function equality testing should work' + ); + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.doesNotThrow(function () { + generator.dec(); + }); + assert.equal(0, outsider, 'calls have side effects'); + + let count = 0; + function wrapper(original: any) { + return function (this: any) { + count++; + const returned = original.apply(this, arguments); + count++; + return returned; + }; + } + (shimmer as any).massWrap(generator, ['inc', 'dec'], wrapper); + + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.doesNotThrow(function () { + generator.dec(); + }); + assert.equal( + 4, + count, + 'both pre and post increments should have happened' + ); + assert.equal(0, outsider, 'original function has still been called'); + }); + + it('should wrap multiple functions on multiple modules safely', function () { + assert.equal( + counter, + arrow.in, + 'basic function equality testing should work' + ); + assert.equal( + counter, + nester.in, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + arrow.out, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + nester.out, + 'basic function equality testing should work' + ); + + assert.doesNotThrow(function () { + arrow.in(); + }); + assert.doesNotThrow(function () { + nester.in(); + }); + assert.doesNotThrow(function () { + arrow.out(); + }); + assert.doesNotThrow(function () { + nester.out(); + }); + + assert.equal(0, outsider, 'calls have side effects'); + + let count = 0; + + function wrapper(original: any) { + return function (this: any) { + count++; + const returned = original.apply(this, arguments); + count++; + return returned; + }; + } + shimmer.massWrap([arrow, nester], ['in', 'out'], wrapper); + + assert.doesNotThrow(function () { + arrow.in(); + }); + assert.doesNotThrow(function () { + arrow.out(); + }); + assert.doesNotThrow(function () { + nester.in(); + }); + assert.doesNotThrow(function () { + nester.out(); + }); + + assert.equal( + 8, + count, + 'both pre and post increments should have happened' + ); + assert.equal(0, outsider, 'original function has still been called'); + }); + + it('wrap called with no arguments', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massWrap(); + }, "wrapping with no arguments doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with module but nothing else', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs('must provide one or more functions to wrap on modules') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massWrap(generator); + }, "wrapping with only 1 argument doesn't throw"); + + assert.doesNotThrow(function () { + mock.verify(); + }, 'logger was called with the expected message'); + }); + + it('wrap called with original but no wrapper', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massWrap(generator, ['inc']); + }, "wrapping with only original function doesn't throw"); + + mock.verify(); + }); + + it('wrap called with non-function original', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs('must provide one or more functions to wrap on modules') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massWrap( + { orange: 'slices' }, + 'orange', + function () {} + ); + }, "wrapping non-function original doesn't throw"); + + mock.verify(); + }); + + it('wrap called with non-function wrapper', function () { + const mock = sinon.expectation + .create('logger') + .withArgs('must provide one or more functions to wrap on modules') + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massWrap( + { orange: function () {} }, + 'orange', + 'hamchunx' + ); + }, "wrapping with non-function wrapper doesn't throw"); + + mock.verify(); + }); + }); + + describe('massUnwrap', function () { + let outsider = 0; + + function counter() { + return ++outsider; + } + function anticounter() { + return --outsider; + } + + const generator: any = { + inc: counter, + dec: anticounter, + }; + + it('should unwrap safely', function () { + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + generator.dec, + 'basic function equality testing should work' + ); + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(1, outsider, 'calls have side effects'); + assert.doesNotThrow(function () { + generator.dec(); + }); + assert.equal(0, outsider, 'calls have side effects'); + + function wrapper(original: any) { + return function (this: any) { + return original.apply(this, arguments); + }; + } + + (shimmer as any).massWrap(generator, ['inc', 'dec'], wrapper); + + assert.notEqual(counter, generator.inc, 'function should be wrapped'); + assert.notEqual(anticounter, generator.dec, 'function should be wrapped'); + + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(1, outsider, 'original function has still been called'); + assert.doesNotThrow(function () { + generator.dec(); + }); + assert.equal(0, outsider, 'original function has still been called'); + + shimmer.massUnwrap(generator, ['inc', 'dec']); + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + generator.dec, + 'basic function equality testing should work' + ); + + assert.doesNotThrow(function () { + generator.inc(); + }); + assert.equal(1, outsider, 'original function has still been called'); + assert.doesNotThrow(function () { + generator.dec(); + }); + assert.equal(0, outsider, 'original function has still been called'); + }); + + it("shouldn't throw on double unwrapping", function () { + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + generator.dec, + 'basic function equality testing should work' + ); + + const mock = sinon.stub(); + shimmer({ logger: mock }); + + function wrapper(original: any) { + return function (this: any) { + return original.apply(this, arguments); + }; + } + shimmer.wrap(generator, 'inc', wrapper); + shimmer.wrap(generator, 'dec', wrapper); + + assert.notEqual(counter, generator.inc, 'function should be wrapped'); + assert.notEqual(anticounter, generator.dec, 'function should be wrapped'); + + shimmer.massUnwrap(generator, ['inc', 'dec']); + assert.equal( + counter, + generator.inc, + 'basic function equality testing should work' + ); + assert.equal( + anticounter, + generator.dec, + 'basic function equality testing should work' + ); + + assert.doesNotThrow(function () { + shimmer.massUnwrap(generator, ['inc', 'dec']); + }, 'should double unwrap without issue'); + assert.equal( + counter, + generator.inc, + 'function is unchanged after unwrapping' + ); + assert.equal( + anticounter, + generator.dec, + 'function is unchanged after unwrapping' + ); + + sinon.assert.calledWith( + mock, + 'no original to unwrap to -- ' + 'has inc already been unwrapped?' + ); + sinon.assert.calledWith( + mock, + 'no original to unwrap to -- ' + 'has dec already been unwrapped?' + ); + sinon.assert.calledTwice(mock); + }); + + it('massUnwrap called with no arguments', function () { + const mock = sinon.expectation.create('logger').twice(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massUnwrap(); + }, 'should log instead of throwing'); + + mock.verify(); + }); + + it('massUnwrap called with module but nothing else', function () { + const mock = sinon.expectation + .create('logger') + .withExactArgs( + 'must provide one or more functions to unwrap on modules' + ) + .once(); + shimmer({ logger: mock }); + + assert.doesNotThrow(function () { + (shimmer as any).massUnwrap(generator); + }, "wrapping with only 1 argument doesn't throw"); + + mock.verify(); + }); + }); +}); diff --git a/package-lock.json b/package-lock.json index 83b3c489238..b23004300c2 100644 --- a/package-lock.json +++ b/package-lock.json @@ -1731,10 +1731,8 @@ "license": "Apache-2.0", "dependencies": { "@opentelemetry/api-logs": "0.201.1", - "@types/shimmer": "^1.2.0", "import-in-the-middle": "^1.8.1", - "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1" + "require-in-the-middle": "^7.1.1" }, "devDependencies": { "@babel/core": "7.27.1", @@ -8929,12 +8927,6 @@ "@types/send": "*" } }, - "node_modules/@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==", - "license": "MIT" - }, "node_modules/@types/sinon": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz", @@ -35664,7 +35656,6 @@ "@opentelemetry/sdk-metrics": "2.0.1", "@types/mocha": "10.0.10", "@types/node": "18.6.5", - "@types/shimmer": "^1.2.0", "@types/sinon": "17.0.4", "@types/webpack-env": "1.16.3", "babel-loader": "10.0.0", @@ -35682,7 +35673,6 @@ "mocha": "11.1.0", "nyc": "17.1.0", "require-in-the-middle": "^7.1.1", - "shimmer": "^1.2.1", "sinon": "15.1.2", "ts-loader": "9.5.2", "typescript": "5.0.4", @@ -38048,11 +38038,6 @@ "@types/send": "*" } }, - "@types/shimmer": { - "version": "1.2.0", - "resolved": "https://registry.npmjs.org/@types/shimmer/-/shimmer-1.2.0.tgz", - "integrity": "sha512-UE7oxhQLLd9gub6JKIAhDq06T0F6FnztwMNRvYgjeQSBeMc1ZG/tA47EwfduvkuQS8apbkM/lpLpWsaCeYsXVg==" - }, "@types/sinon": { "version": "17.0.4", "resolved": "https://registry.npmjs.org/@types/sinon/-/sinon-17.0.4.tgz",