Skip to content

Commit

Permalink
Merge branch 'main' into DEVX-2923
Browse files Browse the repository at this point in the history
  • Loading branch information
tianfeng92 authored Nov 25, 2024
2 parents 2ccebfe + 3fb15d8 commit c9aaddc
Show file tree
Hide file tree
Showing 3 changed files with 157 additions and 12 deletions.
89 changes: 77 additions & 12 deletions src/cucumber-runner.ts
Original file line number Diff line number Diff line change
Expand Up @@ -7,20 +7,32 @@ import type { CucumberRunnerConfig } from './types';
import * as utils from './utils';
import { NodeContext } from 'sauce-testrunner-utils/lib/types';

function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
export function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
const paths: string[] = [];
runCfg.suite.options.paths.forEach((p) => {
paths.push(path.join(runCfg.projectPath, p));
});
const procArgs = [
cucumberBin,
...paths,
'--publish-quiet', // Deprecated in 9.4.0. Will be removed in 11.0.0 or later.
'--force-exit',
'--require-module',
'ts-node/register',
// NOTE: The Cucumber formatter (--format) option uses the "type":"path" format.
// If the "path" is not specified, the output defaults to stdout.
// Cucumber supports only one stdout formatter; if multiple are specified,
// the last one listed takes precedence.
//
// To ensure the Sauce test report file is reliably generated and not overridden
// by a user-specified stdout formatter, configure the following:
// 1. In the --format option, set the output to a file (e.g., cucumber.log) to
// avoid writing to stdout.
// 2. Use the --format-options flag to explicitly specify the outputFile
// (e.g., sauce-test-report.json).
//
// Both settings must be configured correctly to ensure the Sauce test report file is generated.
'--format',
'@saucelabs/cucumber-reporter',
'"@saucelabs/cucumber-reporter":"cucumber.log"',
'--format-options',
JSON.stringify(buildFormatOption(runCfg)),
];
Expand Down Expand Up @@ -50,15 +62,12 @@ function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
procArgs.push('-t');
procArgs.push(tag);
});

runCfg.suite.options.format?.forEach((format) => {
procArgs.push('--format');
const opts = format.split(':');
if (opts.length === 2) {
procArgs.push(`${opts[0]}:${path.join(runCfg.assetsDir, opts[1])}`);
} else {
procArgs.push(format);
}
procArgs.push(normalizeFormat(format, runCfg.assetsDir));
});

