@testomatio/reporter
Version:
Testomatio Reporter Client
355 lines (354 loc) β’ 16 kB
JavaScript
;
var __importDefault = (this && this.__importDefault) || function (mod) {
return (mod && mod.__esModule) ? mod : { "default": mod };
};
Object.defineProperty(exports, "__esModule", { value: true });
const commander_1 = require("commander");
const cross_spawn_1 = require("cross-spawn");
const glob_1 = require("glob");
const debug_1 = __importDefault(require("debug"));
const client_js_1 = __importDefault(require("../client.js"));
const xmlReader_js_1 = __importDefault(require("../xmlReader.js"));
const constants_js_1 = require("../constants.js");
const utils_js_1 = require("../utils/utils.js");
const config_js_1 = require("../config.js");
const utils_js_2 = require("../utils/utils.js");
const picocolors_1 = __importDefault(require("picocolors"));
const filesize_1 = require("filesize");
const dotenv_1 = __importDefault(require("dotenv"));
const replay_js_1 = __importDefault(require("../replay.js"));
const debug = (0, debug_1.default)('@testomatio/reporter:cli');
const version = (0, utils_js_1.getPackageVersion)();
console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` π€© Testomat.io Reporter v${version}`)));
const program = new commander_1.Command();
program
.version(version)
.option('--env-file <envfile>', 'Load environment variables from env file')
.hook('preAction', thisCommand => {
const opts = thisCommand.opts();
if (opts.envFile) {
dotenv_1.default.config({ path: opts.envFile });
}
else {
dotenv_1.default.config();
}
});
program
.command('start')
.description('Start a new run and return its ID')
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
.action(async (opts) => {
(0, utils_js_1.cleanLatestRunId)();
console.log('Starting a new Run on Testomat.io...');
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
const client = new client_js_1.default({ apiKey });
const createRunParams = {};
if (opts.kind) {
createRunParams.kind = opts.kind;
}
client.createRun(createRunParams).then(() => {
console.log(process.env.runId);
process.exit(0);
});
});
program
.command('finish')
.description('Finish Run by its ID')
.action(async () => {
process.env.TESTOMATIO_RUN ||= (0, utils_js_2.readLatestRunId)();
if (!process.env.TESTOMATIO_RUN) {
console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
return process.exit(1);
}
console.log('Finishing Run on Testomat.io...');
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
const client = new client_js_1.default({ apiKey });
// @ts-ignore
client.updateRunStatus(constants_js_1.STATUS.FINISHED).then(() => {
process.exit(0);
});
});
program
.command('run')
.alias('test')
.description('Run tests with the specified command')
.argument('[command]', 'Test runner command')
.option('--filter <filter>', 'Additional execution filter')
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
.action(async (command, opts) => {
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
const title = process.env.TESTOMATIO_TITLE;
const client = new client_js_1.default({ apiKey, title });
if (opts.filter || opts.filterList) {
console.log(constants_js_1.APP_PREFIX, 'Filtering tests...');
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
// Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
const pipeOptions = optsArray.join(':');
const prepareRunParams = { pipe, pipeOptions };
if (opts.filterList) {
client.pipeStore.filterList = true;
}
try {
const tests = await client.prepareRun(prepareRunParams);
if (!tests || tests.length === 0) {
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('No tests found.'));
return;
}
const pattern = `(${tests.join('|')})`;
const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
debug(`Execution pattern: "${pattern}"`);
if (opts.filterList) {
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
if (command)
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
return;
}
if (command && command.split) {
command = filteredCommand;
}
}
catch (err) {
console.log(constants_js_1.APP_PREFIX, err.message || err);
return;
}
}
// just create a run (wich tests which match filters) without executing tests
if (!command || !command.split) {
const createRunParams = {};
if (title) {
createRunParams.title = title;
}
if (opts.kind) {
createRunParams.kind = opts.kind;
}
if (apiKey) {
await client.createRun(createRunParams);
const runId = process.env.TESTOMATIO_RUN || process.env.runId;
if (client.pipeStore.runUrl)
console.log(constants_js_1.APP_PREFIX, `π Report URL: ${picocolors_1.default.magenta(client.pipeStore.runUrl)}`);
if (opts.kind !== 'manual') {
console.log(constants_js_1.APP_PREFIX, `No command passed, so you need to run tests yourself:`);
console.log(constants_js_1.APP_PREFIX, `TESTOMATIO_RUN=${runId} <command>`);
}
}
else {
console.log(constants_js_1.APP_PREFIX, 'β οΈ No API key provided. Cannot create run without TESTOMATIO key.');
process.exit(1);
}
return process.exit(0);
}
console.log(constants_js_1.APP_PREFIX, `π Running`, picocolors_1.default.green(command));
const runTests = async () => {
const testCmds = command.split(' ');
const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), {
stdio: 'inherit',
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
});
cmd.on('close', async (code) => {
const emoji = code === 0 ? 'π’' : 'π΄';
console.log(constants_js_1.APP_PREFIX, emoji, `Runner exited with ${picocolors_1.default.bold(code)}`);
if (apiKey) {
const status = code === 0 ? 'passed' : 'failed';
await client.updateRunStatus(status);
}
process.exit(code);
});
};
const createRunParams = {};
if (title) {
createRunParams.title = title;
}
if (opts.kind) {
createRunParams.kind = opts.kind;
}
if (apiKey) {
await client.createRun(createRunParams).then(runTests);
}
else {
await runTests();
}
});
// program
// .command('xml')
// .description('Parse XML reports and upload to Testomat.io')
// .argument('<pattern>', 'XML file pattern')
// .option('-d, --dir <dir>', 'Project directory')
// .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
// .option('--lang <lang>', 'Language used (python, ruby, java)')
// .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
// .action(async (pattern, opts) => {
// if (!pattern.endsWith('.xml')) {
// pattern += '.xml';
// }
// let { javaTests, lang } = opts;
// if (javaTests === true) javaTests = 'src/test/java';
// lang = lang?.toLowerCase();
// const runReader = new XmlReader({ javaTests, lang });
// const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
// if (!files.length) {
// console.log(APP_PREFIX, `Report can't be created. No XML files found π₯`);
// process.exit(1);
// }
program
.command('xml')
.description('Parse XML reports and upload to Testomat.io')
.argument('<pattern>', 'XML file pattern')
.option('-d, --dir <dir>', 'Project directory')
.option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
.option('--lang <lang>', 'Language used (python, ruby, java)')
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
.action(async (pattern, opts) => {
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
pattern += '.xml';
}
let { javaTests, lang } = opts;
if (javaTests === true)
javaTests = 'src/test/java';
lang = lang?.toLowerCase();
const runReader = new xmlReader_js_1.default({ javaTests, lang });
const files = glob_1.glob.sync(pattern, { cwd: opts.dir || process.cwd() });
if (!files.length) {
console.log(constants_js_1.APP_PREFIX, `Report can't be created. No XML files found π₯`);
process.exit(1);
}
for (const file of files) {
console.log(constants_js_1.APP_PREFIX, `Parsed ${file}`);
runReader.parse(file);
}
let timeoutTimer;
if (opts.timelimit) {
timeoutTimer = setTimeout(() => {
console.log(`β οΈ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
process.exit(0);
}, parseInt(opts.timelimit, 10) * 1000);
}
try {
await runReader.createRun();
await runReader.uploadData();
}
catch (err) {
console.log(constants_js_1.APP_PREFIX, 'Error updating status, skipping...', err);
}
if (timeoutTimer)
clearTimeout(timeoutTimer);
});
program
.command('upload-artifacts')
.description('Upload artifacts to Testomat.io')
.option('--force', 'Re-upload artifacts even if they were uploaded before')
.action(async (opts) => {
const apiKey = config_js_1.config.TESTOMATIO;
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
const runId = process.env.TESTOMATIO_RUN || process.env.runId || (0, utils_js_2.readLatestRunId)();
if (!runId) {
console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
return process.exit(1);
}
const client = new client_js_1.default({
apiKey,
runId,
isBatchEnabled: false,
});
let testruns = client.uploader.readUploadedFiles(runId);
const numTotalArtifacts = testruns.length;
debug('Found testruns:', testruns);
if (!opts.force)
testruns = testruns.filter(tr => !tr.uploaded);
if (!testruns.length) {
console.log(constants_js_1.APP_PREFIX, 'ποΈ Total artifacts:', numTotalArtifacts);
if (numTotalArtifacts) {
console.log(constants_js_1.APP_PREFIX, 'No new artifacts to upload');
console.log(constants_js_1.APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
}
process.exit(0);
}
const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
if (!acc[rid]) {
acc[rid] = [];
}
if (!acc[rid].includes(file))
acc[rid].push(file);
return acc;
}, {});
await client.createRun();
client.uploader.checkEnabled();
client.uploader.disableLogStorage();
for (const rid in testrunsByRid) {
const files = testrunsByRid[rid];
await client.addTestRun(undefined, { rid, files });
}
console.log(constants_js_1.APP_PREFIX, 'ποΈ', client.uploader.successfulUploads.length, 'artifacts π’uploaded');
if (client.uploader.successfulUploads.length) {
debug('\n', constants_js_1.APP_PREFIX, `ποΈ ${client.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
const uploadedArtifacts = client.uploader.successfulUploads.map(file => ({
relativePath: file.path.replace(process.cwd(), ''),
link: file.link,
sizePretty: (0, filesize_1.filesize)(file.size, { round: 0 }).toString(),
}));
uploadedArtifacts.forEach(upload => {
debug(`π’Uploaded artifact`, `${upload.relativePath},`, 'size:', `${upload.sizePretty},`, 'link:', `${upload.link}`);
});
}
const filesizeStrMaxLength = 7;
if (client.uploader.failedUploads.length) {
console.log('\n', constants_js_1.APP_PREFIX, 'ποΈ', client.uploader.failedUploads.length, `artifacts π΄${picocolors_1.default.bold('failed')} to upload`);
const failedUploads = client.uploader.failedUploads.map(({ path, size }) => ({
relativePath: path.replace(process.cwd(), ''),
sizePretty: (0, filesize_1.filesize)(size, { round: 0 }).toString(),
}));
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
failedUploads.forEach(upload => {
console.log(` ${picocolors_1.default.gray('|')} π΄ ${upload.relativePath.padEnd(pathPadding)} ${picocolors_1.default.gray(`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`)}`);
});
}
});
program
.command('replay')
.description('Replay test data from debug file and re-send to Testomat.io')
.argument('[debug-file]', 'Path to debug file (defaults to /tmp/testomatio.debug.latest.json)')
.option('--dry-run', 'Preview the data without sending to Testomat.io')
.action(async (debugFile, opts) => {
try {
const replayService = new replay_js_1.default({
apiKey: config_js_1.config.TESTOMATIO,
dryRun: opts.dryRun,
onLog: message => console.log(constants_js_1.APP_PREFIX, message),
onError: message => console.error(constants_js_1.APP_PREFIX, 'β οΈ ', message),
onProgress: ({ current, total }) => {
if (current % 10 === 0 || current === total) {
console.log(constants_js_1.APP_PREFIX, `π Progress: ${current}/${total} tests processed`);
}
},
});
const result = await replayService.replay(debugFile);
if (result.dryRun) {
console.log(constants_js_1.APP_PREFIX, 'π Dry run completed:');
console.log(constants_js_1.APP_PREFIX, ` - Tests found: ${result.testsCount}`);
console.log(constants_js_1.APP_PREFIX, ` - Environment variables: ${Object.keys(result.envVars).length}`);
console.log(constants_js_1.APP_PREFIX, ` - Run parameters:`, result.runParams);
console.log(constants_js_1.APP_PREFIX, ' Use without --dry-run to actually send the data');
}
else {
console.log(constants_js_1.APP_PREFIX, `β
Successfully replayed ${result.successCount}/${result.testsCount} tests`);
if (result.failureCount > 0) {
console.log(constants_js_1.APP_PREFIX, `β οΈ ${result.failureCount} tests failed to upload`);
}
}
process.exit(0);
}
catch (err) {
console.error(constants_js_1.APP_PREFIX, 'β Error replaying debug data:', err.message);
if (err.message.includes('Debug file not found')) {
console.error(constants_js_1.APP_PREFIX, 'π‘ Hint: Run tests with TESTOMATIO_DEBUG=1 to generate debug files');
}
process.exit(1);
}
});
program.parse(process.argv);
if (!process.argv.slice(2).length) {
program.outputHelp();
}