@temporalio/testing 1.2.0 → 1.3.1

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@temporalio/testing",
3
- "version": "1.2.0",
3
+ "version": "1.3.1",
4
4
  "description": "Temporal.io SDK Testing sub-package",
5
5
  "main": "lib/index.js",
6
6
  "types": "./lib/index.d.ts",
@@ -9,27 +9,16 @@
9
9
  "workflow",
10
10
  "testing"
11
11
  ],
12
- "scripts": {
13
- "install": "node ./scripts/download-test-server.mjs",
14
- "build": "npm-run-all build:protos install",
15
- "build:protos": "node ./scripts/compile-proto.mjs"
16
- },
17
12
  "author": "Temporal Technologies Inc. <sdk@temporal.io>",
18
13
  "license": "MIT",
19
14
  "dependencies": {
20
15
  "@grpc/grpc-js": "^1.6.7",
21
- "@temporalio/activity": "^1.1.0",
22
- "@temporalio/client": "^1.2.0",
23
- "@temporalio/common": "^1.1.0",
24
- "@temporalio/worker": "^1.2.0",
25
- "@types/long": "^4.0.2",
16
+ "@temporalio/activity": "~1.3.1",
17
+ "@temporalio/client": "~1.3.1",
18
+ "@temporalio/common": "~1.3.1",
19
+ "@temporalio/worker": "~1.3.1",
26
20
  "abort-controller": "^3.0.0",
27
- "get-port": "^5.0.0",
28
- "got": "^12.1.0",
29
- "long": "^5.2.0",
30
- "protobufjs": "^7.0.0",
31
- "tar-stream": "^2.2.0",
32
- "unzipper": "^0.10.11"
21
+ "ms": "^2.1.3"
33
22
  },
34
23
  "bugs": {
35
24
  "url": "https://github.com/temporalio/sdk-typescript/issues"
@@ -37,12 +26,10 @@
37
26
  "homepage": "https://github.com/temporalio/sdk-typescript/tree/main/packages/testing",
38
27
  "files": [
39
28
  "lib",
40
- "src",
41
- "generated-protos",
42
- "scripts"
29
+ "src"
43
30
  ],
44
31
  "publishConfig": {
45
32
  "access": "public"
46
33
  },
47
- "gitHead": "9790992f0a7da1638cd3db99518e414107651630"
34
+ "gitHead": "b85b35aed41932dbfb9e158a1c35943c072f95a8"
48
35
  }
@@ -1,7 +1,7 @@
1
1
  import * as grpc from '@grpc/grpc-js';
2
2
  import { Connection as BaseConnection, ConnectionOptions } from '@temporalio/client';
3
3
  import { ConnectionCtorOptions as BaseConnectionCtorOptions } from '@temporalio/client/lib/connection';
4
- import { temporal } from '../generated-protos';
4
+ import { temporal } from '@temporalio/proto';
5
5
 
6
6
  export type TestService = temporal.api.testservice.v1.TestService;
7
7
  export const { TestService } = temporal.api.testservice.v1;
package/src/index.ts CHANGED
@@ -11,54 +11,40 @@
11
11
  import * as activity from '@temporalio/activity';
12
12
  import {
13
13
  AsyncCompletionClient,
14
- WorkflowClient as BaseWorkflowClient,
15
- WorkflowClientOptions as BaseWorkflowClientOptions,
16
- WorkflowResultOptions as BaseWorkflowResultOptions,
17
- WorkflowStartOptions as BaseWorkflowStartOptions,
14
+ Client,
15
+ ClientOptions,
16
+ ConnectionLike,
17
+ WorkflowClient,
18
+ WorkflowClientOptions,
19
+ WorkflowResultOptions,
18
20
  } from '@temporalio/client';
