Skip to content
Draft
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
1 change: 1 addition & 0 deletions packages/test-helpers/package.json
Original file line number Diff line number Diff line change
Expand Up @@ -18,6 +18,7 @@
"license": "MIT",
"dependencies": {
"@temporalio/common": "workspace:*",
"@temporalio/envconfig": "workspace:*",
"@temporalio/proto": "workspace:*",
"@temporalio/testing": "workspace:*",
"@temporalio/worker": "workspace:*",
Expand Down
20 changes: 17 additions & 3 deletions packages/test-helpers/src/environment.ts
Original file line number Diff line number Diff line change
@@ -1,10 +1,12 @@
import type { LocalTestWorkflowEnvironmentOptions } from '@temporalio/testing';
import { workflowInterceptorModules as defaultWorkflowInterceptorModules } from '@temporalio/testing';
import { loadClientConnectConfig } from '@temporalio/envconfig';
import type { BundlerPlugin, WorkflowBundleWithSourceMap, BundleOptions } from '@temporalio/worker';
import { bundleWorkflowCode, DefaultLogger } from '@temporalio/worker';
import { defineSearchAttributeKey, SearchAttributeType } from '@temporalio/common/lib/search-attributes';
import { TestWorkflowEnvironment } from './wrappers';
import { baseBundlerIgnoreModules } from './bundler';
import { isSet } from './flags';

export const defaultDynamicConfigOptions = [
'system.enableActivityEagerExecution=true',
Expand Down Expand Up @@ -76,14 +78,26 @@ export async function createLocalTestEnvironment(
}

/**
* Create a test workflow environment, using an existing server if TEMPORAL_SERVICE_ADDRESS is set,
* otherwise creating a local one.
* Create a test workflow environment.
*
* Uses envconfig for the test server connection when TEMPORAL_TEST_ENV_CONFIG_SERVER is truthy, uses
* TEMPORAL_SERVICE_ADDRESS as a legacy existing-server shortcut when set, otherwise creates a local environment.
*/
export async function createTestWorkflowEnvironment(
opts?: LocalTestWorkflowEnvironmentOptions
): Promise<TestWorkflowEnvironment> {
let env: TestWorkflowEnvironment;
if (process.env.TEMPORAL_SERVICE_ADDRESS) {
if (isSet(process.env.TEMPORAL_TEST_ENV_CONFIG_SERVER, false)) {
const { namespace, connectionOptions } = loadClientConnectConfig();
const { address, apiKey, metadata, tls } = connectionOptions;
env = await TestWorkflowEnvironment.createFromExistingServer({
address,
namespace,
connectionOptions: { apiKey, metadata, tls },
client: opts?.client,
plugins: opts?.plugins,
});
} else if (process.env.TEMPORAL_SERVICE_ADDRESS) {
env = await TestWorkflowEnvironment.createFromExistingServer({
address: process.env.TEMPORAL_SERVICE_ADDRESS,
plugins: opts?.plugins,
Expand Down
1 change: 1 addition & 0 deletions packages/test-helpers/src/helpers.ts
Original file line number Diff line number Diff line change
Expand Up @@ -73,6 +73,7 @@ export function helpers<TEnv extends AnyTestWorkflowEnvironment = TestWorkflowEn
async createWorker(workerOpts?: Partial<WorkerOptions>): Promise<Worker> {
return await Worker.create({
connection: env.nativeConnection,
namespace: env.namespace,
workflowBundle,
taskQueue,
showStackTraceSources: true,
Expand Down
1 change: 1 addition & 0 deletions packages/test-helpers/tsconfig.json
Original file line number Diff line number Diff line change
Expand Up @@ -7,6 +7,7 @@
"references": [
{ "path": "../client" },
{ "path": "../common" },
{ "path": "../envconfig" },
{ "path": "../proto" },
{ "path": "../testing" },
{ "path": "../worker" },
Expand Down
2 changes: 1 addition & 1 deletion packages/test/src/helpers-integration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -171,7 +171,7 @@ export function helpers(t: ExecutionContext<Context>, env?: TestWorkflowEnvironm
return {
...base,
async createNativeConnection(opts?: Partial<NativeConnectionOptions>): Promise<NativeConnection> {
return await NativeConnection.connect({ address: testEnv.address, ...opts });
return await NativeConnection.connect({ ...testEnv.connectionOptions, address: testEnv.address, ...opts });
},
async runReplayHistory(
opts: Partial<ReplayWorkerOptions>,
Expand Down
47 changes: 47 additions & 0 deletions packages/test/src/test-ephemeral-server.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,14 @@
import fs from 'fs/promises';
import { randomUUID } from 'crypto';
import os from 'os';
import path from 'path';
import type { ExecutionContext, TestFn } from 'ava';
import anyTest from 'ava';
import type { WorkflowBundle } from '@temporalio/worker';
import { bundleWorkflowCode } from '@temporalio/worker';
import { Connection } from '@temporalio/client';
import { TestWorkflowEnvironment as RealTestWorkflowEnvironment } from '@temporalio/testing';
import { createTestWorkflowEnvironment } from '@temporalio/test-helpers';
import {
Worker,
TestWorkflowEnvironment,
Expand Down Expand Up @@ -80,6 +83,50 @@ test('TestEnvironment sets up dev server and is able to run a single workflow',
await runSimpleWorkflow(t, testEnv);
});

test.serial('Shared test harness supports envconfig and legacy existing servers', async (t) => {
const namespace = 'envconfig-test';
const sourceEnv = await RealTestWorkflowEnvironment.createLocal({ server: { namespace } });
const originalTemporalEnv = Object.fromEntries(
Object.entries(process.env).filter(([key, value]) => key.startsWith('TEMPORAL_') && value !== undefined)
) as Record<string, string>;
let envconfigEnv: TestWorkflowEnvironment | undefined;
let legacyEnv: TestWorkflowEnvironment | undefined;

const setTemporalEnv = (values: Record<string, string>) => {
for (const key of Object.keys(process.env)) {
if (key.startsWith('TEMPORAL_')) delete process.env[key];
}
Object.assign(process.env, values);
};

try {
setTemporalEnv({
TEMPORAL_TEST_ENV_CONFIG_SERVER: 'true',
TEMPORAL_CONFIG_FILE: path.join(os.tmpdir(), `missing-temporal-config-${randomUUID()}.toml`),
TEMPORAL_ADDRESS: sourceEnv.address,
TEMPORAL_NAMESPACE: namespace,
TEMPORAL_GRPC_META_TEST_HEADER: 'envconfig-test',
});

envconfigEnv = await createTestWorkflowEnvironment();
t.is(envconfigEnv.address, sourceEnv.address);
t.is(envconfigEnv.namespace, namespace);
t.is(envconfigEnv.connectionOptions.metadata?.['test-header'], 'envconfig-test');
await runSimpleWorkflow(t, envconfigEnv);
envconfigEnv = undefined;

setTemporalEnv({ TEMPORAL_SERVICE_ADDRESS: sourceEnv.address });
legacyEnv = await createTestWorkflowEnvironment();
t.is(legacyEnv.address, sourceEnv.address);
await legacyEnv.connection.ensureConnected();
} finally {
await envconfigEnv?.teardown();
await legacyEnv?.teardown();
setTemporalEnv(originalTemporalEnv);
await sourceEnv.teardown();
}
});

test.todo('TestEnvironment sets up test server with extra args');
test.todo('TestEnvironment sets up test server with specified port');
test.todo('TestEnvironment sets up test server with latest version');
Expand Down
24 changes: 20 additions & 4 deletions packages/testing/src/testing-workflow-environment.ts
Original file line number Diff line number Diff line change
Expand Up @@ -33,14 +33,19 @@ export type TimeSkippingTestWorkflowEnvironmentOptions = {
plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[];
};

type ExistingServerConnectionOptions = Pick<NativeConnectionOptions, 'apiKey' | 'metadata' | 'tls'>;

/**
* Options for {@link TestWorkflowEnvironment.createExistingServer}
* Options for {@link TestWorkflowEnvironment.createFromExistingServer}
*
* Accepts connection options that can be used for both the client and worker connections.
*/
export type ExistingServerTestWorkflowEnvironmentOptions = {
/** If not set, defaults to localhost:7233 */
address?: string;
/** If not set, defaults to default */
namespace?: string;
connectionOptions?: ExistingServerConnectionOptions;
client?: ClientOptionsForTestEnv;
plugins?: (ClientPlugin | ConnectionPlugin | NativeConnectionPlugin)[];
};
Expand Down Expand Up @@ -99,7 +104,11 @@ export class TestWorkflowEnvironment {
/**
* Address used when constructing `connection` and `nativeConnection`
*/
public readonly address: string
public readonly address: string,
/**
* Connection options used when constructing `connection` and `nativeConnection`.
*/
public readonly connectionOptions: ExistingServerConnectionOptions
) {
this.connection = connection;
this.nativeConnection = nativeConnection;
Expand Down Expand Up @@ -198,13 +207,15 @@ export class TestWorkflowEnvironment {
static async createFromExistingServer(
opts?: ExistingServerTestWorkflowEnvironmentOptions
): Promise<TestWorkflowEnvironment> {
const { apiKey, metadata, tls } = opts?.connectionOptions ?? {};
return await this.create({
server: { type: 'existing' },
client: opts?.client,
plugins: opts?.plugins,
namespace: opts?.namespace ?? 'default',
supportsTimeSkipping: false,
address: opts?.address,
connectionOptions: { apiKey, metadata, tls },
});
}

Expand All @@ -216,9 +227,10 @@ export class TestWorkflowEnvironment {
supportsTimeSkipping: boolean;
namespace?: string;
address?: string;
connectionOptions?: ExistingServerConnectionOptions;
}
): Promise<TestWorkflowEnvironment> {
const { supportsTimeSkipping, namespace, ...rest } = opts;
const { supportsTimeSkipping, namespace, connectionOptions, ...rest } = opts;
const optsWithDefaults = addDefaults(filterNullAndUndefined(rest));

let address: string;
Expand All @@ -243,12 +255,15 @@ export class TestWorkflowEnvironment {
server = 'existing';
}

const connectionOptionsWithDefaults = { ...(connectionOptions ?? {}), address };
const nativeConnection = await NativeConnection.connect(<NativeConnectionOptions & InternalConnectionOptions>{
...connectionOptionsWithDefaults,
address,
plugins: opts.plugins,
[InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping },
});
const connection = await Connection.connect(<ConnectionOptions & InternalConnectionOptions>{
...connectionOptionsWithDefaults,
address,
plugins: opts.plugins,
[InternalConnectionOptionsSymbol]: { supportsTestService: supportsTimeSkipping },
Expand All @@ -262,7 +277,8 @@ export class TestWorkflowEnvironment {
connection,
nativeConnection,
namespace,
address
address,
connectionOptionsWithDefaults
);
}

Expand Down
3 changes: 3 additions & 0 deletions pnpm-lock.yaml

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

Loading