if (runCfg.suite.options.parallel) {
procArgs.push('--parallel');
procArgs.push(runCfg.suite.options.parallel.toString(10));
Expand All @@ -67,6 +76,64 @@ function buildArgs(runCfg: CucumberRunnerConfig, cucumberBin: string) {
return procArgs;
}

/**
* Normalizes a Cucumber-js format string.
*
* This function handles structured inputs in the format `key:value`, `"key:value"`,
* or `"key":"value"` and returns a normalized string in the form `"key":"value"`.
* For simple inputs (e.g., `usage`) or unstructured formats, the function returns the
* input unchanged.
*
* If the input starts with `file://`, an error is thrown to indicate an invalid format.
*
* @param {string} format - The input format string. Examples include:
* - `"key:value"`
* - `"key":"value"`
* - `key:value`
* - `usage`
* - `"file://implementation":"output_file"`
* @param {string} assetDir - The directory to prepend to the value for relative paths.
* @returns {string} The normalized format string.
*
* Examples:
* - Input: `"html:formatter/report.html"`, `"/project/assets"`
* Output: `"html":"/project/assets/formatter/report.html"`
* - Input: `"usage"`, `"/project/assets"`
* Output: `"usage"`
* - Input: `"file://implementation":"output_file"`, `"/project/assets"`
* Output: `"file://implementation":"output_file"` (unchanged)
*/
export function normalizeFormat(format: string, assetDir: string): string {
// Formats starting with file:// are not supported by the current implementation.
// Restrict users from using this format.
if (format.startsWith('file://')) {
throw new Error(
`Ambiguous colon usage detected. The provided format "${format}" is not allowed.`,
);
}
// Try to match structured inputs in the format key:value, "key:value", or "key":"value".
let match = format.match(/^"?([^:]+):"?([^"]+)"?$/);

if (!match) {
if (!format.startsWith('"file://')) {
return format;
}

// Match file-based structured inputs like "file://implementation":"output_file".
match = format.match(/^"([^"]+)":"([^"]+)"$/);
}

if (!match) {
return format;
}

let [, key, value] = match;
key = key.replaceAll('"', '');
value = value.replaceAll('"', '');

return `"${key}":"${path.join(assetDir, value)}"`;
}

export async function runCucumber(
nodeBin: string,
runCfg: CucumberRunnerConfig,
Expand Down Expand Up @@ -142,9 +209,7 @@ export async function runCucumber(
function buildFormatOption(cfg: CucumberRunnerConfig) {
return {
upload: false,
suiteName: cfg.suite.name,
build: cfg.sauce.metadata?.build,
tags: cfg.sauce.metadata?.tags,
outputFile: path.join(cfg.assetsDir, 'sauce-test-report.json'),
...cfg.suite.options.formatOptions,
};
}
1 change: 1 addition & 0 deletions src/types.ts
Original file line number Diff line number Diff line change
Expand Up @@ -101,6 +101,7 @@ export interface CucumberSuite {
import?: string[];
tags?: string[];
format?: string[];
formatOptions?: { [key: string]: string };
parallel?: number;
paths: string[];
};
Expand Down
79 changes: 79 additions & 0 deletions tests/unit/src/cucumber-runner.spec.js
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
const { buildArgs, normalizeFormat } = require('../../../src/cucumber-runner');

describe('buildArgs', () => {
const cucumberBin = '/usr/local/bin/cucumber';

it('should build correct arguments with basic configuration', () => {
const runCfg = {
sauce: {
metadata: {},
},
projectPath: '/project',
assetsDir: '/project/assets',
suite: {
options: {
paths: ['features/test.feature'],
formatOptions: {
myOption: 'test',
},
},
},
};

const result = buildArgs(runCfg, cucumberBin);

expect(result).toEqual([
cucumberBin,
'/project/features/test.feature',
'--force-exit',
'--require-module',
'ts-node/register',
'--format',
'"@saucelabs/cucumber-reporter":"cucumber.log"',
'--format-options',
'{"upload":false,"outputFile":"/project/assets/sauce-test-report.json","myOption":"test"}',
]);
});
});

describe('normalizeFormat', () => {
const assetDir = '/project/assets';

it('should normalize format with both quoted format type and path', () => {
expect(normalizeFormat(`"html":"formatter/report.html"`, assetDir)).toBe(
`"html":"/project/assets/formatter/report.html"`,
);
});

it('should normalize format with only one pair of quote', () => {
expect(normalizeFormat(`"html:formatter/report.html"`, assetDir)).toBe(
`"html":"/project/assets/formatter/report.html"`,
);
});

it('should normalize format with no quotes', () => {
expect(normalizeFormat(`html:formatter/report.html`, assetDir)).toBe(
`"html":"/project/assets/formatter/report.html"`,
);
});

it('should normalize format with file path type', () => {
expect(
normalizeFormat(
`"file://formatter/implementation":"report.json"`,
assetDir,
),
).toBe(`"file://formatter/implementation":"/project/assets/report.json"`);
});

it('should throw an error for an invalid file path type', () => {
expect(() => {
normalizeFormat(`file://formatter/implementation:report.json`, assetDir);
}).toThrow('Ambiguous colon usage detected');
});

it('should return simple strings as-is', () => {
expect(normalizeFormat(`"usage"`, assetDir)).toBe('"usage"');
expect(normalizeFormat(`usage`, assetDir)).toBe('usage');
});
});

0 comments on commit c9aaddc

Please sign in to comment.