19
- import { ActivityFunction, CancelledFailure, msToTs, Workflow, WorkflowResultType } from '@temporalio/common';
20
- import { NativeConnection, Logger, DefaultLogger } from '@temporalio/worker';
21
+ import { ActivityFunction, CancelledFailure, msToTs, tsToMs } from '@temporalio/common';
22
+ import { NativeConnection, Runtime } from '@temporalio/worker';
23
+ import {
24
+ EphemeralServer,
25
+ EphemeralServerConfig,
26
+ getEphemeralServerTarget,
27
+ TemporaliteConfig,
28
+ TimeSkippingServerConfig,
29
+ } from '@temporalio/core-bridge';
21
30
  import path from 'path';
22
- import os from 'os';
23
31
  import { AbortController } from 'abort-controller';
24
- import { ChildProcess, spawn, StdioOptions } from 'child_process';
25
32
  import events from 'events';
26
- import { kill, waitOnChild } from './child-process';
27
- import getPort from 'get-port';
28
- import { Connection, TestService } from './test-service-client';
29
-
30
- const TEST_SERVER_EXECUTABLE_NAME = os.platform() === 'win32' ? 'test-server.exe' : 'test-server';
33
+ import { Connection, TestService } from './connection';
34
+ import { filterNullAndUndefined } from '@temporalio/internal-non-workflow-common';
35
+ import ms from 'ms';
31
36
 
32
- export const DEFAULT_TEST_SERVER_PATH = path.join(__dirname, `../${TEST_SERVER_EXECUTABLE_NAME}`);
37
+ export { TimeSkippingServerConfig, TemporaliteConfig, EphemeralServerExecutable } from '@temporalio/core-bridge';
38
+ export { EphemeralServerConfig };
33
39
 
