From 3afe03b4ce47ce3503defb1df3a7c61b08e39919 Mon Sep 17 00:00:00 2001 From: Andras-Marozsi Date: Mon, 25 Nov 2019 14:03:17 -0500 Subject: [PATCH] Add stepHook functionality - without tests --- src/index.js | 4 +- src/models/test_step_hook_definition.js | 27 +++++ src/runtime/test_case_runner.js | 100 +++++++++++++++++- src/runtime/test_case_runner_spec.js | 2 + .../build_helpers.js | 22 ++++ src/support_code_library_builder/index.js | 16 +++ .../validate_arguments.js | 12 +++ 7 files changed, 178 insertions(+), 5 deletions(-) create mode 100644 src/models/test_step_hook_definition.js diff --git a/src/index.js b/src/index.js index 1fe90083b..b8a418019 100644 --- a/src/index.js +++ b/src/index.js @@ -27,12 +27,14 @@ export { default as UsageFormatter } from './formatter/usage_formatter' export { default as UsageJsonFormatter } from './formatter/usage_json_formatter' export { formatterHelpers } -// Support Code Fuctions +// Support Code Functions const { methods } = supportCodeLibraryBuilder export const After = methods.After export const AfterAll = methods.AfterAll +export const AfterStep = methods.AfterStep export const Before = methods.Before export const BeforeAll = methods.BeforeAll +export const BeforeStep = methods.BeforeStep export const defineParameterType = methods.defineParameterType export const defineStep = methods.defineStep export const defineSupportCode = methods.defineSupportCode diff --git a/src/models/test_step_hook_definition.js b/src/models/test_step_hook_definition.js new file mode 100644 index 000000000..3c0217003 --- /dev/null +++ b/src/models/test_step_hook_definition.js @@ -0,0 +1,27 @@ +import PickleFilter from '../pickle_filter' +import Definition from './definition' + +export default class TestStepHookDefinition extends Definition { + constructor(...data) { + super(...data) + this.pickleFilter = new PickleFilter({ + tagExpression: this.options.tags, + }) + } + + appliesToTestCase({ pickle, uri }) { + return this.pickleFilter.matches({ pickle, uri }) + } + + getInvalidCodeLengthMessage() { + return this.buildInvalidCodeLengthMessage('0 or 1', '2') + } + + getInvocationParameters({ hookParameter }) { + return [hookParameter] + } + + getValidCodeLengths() { + return [0, 1, 2] + } +} diff --git a/src/runtime/test_case_runner.js b/src/runtime/test_case_runner.js index 51949828c..fc0421a19 100644 --- a/src/runtime/test_case_runner.js +++ b/src/runtime/test_case_runner.js @@ -35,6 +35,8 @@ export default class TestCaseRunner { attach: ::attachmentManager.create, parameters: worldParameters, }) + this.beforeStepHookDefinitions = this.getBeforeStepHookDefinitions() + this.afterStepHookDefinitions = this.getAfterStepHookDefinitions() this.beforeHookDefinitions = this.getBeforeHookDefinitions() this.afterHookDefinitions = this.getAfterHookDefinitions() this.maxTestStepIndex = @@ -113,6 +115,18 @@ export default class TestCaseRunner { ) } + getBeforeStepHookDefinitions() { + return this.supportCodeLibrary.beforeTestStepHookDefinitions.filter( + hookDefinition => hookDefinition.appliesToTestCase(this.testCase) + ) + } + + getAfterStepHookDefinitions() { + return this.supportCodeLibrary.afterTestStepHookDefinitions.filter( + hookDefinition => hookDefinition.appliesToTestCase(this.testCase) + ) + } + getStepDefinitions(step) { return this.supportCodeLibrary.stepDefinitions.filter(stepDefinition => stepDefinition.matchesStepName(step.text) @@ -151,9 +165,11 @@ export default class TestCaseRunner { } } - async aroundTestStep(runStepFn) { + startStep() { this.emit('test-step-started', { index: this.testStepIndex }) - const testStepResult = await runStepFn() + } + + finishStep(testStepResult) { if (testStepResult.duration) { this.result.duration += testStepResult.duration } @@ -170,6 +186,44 @@ export default class TestCaseRunner { this.testStepIndex += 1 } + async aroundTestHook(runStepFn) { + this.startStep() + const testStepResult = await runStepFn() + this.finishStep(testStepResult) + } + + async aroundTestStep(runStepFn, step = null) { + this.startStep() + let stepAfterHooksResult + let testStepResult + const stepBeforeHooksResult = await this.runStepHooks(step, true) + if (stepBeforeHooksResult.status !== Status.FAILED) { + testStepResult = await runStepFn() + if (testStepResult.status === Status.PASSED) { + stepAfterHooksResult = await this.runStepHooks(step, false) + } + } + let cumulatedStepResult = stepBeforeHooksResult + if (testStepResult) { + cumulatedStepResult = testStepResult + if (stepBeforeHooksResult.duration !== undefined) { + cumulatedStepResult.duration += stepBeforeHooksResult.duration + } + } + if (stepAfterHooksResult) { + if (stepAfterHooksResult.duration !== undefined) { + cumulatedStepResult.duration += stepAfterHooksResult.duration + } + if (this.shouldUpdateStatus(stepAfterHooksResult)) { + cumulatedStepResult.status = stepAfterHooksResult.status + } + if (stepAfterHooksResult.exception) { + cumulatedStepResult.exception = stepAfterHooksResult.exception + } + } + this.finishStep(cumulatedStepResult) + } + async run() { this.emitPrepared() for ( @@ -220,12 +274,50 @@ export default class TestCaseRunner { async runHooks(hookDefinitions, hookParameter, isBeforeHook) { await Promise.each(hookDefinitions, async hookDefinition => { - await this.aroundTestStep(() => + await this.aroundTestHook(() => this.runHook(hookDefinition, hookParameter, isBeforeHook) ) }) } + async runStepHooks(step, isBeforeHook) { + const status = + this.result.status === Status.FAILED ? Status.SKIPPED : this.result.status + const stepHooksResult = { status: status } + if (step) { + const stepInfo = { + text: step.text, + arguments: step.arguments, + location: Object.assign({ uri: this.testCase.uri }, step.locations[0]), + } + let stepHooks = [] + if (isBeforeHook) { + stepHooks = this.beforeStepHookDefinitions + } else { + stepHooks = this.afterStepHookDefinitions + } + await Promise.each(stepHooks, async stepHook => { + if (this.isSkippingSteps()) { + stepHooksResult.status = Status.SKIPPED + return + } + const stepHookResult = await this.invokeStep(null, stepHook, stepInfo) + if (this.shouldUpdateStatus(stepHookResult)) { + stepHooksResult.status = stepHookResult.status + } + if (stepHookResult.exception) { + stepHooksResult.exception = stepHookResult.exception + } + if (stepHookResult.duration) { + stepHooksResult.duration = stepHooksResult.duration + ? stepHooksResult.duration + stepHookResult.duration + : stepHookResult.duration + } + }) + } + return stepHooksResult + } + async runStep(step) { const stepDefinitions = this.getStepDefinitions(step) if (stepDefinitions.length === 0) { @@ -243,7 +335,7 @@ export default class TestCaseRunner { async runSteps() { await Promise.each(this.testCase.pickle.steps, async step => { - await this.aroundTestStep(() => this.runStep(step)) + await this.aroundTestStep(() => this.runStep(step), step) }) } } diff --git a/src/runtime/test_case_runner_spec.js b/src/runtime/test_case_runner_spec.js index 96fc01f7e..997db1ae5 100644 --- a/src/runtime/test_case_runner_spec.js +++ b/src/runtime/test_case_runner_spec.js @@ -29,7 +29,9 @@ describe('TestCaseRunner', () => { } this.supportCodeLibrary = { afterTestCaseHookDefinitions: [], + afterTestStepHookDefinitions: [], beforeTestCaseHookDefinitions: [], + beforeTestStepHookDefinitions: [], defaultTimeout: 5000, stepDefinitions: [], parameterTypeRegistry: {}, diff --git a/src/support_code_library_builder/build_helpers.js b/src/support_code_library_builder/build_helpers.js index 8fc7b8b2f..18d23d96d 100644 --- a/src/support_code_library_builder/build_helpers.js +++ b/src/support_code_library_builder/build_helpers.js @@ -12,6 +12,7 @@ import { isFileNameInCucumber } from '../stack_trace_filter' import StepDefinition from '../models/step_definition' import TestCaseHookDefinition from '../models/test_case_hook_definition' import TestRunHookDefinition from '../models/test_run_hook_definition' +import TestStepHookDefinition from '../models/test_step_hook_definition' import validateArguments from './validate_arguments' export function buildTestCaseHookDefinition({ options, code, cwd }) { @@ -35,6 +36,27 @@ export function buildTestCaseHookDefinition({ options, code, cwd }) { }) } +export function buildTestStepHookDefinition({ options, code, cwd }) { + if (typeof options === 'string') { + options = { tags: options } + } else if (typeof options === 'function') { + code = options + options = {} + } + const { line, uri } = getDefinitionLineAndUri(cwd) + validateArguments({ + args: { code, options }, + fnName: 'defineTestStepHook', + location: formatLocation({ line, uri }), + }) + return new TestStepHookDefinition({ + code, + line, + options, + uri, + }) +} + export function buildTestRunHookDefinition({ options, code, cwd }) { if (typeof options === 'string') { options = { tags: options } diff --git a/src/support_code_library_builder/index.js b/src/support_code_library_builder/index.js index 9ec7f1a98..5d0b8b0b0 100644 --- a/src/support_code_library_builder/index.js +++ b/src/support_code_library_builder/index.js @@ -6,6 +6,7 @@ import { buildStepDefinitionConfig, buildStepDefinitionFromConfig, buildTestCaseHookDefinition, + buildTestStepHookDefinition, buildTestRunHookDefinition, } from './build_helpers' import { wrapDefinitions } from './finalize_helpers' @@ -16,8 +17,10 @@ export class SupportCodeLibraryBuilder { defineParameterType: this.defineParameterType.bind(this), After: this.defineTestCaseHook('afterTestCaseHookDefinitions'), AfterAll: this.defineTestRunHook('afterTestRunHookDefinitions'), + AfterStep: this.defineTestStepHook('afterTestStepHookDefinitions'), Before: this.defineTestCaseHook('beforeTestCaseHookDefinitions'), BeforeAll: this.defineTestRunHook('beforeTestRunHookDefinitions'), + BeforeStep: this.defineTestStepHook('beforeTestStepHookDefinitions'), defineStep: this.defineStep.bind(this), defineSupportCode: util.deprecate(fn => { fn(this.methods) @@ -61,6 +64,17 @@ export class SupportCodeLibraryBuilder { } } + defineTestStepHook(collectionName) { + return (options, code) => { + const hookDefinition = buildTestStepHookDefinition({ + options, + code, + cwd: this.cwd, + }) + this.options[collectionName].push(hookDefinition) + } + } + defineTestRunHook(collectionName) { return (options, code) => { const hookDefinition = buildTestRunHookDefinition({ @@ -105,8 +119,10 @@ export class SupportCodeLibraryBuilder { this.options = _.cloneDeep({ afterTestCaseHookDefinitions: [], afterTestRunHookDefinitions: [], + afterTestStepHookDefinitions: [], beforeTestCaseHookDefinitions: [], beforeTestRunHookDefinitions: [], + beforeTestStepHookDefinitions: [], defaultTimeout: 5000, definitionFunctionWrapper: null, stepDefinitionConfigs: [], diff --git a/src/support_code_library_builder/validate_arguments.js b/src/support_code_library_builder/validate_arguments.js index c3772d7da..3a13dbed8 100644 --- a/src/support_code_library_builder/validate_arguments.js +++ b/src/support_code_library_builder/validate_arguments.js @@ -40,6 +40,18 @@ const validations = { optionsTimeoutValidation, { identifier: 'second argument', ...fnValidation }, ], + defineTestStepHook: [ + { identifier: 'first argument', ...optionsValidation }, + { + identifier: '"options.tags"', + expectedType: 'string', + predicate({ options }) { + return !options.tags || _.isString(options.tags) + }, + }, + optionsTimeoutValidation, + { identifier: 'second argument', ...fnValidation }, + ], defineStep: [ { identifier: 'first argument',