Skip to content

Commit

Permalink
adapters need to maintain failing status
Browse files Browse the repository at this point in the history
  • Loading branch information
davidjgoss committed Aug 21, 2024
1 parent 8eaabcf commit 019709f
Show file tree
Hide file tree
Showing 9 changed files with 127 additions and 119 deletions.
1 change: 0 additions & 1 deletion src/api/run_cucumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -150,7 +150,6 @@ Running from: ${__dirname}
environment,
logger,
eventBroadcaster,
eventDataCollector,
filteredPickles,
newId,
supportCodeLibrary,
Expand Down
4 changes: 0 additions & 4 deletions src/api/runtime.ts
Original file line number Diff line number Diff line change
@@ -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'
Expand All @@ -13,7 +12,6 @@ export async function makeRuntime({
environment,
logger,
eventBroadcaster,
eventDataCollector,
filteredPickles,
newId,
supportCodeLibrary,
Expand All @@ -22,7 +20,6 @@ export async function makeRuntime({
environment: IRunEnvironment
logger: ILogger
eventBroadcaster: EventEmitter
eventDataCollector: EventDataCollector
newId: IdGenerator.NewId
filteredPickles: ReadonlyArray<IFilterablePickle>
supportCodeLibrary: SupportCodeLibrary
Expand All @@ -34,7 +31,6 @@ export async function makeRuntime({
environment,
logger,
eventBroadcaster,
eventDataCollector,
options,
supportCodeLibrary
)
Expand Down
111 changes: 49 additions & 62 deletions src/runtime/parallel/adapter.ts
Original file line number Diff line number Diff line change
@@ -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')

Expand All @@ -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<AssembledTestCase> = []
private readonly inProgress: Record<string, AssembledTestCase> = {}
Expand All @@ -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)}`
)
}
}

Expand Down Expand Up @@ -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)
}
}

Expand Down Expand Up @@ -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
}

Expand All @@ -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)
}
}
45 changes: 32 additions & 13 deletions src/runtime/parallel/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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
}
44 changes: 26 additions & 18 deletions src/runtime/parallel/worker.ts
Original file line number Diff line number Diff line change
@@ -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
Expand Down Expand Up @@ -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({
Expand Down Expand Up @@ -84,26 +82,36 @@ export class ChildProcessWorker {
this.supportCodeLibrary
)
await this.worker.runBeforeAllHooks()
this.sendMessage({ ready: true })
this.sendMessage({ type: 'READY' })
}

async finalize(): Promise<void> {
await this.worker.runAfterAllHooks()
this.exit(0)
}

async receiveMessage(message: CoordinatorToWorkerCommand): Promise<void> {
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<void> {
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<void> {
await this.worker.runTestCase(assembledTestCase)
this.sendMessage({ ready: true })
async runTestCase(command: RunCommand): Promise<void> {
const success = await this.worker.runTestCase(
command.assembledTestCase,
command.failing
)
this.sendMessage({
type: 'FINISHED',
success,
})
}
}
8 changes: 6 additions & 2 deletions src/runtime/serial/adapter.ts
Original file line number Diff line number Diff line change
Expand Up @@ -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(
Expand All @@ -29,9 +30,12 @@ export class InProcessAdapter implements RuntimeAdapter {
): Promise<boolean> {
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
}
}
Loading

0 comments on commit 019709f

Please sign in to comment.