Skip to content

Commit

Permalink
implement pickle filtering and ordering with plugins (#2361)
Browse files Browse the repository at this point in the history
  • Loading branch information
davidjgoss authored Dec 21, 2023
1 parent 18a86f3 commit 052ac1a
Show file tree
Hide file tree
Showing 21 changed files with 616 additions and 234 deletions.
1 change: 1 addition & 0 deletions dependency-lint.yml
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ ignoreErrors:
- eslint-plugin-unicorn # used in eslint config
- prettier # peer dependency of eslint-plugin-prettier
- ts-node # .mocharc.yml / cucumber.js
- type-fest # utility types

requiredModules:
files:
Expand Down
22 changes: 17 additions & 5 deletions package-lock.json

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

1 change: 1 addition & 0 deletions package.json
Original file line number Diff line number Diff line change
Expand Up @@ -243,6 +243,7 @@
"strip-ansi": "6.0.1",
"supports-color": "^8.1.1",
"tmp": "^0.2.1",
"type-fest": "^4.8.3",
"util-arity": "^1.1.0",
"verror": "^1.10.0",
"xmlbuilder": "^15.1.1",
Expand Down
74 changes: 20 additions & 54 deletions src/api/gherkin.ts
Original file line number Diff line number Diff line change
Expand Up @@ -2,50 +2,31 @@ import {
GherkinStreams,
IGherkinStreamOptions,
} from '@cucumber/gherkin-streams'
import {
Envelope,
GherkinDocument,
IdGenerator,
Location,
ParseError,
Pickle,
} from '@cucumber/messages'
import { Envelope, IdGenerator, ParseError } from '@cucumber/messages'
import { Query as GherkinQuery } from '@cucumber/gherkin-utils'
import PickleFilter from '../pickle_filter'
import { orderPickles } from '../cli/helpers'
import { ILogger } from '../logger'
import { IFilterablePickle } from '../filter'
import { ISourcesCoordinates } from './types'

interface PickleWithDocument {
gherkinDocument: GherkinDocument
location: Location
pickle: Pickle
}

export async function getFilteredPicklesAndErrors({
export async function getPicklesAndErrors({
newId,
cwd,
logger,
unexpandedFeaturePaths,
featurePaths,
sourcePaths,
coordinates,
onEnvelope,
}: {
newId: IdGenerator.NewId
cwd: string
logger: ILogger
unexpandedFeaturePaths: string[]
featurePaths: string[]
sourcePaths: string[]
coordinates: ISourcesCoordinates
onEnvelope?: (envelope: Envelope) => void
}): Promise<{
filteredPickles: PickleWithDocument[]
filterablePickles: readonly IFilterablePickle[]
parseErrors: ParseError[]
}> {
const gherkinQuery = new GherkinQuery()
const parseErrors: ParseError[] = []
await gherkinFromPaths(
featurePaths,
sourcePaths,
{
newId,
relativeTo: cwd,
Expand All @@ -59,36 +40,21 @@ export async function getFilteredPicklesAndErrors({
onEnvelope?.(envelope)
}
)
const pickleFilter = new PickleFilter({
cwd,
featurePaths: unexpandedFeaturePaths,
names: coordinates.names,
tagExpression: coordinates.tagExpression,
const filterablePickles = gherkinQuery.getPickles().map((pickle) => {
const gherkinDocument = gherkinQuery
.getGherkinDocuments()
.find((doc) => doc.uri === pickle.uri)
const location = gherkinQuery.getLocation(
pickle.astNodeIds[pickle.astNodeIds.length - 1]
)
return {
gherkinDocument,
location,
pickle,
}
})
const filteredPickles: PickleWithDocument[] = gherkinQuery
.getPickles()
.filter((pickle) => {
const gherkinDocument = gherkinQuery
.getGherkinDocuments()
.find((doc) => doc.uri === pickle.uri)
return pickleFilter.matches({ gherkinDocument, pickle })
})
.map((pickle) => {
const gherkinDocument = gherkinQuery
.getGherkinDocuments()
.find((doc) => doc.uri === pickle.uri)
const location = gherkinQuery.getLocation(
pickle.astNodeIds[pickle.astNodeIds.length - 1]
)
return {
gherkinDocument,
location,
pickle,
}
})
orderPickles(filteredPickles, coordinates.order, logger)
return {
filteredPickles,
filterablePickles,
parseErrors,
}
}
Expand Down
45 changes: 28 additions & 17 deletions src/api/load_sources.ts
Original file line number Diff line number Diff line change
@@ -1,16 +1,17 @@
import { IdGenerator } from '@cucumber/messages'
import { ILogger } from '../logger'
import { resolvePaths } from '../paths'
import {
ILoadSourcesResult,
IPlannedPickle,
IRunEnvironment,
ISourcesCoordinates,
ISourcesError,
} from './types'
import { resolvePaths } from './paths'
import { mergeEnvironment } from './environment'
import { getFilteredPicklesAndErrors } from './gherkin'
import { getPicklesAndErrors } from './gherkin'
import { ConsoleLogger } from './console_logger'
import { initializeForLoadSources } from './plugins'

/**
* Load and parse features, produce a filtered and ordered test plan and/or parse errors.
Expand All @@ -23,42 +24,52 @@ export async function loadSources(
coordinates: ISourcesCoordinates,
environment: IRunEnvironment = {}
): Promise<ILoadSourcesResult> {
const { cwd, stderr, debug } = mergeEnvironment(environment)
const mergedEnvironment = mergeEnvironment(environment)
const { cwd, stderr, debug } = mergedEnvironment
const logger: ILogger = new ConsoleLogger(stderr, debug)
const newId = IdGenerator.uuid()
const { unexpandedFeaturePaths, featurePaths } = await resolvePaths(
const pluginManager = await initializeForLoadSources(
logger,
cwd,
coordinates
coordinates,
mergedEnvironment
)
if (featurePaths.length === 0) {
const resolvedPaths = await resolvePaths(logger, cwd, coordinates)
pluginManager.emit('paths:resolve', resolvedPaths)
const { sourcePaths } = resolvedPaths
if (sourcePaths.length === 0) {
return {
plan: [],
errors: [],
}
}
const { filteredPickles, parseErrors } = await getFilteredPicklesAndErrors({
const { filterablePickles, parseErrors } = await getPicklesAndErrors({
newId,
cwd,
logger,
unexpandedFeaturePaths,
featurePaths,
sourcePaths,
coordinates,
onEnvelope: (envelope) => pluginManager.emit('message', envelope),
})
const plan: IPlannedPickle[] = filteredPickles.map(
({ location, pickle }) => ({
name: pickle.name,
uri: pickle.uri,
location,
})
const filteredPickles = await pluginManager.transform(
'pickles:filter',
filterablePickles
)
const orderedPickles = await pluginManager.transform(
'pickles:order',
filteredPickles
)
const plan: IPlannedPickle[] = orderedPickles.map(({ location, pickle }) => ({
name: pickle.name,
uri: pickle.uri,
location,
}))
const errors: ISourcesError[] = parseErrors.map(({ source, message }) => {
return {
uri: source.uri,
location: source.location,
message,
}
})
await pluginManager.cleanup()
return {
plan,
errors,
Expand Down
16 changes: 16 additions & 0 deletions src/api/load_sources_spec.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,6 +36,7 @@ Feature: test fixture
| foo |
| bar |`
)
await fs.writeFile(path.join(cwd, '@rerun.txt'), 'features/test.feature:8')
const stdout = new PassThrough()
return { cwd, stdout }
}
Expand Down Expand Up @@ -146,4 +147,19 @@ describe('loadSources', () => {
)
expect(plan.map((pickle) => pickle.name)).to.deep.eq(['two'])
})

it('should produce a plan based on a rerun file', async () => {
const environment = await setupEnvironment()
const { plan } = await loadSources(
{
defaultDialect: 'en',
order: 'defined',
paths: ['@rerun.txt'],
names: [],
tagExpression: '',
},
environment
)
expect(plan.map((pickle) => pickle.name)).to.deep.eq(['two'])
})
})
15 changes: 11 additions & 4 deletions src/api/load_support.ts
Original file line number Diff line number Diff line change
@@ -1,11 +1,12 @@
import { IdGenerator } from '@cucumber/messages'
import { ISupportCodeLibrary } from '../support_code_library_builder/types'
import { ILogger } from '../logger'
import { resolvePaths } from '../paths'
import { ILoadSupportOptions, IRunEnvironment } from './types'
import { resolvePaths } from './paths'
import { getSupportCodeLibrary } from './support'
import { mergeEnvironment } from './environment'
import { ConsoleLogger } from './console_logger'
import { initializeForLoadSupport } from './plugins'

/**
* Load support code for use in test runs.
Expand All @@ -18,20 +19,26 @@ export async function loadSupport(
options: ILoadSupportOptions,
environment: IRunEnvironment = {}
): Promise<ISupportCodeLibrary> {
const { cwd, stderr, debug } = mergeEnvironment(environment)
const mergedEnvironment = mergeEnvironment(environment)
const { cwd, stderr, debug } = mergedEnvironment
const logger: ILogger = new ConsoleLogger(stderr, debug)
const newId = IdGenerator.uuid()
const { requirePaths, importPaths } = await resolvePaths(
const pluginManager = await initializeForLoadSupport()
const resolvedPaths = await resolvePaths(
logger,
cwd,
options.sources,
options.support
)
return await getSupportCodeLibrary({
pluginManager.emit('paths:resolve', resolvedPaths)
const { requirePaths, importPaths } = resolvedPaths
const supportCodeLibrary = await getSupportCodeLibrary({
cwd,
newId,
requireModules: options.support.requireModules,
requirePaths,
importPaths,
})
await pluginManager.cleanup()
return supportCodeLibrary
}
54 changes: 45 additions & 9 deletions src/api/plugins.ts
Original file line number Diff line number Diff line change
@@ -1,19 +1,55 @@
import { Plugin, PluginManager } from '../plugin'
import { PluginManager } from '../plugin'
import publishPlugin from '../publish'
import { ILogger } from '../logger'
import { IRunEnvironment, IRunOptions } from './types'
import filterPlugin from '../filter'
import {
IRunConfiguration,
IRunEnvironment,
ISourcesCoordinates,
} from './types'

const INTERNAL_PLUGINS: Record<string, Plugin> = {
publish: publishPlugin,
export async function initializeForLoadSources(
logger: ILogger,
coordinates: ISourcesCoordinates,
environment: Required<IRunEnvironment>
): Promise<PluginManager> {
// eventually we'll load plugin packages here
const pluginManager = new PluginManager()
await pluginManager.init(
'loadSources',
filterPlugin,
coordinates,
logger,
environment
)
return pluginManager
}

export async function initializeForLoadSupport(): Promise<PluginManager> {
// eventually we'll load plugin packages here
return new PluginManager()
}

export async function initializePlugins(
export async function initializeForRunCucumber(
logger: ILogger,
configuration: IRunOptions,
environment: IRunEnvironment
configuration: IRunConfiguration,
environment: Required<IRunEnvironment>
): Promise<PluginManager> {
// eventually we'll load plugin packages here
const pluginManager = new PluginManager(Object.values(INTERNAL_PLUGINS))
await pluginManager.init(logger, configuration, environment)
const pluginManager = new PluginManager()
await pluginManager.init(
'runCucumber',
publishPlugin,
configuration.formats.publish,
logger,
environment
)
await pluginManager.init(
'runCucumber',
filterPlugin,
configuration.sources,
logger,
environment
)
return pluginManager
}
Loading

0 comments on commit 052ac1a

Please sign in to comment.