-
-
Notifications
You must be signed in to change notification settings - Fork 1.1k
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
reimplement publish functionality as a plugin #2092
Changes from all commits
63ab911
33058aa
4477a4e
2a12666
186eb16
7b2c398
bcd8b41
dc308f1
49571ef
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,61 @@ | ||
🚨 This functionality is a work in progress and not yet available in a released version of Cucumber. If you have any | ||
feedback about how plugins are going to work, please jump into the comments | ||
on <https://github.com/cucumber/cucumber-js/discussions/2091> 🚨 | ||
|
||
- - - | ||
|
||
# Plugins | ||
|
||
You can extend Cucumber's functionality by writing plugins. They allow you to: | ||
|
||
- Listen to Cucumber's message stream | ||
- Hook into core behaviour and change things | ||
- Output extra information for the user | ||
|
||
The API is described below. Our own Publish functionality is [implemented as a plugin](../src/publish/publish_plugin.ts), which you might find useful as an example. | ||
|
||
## Writing a plugin | ||
|
||
A plugin in its simplest form is a function. It should be the default export of your plugin package. Here's the signature: | ||
|
||
```js | ||
export default async({ | ||
on, | ||
logger, | ||
configuration, | ||
environment | ||
}) => { | ||
// do stuff here | ||
} | ||
``` | ||
|
||
### Lifecycle | ||
|
||
Your plugin is initialised early in Cucumber's lifecycle, just after we have resolved the configuration. You can do async setup work in your plugin function - Cucumber will await the promise. | ||
|
||
Your plugin is stopped after the test run finishes, just before Cucumber exits. If you need to do cleanup work, your plugin function can return a cleanup function - it will be executed (and awaited if it returns a promise) before Cucumber exits. | ||
|
||
A plugin function accepts a single argument which provides the context for your plugin. It has: | ||
|
||
- `on(event: string, handler: Function)` - function for registering handlers for events (see below for supported events) - you can call this as many times as you'd like | ||
- `logger` - a console instance that directs output to stderr (or other appropriate stream) | ||
- `configuration` - the final resolved configuration object being used for this execution of Cucumber | ||
- `environment` - details of the environment for this execution of Cucumber | ||
|
||
### Events | ||
|
||
These are the events for which you can register handlers: | ||
|
||
| Name | Signature | Notes | | ||
|-----------|-------------------------------|----------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------------| | ||
| `message` | `(message: Envelope) => void` | Cucumber emits a message for all significant events over the course of a test run. These are most commonly consumed by formatters, but have other uses too. Note that you can do async work in this handler, but Cucumber won't await the promise. | | ||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,18 @@ | ||
import { Plugin, PluginManager } from '../plugin' | ||
import publishPlugin from '../publish' | ||
import { IRunEnvironment, IRunOptions } from './types' | ||
|
||
const INTERNAL_PLUGINS: Record<string, Plugin> = { | ||
publish: publishPlugin, | ||
} | ||
|
||
export async function initializePlugins( | ||
logger: Console, | ||
configuration: IRunOptions, | ||
environment: IRunEnvironment | ||
): Promise<PluginManager> { | ||
// eventually we'll load plugin packages here | ||
const pluginManager = new PluginManager(Object.values(INTERNAL_PLUGINS)) | ||
await pluginManager.init(logger, configuration, environment) | ||
return pluginManager | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
|
@@ -10,6 +10,7 @@ import { getSupportCodeLibrary } from './support' | |
import { Console } from 'console' | ||
import { mergeEnvironment } from './environment' | ||
import { getFilteredPicklesAndErrors } from './gherkin' | ||
import { initializePlugins } from './plugins' | ||
|
||
/** | ||
* Execute a Cucumber test run. | ||
|
@@ -25,7 +26,7 @@ export async function runCucumber( | |
onMessage?: (message: Envelope) => void | ||
): Promise<IRunResult> { | ||
const { cwd, stdout, stderr, env } = mergeEnvironment(environment) | ||
const logger = new Console(stdout, stderr) | ||
const logger = new Console(stderr) | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This was an oversight when initially added. This |
||
const newId = IdGenerator.uuid() | ||
|
||
const supportCoordinates = | ||
|
@@ -47,14 +48,17 @@ export async function runCucumber( | |
requireModules: supportCoordinates.requireModules, | ||
}) | ||
|
||
const plugins = await initializePlugins(logger, configuration, environment) | ||
|
||
const eventBroadcaster = new EventEmitter() | ||
if (onMessage) { | ||
eventBroadcaster.on('envelope', onMessage) | ||
} | ||
eventBroadcaster.on('envelope', (value) => plugins.emit('message', value)) | ||
const eventDataCollector = new EventDataCollector(eventBroadcaster) | ||
|
||
let formatterStreamError = false | ||
const cleanup = await initializeFormatters({ | ||
const cleanupFormatters = await initializeFormatters({ | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. Renamed for clarity. |
||
env, | ||
cwd, | ||
stdout, | ||
|
@@ -89,7 +93,8 @@ export async function runCucumber( | |
`Parse error in "${parseError.source.uri}" ${parseError.message}` | ||
) | ||
}) | ||
await cleanup() | ||
await cleanupFormatters() | ||
await plugins.cleanup() | ||
return { | ||
success: false, | ||
support: supportCodeLibrary, | ||
|
@@ -116,7 +121,8 @@ export async function runCucumber( | |
options: configuration.runtime, | ||
}) | ||
const success = await runtime.start() | ||
await cleanup() | ||
await cleanupFormatters() | ||
await plugins.cleanup() | ||
|
||
return { | ||
success: success && !formatterStreamError, | ||
|
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -1,12 +1,9 @@ | ||
import { IColorFns } from './get_color_fns' | ||
import { EventDataCollector } from './helpers' | ||
import StepDefinitionSnippetBuilder from './step_definition_snippet_builder' | ||
import { PassThrough, Writable as WritableStream } from 'stream' | ||
import { Writable } from 'stream' | ||
import { ISupportCodeLibrary } from '../support_code_library_builder/types' | ||
import { WriteStream as FsWriteStream } from 'fs' | ||
import { WriteStream as TtyWriteStream } from 'tty' | ||
import { EventEmitter } from 'events' | ||
import HttpStream from './http_stream' | ||
import { valueOrDefault } from '../value_checker' | ||
import { SnippetInterface } from './step_definition_snippet_builder/snippet_syntax' | ||
|
||
|
@@ -23,11 +20,12 @@ export interface FormatOptions { | |
[customKey: string]: any | ||
} | ||
|
||
export type IFormatterStream = | ||
| FsWriteStream | ||
| TtyWriteStream | ||
| PassThrough | ||
| HttpStream | ||
export interface IPublishConfig { | ||
url: string | ||
token: string | ||
} | ||
|
||
export type IFormatterStream = Writable | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more.
|
||
export type IFormatterLogFn = (buffer: string | Uint8Array) => void | ||
export type IFormatterCleanupFn = () => Promise<any> | ||
|
||
|
@@ -39,7 +37,7 @@ export interface IFormatterOptions { | |
log: IFormatterLogFn | ||
parsedArgvOptions: FormatOptions | ||
snippetBuilder: StepDefinitionSnippetBuilder | ||
stream: WritableStream | ||
stream: Writable | ||
cleanup: IFormatterCleanupFn | ||
supportCodeLibrary: ISupportCodeLibrary | ||
} | ||
|
@@ -50,7 +48,7 @@ export default class Formatter { | |
protected eventDataCollector: EventDataCollector | ||
protected log: IFormatterLogFn | ||
protected snippetBuilder: StepDefinitionSnippetBuilder | ||
protected stream: WritableStream | ||
protected stream: Writable | ||
protected supportCodeLibrary: ISupportCodeLibrary | ||
protected printAttachments: boolean | ||
private readonly cleanup: IFormatterCleanupFn | ||
|
This file was deleted.
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,2 @@ | ||
export * from './types' | ||
export * from './plugin_manager' |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,48 @@ | ||
import { Plugin, PluginCleanup, PluginEvents } from './types' | ||
import { IRunEnvironment, IRunOptions } from '../api' | ||
|
||
type HandlerRegistry = { | ||
[K in keyof PluginEvents]: Array<(value: PluginEvents[K]) => void> | ||
} | ||
|
||
export class PluginManager { | ||
private handlers: HandlerRegistry = { message: [] } | ||
private cleanupFns: PluginCleanup[] = [] | ||
|
||
constructor(private pluginFns: Plugin[]) {} | ||
|
||
private async register<K extends keyof PluginEvents>( | ||
event: K, | ||
handler: (value: PluginEvents[K]) => void | ||
) { | ||
this.handlers[event].push(handler) | ||
} | ||
|
||
async init( | ||
logger: Console, | ||
configuration: IRunOptions, | ||
environment: IRunEnvironment | ||
) { | ||
for (const pluginFn of this.pluginFns) { | ||
const cleanupFn = await pluginFn({ | ||
on: this.register.bind(this), | ||
logger, | ||
configuration, | ||
environment, | ||
}) | ||
if (cleanupFn) { | ||
this.cleanupFns.push(cleanupFn) | ||
} | ||
} | ||
} | ||
|
||
emit<K extends keyof PluginEvents>(event: K, value: PluginEvents[K]): void { | ||
this.handlers[event].forEach((handler) => handler(value)) | ||
} | ||
|
||
async cleanup(): Promise<void> { | ||
for (const cleanupFn of this.cleanupFns) { | ||
await cleanupFn() | ||
} | ||
} | ||
} |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,20 @@ | ||
import { IRunEnvironment, IRunOptions } from '../api' | ||
import { Envelope } from '@cucumber/messages' | ||
|
||
export interface PluginEvents { | ||
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This would expand to cover other events we start to support. |
||
message: Envelope | ||
} | ||
|
||
export interface PluginContext { | ||
on: <K extends keyof PluginEvents>( | ||
event: K, | ||
handler: (value: PluginEvents[K]) => void | ||
) => void | ||
logger: Console | ||
configuration: IRunOptions | ||
environment: IRunEnvironment | ||
} | ||
|
||
export type PluginCleanup = () => any | void | Promise<any | void> | ||
|
||
export type Plugin = (context: PluginContext) => Promise<PluginCleanup | void> |
Original file line number | Diff line number | Diff line change |
---|---|---|
@@ -0,0 +1,3 @@ | ||
import { publishPlugin } from './publish_plugin' | ||
|
||
export default publishPlugin |
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
The only functional change here. For formatters, we catch errors on the stream(s), log them and then fail the test run at the end. This is intended to cope with things like file system snafus etc. Publish is now not a formatter, and at any rate this felt a bit off - failing to publish the report seems like it shouldn't fail the test run? That's debatable, though.
It does raise a couple of questions about error handling though: