From 8ff30be333c02b21868262ff5cb242a4e1e90426 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 00:17:47 +0100 Subject: [PATCH 01/21] improve makeRuntime interface --- src/api/run_cucumber.ts | 4 ++-- src/api/runtime.ts | 12 ++++++------ 2 files changed, 8 insertions(+), 8 deletions(-) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index 0d86a0bb1..a1344f542 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -146,8 +146,8 @@ Running from: ${__dirname} newId, }) - const runtime = makeRuntime({ - cwd, + const runtime = await makeRuntime({ + environment, logger, eventBroadcaster, eventDataCollector, diff --git a/src/api/runtime.ts b/src/api/runtime.ts index c3e55fa92..a77f206fd 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -5,10 +5,10 @@ import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import Coordinator from '../runtime/parallel/coordinator' import { ILogger } from '../logger' -import { IRunOptionsRuntime } from './types' +import { IRunEnvironment, IRunOptionsRuntime } from './types' -export function makeRuntime({ - cwd, +export async function makeRuntime({ + environment, logger, eventBroadcaster, eventDataCollector, @@ -17,7 +17,7 @@ export function makeRuntime({ supportCodeLibrary, options: { parallel, ...options }, }: { - cwd: string + environment: IRunEnvironment logger: ILogger eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector @@ -25,10 +25,10 @@ export function makeRuntime({ pickleIds: string[] supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime -}): IRuntime { +}): Promise { if (parallel > 0) { return new Coordinator({ - cwd, + cwd: environment.cwd, logger, eventBroadcaster, eventDataCollector, From 110ee0810a8fd16b13b9213807ab718ebc0690b9 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 00:25:59 +0100 Subject: [PATCH 02/21] simplify stopwatch and timestamp --- src/runtime/index.ts | 7 +++---- src/runtime/parallel/coordinator.ts | 6 +++--- src/runtime/parallel/worker.ts | 4 ---- src/runtime/stopwatch.ts | 10 ++++------ src/runtime/stopwatch_spec.ts | 4 ++-- src/runtime/test_case_runner.ts | 14 +++++--------- src/runtime/test_case_runner_spec.ts | 2 -- 7 files changed, 17 insertions(+), 30 deletions(-) diff --git a/src/runtime/index.ts b/src/runtime/index.ts index b75ad487e..6e942ac55 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -7,7 +7,7 @@ import { SupportCodeLibrary } from '../support_code_library_builder/types' import { assembleTestCases } from './assemble_test_cases' import { retriesForPickle, shouldCauseFailure } from './helpers' import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' -import { IStopwatch, create } from './stopwatch' +import { IStopwatch, create, timestamp } from './stopwatch' import TestCaseRunner from './test_case_runner' export interface IRuntime { @@ -77,7 +77,6 @@ export default class Runtime implements IRuntime { const skip = this.options.dryRun || (this.options.failFast && !this.success) const testCaseRunner = new TestCaseRunner({ eventBroadcaster: this.eventBroadcaster, - stopwatch: this.stopwatch, gherkinDocument: this.eventDataCollector.getGherkinDocument(pickle.uri), newId: this.newId, pickle, @@ -97,7 +96,7 @@ export default class Runtime implements IRuntime { async start(): Promise { const testRunStarted: messages.Envelope = { testRunStarted: { - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), }, } this.eventBroadcaster.emit('envelope', testRunStarted) @@ -124,7 +123,7 @@ export default class Runtime implements IRuntime { this.stopwatch.stop() const testRunFinished: messages.Envelope = { testRunFinished: { - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), success: this.success, }, } diff --git a/src/runtime/parallel/coordinator.ts b/src/runtime/parallel/coordinator.ts index a72c58329..a0f30a686 100644 --- a/src/runtime/parallel/coordinator.ts +++ b/src/runtime/parallel/coordinator.ts @@ -8,7 +8,7 @@ import { EventDataCollector } from '../../formatter/helpers' import { IRuntime, IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' -import { IStopwatch, create } from '../stopwatch' +import { IStopwatch, create, timestamp } from '../stopwatch' import { assembleTestCases, IAssembledTestCases } from '../assemble_test_cases' import { ILogger } from '../../logger' import { ICoordinatorReport, IWorkerCommand } from './command_types' @@ -177,7 +177,7 @@ export default class Coordinator implements IRuntime { ) { const envelope: messages.Envelope = { testRunFinished: { - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), success, }, } @@ -205,7 +205,7 @@ export default class Coordinator implements IRuntime { async start(): Promise { const envelope: messages.Envelope = { testRunStarted: { - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), }, } this.eventBroadcaster.emit('envelope', envelope) diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index 5d2f5aa2a..c166dc644 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -8,7 +8,6 @@ import supportCodeLibraryBuilder from '../../support_code_library_builder' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { makeRunTestRunHooks, RunsTestRunHooks } from '../run_test_run_hooks' -import { create } from '../stopwatch' import TestCaseRunner from '../test_case_runner' import tryRequire from '../../try_require' import { @@ -116,15 +115,12 @@ export default class Worker { gherkinDocument, pickle, testCase, - elapsed, retries, skip, }: IWorkerCommandRun): Promise { - const stopwatch = create(elapsed) const testCaseRunner = new TestCaseRunner({ workerId: this.id, eventBroadcaster: this.eventBroadcaster, - stopwatch, gherkinDocument, newId: this.newId, pickle, diff --git a/src/runtime/stopwatch.ts b/src/runtime/stopwatch.ts index b41711814..9284e4a2b 100644 --- a/src/runtime/stopwatch.ts +++ b/src/runtime/stopwatch.ts @@ -1,4 +1,4 @@ -import { Duration, TimeConversion, Timestamp } from '@cucumber/messages' +import { Duration, TimeConversion } from '@cucumber/messages' import methods from '../time' /** @@ -9,7 +9,6 @@ export interface IStopwatch { start: () => IStopwatch stop: () => IStopwatch duration: () => Duration - timestamp: () => Timestamp } class StopwatchImpl implements IStopwatch { @@ -39,10 +38,9 @@ class StopwatchImpl implements IStopwatch { ) ) } - - timestamp(): Timestamp { - return TimeConversion.millisecondsSinceEpochToTimestamp(methods.Date.now()) - } } export const create = (base?: Duration): IStopwatch => new StopwatchImpl(base) + +export const timestamp = () => + TimeConversion.millisecondsSinceEpochToTimestamp(methods.Date.now()) diff --git a/src/runtime/stopwatch_spec.ts b/src/runtime/stopwatch_spec.ts index 017f44bd4..10bc78579 100644 --- a/src/runtime/stopwatch_spec.ts +++ b/src/runtime/stopwatch_spec.ts @@ -1,7 +1,7 @@ import { describe, it } from 'mocha' import { expect } from 'chai' import { TimeConversion } from '@cucumber/messages' -import { create } from './stopwatch' +import { create, timestamp } from './stopwatch' describe('stopwatch', () => { it('returns a duration between the start and stop', async () => { @@ -47,7 +47,7 @@ describe('stopwatch', () => { it('returns a timestamp close to now', () => { expect( - TimeConversion.timestampToMillisecondsSinceEpoch(create().timestamp()) + TimeConversion.timestampToMillisecondsSinceEpoch(timestamp()) ).to.be.closeTo(Date.now(), 100) }) }) diff --git a/src/runtime/test_case_runner.ts b/src/runtime/test_case_runner.ts index 5471d53ee..efca1392a 100644 --- a/src/runtime/test_case_runner.ts +++ b/src/runtime/test_case_runner.ts @@ -13,7 +13,7 @@ import { IDefinition } from '../models/definition' import { doesHaveValue, doesNotHaveValue } from '../value_checker' import StepDefinition from '../models/step_definition' import { IWorldOptions } from '../support_code_library_builder/world' -import { IStopwatch } from './stopwatch' +import { timestamp } from './stopwatch' import StepRunner from './step_runner' import AttachmentManager from './attachment_manager' import { getAmbiguousStepException } from './helpers' @@ -21,7 +21,6 @@ import { getAmbiguousStepException } from './helpers' export interface INewTestCaseRunnerOptions { workerId?: string eventBroadcaster: EventEmitter - stopwatch: IStopwatch gherkinDocument: messages.GherkinDocument newId: IdGenerator.NewId pickle: messages.Pickle @@ -39,7 +38,6 @@ export default class TestCaseRunner { private currentTestCaseStartedId: string private currentTestStepId: string private readonly eventBroadcaster: EventEmitter - private readonly stopwatch: IStopwatch private readonly gherkinDocument: messages.GherkinDocument private readonly newId: IdGenerator.NewId private readonly pickle: messages.Pickle @@ -55,7 +53,6 @@ export default class TestCaseRunner { constructor({ workerId, eventBroadcaster, - stopwatch, gherkinDocument, newId, pickle, @@ -88,7 +85,6 @@ export default class TestCaseRunner { } ) this.eventBroadcaster = eventBroadcaster - this.stopwatch = stopwatch this.gherkinDocument = gherkinDocument this.maxAttempts = 1 + (skip ? 0 : retries) this.newId = newId @@ -169,7 +165,7 @@ export default class TestCaseRunner { testStepStarted: { testCaseStartedId: this.currentTestCaseStartedId, testStepId, - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), }, } this.eventBroadcaster.emit('envelope', testStepStarted) @@ -182,7 +178,7 @@ export default class TestCaseRunner { testCaseStartedId: this.currentTestCaseStartedId, testStepId, testStepResult, - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), }, } this.eventBroadcaster.emit('envelope', testStepFinished) @@ -215,7 +211,7 @@ export default class TestCaseRunner { attempt, testCaseId: this.testCase.id, id: this.currentTestCaseStartedId, - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), }, } if (this.workerId) { @@ -260,7 +256,7 @@ export default class TestCaseRunner { const testCaseFinished: messages.Envelope = { testCaseFinished: { testCaseStartedId: this.currentTestCaseStartedId, - timestamp: this.stopwatch.timestamp(), + timestamp: timestamp(), willBeRetried, }, } diff --git a/src/runtime/test_case_runner_spec.ts b/src/runtime/test_case_runner_spec.ts index 33a07f752..83df10403 100644 --- a/src/runtime/test_case_runner_spec.ts +++ b/src/runtime/test_case_runner_spec.ts @@ -12,7 +12,6 @@ import { getBaseSupportCodeLibrary } from '../../test/fixtures/steps' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { valueOrDefault } from '../value_checker' import TestCaseRunner from './test_case_runner' -import { create } from './stopwatch' import { assembleTestCases } from './assemble_test_cases' import IEnvelope = messages.Envelope @@ -50,7 +49,6 @@ async function testRunner( const runner = new TestCaseRunner({ workerId: options.workerId, eventBroadcaster, - stopwatch: create(), gherkinDocument: options.gherkinDocument, newId, pickle: options.pickle, From 1087547f92c7965b8534601ba2bcf5f0eb43e891 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 08:06:57 +0100 Subject: [PATCH 03/21] split out test case assembly to own module --- src/api/run_cucumber.ts | 10 ++--- src/api/runtime.ts | 6 ++- .../assemble_test_cases.ts | 38 +++++++++++++++---- .../assemble_test_cases_spec.ts | 21 +++++----- src/assemble/index.ts | 2 + src/assemble/types.ts | 10 +++++ src/runtime/index.ts | 4 +- src/runtime/parallel/coordinator.ts | 9 +++-- src/runtime/test_case_runner_spec.ts | 4 +- 9 files changed, 74 insertions(+), 30 deletions(-) rename src/{runtime => assemble}/assemble_test_cases.ts (79%) rename src/{runtime => assemble}/assemble_test_cases_spec.ts (92%) create mode 100644 src/assemble/index.ts create mode 100644 src/assemble/types.ts diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index a1344f542..ea62ab1cb 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -5,6 +5,7 @@ import { emitMetaMessage, emitSupportCodeMessages } from '../cli/helpers' import { resolvePaths } from '../paths' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { version } from '../version' +import { IFilterablePickle } from '../filter' import { IRunOptions, IRunEnvironment, IRunResult } from './types' import { makeRuntime } from './runtime' import { initializeFormatters } from './formatters' @@ -105,7 +106,7 @@ Running from: ${__dirname} }) await emitMetaMessage(eventBroadcaster, env) - let pickleIds: string[] = [] + let filteredPickles: ReadonlyArray = [] let parseErrors: ParseError[] = [] if (sourcePaths.length > 0) { const gherkinResult = await getPicklesAndErrors({ @@ -115,15 +116,14 @@ Running from: ${__dirname} coordinates: options.sources, onEnvelope: (envelope) => eventBroadcaster.emit('envelope', envelope), }) - const filteredPickles = await pluginManager.transform( + filteredPickles = await pluginManager.transform( 'pickles:filter', gherkinResult.filterablePickles ) - const orderedPickles = await pluginManager.transform( + filteredPickles = await pluginManager.transform( 'pickles:order', filteredPickles ) - pickleIds = orderedPickles.map(({ pickle }) => pickle.id) parseErrors = gherkinResult.parseErrors } if (parseErrors.length) { @@ -151,7 +151,7 @@ Running from: ${__dirname} logger, eventBroadcaster, eventDataCollector, - pickleIds, + filteredPickles, newId, supportCodeLibrary, options: options.runtime, diff --git a/src/api/runtime.ts b/src/api/runtime.ts index a77f206fd..f2528b131 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -5,6 +5,7 @@ import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import Coordinator from '../runtime/parallel/coordinator' import { ILogger } from '../logger' +import { IFilterablePickle } from '../filter' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ @@ -12,7 +13,7 @@ export async function makeRuntime({ logger, eventBroadcaster, eventDataCollector, - pickleIds, + filteredPickles, newId, supportCodeLibrary, options: { parallel, ...options }, @@ -22,10 +23,11 @@ export async function makeRuntime({ eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector newId: IdGenerator.NewId - pickleIds: string[] + filteredPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime }): Promise { + const pickleIds = filteredPickles.map((pickle) => pickle.pickle.id) if (parallel > 0) { return new Coordinator({ cwd: environment.cwd, diff --git a/src/runtime/assemble_test_cases.ts b/src/assemble/assemble_test_cases.ts similarity index 79% rename from src/runtime/assemble_test_cases.ts rename to src/assemble/assemble_test_cases.ts index bab2993e1..07b167d20 100644 --- a/src/runtime/assemble_test_cases.ts +++ b/src/assemble/assemble_test_cases.ts @@ -4,23 +4,47 @@ import { IdGenerator } from '@cucumber/messages' import { Group } from '@cucumber/cucumber-expressions' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { doesHaveValue } from '../value_checker' +import { IFilterablePickle } from '../filter' +import { AssembledTestCase, TestCasesByPickleId } from './types' -export declare type IAssembledTestCases = Record - -export interface IAssembleTestCasesOptions { +export async function assembleTestCases({ + eventBroadcaster, + newId, + filteredPickles, + supportCodeLibrary, +}: { eventBroadcaster: EventEmitter newId: IdGenerator.NewId - pickles: messages.Pickle[] + filteredPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary +}): Promise> { + const testCasesByPickleId = await assembleTestCasesByPickleId({ + eventBroadcaster, + newId, + pickles: filteredPickles.map(({ pickle }) => pickle), + supportCodeLibrary, + }) + return filteredPickles.map(({ gherkinDocument, pickle }) => { + return { + gherkinDocument, + pickle, + testCase: testCasesByPickleId[pickle.id], + } + }) } -export async function assembleTestCases({ +export async function assembleTestCasesByPickleId({ eventBroadcaster, newId, pickles, supportCodeLibrary, -}: IAssembleTestCasesOptions): Promise { - const result: IAssembledTestCases = {} +}: { + eventBroadcaster: EventEmitter + newId: IdGenerator.NewId + pickles: messages.Pickle[] + supportCodeLibrary: SupportCodeLibrary +}): Promise { + const result: TestCasesByPickleId = {} for (const pickle of pickles) { const { id: pickleId } = pickle const testCaseId = newId() diff --git a/src/runtime/assemble_test_cases_spec.ts b/src/assemble/assemble_test_cases_spec.ts similarity index 92% rename from src/runtime/assemble_test_cases_spec.ts rename to src/assemble/assemble_test_cases_spec.ts index eaee9418f..19a23a3d5 100644 --- a/src/runtime/assemble_test_cases_spec.ts +++ b/src/assemble/assemble_test_cases_spec.ts @@ -8,7 +8,8 @@ import timeMethods from '../time' import { buildSupportCodeLibrary } from '../../test/runtime_helpers' import { parse } from '../../test/gherkin_helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' -import { assembleTestCases, IAssembledTestCases } from './assemble_test_cases' +import { assembleTestCasesByPickleId } from './assemble_test_cases' +import { TestCasesByPickleId } from './types' interface IRequest { gherkinDocument: messages.GherkinDocument @@ -18,14 +19,16 @@ interface IRequest { interface IResponse { envelopes: messages.Envelope[] - result: IAssembledTestCases + result: TestCasesByPickleId } -async function testAssembleTestCases(options: IRequest): Promise { +async function testAssembleTestCasesByPickleId( + options: IRequest +): Promise { const envelopes: messages.Envelope[] = [] const eventBroadcaster = new EventEmitter() eventBroadcaster.on('envelope', (e) => envelopes.push(e)) - const result = await assembleTestCases({ + const result = await assembleTestCasesByPickleId({ eventBroadcaster, newId: IdGenerator.incrementing(), pickles: options.pickles, @@ -45,7 +48,7 @@ describe('assembleTestCases', () => { clock.uninstall() }) - describe('assembleTestCases()', () => { + describe('assembleTestCasesByPickleId()', () => { it('emits testCase messages', async () => { // Arrange const supportCodeLibrary = buildSupportCodeLibrary(({ Given }) => { @@ -65,7 +68,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes, result } = await testAssembleTestCases({ + const { envelopes, result } = await testAssembleTestCasesByPickleId({ gherkinDocument, pickles, supportCodeLibrary, @@ -137,7 +140,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCases({ + const { envelopes } = await testAssembleTestCasesByPickleId({ gherkinDocument, pickles, supportCodeLibrary, @@ -211,7 +214,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCases({ + const { envelopes } = await testAssembleTestCasesByPickleId({ gherkinDocument, pickles, supportCodeLibrary, @@ -265,7 +268,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCases({ + const { envelopes } = await testAssembleTestCasesByPickleId({ gherkinDocument, pickles, supportCodeLibrary, diff --git a/src/assemble/index.ts b/src/assemble/index.ts new file mode 100644 index 000000000..549c096ae --- /dev/null +++ b/src/assemble/index.ts @@ -0,0 +1,2 @@ +export * from './assemble_test_cases' +export * from './types' diff --git a/src/assemble/types.ts b/src/assemble/types.ts new file mode 100644 index 000000000..821ae2989 --- /dev/null +++ b/src/assemble/types.ts @@ -0,0 +1,10 @@ +import { GherkinDocument, Pickle, TestCase } from '@cucumber/messages' +import * as messages from '@cucumber/messages' + +export declare type TestCasesByPickleId = Record + +export interface AssembledTestCase { + gherkinDocument: GherkinDocument + pickle: Pickle + testCase: TestCase +} diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 6e942ac55..1d82cab21 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -4,7 +4,7 @@ import { IdGenerator } from '@cucumber/messages' import { JsonObject } from 'type-fest' import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' -import { assembleTestCases } from './assemble_test_cases' +import { assembleTestCasesByPickleId } from '../assemble' import { retriesForPickle, shouldCauseFailure } from './helpers' import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' import { IStopwatch, create, timestamp } from './stopwatch' @@ -105,7 +105,7 @@ export default class Runtime implements IRuntime { this.supportCodeLibrary.beforeTestRunHookDefinitions, 'a BeforeAll' ) - const assembledTestCases = await assembleTestCases({ + const assembledTestCases = await assembleTestCasesByPickleId({ eventBroadcaster: this.eventBroadcaster, newId: this.newId, pickles: this.pickleIds.map((pickleId) => diff --git a/src/runtime/parallel/coordinator.ts b/src/runtime/parallel/coordinator.ts index a0f30a686..84b91dd55 100644 --- a/src/runtime/parallel/coordinator.ts +++ b/src/runtime/parallel/coordinator.ts @@ -9,7 +9,10 @@ import { IRuntime, IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { IStopwatch, create, timestamp } from '../stopwatch' -import { assembleTestCases, IAssembledTestCases } from '../assemble_test_cases' +import { + assembleTestCasesByPickleId, + TestCasesByPickleId, +} from '../../assemble' import { ILogger } from '../../logger' import { ICoordinatorReport, IWorkerCommand } from './command_types' @@ -54,7 +57,7 @@ export default class Coordinator implements IRuntime { private readonly options: IRuntimeOptions private readonly newId: IdGenerator.NewId private readonly pickleIds: string[] - private assembledTestCases: IAssembledTestCases + private assembledTestCases: TestCasesByPickleId private readonly inProgressPickles: Record private readonly workers: Record private readonly supportCodeLibrary: SupportCodeLibrary @@ -210,7 +213,7 @@ export default class Coordinator implements IRuntime { } this.eventBroadcaster.emit('envelope', envelope) this.stopwatch.start() - this.assembledTestCases = await assembleTestCases({ + this.assembledTestCases = await assembleTestCasesByPickleId({ eventBroadcaster: this.eventBroadcaster, newId: this.newId, pickles: this.pickleIds.map((pickleId) => diff --git a/src/runtime/test_case_runner_spec.ts b/src/runtime/test_case_runner_spec.ts index 83df10403..c6163c80c 100644 --- a/src/runtime/test_case_runner_spec.ts +++ b/src/runtime/test_case_runner_spec.ts @@ -11,8 +11,8 @@ import timeMethods from '../time' import { getBaseSupportCodeLibrary } from '../../test/fixtures/steps' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { valueOrDefault } from '../value_checker' +import { assembleTestCasesByPickleId } from '../assemble/assemble_test_cases' import TestCaseRunner from './test_case_runner' -import { assembleTestCases } from './assemble_test_cases' import IEnvelope = messages.Envelope interface ITestRunnerRequest { @@ -36,7 +36,7 @@ async function testRunner( const eventBroadcaster = new EventEmitter() const newId = IdGenerator.incrementing() const testCase = ( - await assembleTestCases({ + await assembleTestCasesByPickleId({ eventBroadcaster, newId, pickles: [options.pickle], From aa0640b99cbcb7ef873fdcc93f85cdbaddb3c34e Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 16:32:23 +0100 Subject: [PATCH 04/21] crudely split out adapter within coordinator --- src/api/runtime.ts | 27 ++++++++++--------- src/runtime/coordinator.ts | 10 +++++++ ...{coordinator.ts => coordinator_adapter.ts} | 5 ++-- src/runtime/types.ts | 3 +++ 4 files changed, 31 insertions(+), 14 deletions(-) create mode 100644 src/runtime/coordinator.ts rename src/runtime/parallel/{coordinator.ts => coordinator_adapter.ts} (98%) create mode 100644 src/runtime/types.ts diff --git a/src/api/runtime.ts b/src/api/runtime.ts index f2528b131..d7c9f959b 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -3,9 +3,10 @@ import { IdGenerator } from '@cucumber/messages' import Runtime, { IRuntime } from '../runtime' import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' -import Coordinator from '../runtime/parallel/coordinator' import { ILogger } from '../logger' import { IFilterablePickle } from '../filter' +import { Coordinator } from '../runtime/coordinator' +import { ChildProcessCoordinatorAdapter } from '../runtime/parallel/coordinator_adapter' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ @@ -29,17 +30,19 @@ export async function makeRuntime({ }): Promise { const pickleIds = filteredPickles.map((pickle) => pickle.pickle.id) if (parallel > 0) { - return new Coordinator({ - cwd: environment.cwd, - logger, - eventBroadcaster, - eventDataCollector, - pickleIds, - options, - newId, - supportCodeLibrary, - numberOfWorkers: parallel, - }) + return new Coordinator( + new ChildProcessCoordinatorAdapter({ + cwd: environment.cwd, + logger, + eventBroadcaster, + eventDataCollector, + pickleIds, + options, + newId, + supportCodeLibrary, + numberOfWorkers: parallel, + }) + ) } return new Runtime({ eventBroadcaster, diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts new file mode 100644 index 000000000..68fb1b409 --- /dev/null +++ b/src/runtime/coordinator.ts @@ -0,0 +1,10 @@ +import { CoordinatorAdapter } from './types' +import { IRuntime } from './index' + +export class Coordinator implements IRuntime { + constructor(private adapter: CoordinatorAdapter) {} + + async start(): Promise { + return await this.adapter.start() + } +} diff --git a/src/runtime/parallel/coordinator.ts b/src/runtime/parallel/coordinator_adapter.ts similarity index 98% rename from src/runtime/parallel/coordinator.ts rename to src/runtime/parallel/coordinator_adapter.ts index 84b91dd55..3f4d5db63 100644 --- a/src/runtime/parallel/coordinator.ts +++ b/src/runtime/parallel/coordinator_adapter.ts @@ -5,7 +5,7 @@ import * as messages from '@cucumber/messages' import { IdGenerator } from '@cucumber/messages' import { retriesForPickle, shouldCauseFailure } from '../helpers' import { EventDataCollector } from '../../formatter/helpers' -import { IRuntime, IRuntimeOptions } from '..' +import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { IStopwatch, create, timestamp } from '../stopwatch' @@ -14,6 +14,7 @@ import { TestCasesByPickleId, } from '../../assemble' import { ILogger } from '../../logger' +import { CoordinatorAdapter } from '../types' import { ICoordinatorReport, IWorkerCommand } from './command_types' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') @@ -48,7 +49,7 @@ interface IPicklePlacement { pickle: messages.Pickle } -export default class Coordinator implements IRuntime { +export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { private readonly cwd: string private readonly eventBroadcaster: EventEmitter private readonly eventDataCollector: EventDataCollector diff --git a/src/runtime/types.ts b/src/runtime/types.ts new file mode 100644 index 000000000..ba321091e --- /dev/null +++ b/src/runtime/types.ts @@ -0,0 +1,3 @@ +export interface CoordinatorAdapter { + start(): Promise +} From df5eb104e52f545a03b480c790ec6f64d597a029 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 16:35:58 +0100 Subject: [PATCH 05/21] enable github actions for this branch --- .github/workflows/build.yaml | 1 + 1 file changed, 1 insertion(+) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index c4e6ea11e..fdfc8b9bd 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,6 +7,7 @@ on: push: branches: - main + - feat/unified-runner pull_request: branches: - main From 869ed2c8c3abf13f49b183fe82e59ed740f4efe6 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 16:42:41 +0100 Subject: [PATCH 06/21] no need for stateful stopwatch here --- src/runtime/parallel/command_types.ts | 1 - src/runtime/parallel/coordinator_adapter.ts | 6 +----- 2 files changed, 1 insertion(+), 6 deletions(-) diff --git a/src/runtime/parallel/command_types.ts b/src/runtime/parallel/command_types.ts index bfb4f26a3..ca16daeb9 100644 --- a/src/runtime/parallel/command_types.ts +++ b/src/runtime/parallel/command_types.ts @@ -26,7 +26,6 @@ export interface ICanonicalSupportCodeIds { export interface IWorkerCommandRun { retries: number skip: boolean - elapsed: messages.Duration pickle: messages.Pickle testCase: messages.TestCase gherkinDocument: messages.GherkinDocument diff --git a/src/runtime/parallel/coordinator_adapter.ts b/src/runtime/parallel/coordinator_adapter.ts index 3f4d5db63..3e73512fc 100644 --- a/src/runtime/parallel/coordinator_adapter.ts +++ b/src/runtime/parallel/coordinator_adapter.ts @@ -8,7 +8,7 @@ import { EventDataCollector } from '../../formatter/helpers' import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' -import { IStopwatch, create, timestamp } from '../stopwatch' +import { timestamp } from '../stopwatch' import { assembleTestCasesByPickleId, TestCasesByPickleId, @@ -53,7 +53,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { private readonly cwd: string private readonly eventBroadcaster: EventEmitter private readonly eventDataCollector: EventDataCollector - private readonly stopwatch: IStopwatch private onFinish: (success: boolean) => void private readonly options: IRuntimeOptions private readonly newId: IdGenerator.NewId @@ -82,7 +81,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { this.logger = logger this.eventBroadcaster = eventBroadcaster this.eventDataCollector = eventDataCollector - this.stopwatch = create() this.options = options this.newId = newId this.supportCodeLibrary = supportCodeLibrary @@ -213,7 +211,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { }, } this.eventBroadcaster.emit('envelope', envelope) - this.stopwatch.start() this.assembledTestCases = await assembleTestCasesByPickleId({ eventBroadcaster: this.eventBroadcaster, newId: this.newId, @@ -291,7 +288,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { run: { retries, skip, - elapsed: this.stopwatch.duration(), pickle, testCase, gherkinDocument, From d8237aa831c07e88eccf7edd9ba7367df30e2f2f Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 16:59:11 +0100 Subject: [PATCH 07/21] move test run start/finish messages up --- src/api/runtime.ts | 1 + src/runtime/coordinator.ts | 22 +++++++++++++++++++-- src/runtime/parallel/coordinator_adapter.ts | 14 ------------- 3 files changed, 21 insertions(+), 16 deletions(-) diff --git a/src/api/runtime.ts b/src/api/runtime.ts index d7c9f959b..1f3629e02 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -31,6 +31,7 @@ export async function makeRuntime({ const pickleIds = filteredPickles.map((pickle) => pickle.pickle.id) if (parallel > 0) { return new Coordinator( + eventBroadcaster, new ChildProcessCoordinatorAdapter({ cwd: environment.cwd, logger, diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts index 68fb1b409..0cc297dd8 100644 --- a/src/runtime/coordinator.ts +++ b/src/runtime/coordinator.ts @@ -1,10 +1,28 @@ +import { EventEmitter } from 'node:events' +import { Envelope } from '@cucumber/messages' import { CoordinatorAdapter } from './types' +import { timestamp } from './stopwatch' import { IRuntime } from './index' export class Coordinator implements IRuntime { - constructor(private adapter: CoordinatorAdapter) {} + constructor( + private eventBroadcaster: EventEmitter, + private adapter: CoordinatorAdapter + ) {} async start(): Promise { - return await this.adapter.start() + this.eventBroadcaster.emit('envelope', { + testRunStarted: { + timestamp: timestamp(), + }, + } satisfies Envelope) + const success = await this.adapter.start() + this.eventBroadcaster.emit('envelope', { + testRunFinished: { + timestamp: timestamp(), + success, + }, + } satisfies Envelope) + return success } } diff --git a/src/runtime/parallel/coordinator_adapter.ts b/src/runtime/parallel/coordinator_adapter.ts index 3e73512fc..ada3dc9a3 100644 --- a/src/runtime/parallel/coordinator_adapter.ts +++ b/src/runtime/parallel/coordinator_adapter.ts @@ -8,7 +8,6 @@ import { EventDataCollector } from '../../formatter/helpers' import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' -import { timestamp } from '../stopwatch' import { assembleTestCasesByPickleId, TestCasesByPickleId, @@ -177,13 +176,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { if ( Object.values(this.workers).every((x) => x.state === WorkerState.closed) ) { - const envelope: messages.Envelope = { - testRunFinished: { - timestamp: timestamp(), - success, - }, - } - this.eventBroadcaster.emit('envelope', envelope) this.onFinish(this.success) } } @@ -205,12 +197,6 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { } async start(): Promise { - const envelope: messages.Envelope = { - testRunStarted: { - timestamp: timestamp(), - }, - } - this.eventBroadcaster.emit('envelope', envelope) this.assembledTestCases = await assembleTestCasesByPickleId({ eventBroadcaster: this.eventBroadcaster, newId: this.newId, From 4bb7e610e475d5fcfe88cb3cee7924db383e32a8 Mon Sep 17 00:00:00 2001 From: David Goss Date: Tue, 20 Aug 2024 17:51:14 +0100 Subject: [PATCH 08/21] move test case assembly up --- src/api/runtime.ts | 10 +-- src/runtime/coordinator.ts | 20 +++++- src/runtime/parallel/coordinator_adapter.ts | 80 +++++++-------------- src/runtime/types.ts | 4 +- 4 files changed, 52 insertions(+), 62 deletions(-) diff --git a/src/api/runtime.ts b/src/api/runtime.ts index 1f3629e02..bd953eab3 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -4,9 +4,9 @@ import Runtime, { IRuntime } from '../runtime' import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { ILogger } from '../logger' -import { IFilterablePickle } from '../filter' import { Coordinator } from '../runtime/coordinator' import { ChildProcessCoordinatorAdapter } from '../runtime/parallel/coordinator_adapter' +import { IFilterablePickle } from '../filter' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ @@ -28,18 +28,18 @@ export async function makeRuntime({ supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime }): Promise { - const pickleIds = filteredPickles.map((pickle) => pickle.pickle.id) if (parallel > 0) { return new Coordinator( eventBroadcaster, + newId, + filteredPickles, + supportCodeLibrary, new ChildProcessCoordinatorAdapter({ cwd: environment.cwd, logger, eventBroadcaster, eventDataCollector, - pickleIds, options, - newId, supportCodeLibrary, numberOfWorkers: parallel, }) @@ -49,7 +49,7 @@ export async function makeRuntime({ eventBroadcaster, eventDataCollector, newId, - pickleIds, + pickleIds: filteredPickles.map(({ pickle }) => pickle.id), supportCodeLibrary, options, }) diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts index 0cc297dd8..30e99894c 100644 --- a/src/runtime/coordinator.ts +++ b/src/runtime/coordinator.ts @@ -1,5 +1,8 @@ import { EventEmitter } from 'node:events' -import { Envelope } from '@cucumber/messages' +import { Envelope, IdGenerator } from '@cucumber/messages' +import { IFilterablePickle } from '../filter' +import { assembleTestCases } from '../assemble' +import { SupportCodeLibrary } from '../support_code_library_builder/types' import { CoordinatorAdapter } from './types' import { timestamp } from './stopwatch' import { IRuntime } from './index' @@ -7,6 +10,9 @@ import { IRuntime } from './index' export class Coordinator implements IRuntime { constructor( private eventBroadcaster: EventEmitter, + private newId: IdGenerator.NewId, + private filteredPickles: ReadonlyArray, + private supportCodeLibrary: SupportCodeLibrary, private adapter: CoordinatorAdapter ) {} @@ -16,13 +22,23 @@ export class Coordinator implements IRuntime { timestamp: timestamp(), }, } satisfies Envelope) - const success = await this.adapter.start() + + const assembledTestCases = await assembleTestCases({ + eventBroadcaster: this.eventBroadcaster, + newId: this.newId, + filteredPickles: this.filteredPickles, + supportCodeLibrary: this.supportCodeLibrary, + }) + + const success = await this.adapter.start(assembledTestCases) + this.eventBroadcaster.emit('envelope', { testRunFinished: { timestamp: timestamp(), success, }, } satisfies Envelope) + return success } } diff --git a/src/runtime/parallel/coordinator_adapter.ts b/src/runtime/parallel/coordinator_adapter.ts index ada3dc9a3..01acbd5ce 100644 --- a/src/runtime/parallel/coordinator_adapter.ts +++ b/src/runtime/parallel/coordinator_adapter.ts @@ -2,16 +2,12 @@ import { ChildProcess, fork } from 'node:child_process' import path from 'node:path' import { EventEmitter } from 'node:events' import * as messages from '@cucumber/messages' -import { IdGenerator } from '@cucumber/messages' import { retriesForPickle, shouldCauseFailure } from '../helpers' import { EventDataCollector } from '../../formatter/helpers' import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' -import { - assembleTestCasesByPickleId, - TestCasesByPickleId, -} from '../../assemble' +import { AssembledTestCase } from '../../assemble' import { ILogger } from '../../logger' import { CoordinatorAdapter } from '../types' import { ICoordinatorReport, IWorkerCommand } from './command_types' @@ -24,8 +20,6 @@ export interface INewCoordinatorOptions { eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector options: IRuntimeOptions - newId: IdGenerator.NewId - pickleIds: string[] supportCodeLibrary: SupportCodeLibrary numberOfWorkers: number } @@ -43,9 +37,9 @@ interface IWorker { id: string } -interface IPicklePlacement { +interface WorkPlacement { index: number - pickle: messages.Pickle + item: AssembledTestCase } export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { @@ -54,10 +48,8 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { private readonly eventDataCollector: EventDataCollector private onFinish: (success: boolean) => void private readonly options: IRuntimeOptions - private readonly newId: IdGenerator.NewId - private readonly pickleIds: string[] - private assembledTestCases: TestCasesByPickleId - private readonly inProgressPickles: Record + private todo: Array + private readonly inProgress: Record private readonly workers: Record private readonly supportCodeLibrary: SupportCodeLibrary private readonly numberOfWorkers: number @@ -70,9 +62,7 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { logger, eventBroadcaster, eventDataCollector, - pickleIds, options, - newId, supportCodeLibrary, numberOfWorkers, }: INewCoordinatorOptions) { @@ -81,13 +71,11 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { this.eventBroadcaster = eventBroadcaster this.eventDataCollector = eventDataCollector this.options = options - this.newId = newId this.supportCodeLibrary = supportCodeLibrary - this.pickleIds = Array.from(pickleIds) this.numberOfWorkers = numberOfWorkers this.success = true this.workers = {} - this.inProgressPickles = {} + this.inProgress = {} this.idleInterventions = 0 } @@ -116,10 +104,7 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { return worker.state !== WorkerState.idle }) - if ( - Object.keys(this.inProgressPickles).length == 0 && - this.pickleIds.length > 0 - ) { + if (Object.keys(this.inProgress).length == 0 && this.todo.length > 0) { this.giveWork(triggeringWorker, true) this.idleInterventions++ } @@ -188,7 +173,7 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { testCaseFinished.testCaseStartedId ) if (!testCaseFinished.willBeRetried) { - delete this.inProgressPickles[workerId] + delete this.inProgress[workerId] if (shouldCauseFailure(worstTestStepResult.status, this.options)) { this.success = false @@ -196,15 +181,10 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { } } - async start(): Promise { - this.assembledTestCases = await assembleTestCasesByPickleId({ - eventBroadcaster: this.eventBroadcaster, - newId: this.newId, - pickles: this.pickleIds.map((pickleId) => - this.eventDataCollector.getPickle(pickleId) - ), - supportCodeLibrary: this.supportCodeLibrary, - }) + async start( + assembledTestCases: ReadonlyArray + ): Promise { + this.todo = Array.from(assembledTestCases) return await new Promise((resolve) => { for (let i = 0; i < this.numberOfWorkers; i++) { this.startWorker(i.toString(), this.numberOfWorkers) @@ -221,13 +201,13 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { }) } - nextPicklePlacement(): IPicklePlacement { - for (let index = 0; index < this.pickleIds.length; index++) { + nextWorkPlacement(): WorkPlacement { + for (let index = 0; index < this.todo.length; index++) { const placement = this.placementAt(index) if ( this.supportCodeLibrary.parallelCanAssign( - placement.pickle, - Object.values(this.inProgressPickles) + placement.item.pickle, + Object.values(this.inProgress).map(({ pickle }) => pickle) ) ) { return placement @@ -237,46 +217,38 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { return null } - placementAt(index: number): IPicklePlacement { + placementAt(index: number): WorkPlacement { return { index, - pickle: this.eventDataCollector.getPickle(this.pickleIds[index]), + item: this.todo[index], } } giveWork(worker: IWorker, force: boolean = false): void { - if (this.pickleIds.length < 1) { + if (this.todo.length < 1) { const finalizeCommand: IWorkerCommand = { finalize: true } worker.state = WorkerState.running worker.process.send(finalizeCommand) return } - const picklePlacement = force - ? this.placementAt(0) - : this.nextPicklePlacement() + const workPlacement = force ? this.placementAt(0) : this.nextWorkPlacement() - if (picklePlacement === null) { + if (workPlacement === null) { return } - const { index: nextPickleIndex, pickle } = picklePlacement + const { index: nextIndex, item } = workPlacement - this.pickleIds.splice(nextPickleIndex, 1) - this.inProgressPickles[worker.id] = pickle - const testCase = this.assembledTestCases[pickle.id] - const gherkinDocument = this.eventDataCollector.getGherkinDocument( - pickle.uri - ) - const retries = retriesForPickle(pickle, this.options) + this.todo.splice(nextIndex, 1) + this.inProgress[worker.id] = item + const retries = retriesForPickle(item.pickle, this.options) const skip = this.options.dryRun || (this.options.failFast && !this.success) const runCommand: IWorkerCommand = { run: { + ...item, retries, skip, - pickle, - testCase, - gherkinDocument, }, } worker.state = WorkerState.running diff --git a/src/runtime/types.ts b/src/runtime/types.ts index ba321091e..2cf762ee5 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -1,3 +1,5 @@ +import { AssembledTestCase } from '../assemble' + export interface CoordinatorAdapter { - start(): Promise + start(assembledTestCases: ReadonlyArray): Promise } From c263147f6c8fa6984e7f6cbf50c5f17c8239fad7 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 09:15:46 +0100 Subject: [PATCH 09/21] split out generic worker, give it runTestCase --- src/runtime/parallel/command_types.ts | 12 +---- src/runtime/parallel/coordinator_adapter.ts | 15 ++---- src/runtime/parallel/run_worker.ts | 4 +- .../parallel/{worker.ts => worker_adapter.ts} | 47 +++++++------------ src/runtime/types.ts | 2 + src/runtime/worker.ts | 47 +++++++++++++++++++ 6 files changed, 74 insertions(+), 53 deletions(-) rename src/runtime/parallel/{worker.ts => worker_adapter.ts} (78%) create mode 100644 src/runtime/worker.ts diff --git a/src/runtime/parallel/command_types.ts b/src/runtime/parallel/command_types.ts index ca16daeb9..e2be04339 100644 --- a/src/runtime/parallel/command_types.ts +++ b/src/runtime/parallel/command_types.ts @@ -1,13 +1,13 @@ -import * as messages from '@cucumber/messages' import { Envelope } from '@cucumber/messages' import { IRuntimeOptions } from '../index' import { ISupportCodeCoordinates } from '../../api' +import { AssembledTestCase } from '../../assemble' // Messages from Coordinator to Worker export interface IWorkerCommand { initialize?: IWorkerCommandInitialize - run?: IWorkerCommandRun + run?: AssembledTestCase finalize?: boolean } @@ -23,14 +23,6 @@ export interface ICanonicalSupportCodeIds { afterTestCaseHookDefinitionIds: string[] } -export interface IWorkerCommandRun { - retries: number - skip: boolean - pickle: messages.Pickle - testCase: messages.TestCase - gherkinDocument: messages.GherkinDocument -} - // Messages from Worker to Coordinator export interface ICoordinatorReport { diff --git a/src/runtime/parallel/coordinator_adapter.ts b/src/runtime/parallel/coordinator_adapter.ts index 01acbd5ce..61ba85bcd 100644 --- a/src/runtime/parallel/coordinator_adapter.ts +++ b/src/runtime/parallel/coordinator_adapter.ts @@ -2,7 +2,7 @@ import { ChildProcess, fork } from 'node:child_process' import path from 'node:path' import { EventEmitter } from 'node:events' import * as messages from '@cucumber/messages' -import { retriesForPickle, shouldCauseFailure } from '../helpers' +import { shouldCauseFailure } from '../helpers' import { EventDataCollector } from '../../formatter/helpers' import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' @@ -242,16 +242,9 @@ export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { this.todo.splice(nextIndex, 1) this.inProgress[worker.id] = item - const retries = retriesForPickle(item.pickle, this.options) - const skip = this.options.dryRun || (this.options.failFast && !this.success) - const runCommand: IWorkerCommand = { - run: { - ...item, - retries, - skip, - }, - } worker.state = WorkerState.running - worker.process.send(runCommand) + worker.process.send({ + run: item, + }) } } diff --git a/src/runtime/parallel/run_worker.ts b/src/runtime/parallel/run_worker.ts index 5ec226ae6..00861d1a3 100644 --- a/src/runtime/parallel/run_worker.ts +++ b/src/runtime/parallel/run_worker.ts @@ -1,5 +1,5 @@ import { doesHaveValue } from '../../value_checker' -import Worker from './worker' +import { ChildProcessWorkerAdapter } from './worker_adapter' function run(): void { const exit = (exitCode: number, error?: Error, message?: string): void => { @@ -8,7 +8,7 @@ function run(): void { } process.exit(exitCode) } - const worker = new Worker({ + const worker = new ChildProcessWorkerAdapter({ id: process.env.CUCUMBER_WORKER_ID, sendMessage: (message: any) => process.send(message), cwd: process.cwd(), diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker_adapter.ts similarity index 78% rename from src/runtime/parallel/worker.ts rename to src/runtime/parallel/worker_adapter.ts index c166dc644..7f265ffcb 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker_adapter.ts @@ -3,18 +3,18 @@ import { pathToFileURL } from 'node:url' import { register } from 'node:module' import * as messages from '@cucumber/messages' import { IdGenerator } from '@cucumber/messages' -import { JsonObject } from 'type-fest' import supportCodeLibraryBuilder from '../../support_code_library_builder' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { makeRunTestRunHooks, RunsTestRunHooks } from '../run_test_run_hooks' -import TestCaseRunner from '../test_case_runner' import tryRequire from '../../try_require' +import { Worker } from '../worker' +import { IRuntimeOptions } from '../index' +import { AssembledTestCase } from '../../assemble' import { ICoordinatorReport, IWorkerCommand, IWorkerCommandInitialize, - IWorkerCommandRun, } from './command_types' const { uuid } = IdGenerator @@ -22,18 +22,18 @@ const { uuid } = IdGenerator type IExitFunction = (exitCode: number, error?: Error, message?: string) => void type IMessageSender = (command: ICoordinatorReport) => void -export default class Worker { +export class ChildProcessWorkerAdapter { private readonly cwd: string private readonly exit: IExitFunction private readonly id: string private readonly eventBroadcaster: EventEmitter - private filterStacktraces: boolean private readonly newId: IdGenerator.NewId private readonly sendMessage: IMessageSender + private options: IRuntimeOptions private supportCodeLibrary: SupportCodeLibrary - private worldParameters: JsonObject private runTestRunHooks: RunsTestRunHooks + private worker: Worker constructor({ cwd, @@ -77,12 +77,11 @@ export default class Worker { } this.supportCodeLibrary = supportCodeLibraryBuilder.finalize(supportCodeIds) - this.worldParameters = options.worldParameters - this.filterStacktraces = options.filterStacktraces + this.options = options this.runTestRunHooks = makeRunTestRunHooks( options.dryRun, this.supportCodeLibrary.defaultTimeout, - this.worldParameters, + this.options.worldParameters, (name, location) => `${name} hook errored on worker ${this.id}, process exiting: ${location}` ) @@ -90,6 +89,13 @@ export default class Worker { this.supportCodeLibrary.beforeTestRunHookDefinitions, 'a BeforeAll' ) + this.worker = new Worker( + this.id, + this.eventBroadcaster, + this.newId, + this.options, + this.supportCodeLibrary + ) this.sendMessage({ ready: true }) } @@ -111,27 +117,8 @@ export default class Worker { } } - async runTestCase({ - gherkinDocument, - pickle, - testCase, - retries, - skip, - }: IWorkerCommandRun): Promise { - const testCaseRunner = new TestCaseRunner({ - workerId: this.id, - eventBroadcaster: this.eventBroadcaster, - gherkinDocument, - newId: this.newId, - pickle, - testCase, - retries, - skip, - filterStackTraces: this.filterStacktraces, - supportCodeLibrary: this.supportCodeLibrary, - worldParameters: this.worldParameters, - }) - await testCaseRunner.run() + async runTestCase(assembledTestCase: AssembledTestCase): Promise { + await this.worker.runTestCase(assembledTestCase) this.sendMessage({ ready: true }) } } diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 2cf762ee5..947e10293 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -3,3 +3,5 @@ import { AssembledTestCase } from '../assemble' export interface CoordinatorAdapter { start(assembledTestCases: ReadonlyArray): Promise } + +export interface WorkerAdapter {} diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts new file mode 100644 index 000000000..cac9b3d3e --- /dev/null +++ b/src/runtime/worker.ts @@ -0,0 +1,47 @@ +import { EventEmitter } from 'node:events' +import { IdGenerator, TestStepResultStatus } from '@cucumber/messages' +import { AssembledTestCase } from '../assemble' +import { SupportCodeLibrary } from '../support_code_library_builder/types' +import TestCaseRunner from './test_case_runner' +import { retriesForPickle, shouldCauseFailure } from './helpers' +import { IRuntimeOptions } from './index' + +export class Worker { + private success: boolean = true + + constructor( + private readonly workerId: string | undefined, + private readonly eventBroadcaster: EventEmitter, + private readonly newId: IdGenerator.NewId, + private readonly options: IRuntimeOptions, + private readonly supportCodeLibrary: SupportCodeLibrary + ) {} + + async runTestCase({ + gherkinDocument, + pickle, + testCase, + }: AssembledTestCase): Promise { + const testCaseRunner = new TestCaseRunner({ + workerId: this.workerId, + eventBroadcaster: this.eventBroadcaster, + newId: this.newId, + gherkinDocument, + pickle, + testCase, + retries: retriesForPickle(pickle, this.options), + skip: this.options.dryRun || (this.options.failFast && !this.success), + filterStackTraces: this.options.filterStacktraces, + supportCodeLibrary: this.supportCodeLibrary, + worldParameters: this.options.worldParameters, + }) + + const status = await testCaseRunner.run() + + if (shouldCauseFailure(status, this.options)) { + this.success = false + } + + return status + } +} From a20e0ea462774ecde71a309facf616e4214dd895 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 09:55:17 +0100 Subject: [PATCH 10/21] move test run hooks into worker --- src/runtime/parallel/worker_adapter.ts | 19 ++------------- src/runtime/worker.ts | 32 +++++++++++++++++++++++++- 2 files changed, 33 insertions(+), 18 deletions(-) diff --git a/src/runtime/parallel/worker_adapter.ts b/src/runtime/parallel/worker_adapter.ts index 7f265ffcb..b8c29c69c 100644 --- a/src/runtime/parallel/worker_adapter.ts +++ b/src/runtime/parallel/worker_adapter.ts @@ -6,7 +6,6 @@ import { IdGenerator } from '@cucumber/messages' import supportCodeLibraryBuilder from '../../support_code_library_builder' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' -import { makeRunTestRunHooks, RunsTestRunHooks } from '../run_test_run_hooks' import tryRequire from '../../try_require' import { Worker } from '../worker' import { IRuntimeOptions } from '../index' @@ -32,7 +31,6 @@ export class ChildProcessWorkerAdapter { private readonly sendMessage: IMessageSender private options: IRuntimeOptions private supportCodeLibrary: SupportCodeLibrary - private runTestRunHooks: RunsTestRunHooks private worker: Worker constructor({ @@ -78,17 +76,6 @@ export class ChildProcessWorkerAdapter { this.supportCodeLibrary = supportCodeLibraryBuilder.finalize(supportCodeIds) this.options = options - this.runTestRunHooks = makeRunTestRunHooks( - options.dryRun, - this.supportCodeLibrary.defaultTimeout, - this.options.worldParameters, - (name, location) => - `${name} hook errored on worker ${this.id}, process exiting: ${location}` - ) - await this.runTestRunHooks( - this.supportCodeLibrary.beforeTestRunHookDefinitions, - 'a BeforeAll' - ) this.worker = new Worker( this.id, this.eventBroadcaster, @@ -96,14 +83,12 @@ export class ChildProcessWorkerAdapter { this.options, this.supportCodeLibrary ) + await this.worker.runBeforeAllHooks() this.sendMessage({ ready: true }) } async finalize(): Promise { - await this.runTestRunHooks( - this.supportCodeLibrary.afterTestRunHookDefinitions, - 'an AfterAll' - ) + await this.worker.runAfterAllHooks() this.exit(0) } diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index cac9b3d3e..9a9f39c09 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -4,10 +4,12 @@ import { AssembledTestCase } from '../assemble' import { SupportCodeLibrary } from '../support_code_library_builder/types' import TestCaseRunner from './test_case_runner' import { retriesForPickle, shouldCauseFailure } from './helpers' +import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' import { IRuntimeOptions } from './index' export class Worker { private success: boolean = true + private readonly runTestRunHooks: RunsTestRunHooks constructor( private readonly workerId: string | undefined, @@ -15,7 +17,28 @@ export class Worker { private readonly newId: IdGenerator.NewId, private readonly options: IRuntimeOptions, private readonly supportCodeLibrary: SupportCodeLibrary - ) {} + ) { + this.runTestRunHooks = makeRunTestRunHooks( + this.options.dryRun, + this.supportCodeLibrary.defaultTimeout, + this.options.worldParameters, + (name, location) => { + let message = `${name} hook errored` + if (this.workerId) { + message += ` on worker ${this.workerId}` + } + message += `, process exiting: ${location}` + return message + } + ) + } + + async runBeforeAllHooks() { + await this.runTestRunHooks( + this.supportCodeLibrary.beforeTestRunHookDefinitions, + 'a BeforeAll' + ) + } async runTestCase({ gherkinDocument, @@ -44,4 +67,11 @@ export class Worker { return status } + + async runAfterAllHooks() { + await this.runTestRunHooks( + this.supportCodeLibrary.afterTestRunHookDefinitions, + 'an AfterAll' + ) + } } From 602efa10d6e5e5862da9015d0637d902048c8e26 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 12:52:39 +0100 Subject: [PATCH 11/21] use an in-process flavour of unified stack for serial --- src/api/runtime.ts | 49 ++++++++++--------- src/runtime/coordinator.ts | 4 +- .../{coordinator_adapter.ts => adapter.ts} | 4 +- src/runtime/parallel/run_worker.ts | 4 +- .../parallel/{worker_adapter.ts => worker.ts} | 2 +- src/runtime/serial/adapter.ts | 37 ++++++++++++++ src/runtime/types.ts | 4 +- src/runtime/worker.ts | 11 +++-- 8 files changed, 78 insertions(+), 37 deletions(-) rename src/runtime/parallel/{coordinator_adapter.ts => adapter.ts} (98%) rename src/runtime/parallel/{worker_adapter.ts => worker.ts} (98%) create mode 100644 src/runtime/serial/adapter.ts diff --git a/src/api/runtime.ts b/src/api/runtime.ts index bd953eab3..a051fee82 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -1,12 +1,14 @@ import { EventEmitter } from 'node:events' import { IdGenerator } from '@cucumber/messages' -import Runtime, { IRuntime } from '../runtime' +import { IRuntime } from '../runtime' import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { ILogger } from '../logger' import { Coordinator } from '../runtime/coordinator' -import { ChildProcessCoordinatorAdapter } from '../runtime/parallel/coordinator_adapter' +import { ChildProcessAdapter } from '../runtime/parallel/adapter' import { IFilterablePickle } from '../filter' +import { InProcessAdapter } from '../runtime/serial/adapter' +import { RuntimeAdapter } from '../runtime/types' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ @@ -28,29 +30,28 @@ export async function makeRuntime({ supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime }): Promise { - if (parallel > 0) { - return new Coordinator( - eventBroadcaster, - newId, - filteredPickles, - supportCodeLibrary, - new ChildProcessCoordinatorAdapter({ - cwd: environment.cwd, - logger, - eventBroadcaster, - eventDataCollector, - options, - supportCodeLibrary, - numberOfWorkers: parallel, - }) - ) - } - return new Runtime({ + const adapter: RuntimeAdapter = + parallel > 0 + ? new ChildProcessAdapter({ + cwd: environment.cwd, + logger, + eventBroadcaster, + eventDataCollector, + options, + supportCodeLibrary, + numberOfWorkers: parallel, + }) + : new InProcessAdapter( + eventBroadcaster, + newId, + options, + supportCodeLibrary + ) + return new Coordinator( eventBroadcaster, - eventDataCollector, newId, - pickleIds: filteredPickles.map(({ pickle }) => pickle.id), + filteredPickles, supportCodeLibrary, - options, - }) + adapter + ) } diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts index 30e99894c..a0be84e4f 100644 --- a/src/runtime/coordinator.ts +++ b/src/runtime/coordinator.ts @@ -3,7 +3,7 @@ import { Envelope, IdGenerator } from '@cucumber/messages' import { IFilterablePickle } from '../filter' import { assembleTestCases } from '../assemble' import { SupportCodeLibrary } from '../support_code_library_builder/types' -import { CoordinatorAdapter } from './types' +import { RuntimeAdapter } from './types' import { timestamp } from './stopwatch' import { IRuntime } from './index' @@ -13,7 +13,7 @@ export class Coordinator implements IRuntime { private newId: IdGenerator.NewId, private filteredPickles: ReadonlyArray, private supportCodeLibrary: SupportCodeLibrary, - private adapter: CoordinatorAdapter + private adapter: RuntimeAdapter ) {} async start(): Promise { diff --git a/src/runtime/parallel/coordinator_adapter.ts b/src/runtime/parallel/adapter.ts similarity index 98% rename from src/runtime/parallel/coordinator_adapter.ts rename to src/runtime/parallel/adapter.ts index 61ba85bcd..6d5794b11 100644 --- a/src/runtime/parallel/coordinator_adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -9,7 +9,7 @@ import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { AssembledTestCase } from '../../assemble' import { ILogger } from '../../logger' -import { CoordinatorAdapter } from '../types' +import { RuntimeAdapter } from '../types' import { ICoordinatorReport, IWorkerCommand } from './command_types' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') @@ -42,7 +42,7 @@ interface WorkPlacement { item: AssembledTestCase } -export class ChildProcessCoordinatorAdapter implements CoordinatorAdapter { +export class ChildProcessAdapter implements RuntimeAdapter { private readonly cwd: string private readonly eventBroadcaster: EventEmitter private readonly eventDataCollector: EventDataCollector diff --git a/src/runtime/parallel/run_worker.ts b/src/runtime/parallel/run_worker.ts index 00861d1a3..dea7f485f 100644 --- a/src/runtime/parallel/run_worker.ts +++ b/src/runtime/parallel/run_worker.ts @@ -1,5 +1,5 @@ import { doesHaveValue } from '../../value_checker' -import { ChildProcessWorkerAdapter } from './worker_adapter' +import { ChildProcessWorker } from './worker' function run(): void { const exit = (exitCode: number, error?: Error, message?: string): void => { @@ -8,7 +8,7 @@ function run(): void { } process.exit(exitCode) } - const worker = new ChildProcessWorkerAdapter({ + const worker = new ChildProcessWorker({ id: process.env.CUCUMBER_WORKER_ID, sendMessage: (message: any) => process.send(message), cwd: process.cwd(), diff --git a/src/runtime/parallel/worker_adapter.ts b/src/runtime/parallel/worker.ts similarity index 98% rename from src/runtime/parallel/worker_adapter.ts rename to src/runtime/parallel/worker.ts index b8c29c69c..cb2e99317 100644 --- a/src/runtime/parallel/worker_adapter.ts +++ b/src/runtime/parallel/worker.ts @@ -21,7 +21,7 @@ const { uuid } = IdGenerator type IExitFunction = (exitCode: number, error?: Error, message?: string) => void type IMessageSender = (command: ICoordinatorReport) => void -export class ChildProcessWorkerAdapter { +export class ChildProcessWorker { private readonly cwd: string private readonly exit: IExitFunction diff --git a/src/runtime/serial/adapter.ts b/src/runtime/serial/adapter.ts new file mode 100644 index 000000000..7dd85a991 --- /dev/null +++ b/src/runtime/serial/adapter.ts @@ -0,0 +1,37 @@ +import { EventEmitter } from 'node:events' +import { IdGenerator } from '@cucumber/messages' +import { RuntimeAdapter } from '../types' +import { AssembledTestCase } from '../../assemble' +import { Worker } from '../worker' +import { IRuntimeOptions } from '../index' +import { SupportCodeLibrary } from '../../support_code_library_builder/types' + +export class InProcessAdapter implements RuntimeAdapter { + #worker: Worker + + constructor( + eventBroadcaster: EventEmitter, + newId: IdGenerator.NewId, + options: IRuntimeOptions, + supportCodeLibrary: SupportCodeLibrary + ) { + this.#worker = new Worker( + undefined, + eventBroadcaster, + newId, + options, + supportCodeLibrary + ) + } + + async start( + assembledTestCases: ReadonlyArray + ): Promise { + await this.#worker.runBeforeAllHooks() + for (const item of assembledTestCases) { + await this.#worker.runTestCase(item) + } + await this.#worker.runAfterAllHooks() + return this.#worker.success + } +} diff --git a/src/runtime/types.ts b/src/runtime/types.ts index 947e10293..c381cf592 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -1,7 +1,5 @@ import { AssembledTestCase } from '../assemble' -export interface CoordinatorAdapter { +export interface RuntimeAdapter { start(assembledTestCases: ReadonlyArray): Promise } - -export interface WorkerAdapter {} diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index 9a9f39c09..049215cb1 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -8,7 +8,7 @@ import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' import { IRuntimeOptions } from './index' export class Worker { - private success: boolean = true + #success: boolean = true private readonly runTestRunHooks: RunsTestRunHooks constructor( @@ -33,6 +33,10 @@ export class Worker { ) } + get success() { + return this.#success + } + async runBeforeAllHooks() { await this.runTestRunHooks( this.supportCodeLibrary.beforeTestRunHookDefinitions, @@ -53,7 +57,8 @@ export class Worker { pickle, testCase, retries: retriesForPickle(pickle, this.options), - skip: this.options.dryRun || (this.options.failFast && !this.success), + // TODO move skip logic to coordinator? + skip: this.options.dryRun || (this.options.failFast && !this.#success), filterStackTraces: this.options.filterStacktraces, supportCodeLibrary: this.supportCodeLibrary, worldParameters: this.options.worldParameters, @@ -62,7 +67,7 @@ export class Worker { const status = await testCaseRunner.run() if (shouldCauseFailure(status, this.options)) { - this.success = false + this.#success = false } return status From 9d19ffab0a6eabc43b7c241b7c9231eadfa73fa9 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 13:10:41 +0100 Subject: [PATCH 12/21] simplify adapter construction --- src/api/runtime.ts | 13 +++---- src/runtime/parallel/adapter.ts | 66 +++++++++------------------------ 2 files changed, 24 insertions(+), 55 deletions(-) diff --git a/src/api/runtime.ts b/src/api/runtime.ts index a051fee82..897a7ee54 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -19,7 +19,7 @@ export async function makeRuntime({ filteredPickles, newId, supportCodeLibrary, - options: { parallel, ...options }, + options, }: { environment: IRunEnvironment logger: ILogger @@ -31,16 +31,15 @@ export async function makeRuntime({ options: IRunOptionsRuntime }): Promise { const adapter: RuntimeAdapter = - parallel > 0 - ? new ChildProcessAdapter({ - cwd: environment.cwd, + options.parallel > 0 + ? new ChildProcessAdapter( + environment, logger, eventBroadcaster, eventDataCollector, options, - supportCodeLibrary, - numberOfWorkers: parallel, - }) + supportCodeLibrary + ) : new InProcessAdapter( eventBroadcaster, newId, diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index 6d5794b11..493b58c4d 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -4,26 +4,16 @@ import { EventEmitter } from 'node:events' import * as messages from '@cucumber/messages' import { shouldCauseFailure } from '../helpers' import { EventDataCollector } from '../../formatter/helpers' -import { IRuntimeOptions } from '..' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import { AssembledTestCase } from '../../assemble' import { ILogger } from '../../logger' import { RuntimeAdapter } from '../types' +import { IRunEnvironment, IRunOptionsRuntime } from '../../api' import { ICoordinatorReport, IWorkerCommand } from './command_types' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') -export interface INewCoordinatorOptions { - cwd: string - logger: ILogger - eventBroadcaster: EventEmitter - eventDataCollector: EventDataCollector - options: IRuntimeOptions - supportCodeLibrary: SupportCodeLibrary - numberOfWorkers: number -} - const enum WorkerState { 'idle', 'closed', @@ -43,41 +33,21 @@ interface WorkPlacement { } export class ChildProcessAdapter implements RuntimeAdapter { - private readonly cwd: string - private readonly eventBroadcaster: EventEmitter - private readonly eventDataCollector: EventDataCollector + private idleInterventions: number = 0 + private success: boolean = true private onFinish: (success: boolean) => void - private readonly options: IRuntimeOptions - private todo: Array - private readonly inProgress: Record - private readonly workers: Record - private readonly supportCodeLibrary: SupportCodeLibrary - private readonly numberOfWorkers: number - private readonly logger: ILogger - private success: boolean - private idleInterventions: number - - constructor({ - cwd, - logger, - eventBroadcaster, - eventDataCollector, - options, - supportCodeLibrary, - numberOfWorkers, - }: INewCoordinatorOptions) { - this.cwd = cwd - this.logger = logger - this.eventBroadcaster = eventBroadcaster - this.eventDataCollector = eventDataCollector - this.options = options - this.supportCodeLibrary = supportCodeLibrary - this.numberOfWorkers = numberOfWorkers - this.success = true - this.workers = {} - this.inProgress = {} - this.idleInterventions = 0 - } + private todo: Array = [] + private readonly inProgress: Record = {} + private readonly workers: Record = {} + + constructor( + private readonly environment: IRunEnvironment, + private readonly logger: ILogger, + private readonly eventBroadcaster: EventEmitter, + private readonly eventDataCollector: EventDataCollector, + private readonly options: IRunOptionsRuntime, + private readonly supportCodeLibrary: SupportCodeLibrary + ) {} parseWorkerMessage(worker: IWorker, message: ICoordinatorReport): void { if (message.ready) { @@ -112,7 +82,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { startWorker(id: string, total: number): void { const workerProcess = fork(runWorkerPath, [], { - cwd: this.cwd, + cwd: this.environment.cwd, env: { ...process.env, CUCUMBER_PARALLEL: 'true', @@ -186,8 +156,8 @@ export class ChildProcessAdapter implements RuntimeAdapter { ): Promise { this.todo = Array.from(assembledTestCases) return await new Promise((resolve) => { - for (let i = 0; i < this.numberOfWorkers; i++) { - this.startWorker(i.toString(), this.numberOfWorkers) + for (let i = 0; i < this.options.parallel; i++) { + this.startWorker(i.toString(), this.options.parallel) } this.onFinish = (status) => { if (this.idleInterventions > 0) { From 6bc43ee5009e8b36c47dbb8fd6de702d9013ddb6 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 13:11:24 +0100 Subject: [PATCH 13/21] honour env from environment object --- src/runtime/parallel/adapter.ts | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index 493b58c4d..fc4ae151a 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -84,7 +84,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { const workerProcess = fork(runWorkerPath, [], { cwd: this.environment.cwd, env: { - ...process.env, + ...this.environment.env, CUCUMBER_PARALLEL: 'true', CUCUMBER_TOTAL_WORKERS: total.toString(), CUCUMBER_WORKER_ID: id, From 27df645259a79cbbdc4fa7b43c000d05749e7003 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 13:51:51 +0100 Subject: [PATCH 14/21] remove old Runtime class and related interfaces --- src/api/run_cucumber.ts | 2 +- src/api/runtime.ts | 6 +- src/formatter/helpers/summary_helpers_spec.ts | 4 +- src/formatter/progress_bar_formatter_spec.ts | 4 +- src/runtime/coordinator.ts | 8 +- src/runtime/helpers.ts | 6 +- src/runtime/index.ts | 135 +----------------- src/runtime/parallel/adapter.ts | 2 +- src/runtime/parallel/command_types.ts | 4 +- src/runtime/parallel/worker.ts | 4 +- src/runtime/serial/adapter.ts | 6 +- src/runtime/types.ts | 17 ++- src/runtime/worker.ts | 4 +- test/formatter_helpers.ts | 85 ++++++----- test/gherkin_helpers.ts | 16 ++- test/runtime_helpers.ts | 6 +- 16 files changed, 108 insertions(+), 201 deletions(-) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index ea62ab1cb..aa271e058 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -156,7 +156,7 @@ Running from: ${__dirname} supportCodeLibrary, options: options.runtime, }) - const success = await runtime.start() + const success = await runtime.run() await pluginManager.cleanup() await cleanupFormatters() diff --git a/src/api/runtime.ts b/src/api/runtime.ts index 897a7ee54..308fb15e3 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -1,14 +1,12 @@ import { EventEmitter } from 'node:events' import { IdGenerator } from '@cucumber/messages' -import { IRuntime } from '../runtime' import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { ILogger } from '../logger' -import { Coordinator } from '../runtime/coordinator' +import { Runtime, Coordinator, RuntimeAdapter } from '../runtime' import { ChildProcessAdapter } from '../runtime/parallel/adapter' import { IFilterablePickle } from '../filter' import { InProcessAdapter } from '../runtime/serial/adapter' -import { RuntimeAdapter } from '../runtime/types' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ @@ -29,7 +27,7 @@ export async function makeRuntime({ filteredPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime -}): Promise { +}): Promise { const adapter: RuntimeAdapter = options.parallel > 0 ? new ChildProcessAdapter( diff --git a/src/formatter/helpers/summary_helpers_spec.ts b/src/formatter/helpers/summary_helpers_spec.ts index 375c31cee..c49d508ba 100644 --- a/src/formatter/helpers/summary_helpers_spec.ts +++ b/src/formatter/helpers/summary_helpers_spec.ts @@ -8,13 +8,13 @@ import { getTestCaseAttempts } from '../../../test/formatter_helpers' import { getBaseSupportCodeLibrary } from '../../../test/fixtures/steps' import timeMethods, { durationBetweenTimestamps } from '../../time' import { buildSupportCodeLibrary } from '../../../test/runtime_helpers' -import { IRuntimeOptions } from '../../runtime' +import { RuntimeOptions } from '../../runtime' import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesNotHaveValue } from '../../value_checker' import { formatSummary } from './summary_helpers' interface ITestFormatSummaryOptions { - runtimeOptions?: Partial + runtimeOptions?: Partial sourceData: string supportCodeLibrary?: SupportCodeLibrary testRunStarted?: messages.TestRunStarted diff --git a/src/formatter/progress_bar_formatter_spec.ts b/src/formatter/progress_bar_formatter_spec.ts index aac4f8beb..15a761ad8 100644 --- a/src/formatter/progress_bar_formatter_spec.ts +++ b/src/formatter/progress_bar_formatter_spec.ts @@ -15,7 +15,7 @@ import { import { buildSupportCodeLibrary } from '../../test/runtime_helpers' import { getBaseSupportCodeLibrary } from '../../test/fixtures/steps' import timeMethods from '../time' -import { IRuntimeOptions } from '../runtime' +import { RuntimeOptions } from '../runtime' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { doesHaveValue, doesNotHaveValue } from '../value_checker' import ProgressBarFormatter from './progress_bar_formatter' @@ -23,7 +23,7 @@ import FormatterBuilder from './builder' import { EventDataCollector } from './helpers' interface ITestProgressBarFormatterOptions { - runtimeOptions?: Partial + runtimeOptions?: Partial shouldStopFn: (envelope: messages.Envelope) => boolean sources?: ITestSource[] supportCodeLibrary?: SupportCodeLibrary diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts index a0be84e4f..e958572f6 100644 --- a/src/runtime/coordinator.ts +++ b/src/runtime/coordinator.ts @@ -5,9 +5,9 @@ import { assembleTestCases } from '../assemble' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { RuntimeAdapter } from './types' import { timestamp } from './stopwatch' -import { IRuntime } from './index' +import { Runtime } from './index' -export class Coordinator implements IRuntime { +export class Coordinator implements Runtime { constructor( private eventBroadcaster: EventEmitter, private newId: IdGenerator.NewId, @@ -16,7 +16,7 @@ export class Coordinator implements IRuntime { private adapter: RuntimeAdapter ) {} - async start(): Promise { + async run(): Promise { this.eventBroadcaster.emit('envelope', { testRunStarted: { timestamp: timestamp(), @@ -30,7 +30,7 @@ export class Coordinator implements IRuntime { supportCodeLibrary: this.supportCodeLibrary, }) - const success = await this.adapter.start(assembledTestCases) + const success = await this.adapter.run(assembledTestCases) this.eventBroadcaster.emit('envelope', { testRunFinished: { diff --git a/src/runtime/helpers.ts b/src/runtime/helpers.ts index 770811ffe..7a5c25d72 100644 --- a/src/runtime/helpers.ts +++ b/src/runtime/helpers.ts @@ -4,7 +4,7 @@ import * as messages from '@cucumber/messages' import { formatLocation } from '../formatter/helpers/location_helpers' import { PickleTagFilter } from '../pickle_filter' import StepDefinition from '../models/step_definition' -import { IRuntimeOptions } from '.' +import { RuntimeOptions } from '.' export function getAmbiguousStepException( stepDefinitions: StepDefinition[] @@ -47,7 +47,7 @@ export function getAmbiguousStepException( export function retriesForPickle( pickle: messages.Pickle, - options: IRuntimeOptions + options: RuntimeOptions ): number { if (!options.retry) { return 0 @@ -69,7 +69,7 @@ export function retriesForPickle( export function shouldCauseFailure( status: messages.TestStepResultStatus, - options: IRuntimeOptions + options: RuntimeOptions ): boolean { if (options.dryRun) { return false diff --git a/src/runtime/index.ts b/src/runtime/index.ts index 1d82cab21..eefce5d8c 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,133 +1,2 @@ -import { EventEmitter } from 'node:events' -import * as messages from '@cucumber/messages' -import { IdGenerator } from '@cucumber/messages' -import { JsonObject } from 'type-fest' -import { EventDataCollector } from '../formatter/helpers' -import { SupportCodeLibrary } from '../support_code_library_builder/types' -import { assembleTestCasesByPickleId } from '../assemble' -import { retriesForPickle, shouldCauseFailure } from './helpers' -import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' -import { IStopwatch, create, timestamp } from './stopwatch' -import TestCaseRunner from './test_case_runner' - -export interface IRuntime { - start: () => Promise -} - -export interface INewRuntimeOptions { - eventBroadcaster: EventEmitter - eventDataCollector: EventDataCollector - newId: IdGenerator.NewId - options: IRuntimeOptions - pickleIds: string[] - supportCodeLibrary: SupportCodeLibrary -} - -export interface IRuntimeOptions { - dryRun: boolean - failFast: boolean - filterStacktraces: boolean - retry: number - retryTagFilter: string - strict: boolean - worldParameters: JsonObject -} - -export default class Runtime implements IRuntime { - private readonly eventBroadcaster: EventEmitter - private readonly eventDataCollector: EventDataCollector - private readonly stopwatch: IStopwatch - private readonly newId: IdGenerator.NewId - private readonly options: IRuntimeOptions - private readonly pickleIds: string[] - private readonly supportCodeLibrary: SupportCodeLibrary - private success: boolean - private readonly runTestRunHooks: RunsTestRunHooks - - constructor({ - eventBroadcaster, - eventDataCollector, - newId, - options, - pickleIds, - supportCodeLibrary, - }: INewRuntimeOptions) { - this.eventBroadcaster = eventBroadcaster - this.eventDataCollector = eventDataCollector - this.stopwatch = create() - this.newId = newId - this.options = options - this.pickleIds = pickleIds - this.supportCodeLibrary = supportCodeLibrary - this.success = true - this.runTestRunHooks = makeRunTestRunHooks( - this.options.dryRun, - this.supportCodeLibrary.defaultTimeout, - this.options.worldParameters, - (name, location) => `${name} hook errored, process exiting: ${location}` - ) - } - - async runTestCase( - pickleId: string, - testCase: messages.TestCase - ): Promise { - const pickle = this.eventDataCollector.getPickle(pickleId) - const retries = retriesForPickle(pickle, this.options) - const skip = this.options.dryRun || (this.options.failFast && !this.success) - const testCaseRunner = new TestCaseRunner({ - eventBroadcaster: this.eventBroadcaster, - gherkinDocument: this.eventDataCollector.getGherkinDocument(pickle.uri), - newId: this.newId, - pickle, - testCase, - retries, - skip, - filterStackTraces: this.options.filterStacktraces, - supportCodeLibrary: this.supportCodeLibrary, - worldParameters: this.options.worldParameters, - }) - const status = await testCaseRunner.run() - if (shouldCauseFailure(status, this.options)) { - this.success = false - } - } - - async start(): Promise { - const testRunStarted: messages.Envelope = { - testRunStarted: { - timestamp: timestamp(), - }, - } - this.eventBroadcaster.emit('envelope', testRunStarted) - this.stopwatch.start() - await this.runTestRunHooks( - this.supportCodeLibrary.beforeTestRunHookDefinitions, - 'a BeforeAll' - ) - const assembledTestCases = await assembleTestCasesByPickleId({ - eventBroadcaster: this.eventBroadcaster, - newId: this.newId, - pickles: this.pickleIds.map((pickleId) => - this.eventDataCollector.getPickle(pickleId) - ), - supportCodeLibrary: this.supportCodeLibrary, - }) - for (const pickleId of this.pickleIds) { - await this.runTestCase(pickleId, assembledTestCases[pickleId]) - } - await this.runTestRunHooks( - this.supportCodeLibrary.afterTestRunHookDefinitions.slice(0).reverse(), - 'an AfterAll' - ) - this.stopwatch.stop() - const testRunFinished: messages.Envelope = { - testRunFinished: { - timestamp: timestamp(), - success: this.success, - }, - } - this.eventBroadcaster.emit('envelope', testRunFinished) - return this.success - } -} +export * from './coordinator' +export * from './types' diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index fc4ae151a..00d031abc 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -151,7 +151,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { } } - async start( + async run( assembledTestCases: ReadonlyArray ): Promise { this.todo = Array.from(assembledTestCases) diff --git a/src/runtime/parallel/command_types.ts b/src/runtime/parallel/command_types.ts index e2be04339..4d0d3254c 100644 --- a/src/runtime/parallel/command_types.ts +++ b/src/runtime/parallel/command_types.ts @@ -1,5 +1,5 @@ import { Envelope } from '@cucumber/messages' -import { IRuntimeOptions } from '../index' +import { RuntimeOptions } from '../index' import { ISupportCodeCoordinates } from '../../api' import { AssembledTestCase } from '../../assemble' @@ -14,7 +14,7 @@ export interface IWorkerCommand { export interface IWorkerCommandInitialize { supportCodeCoordinates: ISupportCodeCoordinates supportCodeIds?: ICanonicalSupportCodeIds - options: IRuntimeOptions + options: RuntimeOptions } export interface ICanonicalSupportCodeIds { diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index cb2e99317..e836ae5dc 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -8,7 +8,7 @@ import { SupportCodeLibrary } from '../../support_code_library_builder/types' import { doesHaveValue } from '../../value_checker' import tryRequire from '../../try_require' import { Worker } from '../worker' -import { IRuntimeOptions } from '../index' +import { RuntimeOptions } from '../index' import { AssembledTestCase } from '../../assemble' import { ICoordinatorReport, @@ -29,7 +29,7 @@ export class ChildProcessWorker { private readonly eventBroadcaster: EventEmitter private readonly newId: IdGenerator.NewId private readonly sendMessage: IMessageSender - private options: IRuntimeOptions + private options: RuntimeOptions private supportCodeLibrary: SupportCodeLibrary private worker: Worker diff --git a/src/runtime/serial/adapter.ts b/src/runtime/serial/adapter.ts index 7dd85a991..3522bf35d 100644 --- a/src/runtime/serial/adapter.ts +++ b/src/runtime/serial/adapter.ts @@ -3,7 +3,7 @@ import { IdGenerator } from '@cucumber/messages' import { RuntimeAdapter } from '../types' import { AssembledTestCase } from '../../assemble' import { Worker } from '../worker' -import { IRuntimeOptions } from '../index' +import { RuntimeOptions } from '../index' import { SupportCodeLibrary } from '../../support_code_library_builder/types' export class InProcessAdapter implements RuntimeAdapter { @@ -12,7 +12,7 @@ export class InProcessAdapter implements RuntimeAdapter { constructor( eventBroadcaster: EventEmitter, newId: IdGenerator.NewId, - options: IRuntimeOptions, + options: RuntimeOptions, supportCodeLibrary: SupportCodeLibrary ) { this.#worker = new Worker( @@ -24,7 +24,7 @@ export class InProcessAdapter implements RuntimeAdapter { ) } - async start( + async run( assembledTestCases: ReadonlyArray ): Promise { await this.#worker.runBeforeAllHooks() diff --git a/src/runtime/types.ts b/src/runtime/types.ts index c381cf592..207b24fd6 100644 --- a/src/runtime/types.ts +++ b/src/runtime/types.ts @@ -1,5 +1,20 @@ +import { JsonObject } from 'type-fest' import { AssembledTestCase } from '../assemble' +export interface RuntimeOptions { + dryRun: boolean + failFast: boolean + filterStacktraces: boolean + retry: number + retryTagFilter: string + strict: boolean + worldParameters: JsonObject +} + +export interface Runtime { + run: () => Promise +} + export interface RuntimeAdapter { - start(assembledTestCases: ReadonlyArray): Promise + run(assembledTestCases: ReadonlyArray): Promise } diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index 049215cb1..4eccc8cbd 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -5,7 +5,7 @@ import { SupportCodeLibrary } from '../support_code_library_builder/types' import TestCaseRunner from './test_case_runner' import { retriesForPickle, shouldCauseFailure } from './helpers' import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' -import { IRuntimeOptions } from './index' +import { RuntimeOptions } from './index' export class Worker { #success: boolean = true @@ -15,7 +15,7 @@ export class Worker { private readonly workerId: string | undefined, private readonly eventBroadcaster: EventEmitter, private readonly newId: IdGenerator.NewId, - private readonly options: IRuntimeOptions, + private readonly options: RuntimeOptions, private readonly supportCodeLibrary: SupportCodeLibrary ) { this.runTestRunHooks = makeRunTestRunHooks( diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index 65a2af137..aa27b40cd 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -3,7 +3,7 @@ import { PassThrough } from 'node:stream' import { promisify } from 'node:util' import { IdGenerator } from '@cucumber/messages' import * as messages from '@cucumber/messages' -import Runtime, { IRuntimeOptions } from '../src/runtime' +import { RuntimeOptions } from '../src/runtime' import { EventDataCollector } from '../src/formatter/helpers' import FormatterBuilder from '../src/formatter/builder' import { SupportCodeLibrary } from '../src/support_code_library_builder/types' @@ -11,7 +11,10 @@ import { ITestCaseAttempt } from '../src/formatter/helpers/event_data_collector' import { doesNotHaveValue } from '../src/value_checker' import { emitSupportCodeMessages } from '../src/cli/helpers' import { FormatOptions } from '../src/formatter' -import { generateEvents } from './gherkin_helpers' +import { Coordinator } from '../src/runtime/coordinator' +import { IFilterablePickle } from '../src/filter' +import { InProcessAdapter } from '../src/runtime/serial/adapter' +import { generatePickles } from './gherkin_helpers' import { buildOptions, buildSupportCodeLibrary } from './runtime_helpers' const { uuid } = IdGenerator @@ -22,7 +25,7 @@ export interface ITestSource { } export interface ITestRunOptions { - runtimeOptions?: Partial + runtimeOptions?: Partial supportCodeLibrary?: SupportCodeLibrary sources?: ITestSource[] pickleFilter?: (pickle: messages.Pickle) => boolean @@ -71,25 +74,29 @@ export async function testFormatter({ cleanup: promisify(passThrough.end.bind(passThrough)), supportCodeLibrary, }) - let pickleIds: string[] = [] + let filteredPickles: IFilterablePickle[] = [] for (const source of sources) { - const { pickles } = await generateEvents({ + const generated = await generatePickles({ data: source.data, eventBroadcaster, uri: source.uri, }) - pickleIds = pickleIds.concat(pickles.map((p) => p.id)) + filteredPickles = filteredPickles.concat(generated) } - const runtime = new Runtime({ + const runtime = new Coordinator( eventBroadcaster, - eventDataCollector, - newId: uuid(), - options: buildOptions(runtimeOptions), - pickleIds, + uuid(), + filteredPickles, supportCodeLibrary, - }) + new InProcessAdapter( + eventBroadcaster, + uuid(), + buildOptions(runtimeOptions), + supportCodeLibrary + ) + ) - await runtime.start() + await runtime.run() return normalizeSummaryDuration(output) } @@ -104,25 +111,29 @@ export async function getTestCaseAttempts({ } const eventBroadcaster = new EventEmitter() const eventDataCollector = new EventDataCollector(eventBroadcaster) - let pickleIds: string[] = [] + let filteredPickles: IFilterablePickle[] = [] for (const source of sources) { - const { pickles } = await generateEvents({ + const generated = await generatePickles({ data: source.data, eventBroadcaster, uri: source.uri, }) - pickleIds = pickleIds.concat(pickles.map((p) => p.id)) + filteredPickles = filteredPickles.concat(generated) } - const runtime = new Runtime({ + const runtime = new Coordinator( eventBroadcaster, - eventDataCollector, - newId: uuid(), - options: buildOptions(runtimeOptions), - pickleIds, + uuid(), + filteredPickles, supportCodeLibrary, - }) + new InProcessAdapter( + eventBroadcaster, + uuid(), + buildOptions(runtimeOptions), + supportCodeLibrary + ) + ) - await runtime.start() + await runtime.run() return eventDataCollector.getTestCaseAttempts() } @@ -145,25 +156,33 @@ export async function getEnvelopesAndEventDataCollector({ eventBroadcaster, newId: IdGenerator.uuid(), }) - let pickleIds: string[] = [] + let filteredPickles: IFilterablePickle[] = [] + for (const source of sources) { - const { pickles } = await generateEvents({ + const generated = await generatePickles({ data: source.data, eventBroadcaster, uri: source.uri, }) - pickleIds = pickleIds.concat(pickles.filter(pickleFilter).map((p) => p.id)) + filteredPickles = filteredPickles.concat( + generated.filter((item) => pickleFilter(item.pickle)) + ) } - const runtime = new Runtime({ + + const runtime = new Coordinator( eventBroadcaster, - eventDataCollector, - newId: uuid(), - options: buildOptions(runtimeOptions), - pickleIds, + uuid(), + filteredPickles, supportCodeLibrary, - }) + new InProcessAdapter( + eventBroadcaster, + uuid(), + buildOptions(runtimeOptions), + supportCodeLibrary + ) + ) - await runtime.start() + await runtime.run() return { envelopes, eventDataCollector } } diff --git a/test/gherkin_helpers.ts b/test/gherkin_helpers.ts index 1e544f98f..a1b3d749b 100644 --- a/test/gherkin_helpers.ts +++ b/test/gherkin_helpers.ts @@ -4,6 +4,7 @@ import { SourceMediaType } from '@cucumber/messages' import { IGherkinOptions } from '@cucumber/gherkin' import { GherkinStreams } from '@cucumber/gherkin-streams' import { doesHaveValue } from '../src/value_checker' +import { IFilterablePickle } from '../src/filter' export interface IParsedSource { pickles: messages.Pickle[] @@ -70,23 +71,28 @@ export async function parse({ }) } -export interface IGenerateEventsRequest { +export interface GeneratePicklesRequest { data: string eventBroadcaster: EventEmitter uri: string } -export async function generateEvents({ +export async function generatePickles({ data, eventBroadcaster, uri, -}: IGenerateEventsRequest): Promise { - const { envelopes, source, gherkinDocument, pickles } = await parse({ +}: GeneratePicklesRequest): Promise> { + const { envelopes, gherkinDocument, pickles } = await parse({ data, uri, }) envelopes.forEach((envelope) => eventBroadcaster.emit('envelope', envelope)) - return { source, gherkinDocument, pickles } + return pickles.map((pickle) => { + return { + gherkinDocument, + pickle, + } as IFilterablePickle + }) } export async function getPickleWithTags( diff --git a/test/runtime_helpers.ts b/test/runtime_helpers.ts index 5b9acc70f..cd9f8fbe4 100644 --- a/test/runtime_helpers.ts +++ b/test/runtime_helpers.ts @@ -1,6 +1,6 @@ import { IdGenerator } from '@cucumber/messages' import { SupportCodeLibraryBuilder } from '../src/support_code_library_builder' -import { IRuntimeOptions } from '../src/runtime' +import { RuntimeOptions } from '../src/runtime' import { IDefineSupportCodeMethods, SupportCodeLibrary, @@ -8,8 +8,8 @@ import { import { doesHaveValue } from '../src/value_checker' export function buildOptions( - overrides: Partial -): IRuntimeOptions { + overrides: Partial +): RuntimeOptions { return { dryRun: false, failFast: false, From 8eaabcf5a86273d8613e49a0ed83f0a1825f25e3 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 13:56:11 +0100 Subject: [PATCH 15/21] rework some naming of child process types --- src/runtime/parallel/adapter.ts | 14 +++++++------- .../parallel/{command_types.ts => types.ts} | 14 +++++++------- src/runtime/parallel/worker.ts | 16 ++++++++-------- src/support_code_library_builder/index.ts | 4 ++-- 4 files changed, 24 insertions(+), 24 deletions(-) rename src/runtime/parallel/{command_types.ts => types.ts} (66%) diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index 00d031abc..3c5e3aaf1 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -10,7 +10,7 @@ import { AssembledTestCase } from '../../assemble' import { ILogger } from '../../logger' import { RuntimeAdapter } from '../types' import { IRunEnvironment, IRunOptionsRuntime } from '../../api' -import { ICoordinatorReport, IWorkerCommand } from './command_types' +import { WorkerToCoordinatorEvent, CoordinatorToWorkerCommand } from './types' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') @@ -49,12 +49,12 @@ export class ChildProcessAdapter implements RuntimeAdapter { private readonly supportCodeLibrary: SupportCodeLibrary ) {} - parseWorkerMessage(worker: IWorker, message: ICoordinatorReport): void { + parseWorkerMessage(worker: IWorker, message: WorkerToCoordinatorEvent): void { if (message.ready) { worker.state = WorkerState.idle this.awakenWorkers(worker) - } else if (doesHaveValue(message.jsonEnvelope)) { - const envelope = message.jsonEnvelope + } else if (doesHaveValue(message.envelope)) { + const envelope = message.envelope this.eventBroadcaster.emit('envelope', envelope) if (doesHaveValue(envelope.testCaseFinished)) { this.parseTestCaseResult(envelope.testCaseFinished, worker.id) @@ -93,14 +93,14 @@ export class ChildProcessAdapter implements RuntimeAdapter { }) const worker = { state: WorkerState.new, process: workerProcess, id } this.workers[id] = worker - worker.process.on('message', (message: ICoordinatorReport) => { + worker.process.on('message', (message: WorkerToCoordinatorEvent) => { this.parseWorkerMessage(worker, message) }) worker.process.on('close', (exitCode) => { worker.state = WorkerState.closed this.onWorkerProcessClose(exitCode) }) - const initializeCommand: IWorkerCommand = { + const initializeCommand: CoordinatorToWorkerCommand = { initialize: { supportCodeCoordinates: this.supportCodeLibrary.originalCoordinates, supportCodeIds: { @@ -196,7 +196,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { giveWork(worker: IWorker, force: boolean = false): void { if (this.todo.length < 1) { - const finalizeCommand: IWorkerCommand = { finalize: true } + const finalizeCommand: CoordinatorToWorkerCommand = { finalize: true } worker.state = WorkerState.running worker.process.send(finalizeCommand) return diff --git a/src/runtime/parallel/command_types.ts b/src/runtime/parallel/types.ts similarity index 66% rename from src/runtime/parallel/command_types.ts rename to src/runtime/parallel/types.ts index 4d0d3254c..e32b3fee2 100644 --- a/src/runtime/parallel/command_types.ts +++ b/src/runtime/parallel/types.ts @@ -5,19 +5,19 @@ import { AssembledTestCase } from '../../assemble' // Messages from Coordinator to Worker -export interface IWorkerCommand { - initialize?: IWorkerCommandInitialize +export interface CoordinatorToWorkerCommand { + initialize?: InitializeCommand run?: AssembledTestCase finalize?: boolean } -export interface IWorkerCommandInitialize { +export interface InitializeCommand { supportCodeCoordinates: ISupportCodeCoordinates - supportCodeIds?: ICanonicalSupportCodeIds + supportCodeIds?: CanonicalSupportCodeIds options: RuntimeOptions } -export interface ICanonicalSupportCodeIds { +export interface CanonicalSupportCodeIds { stepDefinitionIds: string[] beforeTestCaseHookDefinitionIds: string[] afterTestCaseHookDefinitionIds: string[] @@ -25,7 +25,7 @@ export interface ICanonicalSupportCodeIds { // Messages from Worker to Coordinator -export interface ICoordinatorReport { - jsonEnvelope?: Envelope +export interface WorkerToCoordinatorEvent { + envelope?: Envelope ready?: boolean } diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index e836ae5dc..e25f183b1 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -11,15 +11,15 @@ import { Worker } from '../worker' import { RuntimeOptions } from '../index' import { AssembledTestCase } from '../../assemble' import { - ICoordinatorReport, - IWorkerCommand, - IWorkerCommandInitialize, -} from './command_types' + WorkerToCoordinatorEvent, + CoordinatorToWorkerCommand, + InitializeCommand, +} from './types' const { uuid } = IdGenerator type IExitFunction = (exitCode: number, error?: Error, message?: string) => void -type IMessageSender = (command: ICoordinatorReport) => void +type IMessageSender = (command: WorkerToCoordinatorEvent) => void export class ChildProcessWorker { private readonly cwd: string @@ -51,7 +51,7 @@ export class ChildProcessWorker { this.sendMessage = sendMessage this.eventBroadcaster = new EventEmitter() this.eventBroadcaster.on('envelope', (envelope: messages.Envelope) => { - this.sendMessage({ jsonEnvelope: envelope }) + this.sendMessage({ envelope: envelope }) }) } @@ -59,7 +59,7 @@ export class ChildProcessWorker { supportCodeCoordinates, supportCodeIds, options, - }: IWorkerCommandInitialize): Promise { + }: InitializeCommand): Promise { supportCodeLibraryBuilder.reset( this.cwd, this.newId, @@ -92,7 +92,7 @@ export class ChildProcessWorker { this.exit(0) } - async receiveMessage(message: IWorkerCommand): Promise { + async receiveMessage(message: CoordinatorToWorkerCommand): Promise { if (doesHaveValue(message.initialize)) { await this.initialize(message.initialize) } else if (message.finalize) { diff --git a/src/support_code_library_builder/index.ts b/src/support_code_library_builder/index.ts index eb3c4005b..c75b0446a 100644 --- a/src/support_code_library_builder/index.ts +++ b/src/support_code_library_builder/index.ts @@ -11,7 +11,7 @@ import TestRunHookDefinition from '../models/test_run_hook_definition' import StepDefinition from '../models/step_definition' import { formatLocation } from '../formatter/helpers' import { doesHaveValue } from '../value_checker' -import { ICanonicalSupportCodeIds } from '../runtime/parallel/command_types' +import { CanonicalSupportCodeIds } from '../runtime/parallel/types' import { GherkinStepKeyword } from '../models/gherkin_step_keyword' import validateArguments from './validate_arguments' @@ -415,7 +415,7 @@ export class SupportCodeLibraryBuilder { return { stepDefinitions, undefinedParameterTypes } } - finalize(canonicalIds?: ICanonicalSupportCodeIds): SupportCodeLibrary { + finalize(canonicalIds?: CanonicalSupportCodeIds): SupportCodeLibrary { this.status = 'FINALIZED' const stepDefinitionsResult = this.buildStepDefinitions( canonicalIds?.stepDefinitionIds From 019709f8286a48c74fe0cdd507062f89e1ac6988 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 18:09:08 +0100 Subject: [PATCH 16/21] adapters need to maintain failing status --- src/api/run_cucumber.ts | 1 - src/api/runtime.ts | 4 - src/runtime/parallel/adapter.ts | 111 ++++++++++------------ src/runtime/parallel/types.ts | 45 ++++++--- src/runtime/parallel/worker.ts | 44 +++++---- src/runtime/serial/adapter.ts | 8 +- src/runtime/worker.ts | 25 ++--- src/support_code_library_builder/index.ts | 2 +- src/support_code_library_builder/types.ts | 6 ++ 9 files changed, 127 insertions(+), 119 deletions(-) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index aa271e058..35041936d 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -150,7 +150,6 @@ Running from: ${__dirname} environment, logger, eventBroadcaster, - eventDataCollector, filteredPickles, newId, supportCodeLibrary, diff --git a/src/api/runtime.ts b/src/api/runtime.ts index 308fb15e3..319ff56e2 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -1,6 +1,5 @@ import { EventEmitter } from 'node:events' import { IdGenerator } from '@cucumber/messages' -import { EventDataCollector } from '../formatter/helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { ILogger } from '../logger' import { Runtime, Coordinator, RuntimeAdapter } from '../runtime' @@ -13,7 +12,6 @@ export async function makeRuntime({ environment, logger, eventBroadcaster, - eventDataCollector, filteredPickles, newId, supportCodeLibrary, @@ -22,7 +20,6 @@ export async function makeRuntime({ environment: IRunEnvironment logger: ILogger eventBroadcaster: EventEmitter - eventDataCollector: EventDataCollector newId: IdGenerator.NewId filteredPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary @@ -34,7 +31,6 @@ export async function makeRuntime({ environment, logger, eventBroadcaster, - eventDataCollector, options, supportCodeLibrary ) diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index 3c5e3aaf1..30b3f69cb 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -1,16 +1,17 @@ import { ChildProcess, fork } from 'node:child_process' import path from 'node:path' import { EventEmitter } from 'node:events' -import * as messages from '@cucumber/messages' -import { shouldCauseFailure } from '../helpers' -import { EventDataCollector } from '../../formatter/helpers' import { SupportCodeLibrary } from '../../support_code_library_builder/types' -import { doesHaveValue } from '../../value_checker' import { AssembledTestCase } from '../../assemble' import { ILogger } from '../../logger' import { RuntimeAdapter } from '../types' import { IRunEnvironment, IRunOptionsRuntime } from '../../api' -import { WorkerToCoordinatorEvent, CoordinatorToWorkerCommand } from './types' +import { + FinalizeCommand, + InitializeCommand, + RunCommand, + WorkerToCoordinatorEvent, +} from './types' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') @@ -34,7 +35,7 @@ interface WorkPlacement { export class ChildProcessAdapter implements RuntimeAdapter { private idleInterventions: number = 0 - private success: boolean = true + private failing: boolean = false private onFinish: (success: boolean) => void private todo: Array = [] private readonly inProgress: Record = {} @@ -44,25 +45,31 @@ export class ChildProcessAdapter implements RuntimeAdapter { private readonly environment: IRunEnvironment, private readonly logger: ILogger, private readonly eventBroadcaster: EventEmitter, - private readonly eventDataCollector: EventDataCollector, private readonly options: IRunOptionsRuntime, private readonly supportCodeLibrary: SupportCodeLibrary ) {} parseWorkerMessage(worker: IWorker, message: WorkerToCoordinatorEvent): void { - if (message.ready) { - worker.state = WorkerState.idle - this.awakenWorkers(worker) - } else if (doesHaveValue(message.envelope)) { - const envelope = message.envelope - this.eventBroadcaster.emit('envelope', envelope) - if (doesHaveValue(envelope.testCaseFinished)) { - this.parseTestCaseResult(envelope.testCaseFinished, worker.id) - } - } else { - throw new Error( - `Unexpected message from worker: ${JSON.stringify(message)}` - ) + switch (message.type) { + case 'READY': + worker.state = WorkerState.idle + this.awakenWorkers(worker) + break + case 'ENVELOPE': + this.eventBroadcaster.emit('envelope', message.envelope) + break + case 'FINISHED': + if (!message.success) { + this.failing = true + } + delete this.inProgress[worker.id] + worker.state = WorkerState.idle + this.awakenWorkers(worker) + break + default: + throw new Error( + `Unexpected message from worker: ${JSON.stringify(message)}` + ) } } @@ -100,54 +107,33 @@ export class ChildProcessAdapter implements RuntimeAdapter { worker.state = WorkerState.closed this.onWorkerProcessClose(exitCode) }) - const initializeCommand: CoordinatorToWorkerCommand = { - initialize: { - supportCodeCoordinates: this.supportCodeLibrary.originalCoordinates, - supportCodeIds: { - stepDefinitionIds: this.supportCodeLibrary.stepDefinitions.map( - (s) => s.id + worker.process.send({ + type: 'INITIALIZE', + supportCodeCoordinates: this.supportCodeLibrary.originalCoordinates, + supportCodeIds: { + stepDefinitionIds: this.supportCodeLibrary.stepDefinitions.map( + (s) => s.id + ), + beforeTestCaseHookDefinitionIds: + this.supportCodeLibrary.beforeTestCaseHookDefinitions.map( + (h) => h.id ), - beforeTestCaseHookDefinitionIds: - this.supportCodeLibrary.beforeTestCaseHookDefinitions.map( - (h) => h.id - ), - afterTestCaseHookDefinitionIds: - this.supportCodeLibrary.afterTestCaseHookDefinitions.map( - (h) => h.id - ), - }, - options: this.options, + afterTestCaseHookDefinitionIds: + this.supportCodeLibrary.afterTestCaseHookDefinitions.map((h) => h.id), }, - } - worker.process.send(initializeCommand) + options: this.options, + } satisfies InitializeCommand) } onWorkerProcessClose(exitCode: number): void { - const success = exitCode === 0 - if (!success) { - this.success = false + if (exitCode !== 0) { + this.failing = true } if ( Object.values(this.workers).every((x) => x.state === WorkerState.closed) ) { - this.onFinish(this.success) - } - } - - parseTestCaseResult( - testCaseFinished: messages.TestCaseFinished, - workerId: string - ): void { - const { worstTestStepResult } = this.eventDataCollector.getTestCaseAttempt( - testCaseFinished.testCaseStartedId - ) - if (!testCaseFinished.willBeRetried) { - delete this.inProgress[workerId] - - if (shouldCauseFailure(worstTestStepResult.status, this.options)) { - this.success = false - } + this.onFinish(!this.failing) } } @@ -196,9 +182,8 @@ export class ChildProcessAdapter implements RuntimeAdapter { giveWork(worker: IWorker, force: boolean = false): void { if (this.todo.length < 1) { - const finalizeCommand: CoordinatorToWorkerCommand = { finalize: true } worker.state = WorkerState.running - worker.process.send(finalizeCommand) + worker.process.send({ type: 'FINALIZE' } satisfies FinalizeCommand) return } @@ -214,7 +199,9 @@ export class ChildProcessAdapter implements RuntimeAdapter { this.inProgress[worker.id] = item worker.state = WorkerState.running worker.process.send({ - run: item, - }) + type: 'RUN', + assembledTestCase: item, + failing: this.failing, + } satisfies RunCommand) } } diff --git a/src/runtime/parallel/types.ts b/src/runtime/parallel/types.ts index e32b3fee2..fc56ffc83 100644 --- a/src/runtime/parallel/types.ts +++ b/src/runtime/parallel/types.ts @@ -2,30 +2,49 @@ import { Envelope } from '@cucumber/messages' import { RuntimeOptions } from '../index' import { ISupportCodeCoordinates } from '../../api' import { AssembledTestCase } from '../../assemble' +import { CanonicalSupportCodeIds } from '../../support_code_library_builder/types' // Messages from Coordinator to Worker -export interface CoordinatorToWorkerCommand { - initialize?: InitializeCommand - run?: AssembledTestCase - finalize?: boolean -} +export type CoordinatorToWorkerCommand = + | InitializeCommand + | RunCommand + | FinalizeCommand export interface InitializeCommand { + type: 'INITIALIZE' supportCodeCoordinates: ISupportCodeCoordinates - supportCodeIds?: CanonicalSupportCodeIds + supportCodeIds: CanonicalSupportCodeIds options: RuntimeOptions } -export interface CanonicalSupportCodeIds { - stepDefinitionIds: string[] - beforeTestCaseHookDefinitionIds: string[] - afterTestCaseHookDefinitionIds: string[] +export interface RunCommand { + type: 'RUN' + assembledTestCase: AssembledTestCase + failing: boolean +} + +export interface FinalizeCommand { + type: 'FINALIZE' } // Messages from Worker to Coordinator -export interface WorkerToCoordinatorEvent { - envelope?: Envelope - ready?: boolean +export type WorkerToCoordinatorEvent = + | ReadyEvent + | EnvelopeEvent + | FinishedEvent + +export interface ReadyEvent { + type: 'READY' +} + +export interface EnvelopeEvent { + type: 'ENVELOPE' + envelope: Envelope +} + +export interface FinishedEvent { + type: 'FINISHED' + success: boolean } diff --git a/src/runtime/parallel/worker.ts b/src/runtime/parallel/worker.ts index e25f183b1..5fe665d9e 100644 --- a/src/runtime/parallel/worker.ts +++ b/src/runtime/parallel/worker.ts @@ -1,19 +1,17 @@ import { EventEmitter } from 'node:events' import { pathToFileURL } from 'node:url' import { register } from 'node:module' -import * as messages from '@cucumber/messages' -import { IdGenerator } from '@cucumber/messages' +import { Envelope, IdGenerator } from '@cucumber/messages' import supportCodeLibraryBuilder from '../../support_code_library_builder' import { SupportCodeLibrary } from '../../support_code_library_builder/types' -import { doesHaveValue } from '../../value_checker' import tryRequire from '../../try_require' import { Worker } from '../worker' import { RuntimeOptions } from '../index' -import { AssembledTestCase } from '../../assemble' import { WorkerToCoordinatorEvent, CoordinatorToWorkerCommand, InitializeCommand, + RunCommand, } from './types' const { uuid } = IdGenerator @@ -50,9 +48,9 @@ export class ChildProcessWorker { this.exit = exit this.sendMessage = sendMessage this.eventBroadcaster = new EventEmitter() - this.eventBroadcaster.on('envelope', (envelope: messages.Envelope) => { - this.sendMessage({ envelope: envelope }) - }) + this.eventBroadcaster.on('envelope', (envelope: Envelope) => + this.sendMessage({ type: 'ENVELOPE', envelope }) + ) } async initialize({ @@ -84,7 +82,7 @@ export class ChildProcessWorker { this.supportCodeLibrary ) await this.worker.runBeforeAllHooks() - this.sendMessage({ ready: true }) + this.sendMessage({ type: 'READY' }) } async finalize(): Promise { @@ -92,18 +90,28 @@ export class ChildProcessWorker { this.exit(0) } - async receiveMessage(message: CoordinatorToWorkerCommand): Promise { - if (doesHaveValue(message.initialize)) { - await this.initialize(message.initialize) - } else if (message.finalize) { - await this.finalize() - } else if (doesHaveValue(message.run)) { - await this.runTestCase(message.run) + async receiveMessage(command: CoordinatorToWorkerCommand): Promise { + switch (command.type) { + case 'INITIALIZE': + await this.initialize(command) + break + case 'RUN': + await this.runTestCase(command) + break + case 'FINALIZE': + await this.finalize() + break } } - async runTestCase(assembledTestCase: AssembledTestCase): Promise { - await this.worker.runTestCase(assembledTestCase) - this.sendMessage({ ready: true }) + async runTestCase(command: RunCommand): Promise { + const success = await this.worker.runTestCase( + command.assembledTestCase, + command.failing + ) + this.sendMessage({ + type: 'FINISHED', + success, + }) } } diff --git a/src/runtime/serial/adapter.ts b/src/runtime/serial/adapter.ts index 3522bf35d..7d4320f76 100644 --- a/src/runtime/serial/adapter.ts +++ b/src/runtime/serial/adapter.ts @@ -7,6 +7,7 @@ import { RuntimeOptions } from '../index' import { SupportCodeLibrary } from '../../support_code_library_builder/types' export class InProcessAdapter implements RuntimeAdapter { + #failing: boolean = false #worker: Worker constructor( @@ -29,9 +30,12 @@ export class InProcessAdapter implements RuntimeAdapter { ): Promise { await this.#worker.runBeforeAllHooks() for (const item of assembledTestCases) { - await this.#worker.runTestCase(item) + const success = await this.#worker.runTestCase(item, this.#failing) + if (!success) { + this.#failing = true + } } await this.#worker.runAfterAllHooks() - return this.#worker.success + return !this.#failing } } diff --git a/src/runtime/worker.ts b/src/runtime/worker.ts index 4eccc8cbd..0897abaf8 100644 --- a/src/runtime/worker.ts +++ b/src/runtime/worker.ts @@ -1,5 +1,5 @@ import { EventEmitter } from 'node:events' -import { IdGenerator, TestStepResultStatus } from '@cucumber/messages' +import { IdGenerator } from '@cucumber/messages' import { AssembledTestCase } from '../assemble' import { SupportCodeLibrary } from '../support_code_library_builder/types' import TestCaseRunner from './test_case_runner' @@ -8,7 +8,6 @@ import { makeRunTestRunHooks, RunsTestRunHooks } from './run_test_run_hooks' import { RuntimeOptions } from './index' export class Worker { - #success: boolean = true private readonly runTestRunHooks: RunsTestRunHooks constructor( @@ -33,10 +32,6 @@ export class Worker { ) } - get success() { - return this.#success - } - async runBeforeAllHooks() { await this.runTestRunHooks( this.supportCodeLibrary.beforeTestRunHookDefinitions, @@ -44,11 +39,10 @@ export class Worker { ) } - async runTestCase({ - gherkinDocument, - pickle, - testCase, - }: AssembledTestCase): Promise { + async runTestCase( + { gherkinDocument, pickle, testCase }: AssembledTestCase, + failing: boolean + ): Promise { const testCaseRunner = new TestCaseRunner({ workerId: this.workerId, eventBroadcaster: this.eventBroadcaster, @@ -57,8 +51,7 @@ export class Worker { pickle, testCase, retries: retriesForPickle(pickle, this.options), - // TODO move skip logic to coordinator? - skip: this.options.dryRun || (this.options.failFast && !this.#success), + skip: this.options.dryRun || (this.options.failFast && failing), filterStackTraces: this.options.filterStacktraces, supportCodeLibrary: this.supportCodeLibrary, worldParameters: this.options.worldParameters, @@ -66,11 +59,7 @@ export class Worker { const status = await testCaseRunner.run() - if (shouldCauseFailure(status, this.options)) { - this.#success = false - } - - return status + return !shouldCauseFailure(status, this.options) } async runAfterAllHooks() { diff --git a/src/support_code_library_builder/index.ts b/src/support_code_library_builder/index.ts index c75b0446a..4c5cb4fa2 100644 --- a/src/support_code_library_builder/index.ts +++ b/src/support_code_library_builder/index.ts @@ -11,7 +11,6 @@ import TestRunHookDefinition from '../models/test_run_hook_definition' import StepDefinition from '../models/step_definition' import { formatLocation } from '../formatter/helpers' import { doesHaveValue } from '../value_checker' -import { CanonicalSupportCodeIds } from '../runtime/parallel/types' import { GherkinStepKeyword } from '../models/gherkin_step_keyword' import validateArguments from './validate_arguments' @@ -29,6 +28,7 @@ import { ParallelAssignmentValidator, ISupportCodeCoordinates, IDefineStep, + CanonicalSupportCodeIds, } from './types' import World from './world' import { getDefinitionLineAndUri } from './get_definition_line_and_uri' diff --git a/src/support_code_library_builder/types.ts b/src/support_code_library_builder/types.ts index 9ff76284f..779c89c09 100644 --- a/src/support_code_library_builder/types.ts +++ b/src/support_code_library_builder/types.ts @@ -151,6 +151,12 @@ export interface ISupportCodeCoordinates { loaders: string[] } +export interface CanonicalSupportCodeIds { + stepDefinitionIds: string[] + beforeTestCaseHookDefinitionIds: string[] + afterTestCaseHookDefinitionIds: string[] +} + export interface SupportCodeLibrary { readonly originalCoordinates: ISupportCodeCoordinates readonly afterTestCaseHookDefinitions: TestCaseHookDefinition[] From 5a0669a944ff6bf435586059e7e1115b5f5934d2 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 18:52:56 +0100 Subject: [PATCH 17/21] assorted cleaning up --- src/api/run_cucumber.ts | 2 +- src/api/runtime.ts | 8 +-- src/assemble/assemble_test_cases.ts | 82 +++++++++--------------- src/assemble/assemble_test_cases_spec.ts | 77 +++++++++++++--------- src/assemble/types.ts | 6 +- src/runtime/coordinator.ts | 7 +- src/runtime/parallel/adapter.ts | 13 ++-- src/runtime/serial/adapter.ts | 16 ++--- src/runtime/test_case_runner_spec.ts | 30 ++++----- test/formatter_helpers.ts | 11 ++-- test/gherkin_helpers.ts | 6 +- 11 files changed, 126 insertions(+), 132 deletions(-) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index 35041936d..9f9a302c0 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -150,7 +150,7 @@ Running from: ${__dirname} environment, logger, eventBroadcaster, - filteredPickles, + sourcedPickles: filteredPickles, newId, supportCodeLibrary, options: options.runtime, diff --git a/src/api/runtime.ts b/src/api/runtime.ts index 319ff56e2..2ecdcc0ab 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -4,15 +4,15 @@ import { SupportCodeLibrary } from '../support_code_library_builder/types' import { ILogger } from '../logger' import { Runtime, Coordinator, RuntimeAdapter } from '../runtime' import { ChildProcessAdapter } from '../runtime/parallel/adapter' -import { IFilterablePickle } from '../filter' import { InProcessAdapter } from '../runtime/serial/adapter' +import { SourcedPickle } from '../assemble' import { IRunEnvironment, IRunOptionsRuntime } from './types' export async function makeRuntime({ environment, logger, eventBroadcaster, - filteredPickles, + sourcedPickles, newId, supportCodeLibrary, options, @@ -21,7 +21,7 @@ export async function makeRuntime({ logger: ILogger eventBroadcaster: EventEmitter newId: IdGenerator.NewId - filteredPickles: ReadonlyArray + sourcedPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary options: IRunOptionsRuntime }): Promise { @@ -43,7 +43,7 @@ export async function makeRuntime({ return new Coordinator( eventBroadcaster, newId, - filteredPickles, + sourcedPickles, supportCodeLibrary, adapter ) diff --git a/src/assemble/assemble_test_cases.ts b/src/assemble/assemble_test_cases.ts index 07b167d20..0803fb233 100644 --- a/src/assemble/assemble_test_cases.ts +++ b/src/assemble/assemble_test_cases.ts @@ -1,70 +1,47 @@ import { EventEmitter } from 'node:events' -import * as messages from '@cucumber/messages' -import { IdGenerator } from '@cucumber/messages' +import { + Envelope, + IdGenerator, + Pickle, + TestCase, + TestStep, + Group as MessagesGroup, +} from '@cucumber/messages' import { Group } from '@cucumber/cucumber-expressions' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { doesHaveValue } from '../value_checker' -import { IFilterablePickle } from '../filter' -import { AssembledTestCase, TestCasesByPickleId } from './types' +import { AssembledTestCase, SourcedPickle } from './types' export async function assembleTestCases({ eventBroadcaster, newId, - filteredPickles, + sourcedPickles, supportCodeLibrary, }: { eventBroadcaster: EventEmitter newId: IdGenerator.NewId - filteredPickles: ReadonlyArray + sourcedPickles: ReadonlyArray supportCodeLibrary: SupportCodeLibrary }): Promise> { - const testCasesByPickleId = await assembleTestCasesByPickleId({ - eventBroadcaster, - newId, - pickles: filteredPickles.map(({ pickle }) => pickle), - supportCodeLibrary, - }) - return filteredPickles.map(({ gherkinDocument, pickle }) => { - return { - gherkinDocument, - pickle, - testCase: testCasesByPickleId[pickle.id], - } - }) -} - -export async function assembleTestCasesByPickleId({ - eventBroadcaster, - newId, - pickles, - supportCodeLibrary, -}: { - eventBroadcaster: EventEmitter - newId: IdGenerator.NewId - pickles: messages.Pickle[] - supportCodeLibrary: SupportCodeLibrary -}): Promise { - const result: TestCasesByPickleId = {} - for (const pickle of pickles) { - const { id: pickleId } = pickle + return sourcedPickles.map(({ gherkinDocument, pickle }) => { const testCaseId = newId() - const fromBeforeHooks: messages.TestStep[] = makeBeforeHookSteps({ + const fromBeforeHooks: TestStep[] = makeBeforeHookSteps({ supportCodeLibrary, pickle, newId, }) - const fromStepDefinitions: messages.TestStep[] = makeSteps({ + const fromStepDefinitions: TestStep[] = makeSteps({ pickle, supportCodeLibrary, newId, }) - const fromAfterHooks: messages.TestStep[] = makeAfterHookSteps({ + const fromAfterHooks: TestStep[] = makeAfterHookSteps({ supportCodeLibrary, pickle, newId, }) - const testCase: messages.TestCase = { - pickleId, + const testCase: TestCase = { + pickleId: pickle.id, id: testCaseId, testSteps: [ ...fromBeforeHooks, @@ -72,10 +49,13 @@ export async function assembleTestCasesByPickleId({ ...fromAfterHooks, ], } - eventBroadcaster.emit('envelope', { testCase }) - result[pickleId] = testCase - } - return result + eventBroadcaster.emit('envelope', { testCase } satisfies Envelope) + return { + gherkinDocument, + pickle, + testCase, + } + }) } function makeAfterHookSteps({ @@ -84,9 +64,9 @@ function makeAfterHookSteps({ newId, }: { supportCodeLibrary: SupportCodeLibrary - pickle: messages.Pickle + pickle: Pickle newId: IdGenerator.NewId -}): messages.TestStep[] { +}): TestStep[] { return supportCodeLibrary.afterTestCaseHookDefinitions .slice(0) .reverse() @@ -103,9 +83,9 @@ function makeBeforeHookSteps({ newId, }: { supportCodeLibrary: SupportCodeLibrary - pickle: messages.Pickle + pickle: Pickle newId: IdGenerator.NewId -}): messages.TestStep[] { +}): TestStep[] { return supportCodeLibrary.beforeTestCaseHookDefinitions .filter((hookDefinition) => hookDefinition.appliesToTestCase(pickle)) .map((hookDefinition) => ({ @@ -119,10 +99,10 @@ function makeSteps({ supportCodeLibrary, newId, }: { - pickle: messages.Pickle + pickle: Pickle supportCodeLibrary: SupportCodeLibrary newId: () => string -}): messages.TestStep[] { +}): TestStep[] { return pickle.steps.map((pickleStep) => { const stepDefinitions = supportCodeLibrary.stepDefinitions.filter( (stepDefinition) => stepDefinition.matchesStepName(pickleStep.text) @@ -148,7 +128,7 @@ function makeSteps({ }) } -function mapArgumentGroup(group: Group): messages.Group { +function mapArgumentGroup(group: Group): MessagesGroup { return { start: group.start, value: group.value, diff --git a/src/assemble/assemble_test_cases_spec.ts b/src/assemble/assemble_test_cases_spec.ts index 19a23a3d5..9bca85c46 100644 --- a/src/assemble/assemble_test_cases_spec.ts +++ b/src/assemble/assemble_test_cases_spec.ts @@ -1,6 +1,11 @@ import { EventEmitter } from 'node:events' -import { IdGenerator } from '@cucumber/messages' -import * as messages from '@cucumber/messages' +import { + Envelope, + GherkinDocument, + IdGenerator, + Pickle, + TestCase, +} from '@cucumber/messages' import { afterEach, beforeEach, describe, it } from 'mocha' import FakeTimers, { InstalledClock } from '@sinonjs/fake-timers' import { expect } from 'chai' @@ -8,31 +13,29 @@ import timeMethods from '../time' import { buildSupportCodeLibrary } from '../../test/runtime_helpers' import { parse } from '../../test/gherkin_helpers' import { SupportCodeLibrary } from '../support_code_library_builder/types' -import { assembleTestCasesByPickleId } from './assemble_test_cases' -import { TestCasesByPickleId } from './types' +import { assembleTestCases } from './assemble_test_cases' +import { AssembledTestCase } from './types' -interface IRequest { - gherkinDocument: messages.GherkinDocument - pickles: messages.Pickle[] +async function testAssembleTestCases({ + gherkinDocument, + pickles, + supportCodeLibrary, +}: { + gherkinDocument: GherkinDocument + pickles: Pickle[] supportCodeLibrary: SupportCodeLibrary -} - -interface IResponse { - envelopes: messages.Envelope[] - result: TestCasesByPickleId -} - -async function testAssembleTestCasesByPickleId( - options: IRequest -): Promise { - const envelopes: messages.Envelope[] = [] +}): Promise<{ + envelopes: Envelope[] + result: ReadonlyArray +}> { + const envelopes: Envelope[] = [] const eventBroadcaster = new EventEmitter() eventBroadcaster.on('envelope', (e) => envelopes.push(e)) - const result = await assembleTestCasesByPickleId({ + const result = await assembleTestCases({ eventBroadcaster, newId: IdGenerator.incrementing(), - pickles: options.pickles, - supportCodeLibrary: options.supportCodeLibrary, + sourcedPickles: pickles.map((pickle) => ({ gherkinDocument, pickle })), + supportCodeLibrary, }) return { envelopes, result } } @@ -48,7 +51,7 @@ describe('assembleTestCases', () => { clock.uninstall() }) - describe('assembleTestCasesByPickleId()', () => { + describe('assembleTestCases()', () => { it('emits testCase messages', async () => { // Arrange const supportCodeLibrary = buildSupportCodeLibrary(({ Given }) => { @@ -68,13 +71,13 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes, result } = await testAssembleTestCasesByPickleId({ + const { envelopes, result } = await testAssembleTestCases({ gherkinDocument, pickles, supportCodeLibrary, }) - const testCase1: messages.TestCase = { + const testCase0: TestCase = { id: '0', pickleId: pickles[0].id, testSteps: [ @@ -91,7 +94,7 @@ describe('assembleTestCases', () => { ], } - const testCase2: messages.TestCase = { + const testCase1: TestCase = { id: '2', pickleId: pickles[1].id, testSteps: [ @@ -111,15 +114,25 @@ describe('assembleTestCases', () => { // Assert expect(envelopes).to.eql([ { - testCase: testCase1, + testCase: testCase0, }, { - testCase: testCase2, + testCase: testCase1, }, ]) - expect(Object.keys(result)).to.eql([pickles[0].id, pickles[1].id]) - expect(Object.values(result)).to.eql([testCase1, testCase2]) + expect(result).to.eql([ + { + gherkinDocument, + pickle: pickles[0], + testCase: testCase0, + }, + { + gherkinDocument, + pickle: pickles[1], + testCase: testCase1, + }, + ]) }) describe('with a parameterised step', () => { @@ -140,7 +153,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCasesByPickleId({ + const { envelopes } = await testAssembleTestCases({ gherkinDocument, pickles, supportCodeLibrary, @@ -214,7 +227,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCasesByPickleId({ + const { envelopes } = await testAssembleTestCases({ gherkinDocument, pickles, supportCodeLibrary, @@ -268,7 +281,7 @@ describe('assembleTestCases', () => { }) // Act - const { envelopes } = await testAssembleTestCasesByPickleId({ + const { envelopes } = await testAssembleTestCases({ gherkinDocument, pickles, supportCodeLibrary, diff --git a/src/assemble/types.ts b/src/assemble/types.ts index 821ae2989..49ba81fd6 100644 --- a/src/assemble/types.ts +++ b/src/assemble/types.ts @@ -1,7 +1,9 @@ import { GherkinDocument, Pickle, TestCase } from '@cucumber/messages' -import * as messages from '@cucumber/messages' -export declare type TestCasesByPickleId = Record +export interface SourcedPickle { + gherkinDocument: GherkinDocument + pickle: Pickle +} export interface AssembledTestCase { gherkinDocument: GherkinDocument diff --git a/src/runtime/coordinator.ts b/src/runtime/coordinator.ts index e958572f6..116276afd 100644 --- a/src/runtime/coordinator.ts +++ b/src/runtime/coordinator.ts @@ -1,7 +1,6 @@ import { EventEmitter } from 'node:events' import { Envelope, IdGenerator } from '@cucumber/messages' -import { IFilterablePickle } from '../filter' -import { assembleTestCases } from '../assemble' +import { assembleTestCases, SourcedPickle } from '../assemble' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { RuntimeAdapter } from './types' import { timestamp } from './stopwatch' @@ -11,7 +10,7 @@ export class Coordinator implements Runtime { constructor( private eventBroadcaster: EventEmitter, private newId: IdGenerator.NewId, - private filteredPickles: ReadonlyArray, + private sourcedPickles: ReadonlyArray, private supportCodeLibrary: SupportCodeLibrary, private adapter: RuntimeAdapter ) {} @@ -26,7 +25,7 @@ export class Coordinator implements Runtime { const assembledTestCases = await assembleTestCases({ eventBroadcaster: this.eventBroadcaster, newId: this.newId, - filteredPickles: this.filteredPickles, + sourcedPickles: this.sourcedPickles, supportCodeLibrary: this.supportCodeLibrary, }) diff --git a/src/runtime/parallel/adapter.ts b/src/runtime/parallel/adapter.ts index 30b3f69cb..b937800d8 100644 --- a/src/runtime/parallel/adapter.ts +++ b/src/runtime/parallel/adapter.ts @@ -22,7 +22,7 @@ const enum WorkerState { 'new', } -interface IWorker { +interface ManagedWorker { state: WorkerState process: ChildProcess id: string @@ -39,7 +39,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { private onFinish: (success: boolean) => void private todo: Array = [] private readonly inProgress: Record = {} - private readonly workers: Record = {} + private readonly workers: Record = {} constructor( private readonly environment: IRunEnvironment, @@ -49,7 +49,10 @@ export class ChildProcessAdapter implements RuntimeAdapter { private readonly supportCodeLibrary: SupportCodeLibrary ) {} - parseWorkerMessage(worker: IWorker, message: WorkerToCoordinatorEvent): void { + parseWorkerMessage( + worker: ManagedWorker, + message: WorkerToCoordinatorEvent + ): void { switch (message.type) { case 'READY': worker.state = WorkerState.idle @@ -73,7 +76,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { } } - awakenWorkers(triggeringWorker: IWorker): void { + awakenWorkers(triggeringWorker: ManagedWorker): void { Object.values(this.workers).forEach((worker) => { if (worker.state === WorkerState.idle) { this.giveWork(worker) @@ -180,7 +183,7 @@ export class ChildProcessAdapter implements RuntimeAdapter { } } - giveWork(worker: IWorker, force: boolean = false): void { + giveWork(worker: ManagedWorker, force: boolean = false): void { if (this.todo.length < 1) { worker.state = WorkerState.running worker.process.send({ type: 'FINALIZE' } satisfies FinalizeCommand) diff --git a/src/runtime/serial/adapter.ts b/src/runtime/serial/adapter.ts index 7d4320f76..5be77281e 100644 --- a/src/runtime/serial/adapter.ts +++ b/src/runtime/serial/adapter.ts @@ -7,8 +7,8 @@ import { RuntimeOptions } from '../index' import { SupportCodeLibrary } from '../../support_code_library_builder/types' export class InProcessAdapter implements RuntimeAdapter { - #failing: boolean = false - #worker: Worker + private readonly worker: Worker + private failing: boolean = false constructor( eventBroadcaster: EventEmitter, @@ -16,7 +16,7 @@ export class InProcessAdapter implements RuntimeAdapter { options: RuntimeOptions, supportCodeLibrary: SupportCodeLibrary ) { - this.#worker = new Worker( + this.worker = new Worker( undefined, eventBroadcaster, newId, @@ -28,14 +28,14 @@ export class InProcessAdapter implements RuntimeAdapter { async run( assembledTestCases: ReadonlyArray ): Promise { - await this.#worker.runBeforeAllHooks() + await this.worker.runBeforeAllHooks() for (const item of assembledTestCases) { - const success = await this.#worker.runTestCase(item, this.#failing) + const success = await this.worker.runTestCase(item, this.failing) if (!success) { - this.#failing = true + this.failing = true } } - await this.#worker.runAfterAllHooks() - return !this.#failing + await this.worker.runAfterAllHooks() + return !this.failing } } diff --git a/src/runtime/test_case_runner_spec.ts b/src/runtime/test_case_runner_spec.ts index c6163c80c..0a84c3190 100644 --- a/src/runtime/test_case_runner_spec.ts +++ b/src/runtime/test_case_runner_spec.ts @@ -2,8 +2,8 @@ import { EventEmitter } from 'node:events' import sinon from 'sinon' import { expect } from 'chai' import { afterEach, beforeEach, describe, it } from 'mocha' -import { IdGenerator } from '@cucumber/messages' import * as messages from '@cucumber/messages' +import { Envelope, IdGenerator } from '@cucumber/messages' import FakeTimers, { InstalledClock } from '@sinonjs/fake-timers' import { buildSupportCodeLibrary } from '../../test/runtime_helpers' import { parse } from '../../test/gherkin_helpers' @@ -11,38 +11,36 @@ import timeMethods from '../time' import { getBaseSupportCodeLibrary } from '../../test/fixtures/steps' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { valueOrDefault } from '../value_checker' -import { assembleTestCasesByPickleId } from '../assemble/assemble_test_cases' +import { assembleTestCases } from '../assemble' import TestCaseRunner from './test_case_runner' -import IEnvelope = messages.Envelope -interface ITestRunnerRequest { +async function testRunner(options: { workerId?: string gherkinDocument: messages.GherkinDocument pickle: messages.Pickle retries?: number skip?: boolean supportCodeLibrary: SupportCodeLibrary -} - -interface ITestRunnerResponse { +}): Promise<{ envelopes: messages.Envelope[] result: messages.TestStepResultStatus -} - -async function testRunner( - options: ITestRunnerRequest -): Promise { - const envelopes: IEnvelope[] = [] +}> { + const envelopes: Envelope[] = [] const eventBroadcaster = new EventEmitter() const newId = IdGenerator.incrementing() const testCase = ( - await assembleTestCasesByPickleId({ + await assembleTestCases({ eventBroadcaster, newId, - pickles: [options.pickle], + sourcedPickles: [ + { + gherkinDocument: options.gherkinDocument, + pickle: options.pickle, + }, + ], supportCodeLibrary: options.supportCodeLibrary, }) - )[options.pickle.id] + )[0].testCase // listen for envelopers _after_ we've assembled test cases eventBroadcaster.on('envelope', (e) => envelopes.push(e)) diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index aa27b40cd..223a74737 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -3,7 +3,7 @@ import { PassThrough } from 'node:stream' import { promisify } from 'node:util' import { IdGenerator } from '@cucumber/messages' import * as messages from '@cucumber/messages' -import { RuntimeOptions } from '../src/runtime' +import { Coordinator, RuntimeOptions } from '../src/runtime' import { EventDataCollector } from '../src/formatter/helpers' import FormatterBuilder from '../src/formatter/builder' import { SupportCodeLibrary } from '../src/support_code_library_builder/types' @@ -11,9 +11,8 @@ import { ITestCaseAttempt } from '../src/formatter/helpers/event_data_collector' import { doesNotHaveValue } from '../src/value_checker' import { emitSupportCodeMessages } from '../src/cli/helpers' import { FormatOptions } from '../src/formatter' -import { Coordinator } from '../src/runtime/coordinator' -import { IFilterablePickle } from '../src/filter' import { InProcessAdapter } from '../src/runtime/serial/adapter' +import { SourcedPickle } from '../src/assemble' import { generatePickles } from './gherkin_helpers' import { buildOptions, buildSupportCodeLibrary } from './runtime_helpers' @@ -74,7 +73,7 @@ export async function testFormatter({ cleanup: promisify(passThrough.end.bind(passThrough)), supportCodeLibrary, }) - let filteredPickles: IFilterablePickle[] = [] + let filteredPickles: SourcedPickle[] = [] for (const source of sources) { const generated = await generatePickles({ data: source.data, @@ -111,7 +110,7 @@ export async function getTestCaseAttempts({ } const eventBroadcaster = new EventEmitter() const eventDataCollector = new EventDataCollector(eventBroadcaster) - let filteredPickles: IFilterablePickle[] = [] + let filteredPickles: SourcedPickle[] = [] for (const source of sources) { const generated = await generatePickles({ data: source.data, @@ -156,7 +155,7 @@ export async function getEnvelopesAndEventDataCollector({ eventBroadcaster, newId: IdGenerator.uuid(), }) - let filteredPickles: IFilterablePickle[] = [] + let filteredPickles: SourcedPickle[] = [] for (const source of sources) { const generated = await generatePickles({ diff --git a/test/gherkin_helpers.ts b/test/gherkin_helpers.ts index a1b3d749b..fc43ac2e2 100644 --- a/test/gherkin_helpers.ts +++ b/test/gherkin_helpers.ts @@ -4,7 +4,7 @@ import { SourceMediaType } from '@cucumber/messages' import { IGherkinOptions } from '@cucumber/gherkin' import { GherkinStreams } from '@cucumber/gherkin-streams' import { doesHaveValue } from '../src/value_checker' -import { IFilterablePickle } from '../src/filter' +import { SourcedPickle } from '../src/assemble' export interface IParsedSource { pickles: messages.Pickle[] @@ -81,7 +81,7 @@ export async function generatePickles({ data, eventBroadcaster, uri, -}: GeneratePicklesRequest): Promise> { +}: GeneratePicklesRequest): Promise> { const { envelopes, gherkinDocument, pickles } = await parse({ data, uri, @@ -91,7 +91,7 @@ export async function generatePickles({ return { gherkinDocument, pickle, - } as IFilterablePickle + } }) } From 092ba97302cdaae39fc338e7a2889d1c107d9c70 Mon Sep 17 00:00:00 2001 From: David Goss Date: Wed, 21 Aug 2024 19:37:45 +0100 Subject: [PATCH 18/21] update CHANGELOG.md --- CHANGELOG.md | 3 +++ 1 file changed, 3 insertions(+) diff --git a/CHANGELOG.md b/CHANGELOG.md index 66abfc224..530e39d0e 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -8,6 +8,9 @@ and this project adheres to [Semantic Versioning](http://semver.org/). Please see [CONTRIBUTING.md](./CONTRIBUTING.md) on how to contribute to Cucumber. ## [Unreleased] +### Changed +- Major refactor of runtime code for both serial and parallel modes ([#2422](https://github.com/cucumber/cucumber-js/pull/2422)) + ### Removed - BREAKING CHANGE: Remove previously-deprecated `parseGherkinMessageStream` ([#2420](https://github.com/cucumber/cucumber-js/pull/2420)) - BREAKING CHANGE: Remove previously-deprecated `PickleFilter` ([#2420](https://github.com/cucumber/cucumber-js/pull/2420)) From 68d0f0c82b29012507f5850d583f1694e6ee7f1e Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 22 Aug 2024 08:01:51 +0100 Subject: [PATCH 19/21] remove temporary build config --- .github/workflows/build.yaml | 1 - 1 file changed, 1 deletion(-) diff --git a/.github/workflows/build.yaml b/.github/workflows/build.yaml index fdfc8b9bd..c4e6ea11e 100644 --- a/.github/workflows/build.yaml +++ b/.github/workflows/build.yaml @@ -7,7 +7,6 @@ on: push: branches: - main - - feat/unified-runner pull_request: branches: - main From 31b824341a6f9b4c2fa80ff48408af83d006ada1 Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 22 Aug 2024 14:20:07 +0100 Subject: [PATCH 20/21] simplify testing some more --- test/formatter_helpers.ts | 94 +++++++++++++++------------------------ 1 file changed, 35 insertions(+), 59 deletions(-) diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index 223a74737..502ae881c 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -3,7 +3,7 @@ import { PassThrough } from 'node:stream' import { promisify } from 'node:util' import { IdGenerator } from '@cucumber/messages' import * as messages from '@cucumber/messages' -import { Coordinator, RuntimeOptions } from '../src/runtime' +import { RuntimeOptions } from '../src/runtime' import { EventDataCollector } from '../src/formatter/helpers' import FormatterBuilder from '../src/formatter/builder' import { SupportCodeLibrary } from '../src/support_code_library_builder/types' @@ -11,12 +11,12 @@ import { ITestCaseAttempt } from '../src/formatter/helpers/event_data_collector' import { doesNotHaveValue } from '../src/value_checker' import { emitSupportCodeMessages } from '../src/cli/helpers' import { FormatOptions } from '../src/formatter' -import { InProcessAdapter } from '../src/runtime/serial/adapter' import { SourcedPickle } from '../src/assemble' +import { makeRuntime } from '../src/api/runtime' +import { IRunEnvironment } from '../src/api' import { generatePickles } from './gherkin_helpers' import { buildOptions, buildSupportCodeLibrary } from './runtime_helpers' - -const { uuid } = IdGenerator +import { FakeLogger } from './fake_logger' export interface ITestSource { data: string @@ -55,7 +55,7 @@ export async function testFormatter({ emitSupportCodeMessages({ supportCodeLibrary, eventBroadcaster, - newId: uuid(), + newId: IdGenerator.uuid(), }) let output = '' const logFn = (data: string): void => { @@ -73,28 +73,29 @@ export async function testFormatter({ cleanup: promisify(passThrough.end.bind(passThrough)), supportCodeLibrary, }) - let filteredPickles: SourcedPickle[] = [] + + let sourcedPickles: SourcedPickle[] = [] for (const source of sources) { const generated = await generatePickles({ data: source.data, eventBroadcaster, uri: source.uri, }) - filteredPickles = filteredPickles.concat(generated) + sourcedPickles = sourcedPickles.concat(generated) } - const runtime = new Coordinator( + + const runtime = await makeRuntime({ + environment: {} as IRunEnvironment, + logger: new FakeLogger(), eventBroadcaster, - uuid(), - filteredPickles, + sourcedPickles, + newId: IdGenerator.uuid(), supportCodeLibrary, - new InProcessAdapter( - eventBroadcaster, - uuid(), - buildOptions(runtimeOptions), - supportCodeLibrary - ) - ) - + options: { + ...buildOptions(runtimeOptions), + parallel: 0, + }, + }) await runtime.run() return normalizeSummaryDuration(output) @@ -105,35 +106,11 @@ export async function getTestCaseAttempts({ supportCodeLibrary, sources = [], }: ITestRunOptions): Promise { - if (doesNotHaveValue(supportCodeLibrary)) { - supportCodeLibrary = buildSupportCodeLibrary() - } - const eventBroadcaster = new EventEmitter() - const eventDataCollector = new EventDataCollector(eventBroadcaster) - let filteredPickles: SourcedPickle[] = [] - for (const source of sources) { - const generated = await generatePickles({ - data: source.data, - eventBroadcaster, - uri: source.uri, - }) - filteredPickles = filteredPickles.concat(generated) - } - const runtime = new Coordinator( - eventBroadcaster, - uuid(), - filteredPickles, + const { eventDataCollector } = await getEnvelopesAndEventDataCollector({ + runtimeOptions, supportCodeLibrary, - new InProcessAdapter( - eventBroadcaster, - uuid(), - buildOptions(runtimeOptions), - supportCodeLibrary - ) - ) - - await runtime.run() - + sources, + }) return eventDataCollector.getTestCaseAttempts() } @@ -155,32 +132,31 @@ export async function getEnvelopesAndEventDataCollector({ eventBroadcaster, newId: IdGenerator.uuid(), }) - let filteredPickles: SourcedPickle[] = [] + let sourcedPickles: SourcedPickle[] = [] for (const source of sources) { const generated = await generatePickles({ data: source.data, eventBroadcaster, uri: source.uri, }) - filteredPickles = filteredPickles.concat( + sourcedPickles = sourcedPickles.concat( generated.filter((item) => pickleFilter(item.pickle)) ) } - const runtime = new Coordinator( + const runtime = await makeRuntime({ + environment: {} as IRunEnvironment, + logger: new FakeLogger(), eventBroadcaster, - uuid(), - filteredPickles, + sourcedPickles, + newId: IdGenerator.uuid(), supportCodeLibrary, - new InProcessAdapter( - eventBroadcaster, - uuid(), - buildOptions(runtimeOptions), - supportCodeLibrary - ) - ) - + options: { + ...buildOptions(runtimeOptions), + parallel: 0, + }, + }) await runtime.run() return { envelopes, eventDataCollector } From e28477051dd0349ecb5bd6eff26764865fd36a40 Mon Sep 17 00:00:00 2001 From: David Goss Date: Thu, 22 Aug 2024 14:46:49 +0100 Subject: [PATCH 21/21] move makeRuntime down to module --- src/api/run_cucumber.ts | 2 +- src/runtime/index.ts | 4 ++-- src/{api/runtime.ts => runtime/make_runtime.ts} | 11 ++++++----- test/formatter_helpers.ts | 3 +-- 4 files changed, 10 insertions(+), 10 deletions(-) rename src/{api/runtime.ts => runtime/make_runtime.ts} (80%) diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index 9f9a302c0..e9651c84e 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -6,8 +6,8 @@ import { resolvePaths } from '../paths' import { SupportCodeLibrary } from '../support_code_library_builder/types' import { version } from '../version' import { IFilterablePickle } from '../filter' +import { makeRuntime } from '../runtime' import { IRunOptions, IRunEnvironment, IRunResult } from './types' -import { makeRuntime } from './runtime' import { initializeFormatters } from './formatters' import { getSupportCodeLibrary } from './support' import { mergeEnvironment } from './environment' diff --git a/src/runtime/index.ts b/src/runtime/index.ts index eefce5d8c..9a917287a 100644 --- a/src/runtime/index.ts +++ b/src/runtime/index.ts @@ -1,2 +1,2 @@ -export * from './coordinator' -export * from './types' +export * from './make_runtime' +export { Runtime, RuntimeOptions } from './types' diff --git a/src/api/runtime.ts b/src/runtime/make_runtime.ts similarity index 80% rename from src/api/runtime.ts rename to src/runtime/make_runtime.ts index 2ecdcc0ab..8a3b12368 100644 --- a/src/api/runtime.ts +++ b/src/runtime/make_runtime.ts @@ -1,12 +1,13 @@ import { EventEmitter } from 'node:events' import { IdGenerator } from '@cucumber/messages' -import { SupportCodeLibrary } from '../support_code_library_builder/types' +import { IRunEnvironment, IRunOptionsRuntime } from '../api' import { ILogger } from '../logger' -import { Runtime, Coordinator, RuntimeAdapter } from '../runtime' -import { ChildProcessAdapter } from '../runtime/parallel/adapter' -import { InProcessAdapter } from '../runtime/serial/adapter' import { SourcedPickle } from '../assemble' -import { IRunEnvironment, IRunOptionsRuntime } from './types' +import { SupportCodeLibrary } from '../support_code_library_builder/types' +import { Runtime, RuntimeAdapter } from './types' +import { ChildProcessAdapter } from './parallel/adapter' +import { InProcessAdapter } from './serial/adapter' +import { Coordinator } from './coordinator' export async function makeRuntime({ environment, diff --git a/test/formatter_helpers.ts b/test/formatter_helpers.ts index 502ae881c..0e0629605 100644 --- a/test/formatter_helpers.ts +++ b/test/formatter_helpers.ts @@ -3,7 +3,7 @@ import { PassThrough } from 'node:stream' import { promisify } from 'node:util' import { IdGenerator } from '@cucumber/messages' import * as messages from '@cucumber/messages' -import { RuntimeOptions } from '../src/runtime' +import { makeRuntime, RuntimeOptions } from '../src/runtime' import { EventDataCollector } from '../src/formatter/helpers' import FormatterBuilder from '../src/formatter/builder' import { SupportCodeLibrary } from '../src/support_code_library_builder/types' @@ -12,7 +12,6 @@ import { doesNotHaveValue } from '../src/value_checker' import { emitSupportCodeMessages } from '../src/cli/helpers' import { FormatOptions } from '../src/formatter' import { SourcedPickle } from '../src/assemble' -import { makeRuntime } from '../src/api/runtime' import { IRunEnvironment } from '../src/api' import { generatePickles } from './gherkin_helpers' import { buildOptions, buildSupportCodeLibrary } from './runtime_helpers'