@testomatio/reporter
Version:
Testomatio Reporter Client
764 lines (736 loc) • 29.8 kB
JavaScript
;
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
var desc = Object.getOwnPropertyDescriptor(m, k);
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
desc = { enumerable: true, get: function() { return m[k]; } };
}
Object.defineProperty(o, k2, desc);
}) : (function(o, m, k, k2) {
if (k2 === undefined) k2 = k;
o[k2] = m[k];
}));
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
Object.defineProperty(o, "default", { enumerable: true, value: v });
}) : function(o, v) {
o["default"] = v;
});
var __importStar = (this && this.__importStar) || (function () {
var ownKeys = function(o) {
ownKeys = Object.getOwnPropertyNames || function (o) {
var ar = [];
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
return ar;
};
return ownKeys(o);
};
return function (mod) {
if (mod && mod.__esModule) return mod;
var result = {};
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
__setModuleDefault(result, mod);
return result;
};
})();
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
exports.validateSuiteId = exports.testRunnerHelper = exports.specificTestInfo = exports.parseSuite = exports.isValidUrl = exports.humanize = exports.getTestomatIdFromTestTitle = exports.getGitCommitSha = exports.getCurrentDateTime = exports.foundedTestLog = exports.fileSystem = exports.fetchFilesFromStackTrace = exports.fetchIdFromOutput = exports.fetchIdFromCode = exports.fetchSourceCodeFromStackTrace = exports.fetchSourceCode = exports.isSameTest = exports.ansiRegExp = exports.SUITE_ID_REGEX = exports.TEST_ID_REGEX = void 0;
exports.getPackageVersion = getPackageVersion;
exports.truncate = truncate;
exports.cleanLatestRunId = cleanLatestRunId;
exports.formatStep = formatStep;
exports.readLatestRunId = readLatestRunId;
exports.removeColorCodes = removeColorCodes;
exports.storeRunId = storeRunId;
exports.transformEnvVarToBoolean = transformEnvVarToBoolean;
exports.applyFilter = applyFilter;
const url_1 = require("url");
const path_1 = __importStar(require("path"));
const picocolors_1 = __importDefault(require("picocolors"));
const fs_1 = __importDefault(require("fs"));
const is_valid_path_1 = __importDefault(require("is-valid-path"));
const debug_1 = __importDefault(require("debug"));
const os_1 = __importDefault(require("os"));
const url_2 = require("url");
const child_process_1 = require("child_process");
const debug = (0, debug_1.default)('@testomatio/reporter:util');
// Use __dirname directly since we're compiling to CommonJS
// prettier-ignore
// @ts-ignore
// eslint-disable-next-line max-len
/**
* @param {String} testTitle - Test title
*
* @returns {String|null} testId
*/
const getTestomatIdFromTestTitle = testTitle => {
if (!testTitle)
return null;
const captures = testTitle.match(/@T[\w\d]{8}/);
if (captures) {
return captures[0];
}
return null;
};
exports.getTestomatIdFromTestTitle = getTestomatIdFromTestTitle;
/**
* @param {String} suiteTitle - suite title
*
* @returns {String|null} suiteId
*/
const parseSuite = suiteTitle => {
const captures = suiteTitle.match(/@S[\w\d]{8}/);
if (captures) {
return captures[0];
}
return null;
};
exports.parseSuite = parseSuite;
/**
* Validates TESTOMATIO_SUITE environment variable format
* @param {String} suiteId - suite ID to validate
* @returns {String|null} validated suite ID or null if invalid
*/
const validateSuiteId = suiteId => {
if (!suiteId)
return null;
const match = suiteId.match(exports.SUITE_ID_REGEX);
return match ? match[0] : null;
};
exports.validateSuiteId = validateSuiteId;
/**
* Gets current git commit SHA
* @returns {String|null} git commit SHA or null if not available
*/
const getGitCommitSha = () => {
try {
const sha = (0, child_process_1.execSync)('git rev-parse --short HEAD', {
stdio: ['ignore', 'pipe', 'ignore']
}).toString().trim();
return sha || null;
}
catch (error) {
return null;
}
};
exports.getGitCommitSha = getGitCommitSha;
const ansiRegExp = () => {
const pattern = [
'[\\u001B\\u009B][[\\]()#;?]*(?:(?:(?:[a-zA-Z\\d]*(?:;[-a-zA-Z\\d\\/#&.:=?%@~_]*)*)?\\u0007)',
'(?:(?:\\d{1,4}(?:;\\d{0,4})*)?[\\dA-PR-TZcf-ntqry=><~]))',
].join('|');
return new RegExp(pattern, 'g');
};
exports.ansiRegExp = ansiRegExp;
const isValidUrl = s => {
try {
new url_1.URL(s);
return true;
}
catch (err) {
return false;
}
};
exports.isValidUrl = isValidUrl;
const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
let files = Array.from(stack.matchAll(fileMatchRegex))
.map(match => {
// match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
const slashes = match[1] || '';
const path = match[2];
const extension = match[3];
return `${slashes}${path}.${extension}`;
})
.map(f => f.trim())
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
.map(f => {
// Normalize path separators for cross-platform compatibility
return f.replace(/\\/g, '/');
});
// If we're not checking file existence, remove Windows drive letters for consistency
if (!checkExists) {
files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
}
debug('Found files in stack trace: ', files);
return files.filter(f => {
if (!checkExists)
return true;
const isFile = fs_1.default.existsSync(f);
if (!isFile)
debug('File %s could not be found and uploaded as artifact', f);
return isFile;
});
};
exports.fetchFilesFromStackTrace = fetchFilesFromStackTrace;
const fetchSourceCodeFromStackTrace = (stack = '') => {
const stackLines = stack
.split('\n')
.filter(l => l.includes(':'))
.map(l => l.trim())
.map(l => {
// Remove 'at ' prefix if present
if (l.startsWith('at ')) {
return l.substring(3).trim();
}
// Find the part that looks like a file path with line number
const parts = l.split(' ');
for (const part of parts) {
// Check if this part has a colon
if (part.includes(':')) {
// For Windows paths, we need to handle drive letters (C:, D:, etc.)
// Split by colon but keep drive letter with the path
const colonParts = part.split(':');
let filePath;
// Check if first part is a Windows drive letter (single letter)
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
// Windows path like D:\path\file.php:24
// Reconstruct as D:\path\file.php
filePath = colonParts[0] + ':' + colonParts[1];
}
else {
// Unix path like /path/file.php:24
filePath = colonParts[0];
}
// Only consider it valid if the file exists
if (fs_1.default.existsSync(filePath)) {
return part;
}
}
}
// If no valid file path found in parts, return the whole line
// It will be filtered out later if it's not a valid file path
return parts.find(p => p.includes(':')) || l;
})
.filter(l => {
// Extract file path from line (accounting for Windows drive letters)
if (!l)
return false;
const colonParts = l.split(':');
let filePath;
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
// Windows path
filePath = colonParts[0] + ':' + colonParts[1];
}
else {
// Unix path
filePath = colonParts[0];
}
return filePath && fs_1.default.existsSync(filePath);
})
// // filter out 3rd party libs
.filter(l => !l?.includes(`vendor${path_1.sep}`))
.filter(l => !l?.includes(`node_modules${path_1.sep}`))
.filter(l => {
// Extract file path for final check (accounting for Windows drive letters)
const colonParts = l.split(':');
let filePath;
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
filePath = colonParts[0] + ':' + colonParts[1];
}
else {
filePath = colonParts[0];
}
return fs_1.default.lstatSync(filePath).isFile();
});
if (!stackLines.length)
return '';
// Extract file and line number (accounting for Windows drive letters)
const firstLine = stackLines[0];
const colonParts = firstLine.split(':');
let file, line;
if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
// Windows path like D:\path\file.php:24
file = colonParts[0] + ':' + colonParts[1];
line = colonParts[2];
}
else {
// Unix path like /path/file.php:24
file = colonParts[0];
line = colonParts[1];
}
const prepend = 3;
const source = fetchSourceCode(fs_1.default.readFileSync(file).toString(), { line, prepend, limit: 7 });
if (!source)
return '';
return source
.split('\n')
.map((l, i) => {
if (i === prepend)
return `${line} > ${picocolors_1.default.bold(l)}`;
return `${+line - prepend + i} | ${l}`;
})
.join('\n');
};
exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
const fetchIdFromCode = (code, opts = {}) => {
if (!code)
return null;
const comments = code
.split('\n')
.map(l => l.trim())
.filter(l => {
switch (opts.lang) {
case 'ruby':
case 'python':
return l.startsWith('# ');
default:
return l.startsWith('// ');
}
});
return comments.find(c => c.match(exports.TEST_ID_REGEX))?.match(exports.TEST_ID_REGEX)?.[1];
};
exports.fetchIdFromCode = fetchIdFromCode;
const fetchIdFromOutput = output => {
const TID_FULL_PATTERN = new RegExp(`tid:\\/\\/.*?(${exports.TEST_ID_REGEX.source})`);
return output.match(TID_FULL_PATTERN)?.[2];
};
exports.fetchIdFromOutput = fetchIdFromOutput;
const fetchSourceCode = (contents, opts = {}) => {
if (!opts.title && !opts.line)
return '';
// code fragment is 20 lines
const limit = opts.limit || 50;
let lineIndex;
if (opts.line)
lineIndex = opts.line - 1;
const lines = contents.split('\n');
// remove special chars from title
if (!lineIndex && opts.title) {
const title = opts.title.replace(/[([@].*/g, '');
if (opts.lang === 'java') {
lineIndex = lines.findIndex(l => l.includes(`test${title}`));
if (lineIndex === -1)
lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
if (lineIndex === -1)
lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
if (lineIndex === -1)
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
}
else if (opts.lang === 'csharp') {
// Find the method declaration line
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
if (methodLineIndex === -1) {
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
}
if (methodLineIndex === -1) {
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
}
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
if (methodLineIndex !== -1) {
lineIndex = methodLineIndex;
// Scan upwards to find the start of attributes and comments
for (let i = methodLineIndex - 1; i >= 0; i--) {
const trimmedLine = lines[i].trim();
// Include [TestCase], [Test], and other attributes
if (trimmedLine.startsWith('[')) {
lineIndex = i;
continue;
}
// Include XML documentation comments
if (trimmedLine.startsWith('///')) {
lineIndex = i;
continue;
}
// Stop at empty lines (with some tolerance)
if (trimmedLine === '') {
// Check if next non-empty line is an attribute or comment
let hasMoreAttributes = false;
for (let j = i - 1; j >= 0; j--) {
const nextTrimmed = lines[j].trim();
if (nextTrimmed === '')
continue;
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
hasMoreAttributes = true;
lineIndex = j;
}
break;
}
if (!hasMoreAttributes)
break;
continue;
}
// Stop at other method declarations or class-level elements
if (trimmedLine.includes('public ') ||
trimmedLine.includes('private ') ||
trimmedLine.includes('protected ') ||
trimmedLine.includes('internal ')) {
if (!trimmedLine.startsWith('['))
break;
}
}
}
}
else {
lineIndex = lines.findIndex(l => l.includes(title));
}
}
if (opts.prepend) {
lineIndex -= opts.prepend;
}
if (lineIndex !== -1 && lineIndex !== undefined) {
const result = [];
let braceDepth = 0; // Track brace depth for C# methods
let methodStartFound = false; // Flag to indicate we've found the method opening brace
for (let i = lineIndex; i < lineIndex + limit; i++) {
if (lines[i] === undefined)
continue;
// Track brace depth for C# to stop after method closes
if (opts.lang === 'csharp') {
const line = lines[i];
// Count opening and closing braces
const openBraces = (line.match(/\{/g) || []).length;
const closeBraces = (line.match(/\}/g) || []).length;
if (openBraces > 0)
methodStartFound = true;
braceDepth += openBraces - closeBraces;
// If we've started the method and depth returns to 0, method is complete
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
// Don't include the closing brace - just break
break;
}
}
if (i > lineIndex + 2 && !opts.prepend) {
// annotation
if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
break;
if (opts.lang === 'php' && lines[i].includes(' private function '))
break;
if (opts.lang === 'php' && lines[i].includes(' protected function '))
break;
if (opts.lang === 'php' && lines[i].includes(' public function '))
break;
if (opts.lang === 'python' && lines[i].trim().match(/^@\w+/))
break;
if (opts.lang === 'python' && lines[i].includes(' def '))
break;
if (opts.lang === 'ruby' && lines[i].includes(' def '))
break;
if (opts.lang === 'ruby' && lines[i].includes(' test '))
break;
if (opts.lang === 'ruby' && lines[i].includes(' it '))
break;
if (opts.lang === 'ruby' && lines[i].includes(' specify '))
break;
if (opts.lang === 'ruby' && lines[i].includes(' context '))
break;
if (opts.lang === 'ts' && lines[i].includes(' it('))
break;
if (opts.lang === 'ts' && lines[i].includes(' test('))
break;
if (opts.lang === 'js' && lines[i].includes(' it('))
break;
if (opts.lang === 'js' && lines[i].includes(' test('))
break;
if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/))
break;
if (opts.lang === 'java' && lines[i].includes(' public void '))
break;
if (opts.lang === 'java' && lines[i].includes(' class '))
break;
// For C#, additional checks if brace tracking didn't stop us
if (opts.lang === 'csharp') {
const trimmed = lines[i].trim();
// Stop at attribute that marks beginning of next test (but not if we're still in the current method)
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0)
break;
// Stop at XML documentation comments that belong to next method
if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0)
break;
// Stop at another method declaration (but not if we're still in the current method)
if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
methodStartFound &&
braceDepth === 0)
break;
// Stop at class declaration
if (trimmed.includes(' class ') && trimmed.includes('public'))
break;
}
}
result.push(lines[i]);
}
return result.join('\n');
}
};
exports.fetchSourceCode = fetchSourceCode;
const isSameTest = (test, t) => typeof t === 'object' &&
typeof test === 'object' &&
t.title === test.title &&
t.suite_title === test.suite_title &&
Object.values(t.example || {}) === Object.values(test.example || {}) &&
t.test_id === test.test_id;
exports.isSameTest = isSameTest;
const getCurrentDateTime = () => {
const today = new Date();
return `${today.getFullYear()}_${today.getMonth() + 1}_${today.getDate()}_${today.getHours()}_${today.getMinutes()}_${today.getSeconds()}`;
};
exports.getCurrentDateTime = getCurrentDateTime;
/**
* @param {Object} test - Test adapter object
*
* @returns {String|null} testInfo as one string
*/
const specificTestInfo = test => {
// TODO: afterEach has another context.... need to add specific handler, maybe...
if (test?.title && test?.file) {
return `${(0, path_1.basename)(test.file).split('.').join('#')}#${test.title.split(' ').join('#')}`;
}
return null;
};
exports.specificTestInfo = specificTestInfo;
const fileSystem = {
createDir(dirPath) {
if (!fs_1.default.existsSync(dirPath)) {
fs_1.default.mkdirSync(dirPath, { recursive: true });
debug('Created dir: ', dirPath);
}
},
clearDir(dirPath) {
if (fs_1.default.existsSync(dirPath)) {
fs_1.default.rmSync(dirPath, { recursive: true });
debug(`Dir ${dirPath} was deleted`);
}
else {
debug(`Trying to delete ${dirPath} but it doesn't exist`);
}
},
};
exports.fileSystem = fileSystem;
const foundedTestLog = (app, tests) => {
const n = tests.length;
return console.log(app, `✅ We found ${n === 1 ? 'one test' : `${n} tests`} in Testomat.io!`);
};
exports.foundedTestLog = foundedTestLog;
const humanize = text => {
// if there are no spaces, decamelize
if (!text.trim().includes(' '))
text = decamelize(text);
return text
.replace(/_./g, match => ` ${match.charAt(1).toUpperCase()}`)
.trim()
.replace(/^(.)|\s(.)/g, $1 => $1.toUpperCase())
.trim()
.replace(/\sA\s/g, ' a ') // replace a|the
.replace(/\sThe\s/g, ' the ') // replace a|the
.replace(/^Test\s/, '')
.replace(/^Should\s/, '');
};
exports.humanize = humanize;
/**
* From https://github.com/sindresorhus/decamelize/blob/main/index.js
* @param {*} text
* @returns
*/
const decamelize = text => {
const separator = '_';
const replacement = `$1${separator}$2`;
// Split lowercase sequences followed by uppercase character.
// `dataForUSACounties` → `data_For_USACounties`
// `myURLstring → `my_URLstring`
let decamelized = text.replace(/([\p{Lowercase_Letter}\d])(\p{Uppercase_Letter})/gu, replacement);
// Lowercase all single uppercase characters. As we
// want to preserve uppercase sequences, we cannot
// simply lowercase the separated string at the end.
// `data_For_USACounties` → `data_for_USACounties`
decamelized = decamelized.replace(/((?<![\p{Uppercase_Letter}\d])[\p{Uppercase_Letter}\d](?![\p{Uppercase_Letter}\d]))/gu, $0 => $0.toLowerCase());
// Remaining uppercase sequences will be separated from lowercase sequences.
// `data_For_USACounties` → `data_for_USA_counties`
return decamelized.replace(/(\p{Uppercase_Letter}+)(\p{Uppercase_Letter}\p{Lowercase_Letter}+)/gu, (_, $1, $2) => $1 + separator + $2.toLowerCase());
};
/**
* Used to remove color codes
* @param {*} input
* @returns
*/
function removeColorCodes(input) {
return input.replace(/\x1b\[[0-9;]*m/g, '');
}
const testRunnerHelper = {
// for Jest
getNameOfCurrentlyRunningTest: () => {
if (global.testomatioTestTitle)
return global.testomatioTestTitle;
if (!process.env.JEST_WORKER_ID)
return null;
try {
// TODO: expect?.getState()?.testPath + ' ' + expect?.getState()?.currentTestName
// @ts-expect-error "expect" could only be defined inside Jest environement (forbidden to import it outside)
return expect?.getState()?.currentTestName;
}
catch (e) {
return null;
}
},
};
exports.testRunnerHelper = testRunnerHelper;
function storeRunId(runId) {
if (!runId || runId === 'undefined')
return;
const filePath = path_1.default.join(os_1.default.tmpdir(), `testomatio.latest.run`);
try {
fs_1.default.writeFileSync(filePath, runId);
}
catch (e) {
if (e.code === 'ENOENT')
return null;
debug('Could not store latest run ID file: ', e.message);
}
}
/**
*
* @returns {String|null} latest run ID
*/
function readLatestRunId() {
try {
const filePath = path_1.default.join(os_1.default.tmpdir(), `testomatio.latest.run`);
if (!fs_1.default.existsSync(filePath))
return null;
const stats = fs_1.default.statSync(filePath);
const diff = +new Date() - +stats.mtime;
const diffHours = diff / 1000 / 60 / 60;
if (diffHours > 1)
return null;
return fs_1.default.readFileSync(filePath)?.toString()?.trim() ?? null;
}
catch (e) {
return null;
}
}
function cleanLatestRunId() {
try {
const filePath = path_1.default.join(os_1.default.tmpdir(), `testomatio.latest.run`);
const runId = readLatestRunId();
if (fs_1.default.existsSync(filePath)) {
fs_1.default.unlinkSync(filePath);
}
debug(`Cleaned latest run ID (${runId}) file`, filePath);
}
catch (e) {
if (e.code === 'ENOENT')
return null;
console.warn('Could not clean latest run ID file: ', e);
}
}
function formatStep(step, shift = 0) {
const prefix = ' '.repeat(shift);
const lines = [];
if (step.error) {
lines.push(`${prefix}${picocolors_1.default.red(step.title)} ${picocolors_1.default.gray(`${step.duration}ms`)}`);
}
else {
lines.push(`${prefix}${step.title} ${picocolors_1.default.gray(`${step.duration}ms`)}`);
}
for (const child of step.steps || []) {
lines.push(...formatStep(child, shift + 2));
}
return lines;
}
function getPackageVersion() {
const packageJsonPath = path_1.default.resolve(__dirname, '../../package.json');
const packageJson = JSON.parse(fs_1.default.readFileSync(packageJsonPath, 'utf8'));
return packageJson.version;
}
function transformEnvVarToBoolean(value) {
if (value === undefined || value === null || value === 'undefined')
return false;
if (typeof value === 'boolean')
return value;
if (typeof value !== 'string')
value = String(value);
value = value.trim();
if (['1', 'true', 'yes', 'on'].includes(value.toLowerCase()))
return true;
if (['0', 'false', 'no', 'off'].includes(value.toLowerCase()))
return false;
// if not recognized, return truthy if any value is set
return Boolean(value);
}
function truncate(s, size = 255) {
if (s === undefined || s === null) {
return '';
}
const str = s.toString();
if (str.trim().length < size) {
return str;
}
return `${str.substring(0, size)}...`;
}
function applyFilter(command, tests) {
if (!tests || !tests.length)
return command;
const lower = (command || '').toLowerCase();
const regexPattern = `(${tests.join('|')})`;
if (lower.includes('jest')) {
return `${command} --testNamePattern ${regexPattern}`;
}
if (lower.includes('cypress')) {
const grepValue = tests.join(',');
const baseEnv = {
grep: grepValue,
grepFilterSpecs: true,
grepOmitFiltered: true,
};
if (command.includes('--env')) {
return command.replace(/--env\s+(['"]?)([^\s'"]+)\1/, (match, quote, envVal) => {
const existingEnv = {};
if (envVal.startsWith('{') && envVal.endsWith('}')) {
try {
Object.assign(existingEnv, JSON.parse(envVal));
}
catch (e) {
}
}
if (!Object.keys(existingEnv).length) {
envVal.split(',').forEach((pair) => {
const [k, v] = pair.split('=');
if (!k)
return;
if (v === 'true')
existingEnv[k] = true;
else if (v === 'false')
existingEnv[k] = false;
else
existingEnv[k] = v;
});
}
const merged = { ...existingEnv, ...baseEnv };
const json = JSON.stringify(merged);
return `--env ${json}`;
});
}
const json = JSON.stringify(baseEnv);
return `${command} --env ${json}`;
}
return `${command} --grep ${regexPattern}`;
}
module.exports.getPackageVersion = getPackageVersion;
module.exports.truncate = truncate;
module.exports.cleanLatestRunId = cleanLatestRunId;
module.exports.formatStep = formatStep;
module.exports.readLatestRunId = readLatestRunId;
module.exports.removeColorCodes = removeColorCodes;
module.exports.storeRunId = storeRunId;
module.exports.transformEnvVarToBoolean = transformEnvVarToBoolean;
module.exports.applyFilter = applyFilter;
module.exports.getTestomatIdFromTestTitle = getTestomatIdFromTestTitle;
module.exports.parseSuite = parseSuite;
module.exports.validateSuiteId = validateSuiteId;
module.exports.getGitCommitSha = getGitCommitSha;
module.exports.ansiRegExp = ansiRegExp;
module.exports.isValidUrl = isValidUrl;
module.exports.fetchFilesFromStackTrace = fetchFilesFromStackTrace;
module.exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
module.exports.fetchIdFromCode = fetchIdFromCode;
module.exports.fetchIdFromOutput = fetchIdFromOutput;
module.exports.fetchSourceCode = fetchSourceCode;
module.exports.isSameTest = isSameTest;
module.exports.getCurrentDateTime = getCurrentDateTime;
module.exports.specificTestInfo = specificTestInfo;
module.exports.fileSystem = fileSystem;
module.exports.foundedTestLog = foundedTestLog;
module.exports.humanize = humanize;
module.exports.testRunnerHelper = testRunnerHelper;