34
- /**
35
- * Options passed to {@link WorkflowClient.result}, these are the same as the
36
- * {@link BaseWorkflowResultOptions} with an additional option that controls
37
- * whether to toggle time skipping in the Test server while waiting on a
38
- * Workflow's result.
39
- */
40
- export interface WorkflowResultOptions extends BaseWorkflowResultOptions {
41
- /**
42
- * If set to `true`, waiting for the result does not enable time skipping
43
- */
44
- runInNormalTime?: boolean;
40
+ export interface TimeSkippingWorkflowClientOptions extends WorkflowClientOptions {
41
+ connection: Connection;
42
+ enableTimeSkipping: boolean;
45
43
  }
46
44
 
47
- /**
48
- * Options passed to {@link WorkflowClient.execute}, these are the same as the
49
- * {@link BaseWorkflowStartOptions} with an additional option that controls
50
- * whether to toggle time skipping in the Test server while waiting on a
51
- * Workflow's result.
52
- */
53
- export type WorkflowStartOptions<T extends Workflow> = BaseWorkflowStartOptions<T> & {
54
- /**
55
- * If set to `true`, waiting for the result does not enable time skipping
56
- */
57
- runInNormalTime?: boolean;
58
- };
59
-
60
- export interface WorkflowClientOptions extends BaseWorkflowClientOptions {
45
+ export interface TestEnvClientOptions extends ClientOptions {
61
46
  connection: Connection;
47
+ enableTimeSkipping: boolean;
62
48
  }
63
49
 
64
50
  /**
@@ -66,48 +52,63 @@ export interface WorkflowClientOptions extends BaseWorkflowClientOptions {
66
52
  * When this client waits on a Workflow's result, it will enable time skipping
67
53
  * in the test server.
68
54
  */
69
- export class WorkflowClient extends BaseWorkflowClient {
55
+ export class TimeSkippingWorkflowClient extends WorkflowClient {
70
56
  protected readonly testService: TestService;
57
+ protected readonly enableTimeSkipping: boolean;
71
58
 
72
- constructor(options: WorkflowClientOptions) {
59
+ constructor(options: TimeSkippingWorkflowClientOptions) {
73
60
  super(options);
61
+ this.enableTimeSkipping = options.enableTimeSkipping;
74
62
  this.testService = options.connection.testService;
75
63
  }
76
64
 
77
- /**
78
- * Execute a Workflow and wait for completion.
79
- *
80
- * @see {@link BaseWorkflowClient.execute}
81
- */
82
- public async execute<T extends Workflow>(
83
- workflowTypeOrFunc: string | T,
84
- options: WorkflowStartOptions<T>
85
- ): Promise<WorkflowResultType<T>> {
86
- return super.execute(workflowTypeOrFunc, options);
87
- }
88
-
89
65
  /**
90
66
  * Gets the result of a Workflow execution.
91
67
  *
92
- * @see {@link BaseWorkflowClient.result}
68
+ * @see {@link WorkflowClient.result}
93
69
  */
94
70
  override async result<T>(
95
71
  workflowId: string,
96
72
  runId?: string | undefined,
97
73
  opts?: WorkflowResultOptions | undefined
98
74
  ): Promise<T> {
99
- if (opts?.runInNormalTime) {
100
- return await super.result(workflowId, runId, opts);
101
- }
102
- await this.testService.unlockTimeSkipping({});
103
- try {
75
+ if (this.enableTimeSkipping) {
76
+ await this.testService.unlockTimeSkipping({});
77
+ try {
78
+ return await super.result(workflowId, runId, opts);
79
+ } finally {
80
+ await this.testService.lockTimeSkipping({});
81
+ }
82
+ } else {
104
83
  return await super.result(workflowId, runId, opts);
105
- } finally {
106
- await this.testService.lockTimeSkipping({});
107
84
  }
108
85
  }
109
86
  }
110
87
 
88
+ /**
89
+ * A client with the exact same API as the "normal" client with one exception:
90
+ * when `TestEnvClient.workflow` (an instance of {@link TimeSkippingWorkflowClient}) waits on a Workflow's result, it will enable time skipping
91
+ * in the Test Server.
92
+ */
93
+ class TestEnvClient extends Client {
94
+ constructor(options: TestEnvClientOptions) {
95
+ super(options);
96
+
97
+ const { workflow, loadedDataConverter, interceptors, ...base } = this.options;
98
+
99
+ // Recreate the client (this isn't optimal but it's better than adding public methods just for testing).
100
+ // NOTE: we cast to "any" to work around `workflow` being a readonly attribute.
101
+ (this as any).workflow = new TimeSkippingWorkflowClient({
102
+ ...base,
103
+ ...workflow,
104
+ connection: options.connection,
105
+ dataConverter: loadedDataConverter,
106
+ interceptors: interceptors.workflow,
107
+ enableTimeSkipping: options.enableTimeSkipping,
108
+ });
109
+ }
110
+ }
111
+
111
112
  /**
112
113
  * Convenience workflow interceptors
113
114
  *
@@ -116,48 +117,44 @@ export class WorkflowClient extends BaseWorkflowClient {
116
117
  */
117
118
  export const workflowInterceptorModules = [path.join(__dirname, 'assert-to-failure-interceptor')];
118
119
 
119
- export interface TestServerSpawnerOptions {
120
- /**
121
- * @default {@link DEFAULT_TEST_SERVER_PATH}
122
- */
123
- path?: string;
124
- /**
125
- * @default ignore
126
- */
127
- stdio?: StdioOptions;
128
- }
129
-
130
120
  /**
131
- * A generic callback that returns a child process
121
+ * Subset of the "normal" client options that are used to create a client for the test environment.
132
122
  */
133
- export type TestServerSpawner = (port: number) => ChildProcess;
123
+ export type ClientOptionsForTestEnv = Omit<ClientOptions, 'namespace' | 'connection'>;
134
124
 
135
125
  /**
136
126
  * Options for {@link TestWorkflowEnvironment.create}
137
127
  */
138
- export interface TestWorkflowEnvironmentOptions {
139
- testServer?: TestServerSpawner | TestServerSpawnerOptions;
140
- logger?: Logger;
141
- }
128
+ type TestWorkflowEnvironmentOptions = {
129
+ server?: EphemeralServerConfig;
130
+ client?: ClientOptionsForTestEnv;
131
+ };
142
132
 
143
- interface TestWorkflowEnvironmentOptionsWithDefaults {
144
- testServerSpawner: TestServerSpawner;
145
- logger: Logger;
146
- }
133
+ /**
134
+ * Options for {@link TestWorkflowEnvironment.createTimeSkipping}
135
+ */
136
+ export type TimeSkippingTestWorkflowEnvironmentOptions = {
137
+ server?: Omit<TimeSkippingServerConfig, 'type'>;
138
+ client?: ClientOptionsForTestEnv;
139
+ };
140
+
141
+ /**
142
+ * Options for {@link TestWorkflowEnvironment.createLocal}
143
+ */
144
+ export type LocalTestWorkflowEnvironmentOptions = {
145
+ server?: Omit<TemporaliteConfig, 'type'>;
146
+ client?: ClientOptionsForTestEnv;
147
+ };
147
148
 
148
- function addDefaults({
149
- testServer,
150
- logger,
151
- }: TestWorkflowEnvironmentOptions): TestWorkflowEnvironmentOptionsWithDefaults {
149
+ export type TestWorkflowEnvironmentOptionsWithDefaults = Required<TestWorkflowEnvironmentOptions>;
150
+
151
+ function addDefaults(opts: TestWorkflowEnvironmentOptions): TestWorkflowEnvironmentOptionsWithDefaults {
152
152
  return {
153
- testServerSpawner:
154
- typeof testServer === 'function'
155
- ? testServer
156
- : (port: number) =>
157
- spawn(testServer?.path || DEFAULT_TEST_SERVER_PATH, [`${port}`], {
158
- stdio: testServer?.stdio || 'ignore',
159
- }),
160
- logger: logger ?? new DefaultLogger('INFO'),
153
+ server: {
154
+ type: 'time-skipping',
155
+ },
156
+ client: {},
157
+ ...opts,
161
158
  };
162
159
  }
163
160
 
@@ -169,17 +166,30 @@ function addDefaults({
169
166
  */
170
167
  export class TestWorkflowEnvironment {
171
168
  /**
172
- * Get an extablished {@link Connection} to the test server
169
+ * Namespace used in this environment (taken from {@link TestWorkflowEnvironmentOptions})
170
+ */
171
+ public readonly namespace?: string;
172
+ /**
173
+ * Get an established {@link Connection} to the ephemeral server
174
+ */
175
+ public readonly connection: ConnectionLike;
176
+
177
+ /**
178
+ * A {@link TestEnvClient} for interacting with the ephemeral server
173
179
  */
174
- public readonly connection: Connection;
180
+ public readonly client: Client;
175
181
 
176
182
  /**
177
183
  * An {@link AsyncCompletionClient} for interacting with the test server
184
+ *
185
+ * @deprecated - use `client.activity` instead
178
186
  */
179
187
  public readonly asyncCompletionClient: AsyncCompletionClient;
180
188
 
181
189
  /**
182
- * A {@link WorkflowClient} for interacting with the test server
190
+ * A {@link TimeSkippingWorkflowClient} for interacting with the test server
191
+ *
192
+ * @deprecated - use `client.workflow` instead
183
193
  */
184
194
  public readonly workflowClient: WorkflowClient;
185
195
 
@@ -191,49 +201,91 @@ export class TestWorkflowEnvironment {
191
201
  public readonly nativeConnection: NativeConnection;
192
202
 
193
203
  protected constructor(
194
- protected readonly serverProc: ChildProcess,
204
+ public readonly options: TestWorkflowEnvironmentOptionsWithDefaults,
205
+ public readonly supportsTimeSkipping: boolean,
206
+ protected readonly server: EphemeralServer,
195
207
  connection: Connection,
196
208
  nativeConnection: NativeConnection
197
209
  ) {
198
210
  this.connection = connection;
199
211
  this.nativeConnection = nativeConnection;
200
- this.workflowClient = new WorkflowClient({ connection });
201
- this.asyncCompletionClient = new AsyncCompletionClient({ connection });
212
+ this.namespace = options.server.type === 'temporalite' ? options.server.namespace : undefined;
213
+ this.client = new TestEnvClient({
214
+ connection,
215
+ namespace: this.namespace,
216
+ enableTimeSkipping: options.server.type === 'time-skipping',
217
+ ...options.client,
218
+ });
219
+ // eslint-disable-next-line deprecation/deprecation
220
+ this.asyncCompletionClient = this.client.activity;
221
+ // eslint-disable-next-line deprecation/deprecation
222
+ this.workflowClient = this.client.workflow;
202
223
  }
203
224
 
204
225
  /**
205
- * Create a new test environment
226
+ * Start a time skipping workflow environment.
227
+ *
228
+ * By default, this environment will automatically skip to the next events in time when a workflow handle's `result`
229
+ * is awaited on (which includes {@link WorkflowClient.execute}). Before the result is awaited on, time can be
230
+ * manually skipped forward using {@link sleep}. The currently known time can be obtained via {@link currentTimeMs}.
231
+ *
232
+ * Internally, this environment lazily downloads a test-server binary for the current OS/arch from the [Java SDK
233
+ * releases](https://github.com/temporalio/sdk-java/releases) into the temp directory if it is not already there.
234
+ * Then the executable is started and will be killed when {@link teardown} is called.
235
+ *
236
+ * Users can reuse this environment for testing multiple independent workflows, but not concurrently. Time skipping,
237
+ * which is automatically done when awaiting a workflow result and manually done on sleep, is global to the
238
+ * environment, not to the workflow under test.
239
+ *
240
+ * We highly recommend, running tests serially when using a single environment or creating a separate environment per
241
+ * test.
242
+ *
243
+ * In the future, the test server implementation may be changed to another implementation.
206
244
  */
207
- static async create(opts?: TestWorkflowEnvironmentOptions): Promise<TestWorkflowEnvironment> {
208
- const port = await getPort();
209
-
210
- const { testServerSpawner, logger } = addDefaults(opts ?? {});
211
-
212
- const child = testServerSpawner(port);
245
+ static async createTimeSkipping(opts?: TimeSkippingTestWorkflowEnvironmentOptions): Promise<TestWorkflowEnvironment> {
246
+ // eslint-disable-next-line deprecation/deprecation
247
+ return await this.create({ server: { type: 'time-skipping', ...opts?.server }, client: opts?.client });
248
+ }
213
249
 
214
- const address = `127.0.0.1:${port}`;
215
- const connPromise = Connection.connect({ address });
250
+ /**
251
+ * Start a full Temporal server locally, downloading if necessary.
252
+ *
253
+ * This environment is good for testing full server capabilities, but does not support time skipping like
254
+ * {@link createTimeSkipping} does. {@link supportsTimeSkipping} will always return `false` for this environment.
255
+ * {@link sleep} will sleep the actual amount of time and {@link currentTimeMs} will return the current time.
256
+ *
257
+ * Internally, this uses [Temporalite](https://github.com/temporalio/temporalite). Which is a self-contained binary
258
+ * for Temporal using Sqlite persistence. This will download Temporalite to a temporary directory by default if it
259
+ * has not already been downloaded before and {@link LocalTestWorkflowEnvironmentOptions.server.executable.type} is
260
+ * `'cached-download'`.
261
+ *
262
+ * In the future, the Temporalite implementation may be changed to another implementation.
263
+ */
264
+ static async createLocal(opts?: LocalTestWorkflowEnvironmentOptions): Promise<TestWorkflowEnvironment> {
265
+ // eslint-disable-next-line deprecation/deprecation
266
+ return await this.create({ server: { type: 'temporalite', ...opts?.server }, client: opts?.client });
267
+ }
216
268
 
217
- try {
218
- await Promise.race([
219
- connPromise,
220
- waitOnChild(child).then(() => {
221
- throw new Error('Test server child process exited prematurely');
222
- }),
223
- ]);
224
- } catch (err) {
225
- try {
226
- await kill(child);
227
- } catch (error) {
228
- logger.error('Failed to kill test server child process', { error });
229
- }
230
- throw err;
231
- }
269
+ /**
270
+ * Create a new test environment
271
+ *
272
+ * @deprecated - use {@link createTimeSkipping} or {@link createLocal}
273
+ */
274
+ static async create(opts?: TestWorkflowEnvironmentOptions): Promise<TestWorkflowEnvironment> {
275
+ const optsWithDefaults = addDefaults(filterNullAndUndefined(opts ?? {}));
276
+ const server = await Runtime.instance().createEphemeralServer(optsWithDefaults.server);
277
+ const address = getEphemeralServerTarget(server);
232
278
 
233
- const conn = await connPromise;
234
279
  const nativeConnection = await NativeConnection.connect({ address });
235
-
236
- return new this(child, conn, nativeConnection);
280
+ const connection = await Connection.connect({ address });
281
+
282
+ return new this(
283
+ optsWithDefaults,
284
+ optsWithDefaults.server.type === 'time-skipping',
285
+ server,
286
+ connection,
287
+ nativeConnection
288
+ );
237
289
  }
238
290
 
239
291
  /**
@@ -242,20 +294,23 @@ export class TestWorkflowEnvironment {
242
294
  async teardown(): Promise<void> {
243
295
  await this.connection.close();
244
296
  await this.nativeConnection.close();
245
- // TODO: the server should return exit code 0
246
- await kill(this.serverProc, 'SIGINT', { validReturnCodes: [0, 130] });
297
+ await Runtime.instance().shutdownEphemeralServer(this.server);
247
298
  }
248
299
 
249
300
  /**
250
- * Wait for `durationMs` in "test server time".
301
+ * Wait for `durationMs` in "server time".
251
302
  *
252
- * The test server toggles between skipped time and normal time depending on what it needs to execute.
253
- *
254
- * This method is likely to resolve in less than `durationMs` of "real time".
303
+ * This awaits using regular setTimeout in regular environments, or manually skips time in time-skipping environments.
255
304
  *
256
305
  * Useful for simulating events far into the future like completion of long running activities.
257
306
  *
258
- * @param durationMs {@link https://www.npmjs.com/package/ms | ms} formatted string or number of milliseconds
307
+ * **Time skippping**:
308
+ *
309
+ * The time skippping server toggles between skipped time and normal time depending on what it needs to execute.
310
+ *
311
+ * This method is _likely_ to resolve in less than `durationMs` of "real time".
312
+ *
313
+ * @param durationMs number of milliseconds or {@link https://www.npmjs.com/package/ms | ms-formatted string}
259
314
  *
260
315
  * @example
261
316
  *
@@ -287,8 +342,27 @@ export class TestWorkflowEnvironment {
287
342
  * ```
288
343
  */
289
344
  sleep = async (durationMs: number | string): Promise<void> => {
290
- await this.connection.testService.unlockTimeSkippingWithSleep({ duration: msToTs(durationMs) });
345
+ if (this.supportsTimeSkipping) {
346
+ await (this.connection as Connection).testService.unlockTimeSkippingWithSleep({ duration: msToTs(durationMs) });
347
+ } else {
348
+ await new Promise((resolve) => setTimeout(resolve, typeof durationMs === 'string' ? ms(durationMs) : durationMs));
349
+ }
291
350
  };
351
+
352
+ /**
353
+ * Get the current time known to this environment.
354
+ *
355
+ * For non-time-skipping environments this is simply the system time. For time-skipping environments this is whatever
356
+ * time has been skipped to.
357
+ */
358
+ async currentTimeMs(): Promise<number> {
359
+ if (this.supportsTimeSkipping) {
360
+ const { time } = await (this.connection as Connection).testService.getCurrentTime({});
361
+ return tsToMs(time);
362
+ } else {
363
+ return Date.now();
364
+ }
365
+ }
292
366
  }
293
367
 
294
368
  /**