Skip to content

Commit

Permalink
wip initial implementation
Browse files Browse the repository at this point in the history
  • Loading branch information
davidjgoss committed Apr 29, 2024
1 parent 5ce3718 commit 358abbb
Show file tree
Hide file tree
Showing 18 changed files with 226 additions and 133 deletions.
2 changes: 1 addition & 1 deletion exports/root/report.api.md
Original file line number Diff line number Diff line change
Expand Up @@ -132,7 +132,7 @@ export class Formatter {

// @public (undocumented)
export const FormatterBuilder: {
build(type: string, options: IBuildOptions): Promise<Formatter>;
build(FormatterConstructor: string | typeof Formatter, options: IBuildOptions): Promise<Formatter>;
getConstructorByType(type: string, cwd: string): Promise<typeof Formatter>;
getStepDefinitionSnippetBuilder({ cwd, snippetInterface, snippetSyntax, supportCodeLibrary, }: IGetStepDefinitionSnippetBuilderOptions): Promise<StepDefinitionSnippetBuilder>;
loadCustomClass(type: 'formatter' | 'syntax', descriptor: string, cwd: string): Promise<any>;
Expand Down
106 changes: 51 additions & 55 deletions src/api/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,14 +1,14 @@
import { EventEmitter } from 'node:events'
import { promisify } from 'node:util'
import { WriteStream as TtyWriteStream } from 'node:tty'
import path from 'node:path'
import fs from 'mz/fs'
import { mkdirp } from 'mkdirp'
import Formatter, { IFormatterStream } from '../formatter'
import { IFormatterStream } from '../formatter'
import { EventDataCollector } from '../formatter/helpers'
import { SupportCodeLibrary } from '../support_code_library_builder/types'
import FormatterBuilder from '../formatter/builder'
import { ILogger } from '../logger'
import { createStream } from '../formatter/create_stream'
import { resolveImplementation } from '../formatter/resolve_implementation'
import { PluginManager } from '../plugin'
import { IRunOptionsFormats } from './types'

export async function initializeFormatters({
Expand All @@ -21,6 +21,7 @@ export async function initializeFormatters({
eventDataCollector,
configuration,
supportCodeLibrary,
pluginManager,
}: {
env: NodeJS.ProcessEnv
cwd: string
Expand All @@ -32,69 +33,64 @@ export async function initializeFormatters({
eventDataCollector: EventDataCollector
configuration: IRunOptionsFormats
supportCodeLibrary: SupportCodeLibrary
pluginManager: PluginManager
}): Promise<() => Promise<void>> {
const cleanupFns: Array<() => Promise<void>> = []

async function initializeFormatter(
stream: IFormatterStream,
target: string,
type: string
): Promise<Formatter> {
stream.on('error', (error: Error) => {
logger.error(error.message)
onStreamError()
})
const typeOptions = {
env,
cwd,
eventBroadcaster,
eventDataCollector,
log: stream.write.bind(stream),
parsedArgvOptions: configuration.options,
stream,
cleanup:
stream === stdout
? async () => await Promise.resolve()
: promisify<any>(stream.end.bind(stream)),
supportCodeLibrary,
}
if (type === 'progress-bar' && !(stream as TtyWriteStream).isTTY) {
specifier: string
): Promise<void> {
if (specifier === 'progress-bar' && !(stream as TtyWriteStream).isTTY) {
logger.warn(
`Cannot use 'progress-bar' formatter for output to '${target}' as not a TTY. Switching to 'progress' formatter.`
)
type = 'progress'
specifier = 'progress'
}
const implementation = await resolveImplementation(specifier, cwd)
if (typeof implementation === 'function') {
const typeOptions = {
env,
cwd,
eventBroadcaster,
eventDataCollector,
log: stream.write.bind(stream),
parsedArgvOptions: configuration.options,
stream,
cleanup:
stream === stdout
? async () => await Promise.resolve()
: promisify<any>(stream.end.bind(stream)),
supportCodeLibrary,
}
const formatter = await FormatterBuilder.build(
implementation,
typeOptions
)
cleanupFns.push(async () => formatter.finished())
} else {
await pluginManager.initFormatter(
implementation,
configuration.options,
stream.write.bind(stream)
)
if (stream !== stdout) {
cleanupFns.push(promisify<any>(stream.end.bind(stream)))
}
}
return await FormatterBuilder.build(type, typeOptions)
}

const formatters: Formatter[] = []

formatters.push(
await initializeFormatter(stdout, 'stdout', configuration.stdout)
)

const streamPromises: Promise<void>[] = []

Object.entries(configuration.files).forEach(([target, type]) => {
streamPromises.push(
(async (target, type) => {
const absoluteTarget = path.resolve(cwd, target)

try {
await mkdirp(path.dirname(absoluteTarget))
} catch (error) {
logger.warn('Failed to ensure directory for formatter target exists')
}

const stream: IFormatterStream = fs.createWriteStream(null, {
fd: await fs.open(absoluteTarget, 'w'),
})
formatters.push(await initializeFormatter(stream, target, type))
})(target, type)
await initializeFormatter(stdout, 'stdout', configuration.stdout)
for (const [target, specifier] of Object.entries(configuration.files)) {
await initializeFormatter(
await createStream(target, onStreamError, cwd, logger),
target,
specifier
)
})

await Promise.all(streamPromises)
}

return async function () {
await Promise.all(formatters.map(async (f) => await f.finished()))
await Promise.all(cleanupFns.map((cleanupFn) => cleanupFn()))
}
}
6 changes: 3 additions & 3 deletions src/api/plugins.ts
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ export async function initializeForLoadSources(
): Promise<PluginManager> {
// eventually we'll load plugin packages here
const pluginManager = new PluginManager()
await pluginManager.init(
await pluginManager.initCoordinator(
'loadSources',
filterPlugin,
coordinates,
Expand All @@ -37,14 +37,14 @@ export async function initializeForRunCucumber(
): Promise<PluginManager> {
// eventually we'll load plugin packages here
const pluginManager = new PluginManager()
await pluginManager.init(
await pluginManager.initCoordinator(
'runCucumber',
publishPlugin,
configuration.formats.publish,
logger,
environment
)
await pluginManager.init(
await pluginManager.initCoordinator(
'runCucumber',
filterPlugin,
configuration.sources,
Expand Down
3 changes: 2 additions & 1 deletion src/api/run_cucumber.ts
Original file line number Diff line number Diff line change
Expand Up @@ -95,6 +95,7 @@ export async function runCucumber(
eventDataCollector,
configuration: options.formats,
supportCodeLibrary,
pluginManager,
})
await emitMetaMessage(eventBroadcaster, env)

Expand Down Expand Up @@ -150,8 +151,8 @@ export async function runCucumber(
options: options.runtime,
})
const success = await runtime.start()
await cleanupFormatters()
await pluginManager.cleanup()
await cleanupFormatters()

return {
success: success && !formatterStreamError,
Expand Down
8 changes: 6 additions & 2 deletions src/configuration/argv_parser.ts
Original file line number Diff line number Diff line change
@@ -1,8 +1,8 @@
import { Command } from 'commander'
import merge from 'lodash.merge'
import { dialects } from '@cucumber/gherkin'
import Formatters from '../formatter/helpers/formatters'
import { version } from '../version'
import builtin from '../formatter/builtin'
import { IConfiguration } from './types'

export interface IParsedArgvOptions {
Expand Down Expand Up @@ -81,7 +81,11 @@ const ArgvParser = {
.option(
'-f, --format <TYPE[:PATH]>',
'specify the output format, optionally supply PATH to redirect formatter output (repeatable). Available formats:\n' +
Formatters.buildFormattersDocumentationString(),
Object.entries(builtin).reduce(
(previous, [key, formatter]) =>
previous + ` ${key}: ${formatter.documentation}\n`,
''
),
ArgvParser.collect
)
.option(
Expand Down
15 changes: 10 additions & 5 deletions src/formatter/builder.ts
Original file line number Diff line number Diff line change
Expand Up @@ -36,11 +36,16 @@ export interface IBuildOptions {
}

const FormatterBuilder = {
async build(type: string, options: IBuildOptions): Promise<Formatter> {
const FormatterConstructor = await FormatterBuilder.getConstructorByType(
type,
options.cwd
)
async build(
FormatterConstructor: string | typeof Formatter,
options: IBuildOptions
): Promise<Formatter> {
if (typeof FormatterConstructor === 'string') {
FormatterConstructor = await FormatterBuilder.getConstructorByType(
FormatterConstructor,
options.cwd
)
}
const colorFns = getColorFns(
options.stream,
options.env,
Expand Down
25 changes: 25 additions & 0 deletions src/formatter/builtin/html.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,25 @@
import { promisify } from 'node:util'
import { finished } from 'node:stream'
import CucumberHtmlStream from '@cucumber/html-formatter'
import resolvePkg from 'resolve-pkg'
import { FormatterPlugin } from '../../plugin'

export default {
type: 'formatter',
formatter({ on, write }) {
const htmlStream = new CucumberHtmlStream(
resolvePkg('@cucumber/html-formatter', { cwd: __dirname }) +
'/dist/main.css',
resolvePkg('@cucumber/html-formatter', { cwd: __dirname }) +
'/dist/main.js'
)
on('message', (message) => htmlStream.write(message))
htmlStream.on('data', (chunk) => write(chunk))

return async () => {
htmlStream.end()
await promisify(finished)(htmlStream)
}
},
documentation: 'Outputs a HTML report',
} satisfies FormatterPlugin
30 changes: 30 additions & 0 deletions src/formatter/builtin/index.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,30 @@
import { FormatterImplementation } from '../index'
import JsonFormatter from '../json_formatter'
import ProgressFormatter from '../progress_formatter'
import ProgressBarFormatter from '../progress_bar_formatter'
import RerunFormatter from '../rerun_formatter'
import SnippetsFormatter from '../snippets_formatter'
import SummaryFormatter from '../summary_formatter'
import UsageFormatter from '../usage_formatter'
import UsageJsonFormatter from '../usage_json_formatter'
import JunitFormatter from '../junit_formatter'
import messageFormatter from './message'
import htmlFormatter from './html'

const builtin: Record<string, FormatterImplementation> = {
// new plugin-based formatters
html: htmlFormatter,
message: messageFormatter,
// legacy class-based formatters
json: JsonFormatter,
progress: ProgressFormatter,
'progress-bar': ProgressBarFormatter,
rerun: RerunFormatter,
snippets: SnippetsFormatter,
summary: SummaryFormatter,
usage: UsageFormatter,
'usage-json': UsageJsonFormatter,
junit: JunitFormatter,
}

export default builtin
9 changes: 9 additions & 0 deletions src/formatter/builtin/message.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,9 @@
import { FormatterPlugin } from '../../plugin'

export default {
type: 'formatter',
formatter({ on, write }) {
on('message', (message) => write(JSON.stringify(message) + '\n'))
},
documentation: 'Emits Cucumber messages in NDJSON format',
} satisfies FormatterPlugin
31 changes: 31 additions & 0 deletions src/formatter/create_stream.ts
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
import path from 'node:path'
import { Writable } from 'node:stream'
import { mkdirp } from 'mkdirp'
import fs from 'mz/fs'
import { ILogger } from '../logger'

export async function createStream(
target: string,
onStreamError: () => void,
cwd: string,
logger: ILogger
): Promise<Writable> {
const absoluteTarget = path.resolve(cwd, target)

try {
await mkdirp(path.dirname(absoluteTarget))
} catch (error) {
logger.warn('Failed to ensure directory for formatter target exists')
}

const stream: Writable = fs.createWriteStream(null, {
fd: await fs.open(absoluteTarget, 'w'),
})

stream.on('error', (error: Error) => {
logger.error(error.message)
onStreamError()
})

return stream
}
13 changes: 0 additions & 13 deletions src/formatter/helpers/formatters.ts
Original file line number Diff line number Diff line change
@@ -1,22 +1,18 @@
import Formatter from '../.'
import JsonFormatter from '../json_formatter'
import MessageFormatter from '../message_formatter'
import ProgressBarFormatter from '../progress_bar_formatter'
import ProgressFormatter from '../progress_formatter'
import RerunFormatter from '../rerun_formatter'
import SnippetsFormatter from '../snippets_formatter'
import SummaryFormatter from '../summary_formatter'
import UsageFormatter from '../usage_formatter'
import UsageJsonFormatter from '../usage_json_formatter'
import HtmlFormatter from '../html_formatter'
import JunitFormatter from '../junit_formatter'

const Formatters = {
getFormatters(): Record<string, typeof Formatter> {
return {
json: JsonFormatter,
message: MessageFormatter,
html: HtmlFormatter,
progress: ProgressFormatter,
'progress-bar': ProgressBarFormatter,
rerun: RerunFormatter,
Expand All @@ -27,15 +23,6 @@ const Formatters = {
junit: JunitFormatter,
}
},
buildFormattersDocumentationString(): string {
let concatenatedFormattersDocumentation: string = ''
const formatters = this.getFormatters()
for (const formatterName in formatters) {
concatenatedFormattersDocumentation += ` ${formatterName}: ${formatters[formatterName].documentation}\n`
}

return concatenatedFormattersDocumentation
},
}

export default Formatters

0 comments on commit 358abbb

Please sign in to comment.