diff --git a/README.md b/README.md index a0ae439a4..454a68c02 100644 --- a/README.md +++ b/README.md @@ -99,6 +99,7 @@ The following documentation is for `main`, which might contain some unreleased f * [Timeouts](./docs/support_files/timeouts.md) * [World](./docs/support_files/world.md) * Guides + * [Debugging](./docs/debugging.md) * [Dry run](./docs/dry_run.md) * [ES Modules](./docs/esm.md) * [Failing fast](./docs/fail_fast.md) diff --git a/docs/debugging.md b/docs/debugging.md new file mode 100644 index 000000000..6b9329087 --- /dev/null +++ b/docs/debugging.md @@ -0,0 +1,9 @@ +# Debugging + +If things aren't working the way you expect with Cucumber, you can enable debug logging. When this is enabled, Cucumber will emit logging to `stderr` relating to configuration and other things that can help you pin down a problem with your project. + +Cucumber uses the [popular `debug` library](https://www.npmjs.com/package/debug) to detect when debug logging should be enabled, under the `cucumber` scope. To enable debug logging, set the `DEBUG` environment variable, like: + +```shell +DEBUG=cucumber +``` diff --git a/features/debug.feature b/features/debug.feature new file mode 100644 index 000000000..d57dca779 --- /dev/null +++ b/features/debug.feature @@ -0,0 +1,37 @@ +@spawn +Feature: debug + + As a Cucumber user + I want to enable debug logging + So that I can troubleshoot issues with my project + + Background: + Given a file named "features/a.feature" with: + """ + Feature: some feature + Scenario: + Given a step passes + When a step passes + Then a step passes + """ + And a file named "features/step_definitions/cucumber_steps.js" with: + """ + const {Given} = require('@cucumber/cucumber') + + Given(/^a step passes$/, function() {}); + """ + + Scenario: + Given my env includes "DEBUG=cucumber" + When I run cucumber-js + Then the error output contains the text: + """ + No configuration file found + """ + + Scenario: + When I run cucumber-js + Then the error output does not contain the text: + """ + No configuration file found + """ diff --git a/package-lock.json b/package-lock.json index 9daeb905a..526b9c64e 100644 --- a/package-lock.json +++ b/package-lock.json @@ -24,6 +24,7 @@ "chalk": "^4.1.2", "cli-table3": "0.6.2", "commander": "^9.0.0", + "debug": "^4.3.4", "duration": "^0.2.2", "durations": "^3.4.2", "figures": "^3.2.0", @@ -58,6 +59,7 @@ "@microsoft/api-extractor": "7.29.5", "@sinonjs/fake-timers": "9.1.2", "@types/chai": "4.3.3", + "@types/debug": "^4.1.7", "@types/dirty-chai": "2.0.2", "@types/express": "4.17.13", "@types/fs-extra": "9.0.13", @@ -1354,6 +1356,15 @@ "@types/node": "*" } }, + "node_modules/@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "dependencies": { + "@types/ms": "*" + } + }, "node_modules/@types/dirty-chai": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/dirty-chai/-/dirty-chai-2.0.2.tgz", @@ -1487,6 +1498,12 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, + "node_modules/@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, "node_modules/@types/mustache": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.1.tgz", @@ -2662,7 +2679,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "dependencies": { "ms": "2.1.2" }, @@ -5479,8 +5495,7 @@ "node_modules/ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "node_modules/mustache": { "version": "4.2.0", @@ -8967,6 +8982,15 @@ "@types/node": "*" } }, + "@types/debug": { + "version": "4.1.7", + "resolved": "https://registry.npmjs.org/@types/debug/-/debug-4.1.7.tgz", + "integrity": "sha512-9AonUzyTjXXhEOa0DnqpzZi6VHlqKMswga9EXjpXnnqxwLtdvPPtlO8evrI5D9S6asFRCQ6v+wpiUKbw+vKqyg==", + "dev": true, + "requires": { + "@types/ms": "*" + } + }, "@types/dirty-chai": { "version": "2.0.2", "resolved": "https://registry.npmjs.org/@types/dirty-chai/-/dirty-chai-2.0.2.tgz", @@ -9100,6 +9124,12 @@ "integrity": "sha512-Z61JK7DKDtdKTWwLeElSEBcWGRLY8g95ic5FoQqI9CMx0ns/Ghep3B4DfcEimiKMvtamNVULVNKEsiwV3aQmXw==", "dev": true }, + "@types/ms": { + "version": "0.7.31", + "resolved": "https://registry.npmjs.org/@types/ms/-/ms-0.7.31.tgz", + "integrity": "sha512-iiUgKzV9AuaEkZqkOLDIvlQiL6ltuZd9tGcW3gwpnX8JbuiuhFlEGmmFXEXkN50Cvq7Os88IY2v0dkDqXYWVgA==", + "dev": true + }, "@types/mustache": { "version": "4.2.1", "resolved": "https://registry.npmjs.org/@types/mustache/-/mustache-4.2.1.tgz", @@ -9990,7 +10020,6 @@ "version": "4.3.4", "resolved": "https://registry.npmjs.org/debug/-/debug-4.3.4.tgz", "integrity": "sha512-PRWFHuSU3eDtQJPvnNY7Jcket1j0t5OuOsFzPPzsekD52Zl8qUfFIPEiswXqIvHWGVHOgX+7G/vCNNhehwxfkQ==", - "dev": true, "requires": { "ms": "2.1.2" } @@ -12097,8 +12126,7 @@ "ms": { "version": "2.1.2", "resolved": "https://registry.npmjs.org/ms/-/ms-2.1.2.tgz", - "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==", - "dev": true + "integrity": "sha512-sGkPx+VjMtmA6MX27oA4FBFELFCZZ4S4XqeGOXCv68tT+jb3vk/RyaKWP0PTKyWtmLSM0b+adUTEvbs1PEaH2w==" }, "mustache": { "version": "4.2.0", diff --git a/package.json b/package.json index 85ce4c7b9..b0d4c34d9 100644 --- a/package.json +++ b/package.json @@ -210,6 +210,7 @@ "chalk": "^4.1.2", "cli-table3": "0.6.2", "commander": "^9.0.0", + "debug": "^4.3.4", "duration": "^0.2.2", "durations": "^3.4.2", "figures": "^3.2.0", @@ -241,6 +242,7 @@ "@microsoft/api-extractor": "7.29.5", "@sinonjs/fake-timers": "9.1.2", "@types/chai": "4.3.3", + "@types/debug": "^4.1.7", "@types/dirty-chai": "2.0.2", "@types/express": "4.17.13", "@types/fs-extra": "9.0.13", diff --git a/src/api/console_logger.ts b/src/api/console_logger.ts new file mode 100644 index 000000000..e0a6995c3 --- /dev/null +++ b/src/api/console_logger.ts @@ -0,0 +1,22 @@ +import { Console } from 'console' +import { Writable } from 'stream' +import { ILogger } from '../logger' + +export class ConsoleLogger implements ILogger { + private console: Console = new Console(this.stream) + constructor(private stream: Writable, private debugEnabled: boolean) {} + + debug(...content: any[]): void { + if (this.debugEnabled) { + this.console.debug(...content) + } + } + + error(...content: any[]): void { + this.console.error(...content) + } + + warn(...content: any[]): void { + this.console.warn(...content) + } +} diff --git a/src/api/environment.ts b/src/api/environment.ts index e34ac849a..5d88bf4e4 100644 --- a/src/api/environment.ts +++ b/src/api/environment.ts @@ -10,6 +10,7 @@ export function mergeEnvironment( stdout: process.stdout, stderr: process.stderr, env: process.env, + debug: false, }, provided ) diff --git a/src/api/formatters.ts b/src/api/formatters.ts index c999aa74b..c1d3e6be1 100644 --- a/src/api/formatters.ts +++ b/src/api/formatters.ts @@ -8,6 +8,7 @@ import FormatterBuilder from '../formatter/builder' import fs from 'mz/fs' import path from 'path' import { IRunOptionsFormats } from './types' +import { ILogger } from '../logger' export async function initializeFormatters({ env, @@ -24,7 +25,7 @@ export async function initializeFormatters({ cwd: string stdout: IFormatterStream stderr: IFormatterStream - logger: Console + logger: ILogger onStreamError: () => void eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector diff --git a/src/api/gherkin.ts b/src/api/gherkin.ts index 8fd19c1f2..5b2fe2f71 100644 --- a/src/api/gherkin.ts +++ b/src/api/gherkin.ts @@ -14,6 +14,7 @@ import { Query as GherkinQuery } from '@cucumber/gherkin-utils' import PickleFilter from '../pickle_filter' import { orderPickles } from '../cli/helpers' import { ISourcesCoordinates } from './types' +import { ILogger } from '../logger' interface PickleWithDocument { gherkinDocument: GherkinDocument @@ -32,7 +33,7 @@ export async function getFilteredPicklesAndErrors({ }: { newId: IdGenerator.NewId cwd: string - logger: Console + logger: ILogger unexpandedFeaturePaths: string[] featurePaths: string[] coordinates: ISourcesCoordinates diff --git a/src/api/load_configuration.ts b/src/api/load_configuration.ts index 8e4780462..13163311f 100644 --- a/src/api/load_configuration.ts +++ b/src/api/load_configuration.ts @@ -12,6 +12,8 @@ import { import { validateConfiguration } from '../configuration/validate_configuration' import { convertConfiguration } from './convert_configuration' import { mergeEnvironment } from './environment' +import { ILogger } from '../logger' +import { ConsoleLogger } from './console_logger' /** * Load user-authored configuration to be used in a test run. @@ -24,16 +26,23 @@ export async function loadConfiguration( options: ILoadConfigurationOptions = {}, environment: IRunEnvironment = {} ): Promise { - const { cwd, env } = mergeEnvironment(environment) + const { cwd, stderr, env, debug } = mergeEnvironment(environment) + const logger: ILogger = new ConsoleLogger(stderr, debug) const configFile = options.file ?? locateFile(cwd) + if (configFile) { + logger.debug(`Configuration will be loaded from "${configFile}"`) + } else { + logger.debug('No configuration file found') + } const profileConfiguration = configFile - ? await fromFile(cwd, configFile, options.profiles) + ? await fromFile(logger, cwd, configFile, options.profiles) : {} const original = mergeConfigurations( DEFAULT_CONFIGURATION, profileConfiguration, options.provided ) + logger.debug('Resolved configuration:', original) validateConfiguration(original) const runnable = await convertConfiguration(original, env) return { diff --git a/src/api/load_sources.ts b/src/api/load_sources.ts index 74e18c683..c46ae96c2 100644 --- a/src/api/load_sources.ts +++ b/src/api/load_sources.ts @@ -7,9 +7,10 @@ import { } from './types' import { resolvePaths } from './paths' import { IdGenerator } from '@cucumber/messages' -import { Console } from 'console' import { mergeEnvironment } from './environment' import { getFilteredPicklesAndErrors } from './gherkin' +import { ConsoleLogger } from './console_logger' +import { ILogger } from '../logger' /** * Load and parse features, produce a filtered and ordered test plan and/or parse errors. @@ -22,10 +23,11 @@ export async function loadSources( coordinates: ISourcesCoordinates, environment: IRunEnvironment = {} ): Promise { - const { cwd, stderr } = mergeEnvironment(environment) - const logger = new Console(stderr) + const { cwd, stderr, debug } = mergeEnvironment(environment) + const logger: ILogger = new ConsoleLogger(stderr, debug) const newId = IdGenerator.uuid() const { unexpandedFeaturePaths, featurePaths } = await resolvePaths( + logger, cwd, coordinates ) diff --git a/src/api/load_support.ts b/src/api/load_support.ts index 3a1adfe49..9120ae4e2 100644 --- a/src/api/load_support.ts +++ b/src/api/load_support.ts @@ -4,6 +4,8 @@ import { resolvePaths } from './paths' import { getSupportCodeLibrary } from './support' import { mergeEnvironment } from './environment' import { ISupportCodeLibrary } from '../support_code_library_builder/types' +import { ILogger } from '../logger' +import { ConsoleLogger } from './console_logger' /** * Load support code for use in test runs. @@ -16,9 +18,11 @@ export async function loadSupport( options: ILoadSupportOptions, environment: IRunEnvironment = {} ): Promise { - const { cwd } = mergeEnvironment(environment) + const { cwd, stderr, debug } = mergeEnvironment(environment) + const logger: ILogger = new ConsoleLogger(stderr, debug) const newId = IdGenerator.uuid() const { requirePaths, importPaths } = await resolvePaths( + logger, cwd, options.sources, options.support diff --git a/src/api/paths.ts b/src/api/paths.ts index a92defe23..917021a53 100644 --- a/src/api/paths.ts +++ b/src/api/paths.ts @@ -3,8 +3,10 @@ import glob from 'glob' import path from 'path' import fs from 'mz/fs' import { ISourcesCoordinates, ISupportCodeCoordinates } from './types' +import { ILogger } from '../logger' export async function resolvePaths( + logger: ILogger, cwd: string, sources: Pick, support: ISupportCodeCoordinates = { @@ -26,12 +28,21 @@ export async function resolvePaths( cwd, unexpandedFeaturePaths ) + logger.debug('Found feature files based on configuration:', featurePaths) const { requirePaths, importPaths } = await deriveSupportPaths( cwd, featurePaths, support.requirePaths, support.importPaths ) + logger.debug( + 'Found support files to load via `require` based on configuration:', + requirePaths + ) + logger.debug( + 'Found support files to load via `import` based on configuration:', + importPaths + ) return { unexpandedFeaturePaths, featurePaths, diff --git a/src/api/paths_spec.ts b/src/api/paths_spec.ts index 6d84d6ee3..3e6c921a7 100644 --- a/src/api/paths_spec.ts +++ b/src/api/paths_spec.ts @@ -5,6 +5,8 @@ import path from 'path' import { describe, it } from 'mocha' import { expect } from 'chai' import { resolvePaths } from './paths' +import { FakeLogger } from '../../test/fake_logger' +import { ILogger } from '../logger' async function buildTestWorkingDirectory(): Promise { const cwd = await promisify(tmp.dir)({ @@ -34,6 +36,7 @@ describe('resolvePaths', () => { requirePaths, importPaths, } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeFeaturePath], @@ -60,6 +63,7 @@ describe('resolvePaths', () => { await fsExtra.outputFile(featurePath, '') // Act const { featurePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [`${relativeFeaturePath}:3`, `${relativeFeaturePath}:4`], @@ -87,6 +91,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeFeaturePath], @@ -118,6 +123,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeFeaturePath], @@ -151,6 +157,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeFeaturePath], @@ -180,6 +187,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeRerunPath], @@ -209,6 +217,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeRerunPath], @@ -239,6 +248,7 @@ describe('resolvePaths', () => { // Act const { featurePaths, unexpandedFeaturePaths, requirePaths } = await resolvePaths( + new FakeLogger(), cwd, { paths: [relativeRerunPath] }, { @@ -254,4 +264,47 @@ describe('resolvePaths', () => { expect(requirePaths).to.eql([]) }) }) + + describe('logging', () => { + it('should emit debugs logs for the feature, import and require paths', async () => { + // Arrange + const logger: ILogger = new FakeLogger() + const cwd = await buildTestWorkingDirectory() + const relativeFeaturePath = path.join('features', 'a.feature') + const featurePath = path.join(cwd, relativeFeaturePath) + await fsExtra.outputFile(featurePath, '') + const jsSupportCodePath = path.join(cwd, 'features', 'a.js') + await fsExtra.outputFile(jsSupportCodePath, '') + const esmSupportCodePath = path.join(cwd, 'features', 'a.mjs') + await fsExtra.outputFile(esmSupportCodePath, '') + + // Act + await resolvePaths( + logger, + cwd, + { + paths: [relativeFeaturePath], + }, + { + requireModules: [], + requirePaths: [], + importPaths: [], + } + ) + + // Assert + expect(logger.debug).to.have.been.calledWith( + 'Found feature files based on configuration:', + [featurePath] + ) + expect(logger.debug).to.have.been.calledWith( + 'Found support files to load via `require` based on configuration:', + [jsSupportCodePath] + ) + expect(logger.debug).to.have.been.calledWith( + 'Found support files to load via `import` based on configuration:', + [esmSupportCodePath] + ) + }) + }) }) diff --git a/src/api/plugins.ts b/src/api/plugins.ts index e4cdabb7e..a0aa6f1df 100644 --- a/src/api/plugins.ts +++ b/src/api/plugins.ts @@ -1,13 +1,14 @@ import { Plugin, PluginManager } from '../plugin' import publishPlugin from '../publish' import { IRunEnvironment, IRunOptions } from './types' +import { ILogger } from '../logger' const INTERNAL_PLUGINS: Record = { publish: publishPlugin, } export async function initializePlugins( - logger: Console, + logger: ILogger, configuration: IRunOptions, environment: IRunEnvironment ): Promise { diff --git a/src/api/run_cucumber.ts b/src/api/run_cucumber.ts index 3279cf9a9..ab3be13df 100644 --- a/src/api/run_cucumber.ts +++ b/src/api/run_cucumber.ts @@ -7,10 +7,11 @@ import { resolvePaths } from './paths' import { makeRuntime } from './runtime' import { initializeFormatters } from './formatters' import { getSupportCodeLibrary } from './support' -import { Console } from 'console' import { mergeEnvironment } from './environment' import { getFilteredPicklesAndErrors } from './gherkin' import { initializePlugins } from './plugins' +import { ConsoleLogger } from './console_logger' +import { ILogger } from '../logger' /** * Execute a Cucumber test run. @@ -25,8 +26,9 @@ export async function runCucumber( environment: IRunEnvironment = {}, onMessage?: (message: Envelope) => void ): Promise { - const { cwd, stdout, stderr, env } = mergeEnvironment(environment) - const logger = new Console(stderr) + const { cwd, stdout, stderr, env, debug } = mergeEnvironment(environment) + const logger: ILogger = new ConsoleLogger(stderr, debug) + const newId = IdGenerator.uuid() const supportCoordinates = @@ -35,7 +37,7 @@ export async function runCucumber( : configuration.support const { unexpandedFeaturePaths, featurePaths, requirePaths, importPaths } = - await resolvePaths(cwd, configuration.sources, supportCoordinates) + await resolvePaths(logger, cwd, configuration.sources, supportCoordinates) const supportCodeLibrary = 'World' in configuration.support diff --git a/src/api/runtime.ts b/src/api/runtime.ts index f811637d3..8b1828702 100644 --- a/src/api/runtime.ts +++ b/src/api/runtime.ts @@ -5,6 +5,7 @@ import { IdGenerator } from '@cucumber/messages' import { ISupportCodeLibrary } from '../support_code_library_builder/types' import Coordinator from '../runtime/parallel/coordinator' import { IRunOptionsRuntime } from './types' +import { ILogger } from '../logger' export function makeRuntime({ cwd, @@ -20,7 +21,7 @@ export function makeRuntime({ options: { parallel, ...options }, }: { cwd: string - logger: Console + logger: ILogger eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector newId: IdGenerator.NewId diff --git a/src/api/types.ts b/src/api/types.ts index ecd966d79..cffbcaaa7 100644 --- a/src/api/types.ts +++ b/src/api/types.ts @@ -163,6 +163,10 @@ export interface IRunEnvironment { * Environment variables (defaults to `process.env` if omitted). */ env?: NodeJS.ProcessEnv + /** + * Whether debug logging is enabled. + */ + debug?: boolean } /** diff --git a/src/cli/helpers.ts b/src/cli/helpers.ts index 866321202..99223180d 100644 --- a/src/cli/helpers.ts +++ b/src/cli/helpers.ts @@ -15,6 +15,7 @@ import TestRunHookDefinition from '../models/test_run_hook_definition' import { PickleOrder } from '../models/pickle_order' import { builtinParameterTypes } from '../support_code_library_builder' import { version } from '../version' +import { ILogger } from '../logger' interface IParseGherkinMessageStreamRequest { cwd?: string @@ -70,7 +71,7 @@ export async function parseGherkinMessageStream({ export function orderPickles( pickleIds: T[], order: PickleOrder, - logger: Console + logger: ILogger ): void { const [type, seed] = OptionSplitter.split(order) switch (type) { diff --git a/src/cli/index.ts b/src/cli/index.ts index aa2301ae7..a9dacf3f4 100644 --- a/src/cli/index.ts +++ b/src/cli/index.ts @@ -3,6 +3,7 @@ import { IFormatterStream } from '../formatter' import { loadConfiguration, runCucumber } from '../api' import { getKeywords, getLanguages } from './i18n' import { validateInstall } from './install_validator' +import debug from 'debug' export interface ICliRunResult { shouldAdvertisePublish: boolean @@ -63,6 +64,7 @@ export default class Cli { stdout: this.stdout, stderr: this.stderr, env: this.env, + debug: debug.enabled('cucumber'), } const { useConfiguration: configuration, runConfiguration } = await loadConfiguration( diff --git a/src/configuration/from_file.ts b/src/configuration/from_file.ts index 977864d67..9e4c41701 100644 --- a/src/configuration/from_file.ts +++ b/src/configuration/from_file.ts @@ -5,20 +5,24 @@ import { IConfiguration } from './types' import { mergeConfigurations } from './merge_configurations' import ArgvParser from './argv_parser' import { checkSchema } from './check_schema' +import { ILogger } from '../logger' // eslint-disable-next-line @typescript-eslint/no-var-requires const { importer } = require('../importer') export async function fromFile( + logger: ILogger, cwd: string, file: string, profiles: string[] = [] ): Promise> { const definitions = await loadFile(cwd, file) if (!definitions.default) { + logger.debug('No default profile defined in configuration file') definitions.default = {} } if (profiles.length < 1) { + logger.debug('No profiles specified; using default profile') profiles = ['default'] } const definedKeys = Object.keys(definitions) @@ -30,7 +34,7 @@ export async function fromFile( return mergeConfigurations( {}, ...profiles.map((profileKey) => - extractConfiguration(profileKey, definitions[profileKey]) + extractConfiguration(logger, profileKey, definitions[profileKey]) ) ) } @@ -58,10 +62,12 @@ async function loadFile( } function extractConfiguration( + logger: ILogger, name: string, definition: any ): Partial { if (typeof definition === 'string') { + logger.debug(`Profile "${name}" value is a string; parsing as argv`) const { configuration } = ArgvParser.parse([ 'node', 'cucumber-js', diff --git a/src/configuration/from_file_spec.ts b/src/configuration/from_file_spec.ts index 6b672c803..a785b1806 100644 --- a/src/configuration/from_file_spec.ts +++ b/src/configuration/from_file_spec.ts @@ -4,41 +4,43 @@ import fs from 'fs' import tmp, { DirOptions } from 'tmp' import path from 'path' import { fromFile } from './from_file' +import { FakeLogger } from '../../test/fake_logger' async function setup( file: string = 'cucumber.js', content: string = `module.exports = {default: {paths: ['some/path/*.feature']}, p1: {paths: ['other/path/*.feature']}, p2: 'other/other/path/*.feature --no-strict'}` ) { + const logger = new FakeLogger() const cwd = await promisify(tmp.dir)({ unsafeCleanup: true, }) fs.writeFileSync(path.join(cwd, file), content, { encoding: 'utf-8' }) - return { cwd } + return { logger, cwd } } describe('fromFile', () => { it('should return empty config if no default provide and no profiles requested', async () => { - const { cwd } = await setup( + const { logger, cwd } = await setup( 'cucumber.js', `module.exports = {p1: {paths: ['other/path/*.feature']}}` ) - const result = await fromFile(cwd, 'cucumber.js', []) + const result = await fromFile(logger, cwd, 'cucumber.js', []) expect(result).to.deep.eq({}) }) it('should get default config from file if no profiles requested', async () => { - const { cwd } = await setup() + const { logger, cwd } = await setup() - const result = await fromFile(cwd, 'cucumber.js', []) + const result = await fromFile(logger, cwd, 'cucumber.js', []) expect(result).to.deep.eq({ paths: ['some/path/*.feature'] }) }) it('should throw when a requested profile doesnt exist', async () => { - const { cwd } = await setup() + const { logger, cwd } = await setup() try { - await fromFile(cwd, 'cucumber.js', ['nope']) + await fromFile(logger, cwd, 'cucumber.js', ['nope']) expect.fail('should have thrown') } catch (error) { expect(error.message).to.eq(`Requested profile "nope" doesn't exist`) @@ -46,16 +48,16 @@ describe('fromFile', () => { }) it('should get single profile config from file', async () => { - const { cwd } = await setup() + const { logger, cwd } = await setup() - const result = await fromFile(cwd, 'cucumber.js', ['p1']) + const result = await fromFile(logger, cwd, 'cucumber.js', ['p1']) expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) }) it('should merge multiple profiles config from file', async () => { - const { cwd } = await setup() + const { logger, cwd } = await setup() - const result = await fromFile(cwd, 'cucumber.js', ['p1', 'p2']) + const result = await fromFile(logger, cwd, 'cucumber.js', ['p1', 'p2']) expect(result).to.deep.eq({ paths: ['other/path/*.feature', 'other/other/path/*.feature'], strict: false, @@ -63,12 +65,12 @@ describe('fromFile', () => { }) it('should throw when an object doesnt conform to the schema', async () => { - const { cwd } = await setup( + const { logger, cwd } = await setup( 'cucumber.js', `module.exports = {p1: {paths: 4, things: 8, requireModule: 'aardvark'}}` ) try { - await fromFile(cwd, 'cucumber.js', ['p1']) + await fromFile(logger, cwd, 'cucumber.js', ['p1']) expect.fail('should have thrown') } catch (error) { expect(error.message).to.eq( @@ -79,22 +81,22 @@ describe('fromFile', () => { describe('other formats', () => { it('should work with esm', async () => { - const { cwd } = await setup( + const { logger, cwd } = await setup( 'cucumber.mjs', `export default {}; export const p1 = {paths: ['other/path/*.feature']}` ) - const result = await fromFile(cwd, 'cucumber.mjs', ['p1']) + const result = await fromFile(logger, cwd, 'cucumber.mjs', ['p1']) expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) }) it('should work with json', async () => { - const { cwd } = await setup( + const { logger, cwd } = await setup( 'cucumber.json', `{ "default": {}, "p1": { "paths": ["other/path/*.feature"] } }` ) - const result = await fromFile(cwd, 'cucumber.json', ['p1']) + const result = await fromFile(logger, cwd, 'cucumber.json', ['p1']) expect(result).to.deep.eq({ paths: ['other/path/*.feature'] }) }) }) diff --git a/src/logger.ts b/src/logger.ts new file mode 100644 index 000000000..f9bde393d --- /dev/null +++ b/src/logger.ts @@ -0,0 +1,5 @@ +export interface ILogger { + debug: (...content: any[]) => void + error: (...content: any[]) => void + warn: (...content: any[]) => void +} diff --git a/src/plugin/plugin_manager.ts b/src/plugin/plugin_manager.ts index 4ebfd70a9..903a6e68e 100644 --- a/src/plugin/plugin_manager.ts +++ b/src/plugin/plugin_manager.ts @@ -1,5 +1,6 @@ import { Plugin, PluginCleanup, PluginEvents } from './types' import { IRunEnvironment, IRunOptions } from '../api' +import { ILogger } from '../logger' type HandlerRegistry = { [K in keyof PluginEvents]: Array<(value: PluginEvents[K]) => void> @@ -19,7 +20,7 @@ export class PluginManager { } async init( - logger: Console, + logger: ILogger, configuration: IRunOptions, environment: IRunEnvironment ) { diff --git a/src/plugin/types.ts b/src/plugin/types.ts index b4c8e89da..9cacd3b2b 100644 --- a/src/plugin/types.ts +++ b/src/plugin/types.ts @@ -1,5 +1,6 @@ import { IRunEnvironment, IRunOptions } from '../api' import { Envelope } from '@cucumber/messages' +import { ILogger } from '../logger' export interface PluginEvents { message: Envelope @@ -10,7 +11,7 @@ export interface PluginContext { event: K, handler: (value: PluginEvents[K]) => void ) => void - logger: Console + logger: ILogger configuration: IRunOptions environment: IRunEnvironment } diff --git a/src/runtime/parallel/coordinator.ts b/src/runtime/parallel/coordinator.ts index a8634f3b8..c4776c634 100644 --- a/src/runtime/parallel/coordinator.ts +++ b/src/runtime/parallel/coordinator.ts @@ -11,12 +11,13 @@ import { doesHaveValue } from '../../value_checker' import { ITestRunStopwatch, RealTestRunStopwatch } from '../stopwatch' import { assembleTestCases, IAssembledTestCases } from '../assemble_test_cases' import { IdGenerator } from '@cucumber/messages' +import { ILogger } from '../../logger' const runWorkerPath = path.resolve(__dirname, 'run_worker.js') export interface INewCoordinatorOptions { cwd: string - logger: Console + logger: ILogger eventBroadcaster: EventEmitter eventDataCollector: EventDataCollector options: IRuntimeOptions @@ -64,7 +65,7 @@ export default class Coordinator implements IRuntime { private readonly requirePaths: string[] private readonly importPaths: string[] private readonly numberOfWorkers: number - private readonly logger: Console + private readonly logger: ILogger private success: boolean private idleInterventions: number diff --git a/test/fake_logger.ts b/test/fake_logger.ts new file mode 100644 index 000000000..b85805b53 --- /dev/null +++ b/test/fake_logger.ts @@ -0,0 +1,8 @@ +import { ILogger } from '../src/logger' +import sinon from 'sinon' + +export class FakeLogger implements ILogger { + debug = sinon.fake() + error = sinon.fake() + warn = sinon.fake() +}