UNPKG

@temporalio/testing

Version:
323 lines 14.6 kB
"use strict"; /** * `npm i @temporalio/testing` * * Testing library for the SDK. * * [Documentation](https://docs.temporal.io/typescript/testing) * * @module */ var __importDefault = (this && this.__importDefault) || function (mod) { return (mod && mod.__esModule) ? mod : { "default": mod }; }; Object.defineProperty(exports, "__esModule", { value: true }); exports.MockActivityEnvironment = exports.defaultActivityInfo = exports.TestWorkflowEnvironment = exports.workflowInterceptorModules = exports.TimeSkippingWorkflowClient = void 0; require("abort-controller/polyfill"); // eslint-disable-line import/no-unassigned-import const node_path_1 = __importDefault(require("node:path")); const node_events_1 = __importDefault(require("node:events")); const client_1 = require("@temporalio/client"); const common_1 = require("@temporalio/common"); const time_1 = require("@temporalio/common/lib/time"); const worker_1 = require("@temporalio/worker"); const logger_1 = require("@temporalio/worker/lib/logger"); const activity_1 = require("@temporalio/worker/lib/activity"); const core_bridge_1 = require("@temporalio/core-bridge"); const internal_non_workflow_1 = require("@temporalio/common/lib/internal-non-workflow"); const connection_1 = require("./connection"); /** * A client with the exact same API as the "normal" client with 1 exception, * When this client waits on a Workflow's result, it will enable time skipping * in the test server. */ class TimeSkippingWorkflowClient extends client_1.WorkflowClient { constructor(options) { super(options); this.enableTimeSkipping = options.enableTimeSkipping; this.testService = options.connection.testService; } /** * Gets the result of a Workflow execution. * * @see {@link WorkflowClient.result} */ async result(workflowId, runId, opts) { if (this.enableTimeSkipping) { await this.testService.unlockTimeSkipping({}); try { return await super.result(workflowId, runId, opts); } finally { await this.testService.lockTimeSkipping({}); } } else { return await super.result(workflowId, runId, opts); } } } exports.TimeSkippingWorkflowClient = TimeSkippingWorkflowClient; /** * A client with the exact same API as the "normal" client with one exception: * when `TestEnvClient.workflow` (an instance of {@link TimeSkippingWorkflowClient}) waits on a Workflow's result, it will enable time skipping * in the Test Server. */ class TestEnvClient extends client_1.Client { constructor(options) { super(options); // Recreate the client (this isn't optimal but it's better than adding public methods just for testing). // NOTE: we cast to "any" to work around `workflow` being a readonly attribute. this.workflow = new TimeSkippingWorkflowClient({ ...this.workflow.options, connection: options.connection, enableTimeSkipping: options.enableTimeSkipping, }); } } /** * Convenience workflow interceptors * * Contains a single interceptor for transforming `AssertionError`s into non * retryable `ApplicationFailure`s. */ exports.workflowInterceptorModules = [node_path_1.default.join(__dirname, 'assert-to-failure-interceptor')]; function addDefaults(opts) { return { client: {}, ...opts, }; } /** * An execution environment for running Workflow integration tests. * * Runs an external server. * By default, the Java test server is used which supports time skipping. */ class TestWorkflowEnvironment { constructor(options, supportsTimeSkipping, server, connection, nativeConnection, namespace) { this.options = options; this.supportsTimeSkipping = supportsTimeSkipping; this.server = server; /** * Wait for `durationMs` in "server time". * * This awaits using regular setTimeout in regular environments, or manually skips time in time-skipping environments. * * Useful for simulating events far into the future like completion of long running activities. * * **Time skippping**: * * The time skippping server toggles between skipped time and normal time depending on what it needs to execute. * * This method is _likely_ to resolve in less than `durationMs` of "real time". * * @param durationMs number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string} * * @example * * `workflow.ts` * * ```ts * const activities = proxyActivities({ startToCloseTimeout: 2_000_000 }); * * export async function raceActivityAndTimer(): Promise<string> { * return await Promise.race([ * wf.sleep(500_000).then(() => 'timer'), * activities.longRunning().then(() => 'activity'), * ]); * } * ``` * * `test.ts` * * ```ts * const worker = await Worker.create({ * connection: testEnv.nativeConnection, * activities: { * async longRunning() { * await testEnv.sleep(1_000_000); // <-- sleep called here * }, * }, * // ... * }); * ``` */ this.sleep = async (durationMs) => { if (this.supportsTimeSkipping) { await this.connection.testService.unlockTimeSkippingWithSleep({ duration: (0, time_1.msToTs)(durationMs) }); } else { await new Promise((resolve) => setTimeout(resolve, (0, time_1.msToNumber)(durationMs))); } }; this.connection = connection; this.nativeConnection = nativeConnection; this.namespace = namespace; this.client = new TestEnvClient({ connection, namespace: this.namespace, enableTimeSkipping: supportsTimeSkipping, ...options.client, }); // eslint-disable-next-line deprecation/deprecation this.asyncCompletionClient = this.client.activity; // eslint-disable-next-line deprecation/deprecation this.workflowClient = this.client.workflow; } /** * Start a time skipping workflow environment. * * This environment automatically skips to the next events in time when a workflow handle's `result` is awaited on * (which includes {@link WorkflowClient.execute}). Before the result is awaited on, time can be manually skipped * forward using {@link sleep}. The currently known time can be obtained via {@link currentTimeMs}. * * This environment will be powered by the Temporal Time Skipping Test Server (part of the [Java SDK](https://github.com/temporalio/sdk-java)). * Note that the Time Skipping Test Server does not support full capabilities of the regular Temporal Server, and may * occasionally present different behaviors. For general Workflow testing, it is generally preferable to use {@link createLocal} * instead. * * Users can reuse this environment for testing multiple independent workflows, but not concurrently. Time skipping, * which is automatically done when awaiting a workflow result and manually done on sleep, is global to the * environment, not to the workflow under test. We highly recommend running tests serially when using a single * environment or creating a separate environment per test. * * By default, the latest release of the Test Serveer will be downloaded and cached to a temporary directory * (e.g. `$TMPDIR/temporal-test-server-sdk-typescript-*` or `%TEMP%/temporal-test-server-sdk-typescript-*.exe`). Note * that existing cached binairies will be reused without validation that they are still up-to-date, until the SDK * itself is updated. Alternatively, a specific version number of the Test Server may be provided, or the path to an * existing Test Server binary may be supplied; see {@link LocalTestWorkflowEnvironmentOptions.server.executable}. * * Note that the Test Server implementation may be changed to another one in the future. Therefore, there is no * guarantee that Test Server options, and particularly those provided through the `extraArgs` array, will continue to * be supported in the future. * * IMPORTANT: At this time, the Time Skipping Test Server is not supported on ARM platforms. Execution on Apple * silicon Macs will work if Rosetta 2 is installed. */ static async createTimeSkipping(opts) { return await this.create({ server: { type: 'time-skipping', ...opts?.server }, client: opts?.client, supportsTimeSkipping: true, }); } /** * Start a full Temporal server locally. * * This environment is good for testing full server capabilities, but does not support time skipping like * {@link createTimeSkipping} does. {@link supportsTimeSkipping} will always return `false` for this environment. * {@link sleep} will sleep the actual amount of time and {@link currentTimeMs} will return the current time. * * This local environment will be powered by [Temporal CLI](https://github.com/temporalio/cli), which is a * self-contained executable for Temporal. By default, Temporal's database will not be persisted to disk, and no UI * will be launched. * * By default, the latest release of the CLI will be downloaded and cached to a temporary directory * (e.g. `$TMPDIR/temporal-sdk-typescript-*` or `%TEMP%/temporal-sdk-typescript-*.exe`). Note that existing cached * binairies will be reused without validation that they are still up-to-date, until the SDK itself is updated. * Alternatively, a specific version number of the CLI may be provided, or the path to an existing CLI binary may be * supplied; see {@link LocalTestWorkflowEnvironmentOptions.server.executable}. * * Note that the Dev Server implementation may be changed to another one in the future. Therefore, there is no * guarantee that Dev Server options, and particularly those provided through the `extraArgs` array, will continue to * be supported in the future. */ static async createLocal(opts) { return await this.create({ server: { type: 'dev-server', ...opts?.server }, client: opts?.client, namespace: opts?.server?.namespace, supportsTimeSkipping: false, }); } /** * Create a new test environment */ static async create(opts) { const { supportsTimeSkipping, namespace, ...rest } = opts; const optsWithDefaults = addDefaults((0, internal_non_workflow_1.filterNullAndUndefined)(rest)); const server = await worker_1.Runtime.instance().createEphemeralServer(optsWithDefaults.server); const address = (0, core_bridge_1.getEphemeralServerTarget)(server); const nativeConnection = await worker_1.NativeConnection.connect({ address }); const connection = await connection_1.Connection.connect({ address }); return new this(optsWithDefaults, supportsTimeSkipping, server, connection, nativeConnection, namespace); } /** * Kill the test server process and close the connection to it */ async teardown() { await this.connection.close(); await this.nativeConnection.close(); await worker_1.Runtime.instance().shutdownEphemeralServer(this.server); } /** * Get the current time known to this environment. * * For non-time-skipping environments this is simply the system time. For time-skipping environments this is whatever * time has been skipped to. */ async currentTimeMs() { if (this.supportsTimeSkipping) { const { time } = await this.connection.testService.getCurrentTime({}); return (0, time_1.tsToMs)(time); } else { return Date.now(); } } } exports.TestWorkflowEnvironment = TestWorkflowEnvironment; /** * Used as the default activity info for Activities executed in the {@link MockActivityEnvironment} */ exports.defaultActivityInfo = { attempt: 1, taskQueue: 'test', isLocal: false, taskToken: Buffer.from('test'), activityId: 'test', activityType: 'unknown', workflowType: 'test', base64TaskToken: Buffer.from('test').toString('base64'), heartbeatTimeoutMs: undefined, heartbeatDetails: undefined, activityNamespace: 'default', workflowNamespace: 'default', workflowExecution: { workflowId: 'test', runId: 'dead-beef' }, scheduledTimestampMs: 1, startToCloseTimeoutMs: 1000, scheduleToCloseTimeoutMs: 1000, currentAttemptScheduledTimestampMs: 1, }; /** * An execution environment for testing Activities. * * Mocks Activity {@link Context | activity.Context} and exposes hooks for cancellation and heartbeats. * * Note that the `Context` object used by this environment will be reused for all activities that are run in this * environment. Consequently, once `cancel()` is called, any further activity that gets executed in this environment * will immediately be in a cancelled state. */ class MockActivityEnvironment extends node_events_1.default.EventEmitter { constructor(info, opts) { super(); this.cancel = () => undefined; const heartbeatCallback = (details) => this.emit('heartbeat', details); const loadedDataConverter = { payloadConverter: common_1.defaultPayloadConverter, payloadCodecs: [], failureConverter: common_1.defaultFailureConverter, }; this.activity = new activity_1.Activity({ ...exports.defaultActivityInfo, ...info }, undefined, loadedDataConverter, heartbeatCallback, (0, logger_1.withMetadata)(opts?.logger ?? new worker_1.DefaultLogger(), { sdkComponent: common_1.SdkComponent.worker }), opts?.interceptors ?? []); this.context = this.activity.context; this.cancel = this.activity.cancel; } /** * Run a function in Activity Context */ async run(fn, ...args) { return this.activity.runNoEncoding(fn, { args, headers: {} }); } } exports.MockActivityEnvironment = MockActivityEnvironment; //# sourceMappingURL=index.js.map