From 2073c5385f92b0927cf745a1fec2845f19a70b10 Mon Sep 17 00:00:00 2001 From: James Watkins-Harvey Date: Thu, 20 Apr 2023 17:51:43 -0400 Subject: [PATCH] fix: Fix multiple tests on Windows and/or Temporal CLI dev server (#1104) --- packages/nyc-test-coverage/src/index.ts | 2 +- packages/test/src/helpers.ts | 3 +- packages/test/src/integration-tests.ts | 101 +++++++++++--------- packages/test/src/test-native-connection.ts | 2 +- packages/test/src/test-nyc-coverage.ts | 18 ++-- packages/test/src/test-schedules.ts | 4 +- packages/testing/src/utils.ts | 6 +- packages/workflow/src/workflow.ts | 2 +- scripts/create-certs-dir.js | 8 +- scripts/init-from-verdaccio.js | 8 +- scripts/registry.js | 6 +- scripts/utils.js | 13 +-- scripts/wait-on-temporal.mjs | 4 +- 13 files changed, 95 insertions(+), 82 deletions(-) diff --git a/packages/nyc-test-coverage/src/index.ts b/packages/nyc-test-coverage/src/index.ts index a2c23ef79..b4e5b4d9e 100644 --- a/packages/nyc-test-coverage/src/index.ts +++ b/packages/nyc-test-coverage/src/index.ts @@ -172,7 +172,7 @@ export class WorkflowCoverage { enforce: 'post' as const, test: /\.[tj]s$/, exclude: [ - /\/node_modules\//, + /[/\\]node_modules[/\\]/, path.dirname(require.resolve('@temporalio/common')), path.dirname(require.resolve('@temporalio/workflow')), path.dirname(require.resolve('@temporalio/nyc-test-coverage')), diff --git a/packages/test/src/helpers.ts b/packages/test/src/helpers.ts index 0d00382b6..a5e625578 100644 --- a/packages/test/src/helpers.ts +++ b/packages/test/src/helpers.ts @@ -51,7 +51,8 @@ export function cleanStackTrace(ostack: string): string { cleanedStack .replace(/:\d+:\d+/g, '') .replace(/^\s*/gms, ' at ') - .replace(/\[as fn\] /, ''); + .replace(/\[as fn\] /, '') + .replace(/\\/g, '/'); return normalizedStack ? `${firstLine}\n${normalizedStack}` : firstLine; } diff --git a/packages/test/src/integration-tests.ts b/packages/test/src/integration-tests.ts index 0c423068b..7993473dc 100644 --- a/packages/test/src/integration-tests.ts +++ b/packages/test/src/integration-tests.ts @@ -562,8 +562,8 @@ export function runIntegrationTests(codec?: PayloadCodec): void { workflowId: uuid4(), searchAttributes: { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], - CustomDatetimeField: [date, date], + CustomIntField: [1], + CustomDatetimeField: [date], }, memo: { note: 'foo', @@ -575,8 +575,8 @@ export function runIntegrationTests(codec?: PayloadCodec): void { t.deepEqual(execution.memo, { note: 'foo' }); t.true(execution.startTime instanceof Date); t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); - t.deepEqual(execution.searchAttributes!.CustomIntField, [1, 2]); - t.deepEqual(execution.searchAttributes!.CustomDatetimeField, [date, date]); + t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); + t.deepEqual(execution.searchAttributes!.CustomDatetimeField, [date]); t.regex((execution.searchAttributes!.BinaryChecksums as string[])[0], /@temporalio\/worker@/); }); @@ -588,15 +588,15 @@ export function runIntegrationTests(codec?: PayloadCodec): void { workflowId: uuid4(), searchAttributes: { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], - CustomDatetimeField: [date, date], + CustomIntField: [1], + CustomDatetimeField: [date], }, }); const result = await workflow.result(); t.deepEqual(result, { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], - CustomDatetimeField: [date.toISOString(), date.toISOString()], + CustomIntField: [1], + CustomDatetimeField: [date.toISOString()], datetimeInstanceofWorks: [true], arrayInstanceofWorks: [true], datetimeType: ['Date'], @@ -825,7 +825,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { }, searchAttributes: { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], + CustomIntField: [1], }, followRuns: true, }); @@ -836,7 +836,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { t.not(execution.runId, workflow.firstExecutionRunId); t.deepEqual(execution.memo, { note: 'foo' }); t.deepEqual(execution.searchAttributes!.CustomKeywordField, ['test-value']); - t.deepEqual(execution.searchAttributes!.CustomIntField, [1, 2]); + t.deepEqual(execution.searchAttributes!.CustomIntField, [1]); }); test('continue-as-new-to-different-workflow keeps memo and search attributes by default', async (t) => { @@ -850,7 +850,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { }, searchAttributes: { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], + CustomIntField: [1], }, }); await workflow.result(); @@ -859,7 +859,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { t.not(info.runId, workflow.firstExecutionRunId); t.deepEqual(info.memo, { note: 'foo' }); t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value']); - t.deepEqual(info.searchAttributes!.CustomIntField, [1, 2]); + t.deepEqual(info.searchAttributes!.CustomIntField, [1]); }); test('continue-as-new-to-different-workflow can set memo and search attributes', async (t) => { @@ -873,7 +873,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { }, searchAttributes: { CustomKeywordField: ['test-value-2'], - CustomIntField: [3, 4], + CustomIntField: [3], }, }, ], @@ -885,7 +885,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { }, searchAttributes: { CustomKeywordField: ['test-value'], - CustomIntField: [1, 2], + CustomIntField: [1], }, }); await workflow.result(); @@ -894,7 +894,7 @@ export function runIntegrationTests(codec?: PayloadCodec): void { t.not(info.runId, workflow.firstExecutionRunId); t.deepEqual(info.memo, { note: 'bar' }); t.deepEqual(info.searchAttributes!.CustomKeywordField, ['test-value-2']); - t.deepEqual(info.searchAttributes!.CustomIntField, [3, 4]); + t.deepEqual(info.searchAttributes!.CustomIntField, [3]); }); test('signalWithStart works as intended and returns correct runId', async (t) => { @@ -1259,7 +1259,9 @@ export function runIntegrationTests(codec?: PayloadCodec): void { const stacks = enhancedStack.stacks.map((s) => ({ locations: s.locations.map((l) => ({ ...l, - ...(l.filePath ? { filePath: l.filePath.replace(path.resolve(__dirname, '../../../'), '') } : undefined), + ...(l.filePath + ? { filePath: l.filePath.replace(path.resolve(__dirname, '../../../'), '').replace(/\\/g, '/') } + : undefined), })), })); t.is(enhancedStack.sdk.name, 'typescript'); @@ -1341,40 +1343,45 @@ export function runIntegrationTests(codec?: PayloadCodec): void { /** * NOTE: this test uses the `IN` operator API which requires advanced visibility as of server 1.18. - * Run with docker-compose + * It will silently succeed on servers that only support standard visibility (can't dynamically skip a test). */ test('Download and replay multiple executions with client list method', async (t) => { - const { metaClient: client } = t.context; - const taskQueue = 'test'; - const fns = [ - workflows.http, - workflows.cancelFakeProgress, - workflows.childWorkflowInvoke, - workflows.activityFailures, - ]; - const handles = await Promise.all( - fns.map((fn) => - client.workflow.start(fn, { - taskQueue, - workflowId: uuid4(), - }) - ) - ); - // Wait for the workflows to complete first - await Promise.all(handles.map((h) => h.result())); - // Test the list API too while we're at it - const workflowIds = handles.map(({ workflowId }) => `'${workflowId}'`); - const histories = client.workflow.list({ query: `WorkflowId IN (${workflowIds.join(', ')})` }).intoHistories(); - const results = Worker.runReplayHistories( - { - workflowsPath: require.resolve('./workflows'), - dataConverter: t.context.dataConverter, - }, - histories - ); + try { + const { metaClient: client } = t.context; + const taskQueue = 'test'; + const fns = [ + workflows.http, + workflows.cancelFakeProgress, + workflows.childWorkflowInvoke, + workflows.activityFailures, + ]; + const handles = await Promise.all( + fns.map((fn) => + client.workflow.start(fn, { + taskQueue, + workflowId: uuid4(), + }) + ) + ); + // Wait for the workflows to complete first + await Promise.all(handles.map((h) => h.result())); + // Test the list API too while we're at it + const workflowIds = handles.map(({ workflowId }) => `'${workflowId}'`); + const histories = client.workflow.list({ query: `WorkflowId IN (${workflowIds.join(', ')})` }).intoHistories(); + const results = await Worker.runReplayHistories( + { + workflowsPath: require.resolve('./workflows'), + dataConverter: t.context.dataConverter, + }, + histories + ); - for await (const result of results) { - t.is(result.error, undefined); + for await (const result of results) { + t.is(result.error, undefined); + } + } catch (e) { + // Don't report a test failure if the server does not support extended query + if (!(e as Error).message?.includes(`operator 'in' not allowed`)) throw e; } t.pass(); }); diff --git a/packages/test/src/test-native-connection.ts b/packages/test/src/test-native-connection.ts index 6feb0bc9b..ddb5e52bd 100644 --- a/packages/test/src/test-native-connection.ts +++ b/packages/test/src/test-native-connection.ts @@ -21,7 +21,7 @@ if (RUN_INTEGRATION_TESTS) { test('NativeConnection errors have detail', async (t) => { await t.throwsAsync(() => NativeConnection.connect({ address: 'localhost:1' }), { instanceOf: TransportError, - message: /.*Connection refused.*/, + message: /.*Connection[ ]?refused.*/i, }); }); diff --git a/packages/test/src/test-nyc-coverage.ts b/packages/test/src/test-nyc-coverage.ts index a3d5c3948..27fbb145d 100644 --- a/packages/test/src/test-nyc-coverage.ts +++ b/packages/test/src/test-nyc-coverage.ts @@ -2,7 +2,7 @@ import test from 'ava'; import { v4 as uuid4 } from 'uuid'; import * as libCoverage from 'istanbul-lib-coverage'; import { bundleWorkflowCode, Worker } from '@temporalio/worker'; -import { WorkflowClient } from '@temporalio/client'; +import { Client, WorkflowClient } from '@temporalio/client'; import { WorkflowCoverage } from '@temporalio/nyc-test-coverage'; import { RUN_INTEGRATION_TESTS } from './helpers'; import { successString } from './workflows'; @@ -26,13 +26,13 @@ if (RUN_INTEGRATION_TESTS) { workflowsPath: require.resolve('./workflows'), }) ); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(successString, { taskQueue, workflowId: uuid4() })); + const client = new Client(); + await worker.runUntil(client.workflow.execute(successString, { taskQueue, workflowId: uuid4() })); workflowCoverage.mergeIntoGlobalCoverage(); const coverageMap = libCoverage.createCoverageMap(global.__coverage__); - const successStringFileName = coverageMap.files().find((x) => x.match(/\/success-string\.js/)); + const successStringFileName = coverageMap.files().find((x) => x.match(/[/\\]success-string\.js/)); if (successStringFileName) { t.is(coverageMap.fileCoverageFor(successStringFileName).toSummary().lines.pct, 100); } else t.fail(); @@ -57,15 +57,15 @@ if (RUN_INTEGRATION_TESTS) { workflowBundle: { code }, }) ); - const client = new WorkflowClient(); - await worker.runUntil(client.execute(successString, { taskQueue, workflowId: uuid4() })); + const client = new Client(); + await worker.runUntil(client.workflow.execute(successString, { taskQueue, workflowId: uuid4() })); workflowCoverageBundler.mergeIntoGlobalCoverage(); workflowCoverageWorker.mergeIntoGlobalCoverage(); const coverageMap = libCoverage.createCoverageMap(global.__coverage__); console.log(coverageMap.files()); - const successStringFileName = coverageMap.files().find((x) => x.match(/\/success-string\.js/)); + const successStringFileName = coverageMap.files().find((x) => x.match(/[/\\]success-string\.js/)); if (successStringFileName) { t.is(coverageMap.fileCoverageFor(successStringFileName).toSummary().lines.pct, 100); } else t.fail(); @@ -91,7 +91,7 @@ if (RUN_INTEGRATION_TESTS) { const coverageMap = libCoverage.createCoverageMap(global.__coverage__); // Only user code should be included in coverage - t.is(coverageMap.files().filter((x) => x.match(/\/worker-interface.js/)).length, 0); - t.is(coverageMap.files().filter((x) => x.match(/\/ms\//)).length, 0); + t.is(coverageMap.files().filter((x) => x.match(/[/\\]worker-interface.js/)).length, 0); + t.is(coverageMap.files().filter((x) => x.match(/[/\\]ms[/\\]/)).length, 0); }); } diff --git a/packages/test/src/test-schedules.ts b/packages/test/src/test-schedules.ts index 65af71efa..130186438 100644 --- a/packages/test/src/test-schedules.ts +++ b/packages/test/src/test-schedules.ts @@ -2,10 +2,10 @@ import { randomUUID } from 'node:crypto'; import anyTest, { TestFn } from 'ava'; import asyncRetry from 'async-retry'; import { - Client, defaultPayloadConverter, CalendarSpec, CalendarSpecDescription, + Client, Connection, ScheduleHandle, ScheduleSummary, @@ -218,7 +218,7 @@ if (RUN_INTEGRATION_TESTS) { } }); - test('Interceptor is called on create schedule', async (t) => { + test.serial('Interceptor is called on create schedule', async (t) => { const clientWithInterceptor = new Client({ connection: t.context.client.connection, interceptors: { diff --git a/packages/testing/src/utils.ts b/packages/testing/src/utils.ts index d5972ec2a..b8fb301e4 100644 --- a/packages/testing/src/utils.ts +++ b/packages/testing/src/utils.ts @@ -15,7 +15,11 @@ export async function waitOnNamespace( execution: { workflowId: 'fake', runId }, }); } catch (err: any) { - if (err.details.includes('workflow history not found') || err.details.includes(runId)) { + if ( + err.details.includes('workflow history not found') || + err.details.includes('Workflow executionsRow not found') || + err.details.includes(runId) + ) { break; } if (attempt === maxAttempts) { diff --git a/packages/workflow/src/workflow.ts b/packages/workflow/src/workflow.ts index 859c7ae0a..6c5e415be 100644 --- a/packages/workflow/src/workflow.ts +++ b/packages/workflow/src/workflow.ts @@ -1210,7 +1210,7 @@ export function setDefaultSignalHandler(handler: DefaultSignalHandler | undefine * * ```ts * upsertSearchAttributes({ - * CustomIntField: [1, 2, 3], + * CustomIntField: [1], * CustomBoolField: [true] * }); * upsertSearchAttributes({ diff --git a/scripts/create-certs-dir.js b/scripts/create-certs-dir.js index e5389193f..f8f107055 100644 --- a/scripts/create-certs-dir.js +++ b/scripts/create-certs-dir.js @@ -2,6 +2,8 @@ // Used in CI flow to store the Cloud certs from GH secret into local files for testing the mTLS sample. const fs = require('fs-extra'); -fs.mkdirsSync('/tmp/temporal-certs'); -fs.writeFileSync('/tmp/temporal-certs/client.pem', process.env.TEMPORAL_CLIENT_CERT); -fs.writeFileSync('/tmp/temporal-certs/client.key', process.env.TEMPORAL_CLIENT_KEY); +const targetDir = process.argv[2] ?? '/tmp/temporal-certs'; + +fs.mkdirsSync(targetDir); +fs.writeFileSync(`${targetDir}/client.pem`, process.env.TEMPORAL_CLIENT_CERT); +fs.writeFileSync(`${targetDir}/client.key`, process.env.TEMPORAL_CLIENT_KEY); diff --git a/scripts/init-from-verdaccio.js b/scripts/init-from-verdaccio.js index 92d3861ca..a2577e05e 100644 --- a/scripts/init-from-verdaccio.js +++ b/scripts/init-from-verdaccio.js @@ -1,10 +1,10 @@ -const { resolve } = require('path'); +const { resolve, dirname } = require('path'); const { writeFileSync } = require('fs'); const { withRegistry, getArgs } = require('./registry'); const { spawnNpx } = require('./utils'); async function main() { - const { registryDir, initArgs } = await getArgs(); + const { registryDir, targetDir, initArgs } = await getArgs(); await withRegistry(registryDir, async () => { console.log('spawning npx @temporalio/create with args:', initArgs); @@ -14,12 +14,12 @@ async function main() { writeFileSync(npmConfigFile, npmConfig, { encoding: 'utf-8' }); await spawnNpx( - ['@temporalio/create', 'example', '--no-git-init', '--temporalio-version', 'latest', ...initArgs], + ['@temporalio/create', targetDir, '--no-git-init', '--temporalio-version', 'latest', ...initArgs], { stdio: 'inherit', stdout: 'inherit', stderr: 'inherit', - cwd: registryDir, + cwd: dirname(targetDir), env: { ...process.env, NPM_CONFIG_USERCONFIG: npmConfigFile, diff --git a/scripts/registry.js b/scripts/registry.js index 8e7b179c9..63cc027b8 100644 --- a/scripts/registry.js +++ b/scripts/registry.js @@ -83,11 +83,13 @@ async function getArgs() { const opts = arg( { '--registry-dir': String, + '--target-dir': String, }, { permissive: true } ); - const registryDir = opts['--registry-dir'] || (await createTempRegistryDir()); - return { registryDir, initArgs: opts._.length > 0 ? opts._ : [] }; + const registryDir = opts['--registry-dir'] ?? (await createTempRegistryDir()); + const targetDir = opts['--target-dir'] ?? path.join(registryDir, 'example'); + return { registryDir, targetDir, initArgs: opts._.length > 0 ? opts._ : [] }; } module.exports = { getArgs, withRegistry }; diff --git a/scripts/utils.js b/scripts/utils.js index 81a61e1c7..2c18473ca 100644 --- a/scripts/utils.js +++ b/scripts/utils.js @@ -42,17 +42,12 @@ async function kill(child, signal = 'SIGINT') { } async function spawnNpx(args, opts) { - let fullCommand = ['npx', '--prefer-offline', '--timing=true', '--yes', '--', ...args]; - - // NPM is a .cmd on Windows - if (process.platform == 'win32') { - fullCommand = ['cmd', '/C', ...fullCommand]; - } - - await waitOnChild(spawn(fullCommand[0], fullCommand.slice(1), opts)); + const npx = /^win/.test(process.platform) ? 'npx.cmd' : 'npx'; + const npxArgs = ['--prefer-offline', '--timing=true', '--yes', '--', ...args]; + await waitOnChild(spawn(npx, npxArgs, opts)); } -const shell = process.platform === 'win32'; +const shell = /^win/.test(process.platform); const sleep = (ms) => new Promise((resolve) => setTimeout(resolve, ms)); module.exports = { kill, spawnNpx, ChildProcessError, shell, sleep }; diff --git a/scripts/wait-on-temporal.mjs b/scripts/wait-on-temporal.mjs index 071805121..17f949519 100644 --- a/scripts/wait-on-temporal.mjs +++ b/scripts/wait-on-temporal.mjs @@ -17,7 +17,9 @@ try { } catch (err) { if ( err.details && - (err.details.includes('workflow history not found') || err.details.includes('operation GetCurrentExecution')) + (err.details.includes('workflow history not found') || + err.details.includes('Workflow executionsRow not found') || + err.details.includes('operation GetCurrentExecution')) ) { break; }