diff --git a/CHANGELOG.md b/CHANGELOG.md index 07af2286a..41207c8e3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -27,6 +27,10 @@ Please see [CONTRIBUTING.md](https://github.com/cucumber/cucumber/blob/master/CO ### Changed - Switch from `colors` to `chalk` for terminal coloring ([#1895](https://github.com/cucumber/cucumber-js/pull/1895)) +### Deprecated + +- `parseGherkinMessageStream` is deprecated in favour of `loadSources` ([#1957](https://github.com/cucumber/cucumber-js/pull/1957)) + ## [8.0.0-rc.2] - 2022-01-10 ### Added - Export cucumber version number. It is now possible to retrieve the current version diff --git a/docs/api/cucumber.iloadsourcesresult.errors.md b/docs/api/cucumber.iloadsourcesresult.errors.md new file mode 100644 index 000000000..9f98a5f87 --- /dev/null +++ b/docs/api/cucumber.iloadsourcesresult.errors.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ILoadSourcesResult](./cucumber.iloadsourcesresult.md) > [errors](./cucumber.iloadsourcesresult.errors.md) + +## ILoadSourcesResult.errors property + +Signature: + +```typescript +errors: ISourcesError[]; +``` diff --git a/docs/api/cucumber.iloadsourcesresult.md b/docs/api/cucumber.iloadsourcesresult.md new file mode 100644 index 000000000..730105f5f --- /dev/null +++ b/docs/api/cucumber.iloadsourcesresult.md @@ -0,0 +1,20 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ILoadSourcesResult](./cucumber.iloadsourcesresult.md) + +## ILoadSourcesResult interface + + +Signature: + +```typescript +export interface ILoadSourcesResult +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [errors](./cucumber.iloadsourcesresult.errors.md) | [ISourcesError](./cucumber.isourceserror.md)\[\] | | +| [plan](./cucumber.iloadsourcesresult.plan.md) | [IPlannedPickle](./cucumber.iplannedpickle.md)\[\] | | + diff --git a/docs/api/cucumber.iloadsourcesresult.plan.md b/docs/api/cucumber.iloadsourcesresult.plan.md new file mode 100644 index 000000000..8847c0ed6 --- /dev/null +++ b/docs/api/cucumber.iloadsourcesresult.plan.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ILoadSourcesResult](./cucumber.iloadsourcesresult.md) > [plan](./cucumber.iloadsourcesresult.plan.md) + +## ILoadSourcesResult.plan property + +Signature: + +```typescript +plan: IPlannedPickle[]; +``` diff --git a/docs/api/cucumber.iplannedpickle.location.md b/docs/api/cucumber.iplannedpickle.location.md new file mode 100644 index 000000000..4aee3845b --- /dev/null +++ b/docs/api/cucumber.iplannedpickle.location.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [IPlannedPickle](./cucumber.iplannedpickle.md) > [location](./cucumber.iplannedpickle.location.md) + +## IPlannedPickle.location property + +Signature: + +```typescript +location: { + line: number; + column?: number; + }; +``` diff --git a/docs/api/cucumber.iplannedpickle.md b/docs/api/cucumber.iplannedpickle.md new file mode 100644 index 000000000..4c6144682 --- /dev/null +++ b/docs/api/cucumber.iplannedpickle.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [IPlannedPickle](./cucumber.iplannedpickle.md) + +## IPlannedPickle interface + + +Signature: + +```typescript +export interface IPlannedPickle +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [location](./cucumber.iplannedpickle.location.md) | { line: number; column?: number; } | | +| [name](./cucumber.iplannedpickle.name.md) | string | | +| [uri](./cucumber.iplannedpickle.uri.md) | string | | + diff --git a/docs/api/cucumber.iplannedpickle.name.md b/docs/api/cucumber.iplannedpickle.name.md new file mode 100644 index 000000000..b09ab16b9 --- /dev/null +++ b/docs/api/cucumber.iplannedpickle.name.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [IPlannedPickle](./cucumber.iplannedpickle.md) > [name](./cucumber.iplannedpickle.name.md) + +## IPlannedPickle.name property + +Signature: + +```typescript +name: string; +``` diff --git a/docs/api/cucumber.iplannedpickle.uri.md b/docs/api/cucumber.iplannedpickle.uri.md new file mode 100644 index 000000000..905aefb67 --- /dev/null +++ b/docs/api/cucumber.iplannedpickle.uri.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [IPlannedPickle](./cucumber.iplannedpickle.md) > [uri](./cucumber.iplannedpickle.uri.md) + +## IPlannedPickle.uri property + +Signature: + +```typescript +uri: string; +``` diff --git a/docs/api/cucumber.isourceserror.location.md b/docs/api/cucumber.isourceserror.location.md new file mode 100644 index 000000000..eb37941cd --- /dev/null +++ b/docs/api/cucumber.isourceserror.location.md @@ -0,0 +1,14 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ISourcesError](./cucumber.isourceserror.md) > [location](./cucumber.isourceserror.location.md) + +## ISourcesError.location property + +Signature: + +```typescript +location: { + line: number; + column?: number; + }; +``` diff --git a/docs/api/cucumber.isourceserror.md b/docs/api/cucumber.isourceserror.md new file mode 100644 index 000000000..7e5f4fc3a --- /dev/null +++ b/docs/api/cucumber.isourceserror.md @@ -0,0 +1,21 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ISourcesError](./cucumber.isourceserror.md) + +## ISourcesError interface + + +Signature: + +```typescript +export interface ISourcesError +``` + +## Properties + +| Property | Type | Description | +| --- | --- | --- | +| [location](./cucumber.isourceserror.location.md) | { line: number; column?: number; } | | +| [message](./cucumber.isourceserror.message.md) | string | | +| [uri](./cucumber.isourceserror.uri.md) | string | | + diff --git a/docs/api/cucumber.isourceserror.message.md b/docs/api/cucumber.isourceserror.message.md new file mode 100644 index 000000000..42962131e --- /dev/null +++ b/docs/api/cucumber.isourceserror.message.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ISourcesError](./cucumber.isourceserror.md) > [message](./cucumber.isourceserror.message.md) + +## ISourcesError.message property + +Signature: + +```typescript +message: string; +``` diff --git a/docs/api/cucumber.isourceserror.uri.md b/docs/api/cucumber.isourceserror.uri.md new file mode 100644 index 000000000..5a27920dc --- /dev/null +++ b/docs/api/cucumber.isourceserror.uri.md @@ -0,0 +1,11 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [ISourcesError](./cucumber.isourceserror.md) > [uri](./cucumber.isourceserror.uri.md) + +## ISourcesError.uri property + +Signature: + +```typescript +uri: string; +``` diff --git a/docs/api/cucumber.loadsources.md b/docs/api/cucumber.loadsources.md new file mode 100644 index 000000000..884c7edfb --- /dev/null +++ b/docs/api/cucumber.loadsources.md @@ -0,0 +1,25 @@ + + +[Home](./index.md) > [@cucumber/cucumber](./cucumber.md) > [loadSources](./cucumber.loadsources.md) + +## loadSources() function + +Load and parse features, produce a filtered and ordered test plan and/or parse errors. + +Signature: + +```typescript +export declare function loadSources(coordinates: ISourcesCoordinates, environment?: IRunEnvironment): Promise; +``` + +## Parameters + +| Parameter | Type | Description | +| --- | --- | --- | +| coordinates | [ISourcesCoordinates](./cucumber.isourcescoordinates.md) | Coordinates required to find features | +| environment | [IRunEnvironment](./cucumber.irunenvironment.md) | Project environment. | + +Returns: + +Promise<[ILoadSourcesResult](./cucumber.iloadsourcesresult.md)> + diff --git a/docs/api/cucumber.md b/docs/api/cucumber.md index 915718081..afd0e5813 100644 --- a/docs/api/cucumber.md +++ b/docs/api/cucumber.md @@ -15,6 +15,7 @@ These docs cover the API used for running Cucumber programmatically. The entry p | Function | Description | | --- | --- | | [loadConfiguration(options, environment)](./cucumber.loadconfiguration.md) | Load user-authored configuration to be used in a test run. | +| [loadSources(coordinates, environment)](./cucumber.loadsources.md) | Load and parse features, produce a filtered and ordered test plan and/or parse errors. | | [loadSupport(options, environment)](./cucumber.loadsupport.md) | Load support code for use in test runs. | | [runCucumber(configuration, environment, onMessage)](./cucumber.runcucumber.md) | Execute a Cucumber test run. | @@ -23,7 +24,9 @@ These docs cover the API used for running Cucumber programmatically. The entry p | Interface | Description | | --- | --- | | [ILoadConfigurationOptions](./cucumber.iloadconfigurationoptions.md) | | +| [ILoadSourcesResult](./cucumber.iloadsourcesresult.md) | | | [ILoadSupportOptions](./cucumber.iloadsupportoptions.md) | | +| [IPlannedPickle](./cucumber.iplannedpickle.md) | | | [IResolvedConfiguration](./cucumber.iresolvedconfiguration.md) | | | [IRunConfiguration](./cucumber.irunconfiguration.md) | | | [IRunEnvironment](./cucumber.irunenvironment.md) | Contextual data about the project environment. | @@ -32,6 +35,7 @@ These docs cover the API used for running Cucumber programmatically. The entry p | [IRunOptionsRuntime](./cucumber.irunoptionsruntime.md) | | | [IRunResult](./cucumber.irunresult.md) | Result of a Cucumber test run. | | [ISourcesCoordinates](./cucumber.isourcescoordinates.md) | | +| [ISourcesError](./cucumber.isourceserror.md) | | | [ISupportCodeCoordinates](./cucumber.isupportcodecoordinates.md) | | ## Type Aliases diff --git a/docs/javascript_api.md b/docs/javascript_api.md index a533096d2..a888e7125 100644 --- a/docs/javascript_api.md +++ b/docs/javascript_api.md @@ -35,3 +35,17 @@ export async function runTests(directory, configFile, failFast) { return success } ``` + +## Sources example + +You can use the `loadSources` function to load and parse your feature files, calculate the test plan (accounting for filtering and ordering) and report any parse errors: + +```javascript +import { loadConfiguration, loadSources } from '@cucumber/cucumber/api' + +export async function calculatePlan() { + const { runConfiguration } = await loadConfiguration() + const { plan } = await loadSources(runConfiguration.sources) + return plan +} +``` diff --git a/package-lock.json b/package-lock.json index be449c1b0..df72d27cc 100644 --- a/package-lock.json +++ b/package-lock.json @@ -14,6 +14,7 @@ "@cucumber/cucumber-expressions": "15.0.2", "@cucumber/gherkin": "22.0.0", "@cucumber/gherkin-streams": "4.0.0", + "@cucumber/gherkin-utils": "^7.0.0", "@cucumber/html-formatter": "18.0.0", "@cucumber/messages": "17.1.1", "@cucumber/tag-expressions": "4.1.0", @@ -589,6 +590,27 @@ "node": ">= 12" } }, + "node_modules/@cucumber/gherkin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-7.0.0.tgz", + "integrity": "sha512-tDkSRITTPA6Df501doqeRH3+1jAM4ls6+tlFEVvkvuzTH3C8DXwQ5xBPWmUNmDhR/gJeZ+yj7gDRbDWr7Qc6Zw==", + "dependencies": { + "@cucumber/messages": "^17.1.0", + "@teppeis/multimaps": "2.0.0", + "commander": "8.1.0" + }, + "bin": { + "gherkin-utils": "bin/gherkin-utils" + } + }, + "node_modules/@cucumber/gherkin-utils/node_modules/commander": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", + "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==", + "engines": { + "node": ">= 12" + } + }, "node_modules/@cucumber/html-formatter": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-18.0.0.tgz", @@ -1185,7 +1207,6 @@ "version": "2.0.0", "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-2.0.0.tgz", "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==", - "dev": true, "engines": { "node": ">=10.17" } @@ -8052,6 +8073,23 @@ } } }, + "@cucumber/gherkin-utils": { + "version": "7.0.0", + "resolved": "https://registry.npmjs.org/@cucumber/gherkin-utils/-/gherkin-utils-7.0.0.tgz", + "integrity": "sha512-tDkSRITTPA6Df501doqeRH3+1jAM4ls6+tlFEVvkvuzTH3C8DXwQ5xBPWmUNmDhR/gJeZ+yj7gDRbDWr7Qc6Zw==", + "requires": { + "@cucumber/messages": "^17.1.0", + "@teppeis/multimaps": "2.0.0", + "commander": "8.1.0" + }, + "dependencies": { + "commander": { + "version": "8.1.0", + "resolved": "https://registry.npmjs.org/commander/-/commander-8.1.0.tgz", + "integrity": "sha512-mf45ldcuHSYShkplHHGKWb4TrmwQadxOn7v4WuhDJy0ZVoY5JFajaRDKD0PNe5qXzBX0rhovjTnP6Kz9LETcuA==" + } + } + }, "@cucumber/html-formatter": { "version": "18.0.0", "resolved": "https://registry.npmjs.org/@cucumber/html-formatter/-/html-formatter-18.0.0.tgz", @@ -8553,8 +8591,7 @@ "@teppeis/multimaps": { "version": "2.0.0", "resolved": "https://registry.npmjs.org/@teppeis/multimaps/-/multimaps-2.0.0.tgz", - "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==", - "dev": true + "integrity": "sha512-TL1adzq1HdxUf9WYduLcQ/DNGYiz71U31QRgbnr0Ef1cPyOUOsBojxHVWpFeOSUucB6Lrs0LxFRA14ntgtkc9w==" }, "@tsconfig/node10": { "version": "1.0.8", diff --git a/package.json b/package.json index 79686baa6..903f9af1a 100644 --- a/package.json +++ b/package.json @@ -194,6 +194,7 @@ "@cucumber/cucumber-expressions": "15.0.2", "@cucumber/gherkin": "22.0.0", "@cucumber/gherkin-streams": "4.0.0", + "@cucumber/gherkin-utils": "^7.0.0", "@cucumber/html-formatter": "18.0.0", "@cucumber/messages": "17.1.1", "@cucumber/tag-expressions": "4.1.0", diff --git a/reports/cucumber.api.md b/reports/cucumber.api.md index 852bec264..1d99b74a2 100644 --- a/reports/cucumber.api.md +++ b/reports/cucumber.api.md @@ -24,6 +24,14 @@ export interface ILoadConfigurationOptions { provided?: Partial; } +// @public (undocumented) +export interface ILoadSourcesResult { + // (undocumented) + errors: ISourcesError[]; + // (undocumented) + plan: IPlannedPickle[]; +} + // @public (undocumented) export interface ILoadSupportOptions { // (undocumented) @@ -32,6 +40,19 @@ export interface ILoadSupportOptions { support: ISupportCodeCoordinates; } +// @public (undocumented) +export interface IPlannedPickle { + // (undocumented) + location: { + line: number; + column?: number; + }; + // (undocumented) + name: string; + // (undocumented) + uri: string; +} + // @public (undocumented) export interface IResolvedConfiguration { runConfiguration: IRunConfiguration; @@ -119,6 +140,19 @@ export interface ISourcesCoordinates { tagExpression: string; } +// @public (undocumented) +export interface ISourcesError { + // (undocumented) + location: { + line: number; + column?: number; + }; + // (undocumented) + message: string; + // (undocumented) + uri: string; +} + // @public (undocumented) export interface ISupportCodeCoordinates { // (undocumented) @@ -135,6 +169,9 @@ export type ISupportCodeCoordinatesOrLibrary = ISupportCodeCoordinates | ISuppor // @public export function loadConfiguration(options?: ILoadConfigurationOptions, environment?: IRunEnvironment): Promise; +// @public +export function loadSources(coordinates: ISourcesCoordinates, environment?: IRunEnvironment): Promise; + // @public export function loadSupport(options: ILoadSupportOptions, environment?: IRunEnvironment): Promise; diff --git a/src/api/gherkin.ts b/src/api/gherkin.ts new file mode 100644 index 000000000..8fd19c1f2 --- /dev/null +++ b/src/api/gherkin.ts @@ -0,0 +1,106 @@ +import { + GherkinStreams, + IGherkinStreamOptions, +} from '@cucumber/gherkin-streams' +import { + Envelope, + GherkinDocument, + IdGenerator, + Location, + ParseError, + Pickle, +} from '@cucumber/messages' +import { Query as GherkinQuery } from '@cucumber/gherkin-utils' +import PickleFilter from '../pickle_filter' +import { orderPickles } from '../cli/helpers' +import { ISourcesCoordinates } from './types' + +interface PickleWithDocument { + gherkinDocument: GherkinDocument + location: Location + pickle: Pickle +} + +export async function getFilteredPicklesAndErrors({ + newId, + cwd, + logger, + unexpandedFeaturePaths, + featurePaths, + coordinates, + onEnvelope, +}: { + newId: IdGenerator.NewId + cwd: string + logger: Console + unexpandedFeaturePaths: string[] + featurePaths: string[] + coordinates: ISourcesCoordinates + onEnvelope?: (envelope: Envelope) => void +}): Promise<{ + filteredPickles: PickleWithDocument[] + parseErrors: ParseError[] +}> { + const gherkinQuery = new GherkinQuery() + const parseErrors: ParseError[] = [] + await gherkinFromPaths( + featurePaths, + { + newId, + relativeTo: cwd, + defaultDialect: coordinates.defaultDialect, + }, + (envelope) => { + gherkinQuery.update(envelope) + if (envelope.parseError) { + parseErrors.push(envelope.parseError) + } + onEnvelope?.(envelope) + } + ) + const pickleFilter = new PickleFilter({ + cwd, + featurePaths: unexpandedFeaturePaths, + names: coordinates.names, + tagExpression: coordinates.tagExpression, + }) + 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, + parseErrors, + } +} + +async function gherkinFromPaths( + paths: string[], + options: IGherkinStreamOptions, + onEnvelope: (envelope: Envelope) => void +): Promise { + return new Promise((resolve, reject) => { + const gherkinMessageStream = GherkinStreams.fromPaths(paths, options) + gherkinMessageStream.on('data', onEnvelope) + gherkinMessageStream.on('end', resolve) + gherkinMessageStream.on('error', reject) + }) +} diff --git a/src/api/index.ts b/src/api/index.ts index 51dbcef03..ddf849e54 100644 --- a/src/api/index.ts +++ b/src/api/index.ts @@ -6,6 +6,7 @@ */ export * from './load_configuration' +export * from './load_sources' export * from './load_support' export * from './run_cucumber' export * from './types' diff --git a/src/api/load_sources.ts b/src/api/load_sources.ts new file mode 100644 index 000000000..74e18c683 --- /dev/null +++ b/src/api/load_sources.ts @@ -0,0 +1,64 @@ +import { + ILoadSourcesResult, + IPlannedPickle, + IRunEnvironment, + ISourcesCoordinates, + ISourcesError, +} from './types' +import { resolvePaths } from './paths' +import { IdGenerator } from '@cucumber/messages' +import { Console } from 'console' +import { mergeEnvironment } from './environment' +import { getFilteredPicklesAndErrors } from './gherkin' + +/** + * Load and parse features, produce a filtered and ordered test plan and/or parse errors. + * + * @public + * @param coordinates - Coordinates required to find features + * @param environment - Project environment. + */ +export async function loadSources( + coordinates: ISourcesCoordinates, + environment: IRunEnvironment = {} +): Promise { + const { cwd, stderr } = mergeEnvironment(environment) + const logger = new Console(stderr) + const newId = IdGenerator.uuid() + const { unexpandedFeaturePaths, featurePaths } = await resolvePaths( + cwd, + coordinates + ) + if (featurePaths.length === 0) { + return { + plan: [], + errors: [], + } + } + const { filteredPickles, parseErrors } = await getFilteredPicklesAndErrors({ + newId, + cwd, + logger, + unexpandedFeaturePaths, + featurePaths, + coordinates, + }) + const plan: IPlannedPickle[] = filteredPickles.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, + } + }) + return { + plan, + errors, + } +} diff --git a/src/api/load_sources_spec.ts b/src/api/load_sources_spec.ts new file mode 100644 index 000000000..f43c88986 --- /dev/null +++ b/src/api/load_sources_spec.ts @@ -0,0 +1,149 @@ +import { expect } from 'chai' +import path from 'path' +import { PassThrough } from 'stream' +import fs from 'mz/fs' +import { IdGenerator } from '@cucumber/messages' +import { IRunEnvironment } from './types' +import { loadSources } from './load_sources' + +const newId = IdGenerator.uuid() + +async function setupEnvironment(): Promise> { + const cwd = path.join(__dirname, '..', '..', 'tmp', `loadSources_${newId()}`) + await fs.mkdir(path.join(cwd, 'features'), { recursive: true }) + await fs.writeFile( + path.join(cwd, 'features', 'test.feature'), + `@tag1 +Feature: test fixture + Scenario: one + Given a step + Then another step + + @tag2 + Scenario: two + Given a step + Then another step + + Scenario: three + Given a step + Then another step + + Scenario Outline: four with + Given a step + Then another step\` + Examples: + | param | + | foo | + | bar |` + ) + const stdout = new PassThrough() + return { cwd, stdout } +} + +describe('loadSources', () => { + it('should produce a plan with all pickles', async () => { + const environment = await setupEnvironment() + const { plan } = await loadSources( + { + defaultDialect: 'en', + order: 'defined', + paths: [], + names: [], + tagExpression: '', + }, + environment + ) + expect( + plan.map((planned) => ({ + ...planned, + uri: planned.uri.replace(/\\/g, '/'), + })) + ).to.deep.eq([ + { + name: 'one', + uri: 'features/test.feature', + location: { + line: 3, + column: 3, + }, + }, + { + name: 'two', + uri: 'features/test.feature', + location: { + line: 8, + column: 3, + }, + }, + { + name: 'three', + uri: 'features/test.feature', + location: { + line: 12, + column: 3, + }, + }, + { + name: 'four with foo', + uri: 'features/test.feature', + location: { + line: 21, + column: 5, + }, + }, + { + name: 'four with bar', + uri: 'features/test.feature', + location: { + line: 22, + column: 5, + }, + }, + ]) + }) + + it('should produce a plan with pickles filtered by path:line', async () => { + const environment = await setupEnvironment() + const { plan } = await loadSources( + { + defaultDialect: 'en', + order: 'defined', + paths: ['features/test.feature:8'], + names: [], + tagExpression: '', + }, + environment + ) + expect(plan.map((pickle) => pickle.name)).to.deep.eq(['two']) + }) + + it('should produce a plan with pickles filtered by name', async () => { + const environment = await setupEnvironment() + const { plan } = await loadSources( + { + defaultDialect: 'en', + order: 'defined', + paths: [], + names: ['two'], + tagExpression: '', + }, + environment + ) + expect(plan.map((pickle) => pickle.name)).to.deep.eq(['two']) + }) + + it('should produce a plan with pickles filtered by tags', async () => { + const environment = await setupEnvironment() + const { plan } = await loadSources( + { + defaultDialect: 'en', + order: 'defined', + paths: [], + names: [], + tagExpression: '@tag2', + }, + environment + ) + expect(plan.map((pickle) => pickle.name)).to.deep.eq(['two']) + }) +}) diff --git a/src/api/paths.ts b/src/api/paths.ts index f6ecba74f..a92defe23 100644 --- a/src/api/paths.ts +++ b/src/api/paths.ts @@ -7,7 +7,11 @@ import { ISourcesCoordinates, ISupportCodeCoordinates } from './types' export async function resolvePaths( cwd: string, sources: Pick, - support: ISupportCodeCoordinates + support: ISupportCodeCoordinates = { + requireModules: [], + requirePaths: [], + importPaths: [], + } ): Promise<{ unexpandedFeaturePaths: string[] featurePaths: string[] diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index f279c41c5..cf5faa05c 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -1,22 +1,15 @@ import { Envelope, IdGenerator, ParseError } from '@cucumber/messages' import { EventEmitter } from 'events' import { EventDataCollector } from '../formatter/helpers' -import { - emitMetaMessage, - emitSupportCodeMessages, - parseGherkinMessageStream, -} from '../cli/helpers' -import { GherkinStreams } from '@cucumber/gherkin-streams' -import PickleFilter from '../pickle_filter' +import { emitMetaMessage, emitSupportCodeMessages } from '../cli/helpers' import { IRunOptions, IRunEnvironment, IRunResult } from './types' import { resolvePaths } from './paths' import { makeRuntime } from './runtime' import { initializeFormatters } from './formatters' import { getSupportCodeLibrary } from './support' import { Console } from 'console' -import * as messages from '@cucumber/messages' -import { doesHaveValue } from '../value_checker' import { mergeEnvironment } from './environment' +import { getFilteredPicklesAndErrors } from './gherkin' /** * Execute a Cucumber test run. @@ -73,35 +66,21 @@ export async function runCucumber( }) await emitMetaMessage(eventBroadcaster, env) - const gherkinMessageStream = GherkinStreams.fromPaths(featurePaths, { - defaultDialect: configuration.sources.defaultDialect, - newId, - relativeTo: cwd, - }) let pickleIds: string[] = [] - const parseErrors: ParseError[] = [] - gherkinMessageStream.on('data', (envelope: messages.Envelope) => { - if (doesHaveValue(envelope.parseError)) { - parseErrors.push(envelope.parseError) - } - }) - + let parseErrors: ParseError[] = [] if (featurePaths.length > 0) { - pickleIds = await parseGherkinMessageStream({ + const gherkinResult = await getFilteredPicklesAndErrors({ + newId, + cwd, logger, - eventBroadcaster, - eventDataCollector, - gherkinMessageStream, - order: configuration.sources.order ?? 'defined', - pickleFilter: new PickleFilter({ - cwd, - featurePaths: unexpandedFeaturePaths, - names: configuration.sources.names, - tagExpression: configuration.sources.tagExpression, - }), + unexpandedFeaturePaths, + featurePaths, + coordinates: configuration.sources, + onEnvelope: (envelope) => eventBroadcaster.emit('envelope', envelope), }) + pickleIds = gherkinResult.filteredPickles.map(({ pickle }) => pickle.id) + parseErrors = gherkinResult.parseErrors } - if (parseErrors.length) { parseErrors.forEach((parseError) => { logger.error( diff --git a/src/api/types.ts b/src/api/types.ts index 576428a32..046cb70b1 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -47,6 +47,38 @@ export interface ISourcesCoordinates { order: PickleOrder } +/** + * @public + */ +export interface IPlannedPickle { + name: string + uri: string + location: { + line: number + column?: number + } +} + +/** + * @public + */ +export interface ISourcesError { + uri: string + location: { + line: number + column?: number + } + message: string +} + +/** + * @public + */ +export interface ILoadSourcesResult { + plan: IPlannedPickle[] + errors: ISourcesError[] +} + /** * @public */ diff --git a/src/api/wrapper.mjs b/src/api/wrapper.mjs index f34a7ca7a..7f1119497 100644 --- a/src/api/wrapper.mjs +++ b/src/api/wrapper.mjs @@ -2,4 +2,5 @@ import api from './index.js' export const loadConfiguration = api.loadConfiguration export const loadSupport = api.loadSupport +export const loadSources = api.loadSources export const runCucumber = api.runCucumber diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts index 1a1290d8f..1d54419f6 100644 --- a/src/cli/helpers.ts +++ b/src/cli/helpers.ts @@ -17,16 +17,26 @@ import { builtinParameterTypes } from '../support_code_library_builder' import { version } from '../version' interface IParseGherkinMessageStreamRequest { - logger: Console + cwd?: string eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector gherkinMessageStream: Readable - order: PickleOrder + order: string pickleFilter: PickleFilter } +/** + * Process a stream of envelopes from Gherkin and resolve to an array of filtered, ordered pickle Ids + * + * @deprecated use `loadSources` instead + * + * @param eventBroadcaster + * @param eventDataCollector + * @param gherkinMessageStream + * @param order + * @param pickleFilter + */ export async function parseGherkinMessageStream({ - logger, eventBroadcaster, eventDataCollector, gherkinMessageStream, @@ -49,7 +59,7 @@ export async function parseGherkinMessageStream({ } }) gherkinMessageStream.on('end', () => { - orderPickleIds(result, order, logger) + orderPickles(result, order, console) resolve(result) }) gherkinMessageStream.on('error', reject) @@ -57,8 +67,8 @@ export async function parseGherkinMessageStream({ } // Orders the pickleIds in place - morphs input -export function orderPickleIds( - pickleIds: string[], +export function orderPickles( + pickleIds: T[], order: PickleOrder, logger: Console ): void { diff --git a/src/cli/helpers_spec.ts b/src/cli/helpers_spec.ts index a412700b8..214269d60 100644 --- a/src/cli/helpers_spec.ts +++ b/src/cli/helpers_spec.ts @@ -47,7 +47,6 @@ async function testParseGherkinMessageStream( eventBroadcaster.on('envelope', (e) => envelopes.push(e)) const eventDataCollector = new EventDataCollector(eventBroadcaster) const result = await parseGherkinMessageStream({ - logger: console, eventBroadcaster, eventDataCollector, gherkinMessageStream: options.gherkinMessageStream,