Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Enchanced features filter #2134

Open
wants to merge 8 commits into
base: main
Choose a base branch
from
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
48 changes: 25 additions & 23 deletions docs/configuration.md
Original file line number Diff line number Diff line change
Expand Up @@ -67,29 +67,31 @@ These options can be used in a configuration file (see [above](#files)) or on th
- Where options are repeatable, they are appended/merged if provided more than once.
- Where options aren't repeatable, the CLI takes precedence over a configuration file.

| Name | Type | Repeatable | CLI Option | Description | Default |
|-------------------|------------|------------|---------------------------|-------------------------------------------------------------------------------------------------------------------|---------|
| `paths` | `string[]` | Yes | (as arguments) | Paths to where your feature files are - see [below](#finding-your-features) | [] |
| `backtrace` | `boolean` | No | `--backtrace`, `-b` | Show the full backtrace for errors | false |
| `dryRun` | `boolean` | No | `--dry-run`, `-d` | Prepare a test run but don't run it - see [Dry Run](./dry_run.md) | false |
| `forceExit` | `boolean` | No | `--exit`, `--force-exit` | Explicitly call `process.exit()` after the test run (when run via CLI) - see [CLI](./cli.md) | false |
| `failFast` | `boolean` | No | `--fail-fast` | Stop running tests when a test fails - see [Fail Fast](./fail_fast.md) | false |
| `format` | `string[]` | Yes | `--format`, `-f` | Name/path and (optionally) output file path of each formatter to use - see [Formatters](./formatters.md) | [] |
| `formatOptions` | `object` | Yes | `--format-options` | Options to be provided to formatters - see [Formatters](./formatters.md) | {} |
| `import` | `string[]` | Yes | `--import`, `-i` | Paths to where your support code is, for ESM - see [ESM](./esm.md) | [] |
| `language` | `string` | No | `--language` | Default language for your feature files | en |
| `name` | `string` | No | `--name` | Regular expressions of which scenario names should match one of to be run - see [Filtering](./filtering.md#names) | [] |
| `order` | `string` | No | `--order` | Run in the order defined, or in a random order | defined |
| `parallel` | `number` | No | `--parallel` | Run tests in parallel with the given number of worker processes - see [Parallel](./parallel.md) | 0 |
| `publish` | `boolean` | No | `--publish` | Publish a report of your test run to <https://reports.cucumber.io/> | false |
| `publishQuiet` | `boolean` | No | `--publish-quiet` | Don't show info about publishing reports | false |
| `require` | `string[]` | Yes | `--require`, `-r` | Paths to where your support code is, for CommonJS - see [below](#finding-your-code) | [] |
| `requireModule` | `string[]` | Yes | `--require-module` | Names of transpilation modules to load, loaded via `require()` - see [Transpiling](./transpiling.md) | [] |
| `retry` | `number` | No | `--retry` | Retry failing tests up to the given number of times - see [Retry](./retry.md) | 0 |
| `retryTagFilter` | `string` | Yes | `--retry-tag-filter` | Tag expression to filter which scenarios can be retried - see [Retry](./retry.md) | |
| `strict` | `boolean` | No | `--strict`, `--no-strict` | Fail the test run if there are pending steps | true |
| `tags` | `string` | Yes | `--tags`, `-t` | Tag expression to filter which scenarios should be run - see [Filtering](./filtering.md#tags) | |
| `worldParameters` | `object` | Yes | `--world-parameters` | Parameters to be passed to your World - see [World](./support_files/world.md) | {} |
| Name | Type | Repeatable | CLI Option | Description | Default |
| ----------------- | ----------------------------- | ---------- | ------------------------- | ----------------------------------------------------------------------------------------------------------------- | ------------- |
| `paths` | `string[]` | Yes | (as arguments) | Paths to where your feature files are - see [below](#finding-your-features) | [] |
| `backtrace` | `boolean` | No | `--backtrace`, `-b` | Show the full backtrace for errors | false |
| `dryRun` | `boolean` | No | `--dry-run`, `-d` | Prepare a test run but don't run it - see [Dry Run](./dry_run.md) | false |
| `forceExit` | `boolean` | No | `--exit`, `--force-exit` | Explicitly call `process.exit()` after the test run (when run via CLI) - see [CLI](./cli.md) | false |
| `failFast` | `boolean` | No | `--fail-fast` | Stop running tests when a test fails - see [Fail Fast](./fail_fast.md) | false |
| `format` | `string[]` | Yes | `--format`, `-f` | Name/path and (optionally) output file path of each formatter to use - see [Formatters](./formatters.md) | [] |
| `formatOptions` | `object` | Yes | `--format-options` | Options to be provided to formatters - see [Formatters](./formatters.md) | {} |
| `import` | `string[]` | Yes | `--import`, `-i` | Paths to where your support code is, for ESM - see [ESM](./esm.md) | [] |
| `language` | `string` | No | `--language` | Default language for your feature files | en |
| `name` | `string` | No | `--name` | Regular expressions of which scenario names should match one of to be run - see [Filtering](./filtering.md#names) | [] |
| `order` | `string` | No | `--order` | Run in the order defined, or in a random order | defined |
| `parallel` | `number` | No | `--parallel` | Run tests in parallel with the given number of worker processes - see [Parallel](./parallel.md) | 0 |
| `publish` | `boolean` | No | `--publish` | Publish a report of your test run to <https://reports.cucumber.io/> | false |
| `publishQuiet` | `boolean` | No | `--publish-quiet` | Don't show info about publishing reports | false |
| `require` | `string[]` | Yes | `--require`, `-r` | Paths to where your support code is, for CommonJS - see [below](#finding-your-code) | [] |
| `requireModule` | `string[]` | Yes | `--require-module` | Names of transpilation modules to load, loaded via `require()` - see [Transpiling](./transpiling.md) | [] |
| `retry` | `number` | No | `--retry` | Retry failing tests up to the given number of times - see [Retry](./retry.md) | 0 |
| `retryTagFilter` | `string` | Yes | `--retry-tag-filter` | Tag expression to filter which scenarios can be retried - see [Retry](./retry.md) | |
| `strict` | `boolean` | No | `--strict`, `--no-strict` | Fail the test run if there are pending steps | true |
| `tags` | `string` | Yes | `--tags`, `-t` | Tag expression to filter which scenarios should be run - see [Filtering](./filtering.md#tags) | |
| `worldParameters` | `object` | Yes | `--world-parameters` | Parameters to be passed to your World - see [World](./support_files/world.md) | {} |
| `include` | `(pickle: Pickle) => boolean` | No | `N/A` | Runtime filter to include test cases from the run | `() => true` |
| `exclude` | `(pickle: Pickle) => boolean` | No | `N/A` | Runtime filter to exclude test cases from the run | `() => false` |

## Finding your features

Expand Down
13 changes: 13 additions & 0 deletions docs/filtering.md
Original file line number Diff line number Diff line change
Expand Up @@ -35,3 +35,16 @@ You can specify a [Cucumber tag expression](https://docs.cucumber.io/cucumber/ap
- On the CLI `$ cucumber-js --tags "@foo or @bar"`

This option is repeatable, so you can provide several expressions and they'll be combined with an `and` operator, meaning a scenario needs to match all of them.

## Runtime filters

You can specify runtime filters to exclude or include test cases from the run:

```js
module.exports = {
default: {
include: (pickle) => requiredTestsCasesIds.includes(pickle.id),
exclude: (pickle) => /specific expression/.test(pickle.name),
}
}
```
2 changes: 2 additions & 0 deletions src/api/convert_configuration.ts
Original file line number Diff line number Diff line change
Expand Up @@ -31,6 +31,8 @@ export async function convertConfiguration(
retryTagFilter: flatConfiguration.retryTagFilter,
strict: flatConfiguration.strict,
worldParameters: flatConfiguration.worldParameters,
include: flatConfiguration.include,
exclude: flatConfiguration.exclude,
},
formats: convertFormats(flatConfiguration, env),
}
Expand Down
2 changes: 2 additions & 0 deletions src/api/convert_configuration_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,8 @@ describe('convertConfiguration', () => {
retryTagFilter: '',
strict: true,
worldParameters: {},
include: undefined,
exclude: undefined,
},
sources: {
defaultDialect: 'en',
Expand Down
8 changes: 8 additions & 0 deletions src/api/gherkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,6 +13,7 @@ import {
import { Query as GherkinQuery } from '@cucumber/gherkin-utils'
import PickleFilter from '../pickle_filter'
import { orderPickles } from '../cli/helpers'
import { RuntimePickleFilter } from '../configuration/types'
import { ISourcesCoordinates } from './types'

interface PickleWithDocument {
Expand All @@ -28,6 +29,8 @@ export async function getFilteredPicklesAndErrors({
unexpandedFeaturePaths,
featurePaths,
coordinates,
include = () => true,
exclude = () => false,
onEnvelope,
}: {
newId: IdGenerator.NewId
Expand All @@ -36,6 +39,8 @@ export async function getFilteredPicklesAndErrors({
unexpandedFeaturePaths: string[]
featurePaths: string[]
coordinates: ISourcesCoordinates
include?: RuntimePickleFilter
exclude?: RuntimePickleFilter
onEnvelope?: (envelope: Envelope) => void
}): Promise<{
filteredPickles: PickleWithDocument[]
Expand Down Expand Up @@ -85,6 +90,9 @@ export async function getFilteredPicklesAndErrors({
pickle,
}
})
.filter(({ pickle }) => include(pickle))
.filter(({ pickle }) => !exclude(pickle))

orderPickles(filteredPickles, coordinates.order, logger)
return {
filteredPickles,
Expand Down
2 changes: 2 additions & 0 deletions src/api/run_cucumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -78,6 +78,8 @@ export async function runCucumber(
unexpandedFeaturePaths,
featurePaths,
coordinates: configuration.sources,
include: configuration.runtime.include,
exclude: configuration.runtime.exclude,
onEnvelope: (envelope) => eventBroadcaster.emit('envelope', envelope),
})
pickleIds = gherkinResult.filteredPickles.map(({ pickle }) => pickle.id)
Expand Down
179 changes: 154 additions & 25 deletions src/api/run_cucumber_spec.ts
Original file line number Diff line number Diff line change
@@ -1,4 +1,9 @@
import { Envelope, TestStepResultStatus, IdGenerator } from '@cucumber/messages'
import {
Envelope,
TestStepResultStatus,
IdGenerator,
Pickle,
} from '@cucumber/messages'
import fs from 'mz/fs'
import path from 'path'
import { reindent } from 'reindent-template-literals'
Expand All @@ -11,36 +16,42 @@ import { loadConfiguration } from './load_configuration'

const newId = IdGenerator.uuid()

async function setupEnvironment(): Promise<Partial<IRunEnvironment>> {
const cwd = path.join(__dirname, '..', '..', 'tmp', `runCucumber_${newId()}`)
await fs.mkdir(path.join(cwd, 'features'), { recursive: true })
await fs.writeFile(
path.join(cwd, 'features', 'test.feature'),
reindent(`Feature: test fixture
async function teardownEnvironment(environment: IRunEnvironment) {
await fs.rmdir(environment.cwd, { recursive: true })
environment.stdout.end()
}

describe('runCucumber without runtime filters', () => {
async function setupEnvironment(): Promise<Partial<IRunEnvironment>> {
const cwd = path.join(
__dirname,
'..',
'..',
'tmp',
`runCucumber_${newId()}`
)
await fs.mkdir(path.join(cwd, 'features'), { recursive: true })
await fs.writeFile(
path.join(cwd, 'features', 'test.feature'),
reindent(`Feature: test fixture
Scenario: one
Given a step
Then another step`)
)
await fs.writeFile(
path.join(cwd, 'features', 'steps.ts'),
reindent(`import { Given, Then } from '../../../src'
)
await fs.writeFile(
path.join(cwd, 'features', 'steps.ts'),
reindent(`import { Given, Then } from '../../../src'
Given('a step', function () {})
Then('another step', function () {})`)
)
await fs.writeFile(
path.join(cwd, 'cucumber.mjs'),
`export default {paths: ['features/test.feature'], requireModule: ['ts-node/register'], require: ['features/steps.ts']}`
)
const stdout = new PassThrough()
return { cwd, stdout }
}

async function teardownEnvironment(environment: IRunEnvironment) {
await fs.rmdir(environment.cwd, { recursive: true })
environment.stdout.end()
}
)
await fs.writeFile(
path.join(cwd, 'cucumber.mjs'),
`export default {paths: ['features/test.feature'], requireModule: ['ts-node/register'], require: ['features/steps.ts']}`
)
const stdout = new PassThrough()
return { cwd, stdout }
}

describe('runCucumber', () => {
describe('preloading support code', () => {
let environment: IRunEnvironment
beforeEach(async () => {
Expand Down Expand Up @@ -115,3 +126,121 @@ describe('runCucumber', () => {
})
})
})

describe('runCucumber with runtime filters', () => {
async function setupEnvironment(): Promise<Partial<IRunEnvironment>> {
const cwd = path.join(
__dirname,
'..',
'..',
'tmp',
`runCucumber_${newId()}`
)
await fs.mkdir(path.join(cwd, 'features'), { recursive: true })
await fs.writeFile(
path.join(cwd, 'features', 'test.feature'),
reindent(`Feature: test fixture
@foo
Scenario: one
Given a step
Then another step

@bar
Scenario: two
Given a step
Then another step`)
)
await fs.writeFile(
path.join(cwd, 'features', 'steps.ts'),
reindent(`import { Given, Then } from '../../../src'
Given('a step', function () {})
Then('another step', function () {})`)
)
await fs.writeFile(
path.join(cwd, 'cucumber.mjs'),
`export default {paths: ['features/test.feature'], requireModule: ['ts-node/register'], require: ['features/steps.ts']}`
)
const stdout = new PassThrough()
return { cwd, stdout }
}

let environment: IRunEnvironment
beforeEach(async () => {
environment = await setupEnvironment()
})
afterEach(async () => teardownEnvironment(environment))

describe('include filter', () => {
it('skips the matched test to exclude filter', async () => {
const messages: Envelope[] = []
const { runConfiguration } = await loadConfiguration({}, environment)
await runCucumber(
{
...runConfiguration,
runtime: {
...runConfiguration.runtime,
include: (pickle: Pickle) => pickle.name === 'one',
},
},
environment,
(envelope) => messages.push(envelope)
)

const picklesEnvelopes = messages.filter((envelope) => !!envelope.pickle)
const testCasesEnvelopes = messages.filter(
(envelope) => !!envelope.testCase
)
const includedPickleEnvelope = picklesEnvelopes.find(
(envelope) => envelope.pickle.name === 'one'
)
const excludedPickleEnvelope = picklesEnvelopes.find(
(envelope) => envelope.pickle.name === 'two'
)

expect(testCasesEnvelopes).to.have.length(1)
expect(includedPickleEnvelope.pickle.id).eq(
testCasesEnvelopes[0].testCase.pickleId
)
expect(excludedPickleEnvelope.pickle.id).not.eq(
testCasesEnvelopes[0].testCase.pickleId
)
})
})

describe('exclude filter', () => {
it('skips the matched test to exclude filter', async () => {
const messages: Envelope[] = []
const { runConfiguration } = await loadConfiguration({}, environment)
await runCucumber(
{
...runConfiguration,
runtime: {
...runConfiguration.runtime,
exclude: (pickle: Pickle) => pickle.name === 'one',
},
},
environment,
(envelope) => messages.push(envelope)
)

const picklesEnvelopes = messages.filter((envelope) => !!envelope.pickle)
const testCasesEnvelopes = messages.filter(
(envelope) => !!envelope.testCase
)
const includedPickleEnvelope = picklesEnvelopes.find(
(envelope) => envelope.pickle.name === 'two'
)
const excludedPickleEnvelope = picklesEnvelopes.find(
(envelope) => envelope.pickle.name === 'one'
)

expect(testCasesEnvelopes).to.have.length(1)
expect(includedPickleEnvelope.pickle.id).eq(
testCasesEnvelopes[0].testCase.pickleId
)
expect(excludedPickleEnvelope.pickle.id).not.eq(
testCasesEnvelopes[0].testCase.pickleId
)
})
})
})