diff --git a/.gitignore b/.gitignore index f381e7e6e24d..36f8a3f6b9fe 100644 --- a/.gitignore +++ b/.gitignore @@ -61,3 +61,6 @@ packages/gatsby/gatsby-node.d.ts # intellij *.iml /**/.wrangler/* + +#junit reports +packages/**/*.junit.xml diff --git a/dev-packages/cloudflare-integration-tests/runner.ts b/dev-packages/cloudflare-integration-tests/runner.ts index 849b011250f9..b945bee2eeea 100644 --- a/dev-packages/cloudflare-integration-tests/runner.ts +++ b/dev-packages/cloudflare-integration-tests/runner.ts @@ -86,7 +86,7 @@ export function createRunner(...paths: string[]) { } return this; }, - start: function (): StartResult { + start: function (signal?: AbortSignal): StartResult { const { resolve, reject, promise: isComplete } = deferredPromise(cleanupChildProcesses); const expectedEnvelopeCount = expectedEnvelopes.length; @@ -155,7 +155,7 @@ export function createRunner(...paths: string[]) { '--var', `SENTRY_DSN:http://public@localhost:${mockServerPort}/1337`, ], - { stdio }, + { stdio, signal }, ); CLEANUP_STEPS.add(() => { diff --git a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts index b785e6e37fd1..347c0d3530d8 100644 --- a/dev-packages/cloudflare-integration-tests/suites/basic/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/basic/test.ts @@ -2,7 +2,7 @@ import { expect, it } from 'vitest'; import { eventEnvelope } from '../../expect'; import { createRunner } from '../../runner'; -it('Basic error in fetch handler', async () => { +it('Basic error in fetch handler', async ({ signal }) => { const runner = createRunner(__dirname) .expect( eventEnvelope({ @@ -26,7 +26,7 @@ it('Basic error in fetch handler', async () => { }, }), ) - .start(); + .start(signal); await runner.makeRequest('get', '/', { expectError: true }); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts index 13966caaf460..c9e112b32241 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/anthropic-ai/test.ts @@ -6,7 +6,7 @@ import { createRunner } from '../../../runner'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces a basic message creation request', async () => { +it('traces a basic message creation request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { @@ -35,7 +35,7 @@ it('traces a basic message creation request', async () => { ]), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/'); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts index a9daae21480f..e86508c0f101 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/durableobject/test.ts @@ -1,7 +1,7 @@ import { expect, it } from 'vitest'; import { createRunner } from '../../../runner'; -it('traces a durable object method', async () => { +it('traces a durable object method', async ({ signal }) => { const runner = createRunner(__dirname) .expect(envelope => { const transactionEvent = envelope[1]?.[0]?.[1]; @@ -21,7 +21,7 @@ it('traces a durable object method', async () => { }), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/hello'); await runner.completed(); }); diff --git a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts index c1aee24136a4..eb15fd80fc97 100644 --- a/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts +++ b/dev-packages/cloudflare-integration-tests/suites/tracing/openai/test.ts @@ -6,7 +6,7 @@ import { createRunner } from '../../../runner'; // want to test that the instrumentation does not break in our // cloudflare SDK. -it('traces a basic chat completion request', async () => { +it('traces a basic chat completion request', async ({ signal }) => { const runner = createRunner(__dirname) .ignore('event') .expect(envelope => { @@ -37,7 +37,7 @@ it('traces a basic chat completion request', async () => { ]), ); }) - .start(); + .start(signal); await runner.makeRequest('get', '/'); await runner.completed(); }); diff --git a/packages/cloudflare/src/utils/copyExecutionContext.ts b/packages/cloudflare/src/utils/copyExecutionContext.ts new file mode 100644 index 000000000000..1f1c09aa656d --- /dev/null +++ b/packages/cloudflare/src/utils/copyExecutionContext.ts @@ -0,0 +1,64 @@ +import { type DurableObjectState, type ExecutionContext } from '@cloudflare/workers-types'; + +type ContextType = ExecutionContext | DurableObjectState; +type OverridesStore = Map unknown>; + +/** + * Creates a new copy of the given execution context, optionally overriding methods. + * + * @param {ContextType|void} ctx - The execution context to be copied. Can be of type `ContextType` or `void`. + * @return {ContextType|void} A new execution context with the same properties and overridden methods if applicable. + */ +export function copyExecutionContext(ctx: T): T { + if (!ctx) return ctx; + + const overrides: OverridesStore = new Map(); + const contextPrototype = Object.getPrototypeOf(ctx); + const methodNames = Object.getOwnPropertyNames(contextPrototype) as unknown as (keyof T)[]; + const descriptors = methodNames.reduce((prevDescriptors, methodName) => { + if (methodName === 'constructor') return prevDescriptors; + if (typeof ctx[methodName] !== 'function') return prevDescriptors; + const overridableDescriptor = makeOverridableDescriptor(overrides, ctx, methodName); + return { + ...prevDescriptors, + [methodName]: overridableDescriptor, + }; + }, {}); + + return Object.create(ctx, descriptors); +} + +/** + * Creates a property descriptor that allows overriding of a method on the given context object. + * + * This descriptor supports property overriding with functions only. It delegates method calls to + * the provided store if an override exists or to the original method on the context otherwise. + * + * @param {OverridesStore} store - The storage for overridden methods specific to the context type. + * @param {ContextType} ctx - The context object that contains the method to be overridden. + * @param {keyof ContextType} method - The method on the context object to create the overridable descriptor for. + * @return {PropertyDescriptor} A property descriptor enabling the overriding of the specified method. + */ +function makeOverridableDescriptor( + store: OverridesStore, + ctx: T, + method: keyof T, +): PropertyDescriptor { + return { + configurable: true, + enumerable: true, + set: newValue => { + if (typeof newValue !== 'function') throw new Error('Cannot override non-function'); + store.set(method, newValue); + return true; + }, + + get: () => { + if (store.has(method)) return store.get(method); + const methodFunction = Reflect.get(ctx, method); + if (typeof methodFunction !== 'function') return methodFunction; + // We should do bind() to make sure that the method is bound to the context object - otherwise it will not work + return methodFunction.bind(ctx); + }, + }; +} diff --git a/packages/cloudflare/test/copy-execution-context.test.ts b/packages/cloudflare/test/copy-execution-context.test.ts new file mode 100644 index 000000000000..3ee71a10b695 --- /dev/null +++ b/packages/cloudflare/test/copy-execution-context.test.ts @@ -0,0 +1,56 @@ +import { type Mocked, describe, expect, it, vi } from 'vitest'; +import { copyExecutionContext } from '../src/utils/copyExecutionContext'; + +describe('Copy of the execution context', () => { + describe.for([ + 'waitUntil', + 'passThroughOnException', + 'acceptWebSocket', + 'blockConcurrencyWhile', + 'getWebSockets', + 'arbitraryMethod', + 'anythingElse', + ])('%s', method => { + it('Override without changing original', async () => { + const context = { + [method]: vi.fn(), + } as any; + const copy = copyExecutionContext(context); + copy[method] = vi.fn(); + expect(context[method]).not.toBe(copy[method]); + }); + + it('Overridden method was called', async () => { + const context = { + [method]: vi.fn(), + } as any; + const copy = copyExecutionContext(context); + const overridden = vi.fn(); + copy[method] = overridden; + copy[method](); + expect(overridden).toBeCalled(); + expect(context[method]).not.toBeCalled(); + }); + }); + + it('No side effects', async () => { + const context = makeExecutionContextMock(); + expect(() => copyExecutionContext(Object.freeze(context))).not.toThrow( + /Cannot define property \w+, object is not extensible/, + ); + }); + it('Respects symbols', async () => { + const s = Symbol('test'); + const context = makeExecutionContextMock(); + context[s] = {}; + const copy = copyExecutionContext(context); + expect(copy[s]).toBe(context[s]); + }); +}); + +function makeExecutionContextMock() { + return { + waitUntil: vi.fn(), + passThroughOnException: vi.fn(), + } as unknown as Mocked; +}