testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- package/README.md +3 -3
- package/bin/cli.js +6 -26
- package/package.json +39 -4
- package/src/adapter/codecept.js +626 -0
- package/src/adapter/cucumber/current.js +230 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +108 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +211 -0
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +343 -0
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +455 -0
- package/src/adapter/webdriver.js +201 -0
- package/src/bin/cli.js +507 -0
- package/src/bin/reportXml.js +79 -0
- package/src/bin/startTest.js +54 -0
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +524 -0
- package/src/config.js +30 -0
- package/src/constants.js +72 -0
- package/src/data-storage.js +204 -0
- package/src/helpers.js +1 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +70 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +285 -0
- package/src/pipe/coverage.js +500 -0
- package/src/pipe/csv.js +161 -0
- package/src/pipe/debug.js +143 -0
- package/src/pipe/github.js +256 -0
- package/src/pipe/gitlab.js +258 -0
- package/src/pipe/html.js +1153 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/markdown.js +753 -0
- package/src/pipe/testomatio.js +707 -0
- package/src/replay.js +274 -0
- package/src/reporter-functions.js +155 -0
- package/src/reporter.js +42 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +15 -0
- package/src/services/key-values.js +59 -0
- package/src/services/links.js +69 -0
- package/src/services/logger.js +320 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3726 -0
- package/src/uploader.js +382 -0
- package/src/utils/constants.js +12 -0
- package/src/utils/debug.js +20 -0
- package/src/utils/log-formatter.js +118 -0
- package/src/utils/log.js +88 -0
- package/src/utils/pipe_utils.js +193 -0
- package/src/utils/utils.js +732 -0
- package/src/xmlReader.js +834 -0
- package/types/types.d.ts +425 -0
- package/types/vitest.types.d.ts +93 -0
package/src/bin/cli.js
ADDED
|
@@ -0,0 +1,507 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import { spawn } from 'cross-spawn';
|
|
5
|
+
import { glob } from 'glob';
|
|
6
|
+
import createDebugMessages from 'debug';
|
|
7
|
+
import TestomatClient from '../client.js';
|
|
8
|
+
import XmlReader from '../xmlReader.js';
|
|
9
|
+
import { APP_PREFIX, STATUS, DEBUG_FILE } from '../constants.js';
|
|
10
|
+
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
11
|
+
import { config } from '../config.js';
|
|
12
|
+
import { readLatestRunId } from '../utils/utils.js';
|
|
13
|
+
import pc from 'picocolors';
|
|
14
|
+
import { filesize as prettyBytes } from 'filesize';
|
|
15
|
+
import dotenv from 'dotenv';
|
|
16
|
+
import Replay from '../replay.js';
|
|
17
|
+
import { log } from '../utils/log.js';
|
|
18
|
+
import { formatFilterListIds } from '../utils/pipe_utils.js';
|
|
19
|
+
|
|
20
|
+
const debug = createDebugMessages('@testomatio/reporter:cli');
|
|
21
|
+
const version = getPackageVersion();
|
|
22
|
+
const program = new Command();
|
|
23
|
+
|
|
24
|
+
program
|
|
25
|
+
.version(version)
|
|
26
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
27
|
+
.hook('preAction', (thisCommand, actionCommand) => {
|
|
28
|
+
const opts = thisCommand.opts();
|
|
29
|
+
if (opts.envFile) {
|
|
30
|
+
dotenv.config({ path: opts.envFile });
|
|
31
|
+
} else {
|
|
32
|
+
dotenv.config();
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
// --format / --filter-list produce machine-readable output on stdout, so route
|
|
36
|
+
// remaining output to stderr, skip the banner, and silence info-level logs so
|
|
37
|
+
// stdout stays clean for capture (e.g. RUN_ID=$(... start --format id)).
|
|
38
|
+
// Set TESTOMATIO_LOG_LEVEL=INFO to re-enable progress logs for debugging.
|
|
39
|
+
const subOpts = actionCommand.opts();
|
|
40
|
+
if (subOpts.filterList || subOpts.format) {
|
|
41
|
+
process.env.TESTOMATIO_LOG_STDERR = '1';
|
|
42
|
+
process.env.TESTOMATIO_LOG_LEVEL ||= 'WARN';
|
|
43
|
+
} else {
|
|
44
|
+
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
45
|
+
}
|
|
46
|
+
});
|
|
47
|
+
|
|
48
|
+
program
|
|
49
|
+
.command('start')
|
|
50
|
+
.description('Start a new run and return its ID')
|
|
51
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
52
|
+
.option('--filter <filter>', 'Scope the prepared run to tests matching the filter (no execution)')
|
|
53
|
+
.option('--format <format>', 'Machine-readable output: print only the run id to stdout (e.g. --format id)')
|
|
54
|
+
.action(async opts => {
|
|
55
|
+
cleanLatestRunId();
|
|
56
|
+
|
|
57
|
+
log.info('Starting a new Run on Testomat.io...');
|
|
58
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
59
|
+
const client = new TestomatClient({ apiKey });
|
|
60
|
+
|
|
61
|
+
const createRunParams = {};
|
|
62
|
+
if (opts.kind) createRunParams.kind = opts.kind;
|
|
63
|
+
|
|
64
|
+
if (opts.filter) {
|
|
65
|
+
const [pipe, ...optsArray] = opts.filter.split(':');
|
|
66
|
+
const tests = await client.prepareRun({ pipe, pipeOptions: optsArray.join(':') });
|
|
67
|
+
if (!tests || tests.length === 0) {
|
|
68
|
+
log.warn(pc.yellow('No tests found for the filter. Run not created.'));
|
|
69
|
+
process.exit(1);
|
|
70
|
+
}
|
|
71
|
+
createRunParams.configuration = {
|
|
72
|
+
tests: tests.filter(id => id.startsWith('T')).map(id => id.slice(1)),
|
|
73
|
+
suites: tests.filter(id => id.startsWith('S')).map(id => id.slice(1)),
|
|
74
|
+
};
|
|
75
|
+
}
|
|
76
|
+
|
|
77
|
+
await client.createRun(createRunParams);
|
|
78
|
+
|
|
79
|
+
const runId = client.pipeStore.runId || process.env.runId;
|
|
80
|
+
if (!runId) {
|
|
81
|
+
log.error(pc.red('Failed to create run on Testomat.io.'));
|
|
82
|
+
process.exit(1);
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
// stdout carries ONLY the run id so it can be captured: RUN_ID=$(reporter start)
|
|
86
|
+
console.log(runId);
|
|
87
|
+
process.exit(0);
|
|
88
|
+
});
|
|
89
|
+
|
|
90
|
+
program
|
|
91
|
+
.command('finish')
|
|
92
|
+
.description('Finish Run by its ID')
|
|
93
|
+
.action(async () => {
|
|
94
|
+
process.env.TESTOMATIO_RUN ||= readLatestRunId();
|
|
95
|
+
|
|
96
|
+
if (!process.env.TESTOMATIO_RUN) {
|
|
97
|
+
console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
|
|
98
|
+
return process.exit(1);
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
console.log('Finishing Run on Testomat.io...');
|
|
102
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
103
|
+
const client = new TestomatClient({ apiKey });
|
|
104
|
+
|
|
105
|
+
// @ts-ignore
|
|
106
|
+
client.updateRunStatus(STATUS.FINISHED).then(() => {
|
|
107
|
+
process.exit(0);
|
|
108
|
+
});
|
|
109
|
+
});
|
|
110
|
+
|
|
111
|
+
program
|
|
112
|
+
.command('run')
|
|
113
|
+
.alias('test')
|
|
114
|
+
.description('Run tests with the specified command')
|
|
115
|
+
.argument('[command]', 'Test runner command')
|
|
116
|
+
.option('--filter <filter>', 'Additional execution filter')
|
|
117
|
+
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
118
|
+
.option('--format <format>', 'Machine-readable output format for --filter-list (grep, json, newline, ids)')
|
|
119
|
+
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
120
|
+
.option('--remote <profile>', 'Trigger run on the named Testomat.io CI profile instead of executing locally')
|
|
121
|
+
.option(
|
|
122
|
+
'--remote-param <kv>',
|
|
123
|
+
'key=value pair forwarded to the CI profile config (repeat for multiple)',
|
|
124
|
+
(value, prev) => prev.concat([value]),
|
|
125
|
+
[],
|
|
126
|
+
)
|
|
127
|
+
.action(async (command, opts) => {
|
|
128
|
+
if (opts.remote) {
|
|
129
|
+
if (opts.filterList) {
|
|
130
|
+
log.warn(pc.red('⚠️ --filter-list cannot be combined with --remote'));
|
|
131
|
+
process.exit(1);
|
|
132
|
+
}
|
|
133
|
+
process.env.TESTOMATIO_CI_PROFILE = opts.remote;
|
|
134
|
+
if (opts.remoteParam?.length) {
|
|
135
|
+
process.env.TESTOMATIO_CI_PARAMS = opts.remoteParam.join(',');
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
140
|
+
const title = process.env.TESTOMATIO_TITLE;
|
|
141
|
+
const client = new TestomatClient({ apiKey, title });
|
|
142
|
+
|
|
143
|
+
if (opts.filter || opts.filterList) {
|
|
144
|
+
log.info('Filtering tests...');
|
|
145
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
|
|
146
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
|
|
147
|
+
// Example of use: npx @testomatio/reporter run --filter-list "coverage:file=coverage.yml" --format grep
|
|
148
|
+
const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
|
|
149
|
+
const pipeOptions = optsArray.join(':');
|
|
150
|
+
|
|
151
|
+
const prepareRunParams = { pipe, pipeOptions };
|
|
152
|
+
if (opts.filterList) {
|
|
153
|
+
client.pipeStore.filterList = true;
|
|
154
|
+
}
|
|
155
|
+
|
|
156
|
+
try {
|
|
157
|
+
const tests = await client.prepareRun(prepareRunParams);
|
|
158
|
+
|
|
159
|
+
if (!tests || tests.length === 0) {
|
|
160
|
+
log.warn( pc.yellow('No tests found.'));
|
|
161
|
+
// Exit non-zero on --filter-list so scripts can detect "nothing to run"
|
|
162
|
+
// via $? and skip launching the runner.
|
|
163
|
+
if (opts.filterList) process.exit(1);
|
|
164
|
+
return;
|
|
165
|
+
}
|
|
166
|
+
|
|
167
|
+
if (opts.filterList) {
|
|
168
|
+
const out = formatFilterListIds(tests, opts.format || 'ids');
|
|
169
|
+
if (out) console.log(out);
|
|
170
|
+
// Show the runnable-command hint only in interactive mode (no explicit --format).
|
|
171
|
+
// When --format is set the user is scripting and doesn't need stderr noise.
|
|
172
|
+
if (command && !opts.format) {
|
|
173
|
+
log.info(pc.green(`Full Running Command: ${applyFilter(command, tests)}`));
|
|
174
|
+
}
|
|
175
|
+
return;
|
|
176
|
+
}
|
|
177
|
+
|
|
178
|
+
if (command && command.split && !opts.remote) {
|
|
179
|
+
command = applyFilter(command, tests);
|
|
180
|
+
}
|
|
181
|
+
}
|
|
182
|
+
catch (err) {
|
|
183
|
+
log.error( err.message || err);
|
|
184
|
+
if (opts.filterList) process.exit(1);
|
|
185
|
+
return;
|
|
186
|
+
}
|
|
187
|
+
}
|
|
188
|
+
|
|
189
|
+
if (opts.remote) {
|
|
190
|
+
if (!apiKey) {
|
|
191
|
+
log.warn(pc.red('⚠️ TESTOMATIO API key required for --remote'));
|
|
192
|
+
process.exit(1);
|
|
193
|
+
}
|
|
194
|
+
if (command) {
|
|
195
|
+
log.warn(pc.yellow('Note: positional command is ignored when --remote is set; CI runs the workflow.'));
|
|
196
|
+
}
|
|
197
|
+
|
|
198
|
+
const createRunParams = {};
|
|
199
|
+
if (title) createRunParams.title = title;
|
|
200
|
+
if (opts.kind) createRunParams.kind = opts.kind;
|
|
201
|
+
|
|
202
|
+
try {
|
|
203
|
+
await client.createRun(createRunParams);
|
|
204
|
+
} catch (err) {
|
|
205
|
+
log.error(pc.red(`CI launch failed: ${err.message || err}`));
|
|
206
|
+
process.exit(1);
|
|
207
|
+
}
|
|
208
|
+
|
|
209
|
+
// createRun swallows pipe-level errors, so a resolved promise is not proof
|
|
210
|
+
// the launch succeeded — the pipe only records runUrl on a real 2xx response.
|
|
211
|
+
if (!client.pipeStore.runUrl) {
|
|
212
|
+
log.error(pc.red('CI launch failed — no run was created (see the error above).'));
|
|
213
|
+
process.exit(1);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
log.info(`🚀 CI build triggered on profile ${pc.cyan(opts.remote)}`);
|
|
217
|
+
log.info(`📊 Report URL: ${pc.magenta(client.pipeStore.runUrl)}`);
|
|
218
|
+
return process.exit(0);
|
|
219
|
+
}
|
|
220
|
+
|
|
221
|
+
// just create a run (wich tests which match filters) without executing tests
|
|
222
|
+
if (!command || !command.split) {
|
|
223
|
+
const createRunParams = {};
|
|
224
|
+
if (title) {
|
|
225
|
+
createRunParams.title = title;
|
|
226
|
+
}
|
|
227
|
+
if (opts.kind) {
|
|
228
|
+
createRunParams.kind = opts.kind;
|
|
229
|
+
}
|
|
230
|
+
|
|
231
|
+
if (apiKey) {
|
|
232
|
+
await client.createRun(createRunParams);
|
|
233
|
+
const runId = process.env.TESTOMATIO_RUN || process.env.runId;
|
|
234
|
+
if (client.pipeStore.runUrl) log.info( `📊 Report URL: ${pc.magenta(client.pipeStore.runUrl)}`);
|
|
235
|
+
|
|
236
|
+
if (opts.kind !== 'manual') {
|
|
237
|
+
log.info( `No command passed, so you need to run tests yourself:`);
|
|
238
|
+
log.info( `TESTOMATIO_RUN=${runId} <command>`);
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
log.info( '⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
|
|
242
|
+
process.exit(1);
|
|
243
|
+
}
|
|
244
|
+
return process.exit(0);
|
|
245
|
+
}
|
|
246
|
+
|
|
247
|
+
log.info( `🚀 Running`, pc.green(command));
|
|
248
|
+
|
|
249
|
+
const runTests = async () => {
|
|
250
|
+
const testCmds = command.split(' ');
|
|
251
|
+
const cmd = spawn(testCmds[0], testCmds.slice(1), {
|
|
252
|
+
stdio: 'inherit',
|
|
253
|
+
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
|
|
254
|
+
});
|
|
255
|
+
|
|
256
|
+
cmd.on('close', async code => {
|
|
257
|
+
const emoji = code === 0 ? '🟢' : '🔴';
|
|
258
|
+
log.info( emoji, `Runner exited with ${pc.bold(code)}`);
|
|
259
|
+
if (apiKey) {
|
|
260
|
+
const status = code === 0 ? 'passed' : 'failed';
|
|
261
|
+
await client.updateRunStatus(status);
|
|
262
|
+
}
|
|
263
|
+
process.exit(code);
|
|
264
|
+
});
|
|
265
|
+
};
|
|
266
|
+
|
|
267
|
+
const createRunParams = {};
|
|
268
|
+
if (title) {
|
|
269
|
+
createRunParams.title = title;
|
|
270
|
+
}
|
|
271
|
+
if (opts.kind) {
|
|
272
|
+
createRunParams.kind = opts.kind;
|
|
273
|
+
}
|
|
274
|
+
|
|
275
|
+
if (apiKey) {
|
|
276
|
+
await client.createRun(createRunParams).then(runTests);
|
|
277
|
+
} else {
|
|
278
|
+
await runTests();
|
|
279
|
+
}
|
|
280
|
+
});
|
|
281
|
+
|
|
282
|
+
// program
|
|
283
|
+
// .command('xml')
|
|
284
|
+
// .description('Parse XML reports and upload to Testomat.io')
|
|
285
|
+
// .argument('<pattern>', 'XML file pattern')
|
|
286
|
+
// .option('-d, --dir <dir>', 'Project directory')
|
|
287
|
+
// .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
|
|
288
|
+
// .option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
289
|
+
// .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
290
|
+
// .action(async (pattern, opts) => {
|
|
291
|
+
// if (!pattern.endsWith('.xml')) {
|
|
292
|
+
// pattern += '.xml';
|
|
293
|
+
// }
|
|
294
|
+
// let { javaTests, lang } = opts;
|
|
295
|
+
// if (javaTests === true) javaTests = 'src/test/java';
|
|
296
|
+
// lang = lang?.toLowerCase();
|
|
297
|
+
// const runReader = new XmlReader({ javaTests, lang });
|
|
298
|
+
// const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
299
|
+
// if (!files.length) {
|
|
300
|
+
// log.info( `Report can't be created. No XML files found 😥`);
|
|
301
|
+
// process.exit(1);
|
|
302
|
+
// }
|
|
303
|
+
|
|
304
|
+
program
|
|
305
|
+
.command('xml')
|
|
306
|
+
.description('Parse XML reports and upload to Testomat.io')
|
|
307
|
+
.argument('<pattern>', 'XML file pattern')
|
|
308
|
+
.option('-d, --dir <dir>', 'Project directory')
|
|
309
|
+
.option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
|
|
310
|
+
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
311
|
+
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
312
|
+
.action(async (pattern, opts) => {
|
|
313
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
314
|
+
pattern += '.xml';
|
|
315
|
+
}
|
|
316
|
+
let { javaTests, lang } = opts;
|
|
317
|
+
if (javaTests === true) javaTests = 'src/test/java';
|
|
318
|
+
lang = lang?.toLowerCase();
|
|
319
|
+
const runReader = new XmlReader({ javaTests, lang });
|
|
320
|
+
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
321
|
+
if (!files.length) {
|
|
322
|
+
log.info( `Report can't be created. No XML files found 😥`);
|
|
323
|
+
process.exit(1);
|
|
324
|
+
}
|
|
325
|
+
|
|
326
|
+
for (const file of files) {
|
|
327
|
+
log.info( `Parsed ${file}`);
|
|
328
|
+
runReader.parse(file);
|
|
329
|
+
}
|
|
330
|
+
|
|
331
|
+
let timeoutTimer;
|
|
332
|
+
if (opts.timelimit) {
|
|
333
|
+
timeoutTimer = setTimeout(
|
|
334
|
+
() => {
|
|
335
|
+
console.log(
|
|
336
|
+
`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
|
|
337
|
+
);
|
|
338
|
+
process.exit(0);
|
|
339
|
+
},
|
|
340
|
+
parseInt(opts.timelimit, 10) * 1000,
|
|
341
|
+
);
|
|
342
|
+
}
|
|
343
|
+
|
|
344
|
+
try {
|
|
345
|
+
await runReader.createRun();
|
|
346
|
+
await runReader.uploadData();
|
|
347
|
+
} catch (err) {
|
|
348
|
+
log.info( 'Error updating status, skipping...', err);
|
|
349
|
+
}
|
|
350
|
+
|
|
351
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
352
|
+
});
|
|
353
|
+
|
|
354
|
+
program
|
|
355
|
+
.command('upload-artifacts')
|
|
356
|
+
.description('Upload artifacts to Testomat.io')
|
|
357
|
+
.option('--force', 'Re-upload artifacts even if they were uploaded before')
|
|
358
|
+
.action(async opts => {
|
|
359
|
+
const apiKey = config.TESTOMATIO;
|
|
360
|
+
|
|
361
|
+
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
|
|
362
|
+
const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
|
|
363
|
+
|
|
364
|
+
if (!runId) {
|
|
365
|
+
console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
|
|
366
|
+
return process.exit(1);
|
|
367
|
+
}
|
|
368
|
+
|
|
369
|
+
const client = new TestomatClient({
|
|
370
|
+
apiKey,
|
|
371
|
+
runId,
|
|
372
|
+
isBatchEnabled: false,
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
let testruns = client.uploader.readUploadedFiles(runId);
|
|
376
|
+
const numTotalArtifacts = testruns.length;
|
|
377
|
+
|
|
378
|
+
debug('Found testruns:', testruns);
|
|
379
|
+
|
|
380
|
+
if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
|
|
381
|
+
|
|
382
|
+
if (!testruns.length) {
|
|
383
|
+
log.info( '🗄️ Total artifacts:', numTotalArtifacts);
|
|
384
|
+
if (numTotalArtifacts) {
|
|
385
|
+
log.info( 'No new artifacts to upload');
|
|
386
|
+
log.info( 'To re-upload artifacts run this command with --force flag');
|
|
387
|
+
}
|
|
388
|
+
process.exit(0);
|
|
389
|
+
}
|
|
390
|
+
|
|
391
|
+
const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
|
|
392
|
+
if (!acc[rid]) {
|
|
393
|
+
acc[rid] = [];
|
|
394
|
+
}
|
|
395
|
+
if (!acc[rid].includes(file)) acc[rid].push(file);
|
|
396
|
+
return acc;
|
|
397
|
+
}, {});
|
|
398
|
+
|
|
399
|
+
await client.createRun();
|
|
400
|
+
client.uploader.checkEnabled();
|
|
401
|
+
client.uploader.disableLogStorage();
|
|
402
|
+
|
|
403
|
+
for (const rid in testrunsByRid) {
|
|
404
|
+
const files = testrunsByRid[rid];
|
|
405
|
+
await client.addTestRun(undefined, { rid, files });
|
|
406
|
+
}
|
|
407
|
+
|
|
408
|
+
log.info( '🗄️', client.uploader.successfulUploads.length, 'artifacts 🟢uploaded');
|
|
409
|
+
|
|
410
|
+
if (client.uploader.successfulUploads.length) {
|
|
411
|
+
debug('\n', APP_PREFIX, `🗄️ ${client.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
|
|
412
|
+
const uploadedArtifacts = client.uploader.successfulUploads.map(file => ({
|
|
413
|
+
relativePath: file.path.replace(process.cwd(), ''),
|
|
414
|
+
link: file.link,
|
|
415
|
+
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
416
|
+
}));
|
|
417
|
+
|
|
418
|
+
uploadedArtifacts.forEach(upload => {
|
|
419
|
+
debug(
|
|
420
|
+
`🟢Uploaded artifact`,
|
|
421
|
+
`${upload.relativePath},`,
|
|
422
|
+
'size:',
|
|
423
|
+
`${upload.sizePretty},`,
|
|
424
|
+
'link:',
|
|
425
|
+
`${upload.link}`,
|
|
426
|
+
);
|
|
427
|
+
});
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
const filesizeStrMaxLength = 7;
|
|
431
|
+
|
|
432
|
+
if (client.uploader.failedUploads.length) {
|
|
433
|
+
console.log(
|
|
434
|
+
'\n',
|
|
435
|
+
APP_PREFIX,
|
|
436
|
+
'🗄️',
|
|
437
|
+
client.uploader.failedUploads.length,
|
|
438
|
+
`artifacts 🔴${pc.bold('failed')} to upload`,
|
|
439
|
+
);
|
|
440
|
+
|
|
441
|
+
const failedUploads = client.uploader.failedUploads.map(({ path, size }) => ({
|
|
442
|
+
relativePath: path.replace(process.cwd(), ''),
|
|
443
|
+
sizePretty: prettyBytes(size, { round: 0 }).toString(),
|
|
444
|
+
}));
|
|
445
|
+
|
|
446
|
+
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
447
|
+
failedUploads.forEach(upload => {
|
|
448
|
+
console.log(
|
|
449
|
+
` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
|
|
450
|
+
`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
|
|
451
|
+
)}`,
|
|
452
|
+
);
|
|
453
|
+
});
|
|
454
|
+
}
|
|
455
|
+
});
|
|
456
|
+
|
|
457
|
+
program
|
|
458
|
+
.command('replay')
|
|
459
|
+
.description('Replay test data from debug file and re-send to Testomat.io')
|
|
460
|
+
.argument('[debug-file]', `Path to debug file. Defaults to ./${DEBUG_FILE}.json`)
|
|
461
|
+
.option('--dry-run', 'Preview the data without sending to Testomat.io')
|
|
462
|
+
.action(async (debugFile, opts) => {
|
|
463
|
+
try {
|
|
464
|
+
const replayService = new Replay({
|
|
465
|
+
apiKey: config.TESTOMATIO,
|
|
466
|
+
dryRun: opts.dryRun,
|
|
467
|
+
onLog: message => log.info( message),
|
|
468
|
+
onError: message => log.error( '⚠️ ', message),
|
|
469
|
+
onProgress: ({ current, total }) => {
|
|
470
|
+
if (current % 10 === 0 || current === total) {
|
|
471
|
+
log.info( `📊 Progress: ${current}/${total} tests processed`);
|
|
472
|
+
}
|
|
473
|
+
},
|
|
474
|
+
});
|
|
475
|
+
|
|
476
|
+
const result = await replayService.replay(debugFile);
|
|
477
|
+
|
|
478
|
+
if (result.dryRun) {
|
|
479
|
+
log.info(
|
|
480
|
+
'🔍 Dry run completed:\n',
|
|
481
|
+
` - Tests found: ${result.testsCount}\n`,
|
|
482
|
+
` - Environment variables: ${Object.keys(result.envVars).length}\n`,
|
|
483
|
+
' - Run parameters:', result.runParams, '\n',
|
|
484
|
+
' Use without --dry-run to actually send the data',
|
|
485
|
+
);
|
|
486
|
+
} else {
|
|
487
|
+
log.info( `✅ Successfully replayed ${result.successCount}/${result.testsCount} tests`);
|
|
488
|
+
if (result.failureCount > 0) {
|
|
489
|
+
log.info( `⚠️ ${result.failureCount} tests failed to upload`);
|
|
490
|
+
}
|
|
491
|
+
}
|
|
492
|
+
|
|
493
|
+
process.exit(0);
|
|
494
|
+
} catch (err) {
|
|
495
|
+
log.error( '❌ Error replaying debug data:', err.message);
|
|
496
|
+
if (err.message.includes('Debug file not found')) {
|
|
497
|
+
log.error( '💡 Hint: Run tests with TESTOMATIO_DEBUG=1 to generate debug files');
|
|
498
|
+
}
|
|
499
|
+
process.exit(1);
|
|
500
|
+
}
|
|
501
|
+
});
|
|
502
|
+
|
|
503
|
+
program.parse(process.argv);
|
|
504
|
+
|
|
505
|
+
if (!process.argv.slice(2).length) {
|
|
506
|
+
program.outputHelp();
|
|
507
|
+
}
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { Command } from 'commander';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { glob } from 'glob';
|
|
5
|
+
import createDebugMessages from 'debug';
|
|
6
|
+
import XmlReader from '../xmlReader.js';
|
|
7
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
8
|
+
import dotenv from 'dotenv';
|
|
9
|
+
import { log } from '../utils/log.js';
|
|
10
|
+
|
|
11
|
+
const version = getPackageVersion();
|
|
12
|
+
|
|
13
|
+
const debug = createDebugMessages('@testomatio/reporter:xml-cli');
|
|
14
|
+
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
|
|
15
|
+
const program = new Command();
|
|
16
|
+
|
|
17
|
+
program
|
|
18
|
+
.arguments('<pattern>')
|
|
19
|
+
.option('-d, --dir <dir>', 'Project directory')
|
|
20
|
+
.option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
|
|
21
|
+
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
22
|
+
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
23
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
24
|
+
.action(async (pattern, opts) => {
|
|
25
|
+
if (!pattern.endsWith('.xml') && !pattern.includes('*')) {
|
|
26
|
+
pattern += '.xml';
|
|
27
|
+
}
|
|
28
|
+
let { javaTests, lang } = opts;
|
|
29
|
+
if (opts.envFile) {
|
|
30
|
+
log.info( 'Loading env file:', opts.envFile);
|
|
31
|
+
debug('Loading env file: %s', opts.envFile);
|
|
32
|
+
dotenv.config({ path: opts.envFile });
|
|
33
|
+
}
|
|
34
|
+
lang = lang?.toLowerCase();
|
|
35
|
+
if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
|
|
36
|
+
const runReader = new XmlReader({
|
|
37
|
+
javaTests,
|
|
38
|
+
lang,
|
|
39
|
+
});
|
|
40
|
+
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
41
|
+
if (!files.length) {
|
|
42
|
+
log.info( `Report can't be created. No XML files found 😥`);
|
|
43
|
+
process.exitCode = 1;
|
|
44
|
+
return;
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
for (const file of files) {
|
|
48
|
+
log.info( `Parsed ${file}`);
|
|
49
|
+
runReader.parse(file);
|
|
50
|
+
}
|
|
51
|
+
|
|
52
|
+
let timeoutTimer;
|
|
53
|
+
if (opts.timelimit) {
|
|
54
|
+
timeoutTimer = setTimeout(
|
|
55
|
+
() => {
|
|
56
|
+
console.log(
|
|
57
|
+
`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
|
|
58
|
+
);
|
|
59
|
+
process.exit(0);
|
|
60
|
+
},
|
|
61
|
+
parseInt(opts.timelimit, 10) * 1000,
|
|
62
|
+
);
|
|
63
|
+
}
|
|
64
|
+
|
|
65
|
+
try {
|
|
66
|
+
await runReader.createRun();
|
|
67
|
+
await runReader.uploadData();
|
|
68
|
+
} catch (err) {
|
|
69
|
+
log.info( 'Error updating status, skipping...', err);
|
|
70
|
+
}
|
|
71
|
+
|
|
72
|
+
if (timeoutTimer) clearTimeout(timeoutTimer);
|
|
73
|
+
});
|
|
74
|
+
|
|
75
|
+
if (process.argv.length < 3) {
|
|
76
|
+
program.outputHelp();
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
program.parse(process.argv);
|
|
@@ -0,0 +1,54 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
import { spawn } from 'node:child_process';
|
|
3
|
+
import { join, dirname } from 'node:path';
|
|
4
|
+
import { fileURLToPath } from 'node:url';
|
|
5
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
6
|
+
import pc from 'picocolors';
|
|
7
|
+
|
|
8
|
+
// @ts-ignore
|
|
9
|
+
const __dirname = typeof global.__dirname !== 'undefined' ? global.__dirname : dirname(fileURLToPath(import.meta.url));
|
|
10
|
+
const cliPath = join(__dirname, 'cli.js');
|
|
11
|
+
|
|
12
|
+
const version = getPackageVersion();
|
|
13
|
+
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
14
|
+
|
|
15
|
+
// Parse command line arguments to map start-test-run options to @testomatio/reporter run format
|
|
16
|
+
const args = process.argv.slice(2);
|
|
17
|
+
const newArgs = ['run'];
|
|
18
|
+
|
|
19
|
+
let i = 0;
|
|
20
|
+
while (i < args.length) {
|
|
21
|
+
const arg = args[i];
|
|
22
|
+
|
|
23
|
+
if (arg === '-c' || arg === '--command') {
|
|
24
|
+
// Map -c/--command to positional argument for run command
|
|
25
|
+
i++;
|
|
26
|
+
if (i < args.length) {
|
|
27
|
+
newArgs.push(args[i]);
|
|
28
|
+
}
|
|
29
|
+
} else if (arg.startsWith('--command=')) {
|
|
30
|
+
// Handle --command=value format
|
|
31
|
+
const command = arg.split('=', 2)[1];
|
|
32
|
+
newArgs.push(command);
|
|
33
|
+
} else if (arg === '--launch') {
|
|
34
|
+
// Map --launch to start command
|
|
35
|
+
newArgs[0] = 'start';
|
|
36
|
+
} else if (arg === '--finish') {
|
|
37
|
+
// Map --finish to finish command
|
|
38
|
+
newArgs[0] = 'finish';
|
|
39
|
+
} else {
|
|
40
|
+
// Pass through other arguments
|
|
41
|
+
newArgs.push(arg);
|
|
42
|
+
}
|
|
43
|
+
i++;
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
// Execute the main CLI with mapped arguments
|
|
47
|
+
|
|
48
|
+
const child = spawn(process.execPath, [cliPath, ...newArgs], {
|
|
49
|
+
stdio: 'inherit',
|
|
50
|
+
});
|
|
51
|
+
|
|
52
|
+
child.on('exit', code => {
|
|
53
|
+
process.exit(code);
|
|
54
|
+
});
|