@testomatio/reporter 2.0.1-beta.5-timestamp → 2.0.1-beta.7

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.
Files changed (152) hide show
  1. package/README.md +1 -0
  2. package/lib/adapter/codecept.d.ts +2 -0
  3. package/lib/adapter/codecept.js +297 -335
  4. package/lib/adapter/cucumber/current.d.ts +14 -0
  5. package/lib/adapter/cucumber/current.js +195 -203
  6. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  7. package/lib/adapter/cucumber/legacy.js +130 -155
  8. package/lib/adapter/cucumber.d.ts +2 -0
  9. package/lib/adapter/cucumber.js +5 -16
  10. package/lib/adapter/cypress-plugin/index.d.ts +2 -0
  11. package/lib/adapter/cypress-plugin/index.js +91 -105
  12. package/lib/adapter/jasmine.d.ts +11 -0
  13. package/lib/adapter/jasmine.js +54 -53
  14. package/lib/adapter/jest.d.ts +13 -0
  15. package/lib/adapter/jest.js +97 -99
  16. package/lib/adapter/mocha.d.ts +2 -0
  17. package/lib/adapter/mocha.js +112 -141
  18. package/lib/adapter/nightwatch.d.ts +4 -0
  19. package/lib/adapter/nightwatch.js +80 -0
  20. package/lib/adapter/playwright.d.ts +14 -0
  21. package/lib/adapter/playwright.js +208 -231
  22. package/lib/adapter/vitest.d.ts +35 -0
  23. package/lib/adapter/vitest.js +150 -149
  24. package/lib/adapter/webdriver.d.ts +24 -0
  25. package/lib/adapter/webdriver.js +144 -121
  26. package/lib/bin/cli.d.ts +2 -0
  27. package/lib/bin/cli.js +229 -211
  28. package/lib/bin/reportXml.d.ts +2 -0
  29. package/lib/bin/reportXml.js +51 -52
  30. package/lib/bin/startTest.d.ts +2 -0
  31. package/lib/bin/startTest.js +83 -95
  32. package/lib/bin/uploadArtifacts.d.ts +2 -0
  33. package/lib/bin/uploadArtifacts.js +56 -61
  34. package/lib/client.d.ts +76 -0
  35. package/lib/client.js +431 -465
  36. package/lib/config.d.ts +1 -0
  37. package/lib/config.js +18 -23
  38. package/lib/constants.d.ts +25 -0
  39. package/lib/constants.js +50 -44
  40. package/lib/data-storage.d.ts +34 -0
  41. package/lib/data-storage.js +216 -188
  42. package/lib/junit-adapter/adapter.d.ts +9 -0
  43. package/lib/junit-adapter/adapter.js +17 -20
  44. package/lib/junit-adapter/csharp.d.ts +5 -0
  45. package/lib/junit-adapter/csharp.js +28 -14
  46. package/lib/junit-adapter/index.d.ts +3 -0
  47. package/lib/junit-adapter/index.js +27 -25
  48. package/lib/junit-adapter/java.d.ts +5 -0
  49. package/lib/junit-adapter/java.js +41 -53
  50. package/lib/junit-adapter/javascript.d.ts +4 -0
  51. package/lib/junit-adapter/javascript.js +30 -27
  52. package/lib/junit-adapter/python.d.ts +5 -0
  53. package/lib/junit-adapter/python.js +38 -37
  54. package/lib/junit-adapter/ruby.d.ts +4 -0
  55. package/lib/junit-adapter/ruby.js +11 -8
  56. package/lib/output.d.ts +11 -0
  57. package/lib/output.js +44 -52
  58. package/lib/package.json +3 -0
  59. package/lib/pipe/bitbucket.d.ts +25 -0
  60. package/lib/pipe/bitbucket.js +223 -230
  61. package/lib/pipe/csv.d.ts +47 -0
  62. package/lib/pipe/csv.js +113 -126
  63. package/lib/pipe/debug.d.ts +29 -0
  64. package/lib/pipe/debug.js +125 -99
  65. package/lib/pipe/github.d.ts +30 -0
  66. package/lib/pipe/github.js +218 -213
  67. package/lib/pipe/gitlab.d.ts +25 -0
  68. package/lib/pipe/gitlab.js +183 -206
  69. package/lib/pipe/html.d.ts +35 -0
  70. package/lib/pipe/html.js +258 -321
  71. package/lib/pipe/index.d.ts +1 -0
  72. package/lib/pipe/index.js +94 -66
  73. package/lib/pipe/testomatio.d.ts +71 -0
  74. package/lib/pipe/testomatio.js +429 -474
  75. package/lib/replay.d.ts +31 -0
  76. package/lib/replay.js +255 -0
  77. package/lib/reporter-functions.d.ts +41 -0
  78. package/lib/reporter-functions.js +64 -26
  79. package/lib/reporter.d.ts +235 -0
  80. package/lib/reporter.js +37 -29
  81. package/lib/services/artifacts.d.ts +33 -0
  82. package/lib/services/artifacts.js +55 -51
  83. package/lib/services/index.d.ts +11 -0
  84. package/lib/services/index.js +16 -12
  85. package/lib/services/key-values.d.ts +27 -0
  86. package/lib/services/key-values.js +56 -53
  87. package/lib/services/labels.d.ts +22 -0
  88. package/lib/services/labels.js +62 -0
  89. package/lib/services/logger.d.ts +64 -0
  90. package/lib/services/logger.js +226 -245
  91. package/lib/template/testomatio.hbs +1026 -1366
  92. package/lib/uploader.d.ts +60 -0
  93. package/lib/uploader.js +295 -364
  94. package/lib/utils/pipe_utils.d.ts +41 -0
  95. package/lib/utils/pipe_utils.js +89 -85
  96. package/lib/utils/utils.d.ts +54 -0
  97. package/lib/utils/utils.js +398 -307
  98. package/lib/xmlReader.d.ts +92 -0
  99. package/lib/xmlReader.js +525 -532
  100. package/package.json +64 -21
  101. package/src/adapter/codecept.js +377 -0
  102. package/src/adapter/cucumber/current.js +228 -0
  103. package/src/adapter/cucumber/legacy.js +158 -0
  104. package/src/adapter/cucumber.js +4 -0
  105. package/src/adapter/cypress-plugin/index.js +110 -0
  106. package/src/adapter/jasmine.js +60 -0
  107. package/src/adapter/jest.js +107 -0
  108. package/src/adapter/mocha.cjs +2 -0
  109. package/src/adapter/mocha.js +156 -0
  110. package/src/adapter/nightwatch.js +88 -0
  111. package/src/adapter/playwright.js +258 -0
  112. package/src/adapter/vitest.js +183 -0
  113. package/src/adapter/webdriver.js +142 -0
  114. package/src/bin/cli.js +348 -0
  115. package/src/bin/reportXml.js +77 -0
  116. package/src/bin/startTest.js +124 -0
  117. package/src/bin/uploadArtifacts.js +91 -0
  118. package/src/client.js +519 -0
  119. package/src/config.js +30 -0
  120. package/src/constants.js +53 -0
  121. package/src/data-storage.js +204 -0
  122. package/src/junit-adapter/adapter.js +23 -0
  123. package/src/junit-adapter/csharp.js +28 -0
  124. package/src/junit-adapter/index.js +28 -0
  125. package/src/junit-adapter/java.js +58 -0
  126. package/src/junit-adapter/javascript.js +31 -0
  127. package/src/junit-adapter/python.js +42 -0
  128. package/src/junit-adapter/ruby.js +10 -0
  129. package/src/output.js +57 -0
  130. package/src/pipe/bitbucket.js +252 -0
  131. package/src/pipe/csv.js +140 -0
  132. package/src/pipe/debug.js +125 -0
  133. package/src/pipe/github.js +232 -0
  134. package/src/pipe/gitlab.js +247 -0
  135. package/src/pipe/html.js +373 -0
  136. package/src/pipe/index.js +71 -0
  137. package/src/pipe/testomatio.js +504 -0
  138. package/src/replay.js +262 -0
  139. package/src/reporter-functions.js +96 -0
  140. package/src/reporter.cjs_decprecated +21 -0
  141. package/src/reporter.js +36 -0
  142. package/src/services/artifacts.js +59 -0
  143. package/src/services/index.js +15 -0
  144. package/src/services/key-values.js +59 -0
  145. package/src/services/labels.js +59 -0
  146. package/src/services/logger.js +315 -0
  147. package/src/template/emptyData.svg +23 -0
  148. package/src/template/testomatio.hbs +1081 -0
  149. package/src/uploader.js +376 -0
  150. package/src/utils/pipe_utils.js +119 -0
  151. package/src/utils/utils.js +416 -0
  152. package/src/xmlReader.js +614 -0
@@ -0,0 +1,142 @@
1
+ import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
2
+ import TestomatClient from '../client.js';
3
+ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
4
+ import { services } from '../services/index.js';
5
+ import { TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
6
+
7
+ class WebdriverReporter extends WDIOReporter {
8
+ constructor(options) {
9
+ super(options);
10
+
11
+ this.client = new TestomatClient({ apiKey: options?.apiKey });
12
+ options = Object.assign(options, { stdout: true });
13
+
14
+ this._addTestPromises = [];
15
+
16
+ this._isSynchronising = false;
17
+ // NOTE: new functionality; may break everything
18
+ this.client.createRun();
19
+ }
20
+
21
+ get isSynchronised() {
22
+ return this._isSynchronising === false;
23
+ }
24
+
25
+ /**
26
+ *
27
+ * @param {RunnerStats} runData
28
+ */
29
+ async onRunnerEnd(runData) {
30
+ this._isSynchronising = true;
31
+
32
+ await Promise.all(this._addTestPromises);
33
+
34
+ this._isSynchronising = false;
35
+
36
+ // NOTE: new functionality; may break everything
37
+ // also this may require additional status mapping
38
+ await this.client.updateRunStatus(runData.failures ? 'failed' : 'passed');
39
+ }
40
+
41
+ onRunnerStart() {
42
+ // clear dir with artifacts/logs
43
+ //
44
+ fileSystem.clearDir(TESTOMAT_TMP_STORAGE_DIR);
45
+ }
46
+
47
+ onTestStart(test) {
48
+ services.setContext(test.fullTitle);
49
+ }
50
+
51
+ onTestEnd(test) {
52
+ test.suite = test.parent;
53
+ const logs = getTestLogs(test.fullTitle);
54
+ // TODO: FIX: artifacts for some reason leads to empty report on Testomat.io
55
+ // const artifacts = services.artifacts.get(test.fullTitle);
56
+ // const keyValues = services.keyValues.get(test.fullTitle);
57
+ test.logs = logs;
58
+ // test.artifacts = artifacts;
59
+ // test.meta = keyValues;
60
+
61
+ this._addTestPromises.push(this.addTest(test));
62
+ }
63
+
64
+ // wdio-cucumber does not trigger onTestEnd hook, thus, using this one
65
+ onSuiteEnd(scerario) {
66
+ if (scerario.type === 'scenario') {
67
+ this._addTestPromises.push(this.addBddScenario(scerario));
68
+ }
69
+ }
70
+
71
+ async addTest(test) {
72
+ if (!this.client) return;
73
+
74
+ const { title, _duration: duration, state, error, output } = test;
75
+
76
+ const testId = getTestomatIdFromTestTitle(title);
77
+
78
+ const screenshotEndpoint = '/session/:sessionId/screenshot';
79
+ const screenshotsBuffers = output
80
+ .filter(el => el.endpoint === screenshotEndpoint && el.result && el.result.value)
81
+ .map(el => Buffer.from(el.result.value, 'base64'));
82
+
83
+ await this.client.addTestRun(state, {
84
+ rid: test.uid || '',
85
+ manuallyAttachedArtifacts: test.artifacts,
86
+ error,
87
+ logs: test.logs,
88
+ meta: test.meta,
89
+ title,
90
+ test_id: testId,
91
+ time: duration,
92
+ filesBuffers: screenshotsBuffers,
93
+ });
94
+ }
95
+
96
+ /**
97
+ * @param {import('../../types/types.js').WebdriverIOScenario} scenario
98
+ */
99
+ addBddScenario(scenario) {
100
+ if (!this.client) return;
101
+
102
+ const { title, _duration: duration } = scenario;
103
+
104
+ const testId = getTestomatIdFromTestTitle(title || scenario.tags.map(tag => tag.name).join(' '));
105
+
106
+ let scenarioState = scenario.tests.every(test => test.state === 'passed') ? 'passed' : 'failed';
107
+ if (scenario.tests.every(test => test.state === 'skipped')) {
108
+ scenarioState = 'skipped';
109
+ }
110
+ const errors = scenario.tests
111
+ .filter(test => test.state === 'failed')
112
+ .map(test => test.error?.stack)
113
+ .filter(Boolean);
114
+ const error = errors.join('\n');
115
+
116
+ const tags = scenario.tags.map(tag => tag.name);
117
+
118
+ return this.client.addTestRun(scenarioState, {
119
+ error: error ? Error(error) : null,
120
+ title,
121
+ test_id: testId,
122
+ time: duration,
123
+ tags,
124
+ file: scenario.file,
125
+ // filesBuffers: screenshotsBuffers,
126
+ });
127
+ }
128
+ }
129
+
130
+ /**
131
+ *
132
+ * @param {*} fullTestTitle
133
+ * @returns string
134
+ */
135
+ function getTestLogs(fullTestTitle) {
136
+ const logsArr = services.logger.getLogs(fullTestTitle);
137
+ // remove duplicates (for some reason, logs are duplicated several times)
138
+ const logs = logsArr ? Array.from(new Set(logsArr)).join('\n').trim() : '';
139
+ return logs;
140
+ }
141
+
142
+ export default WebdriverReporter;
package/src/bin/cli.js ADDED
@@ -0,0 +1,348 @@
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 } from '../constants.js';
10
+ import { getPackageVersion } 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
+
18
+ const debug = createDebugMessages('@testomatio/reporter:xml-cli');
19
+ const version = getPackageVersion();
20
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
21
+ const program = new Command();
22
+
23
+ program
24
+ .version(version)
25
+ .option('--env-file <envfile>', 'Load environment variables from env file')
26
+ .hook('preAction', thisCommand => {
27
+ const opts = thisCommand.opts();
28
+ if (opts.envFile) {
29
+ dotenv.config({ path: opts.envFile });
30
+ } else {
31
+ dotenv.config();
32
+ }
33
+ });
34
+
35
+ program
36
+ .command('start')
37
+ .description('Start a new run and return its ID')
38
+ .action(async () => {
39
+ console.log('Starting a new Run on Testomat.io...');
40
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
41
+ const client = new TestomatClient({ apiKey });
42
+
43
+ client.createRun().then(() => {
44
+ console.log(process.env.runId);
45
+ process.exit(0);
46
+ });
47
+ });
48
+
49
+ program
50
+ .command('finish')
51
+ .description('Finish Run by its ID')
52
+ .action(async () => {
53
+ process.env.TESTOMATIO_RUN ||= readLatestRunId();
54
+
55
+ if (!process.env.TESTOMATIO_RUN) {
56
+ console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
57
+ return process.exit(1);
58
+ }
59
+
60
+ console.log('Finishing Run on Testomat.io...');
61
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
62
+ const client = new TestomatClient({ apiKey });
63
+
64
+ // @ts-ignore
65
+ client.updateRunStatus(STATUS.FINISHED).then(() => {
66
+ console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
67
+ process.exit(0);
68
+ });
69
+ });
70
+
71
+ program
72
+ .command('run')
73
+ .description('Run tests with the specified command')
74
+ .argument('<command>', 'Test runner command')
75
+ .option('--filter <filter>', 'Additional execution filter')
76
+ .action(async (command, opts) => {
77
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
78
+ const title = process.env.TESTOMATIO_TITLE;
79
+
80
+ if (!command || !command.split) {
81
+ console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
82
+ return process.exit(255);
83
+ }
84
+
85
+ const client = new TestomatClient({ apiKey, title, parallel: true });
86
+
87
+ if (opts.filter) {
88
+ const [pipe, ...optsArray] = opts.filter.split(':');
89
+ const pipeOptions = optsArray.join(':');
90
+
91
+ try {
92
+ const tests = await client.prepareRun({ pipe, pipeOptions });
93
+ if (tests && tests.length > 0) {
94
+ command += ` --grep (${tests.join('|')})`;
95
+ }
96
+ } catch (err) {
97
+ console.log(APP_PREFIX, err);
98
+ }
99
+ }
100
+
101
+ console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
102
+
103
+ const runTests = () => {
104
+ const testCmds = command.split(' ');
105
+ const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
106
+
107
+ cmd.on('close', code => {
108
+ const emoji = code === 0 ? '🟢' : '🔴';
109
+ console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
110
+ if (apiKey) {
111
+ const status = code === 0 ? 'passed' : 'failed';
112
+ client.updateRunStatus(status, true);
113
+ }
114
+ process.exit(code);
115
+ });
116
+ };
117
+
118
+ if (apiKey) {
119
+ client.createRun().then(runTests);
120
+ } else {
121
+ runTests();
122
+ }
123
+ });
124
+
125
+ // program
126
+ // .command('xml')
127
+ // .description('Parse XML reports and upload to Testomat.io')
128
+ // .argument('<pattern>', 'XML file pattern')
129
+ // .option('-d, --dir <dir>', 'Project directory')
130
+ // .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
131
+ // .option('--lang <lang>', 'Language used (python, ruby, java)')
132
+ // .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
133
+ // .action(async (pattern, opts) => {
134
+ // if (!pattern.endsWith('.xml')) {
135
+ // pattern += '.xml';
136
+ // }
137
+ // let { javaTests, lang } = opts;
138
+ // if (javaTests === true) javaTests = 'src/test/java';
139
+ // lang = lang?.toLowerCase();
140
+ // const runReader = new XmlReader({ javaTests, lang });
141
+ // const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
142
+ // if (!files.length) {
143
+ // console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
144
+ // process.exit(1);
145
+ // }
146
+
147
+ program
148
+ .command('xml')
149
+ .description('Parse XML reports and upload to Testomat.io')
150
+ .argument('<pattern>', 'XML file pattern')
151
+ .option('-d, --dir <dir>', 'Project directory')
152
+ .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
153
+ .option('--lang <lang>', 'Language used (python, ruby, java)')
154
+ .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
155
+ .action(async (pattern, opts) => {
156
+ if (!pattern.endsWith('.xml')) {
157
+ pattern += '.xml';
158
+ }
159
+ let { javaTests, lang } = opts;
160
+ if (javaTests === true) javaTests = 'src/test/java';
161
+ lang = lang?.toLowerCase();
162
+ const runReader = new XmlReader({ javaTests, lang });
163
+ const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
164
+ if (!files.length) {
165
+ console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
166
+ process.exit(1);
167
+ }
168
+
169
+ for (const file of files) {
170
+ console.log(APP_PREFIX, `Parsed ${file}`);
171
+ runReader.parse(file);
172
+ }
173
+
174
+ let timeoutTimer;
175
+ if (opts.timelimit) {
176
+ timeoutTimer = setTimeout(
177
+ () => {
178
+ console.log(
179
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
180
+ );
181
+ process.exit(0);
182
+ },
183
+ parseInt(opts.timelimit, 10) * 1000,
184
+ );
185
+ }
186
+
187
+ try {
188
+ await runReader.createRun();
189
+ await runReader.uploadData();
190
+ } catch (err) {
191
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
192
+ }
193
+
194
+ if (timeoutTimer) clearTimeout(timeoutTimer);
195
+ });
196
+
197
+ program
198
+ .command('upload-artifacts')
199
+ .description('Upload artifacts to Testomat.io')
200
+ .option('--force', 'Re-upload artifacts even if they were uploaded before')
201
+ .action(async opts => {
202
+ const apiKey = config.TESTOMATIO;
203
+
204
+ process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
205
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
206
+
207
+ if (!runId) {
208
+ console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
209
+ return process.exit(1);
210
+ }
211
+
212
+ const client = new TestomatClient({
213
+ apiKey,
214
+ runId,
215
+ isBatchEnabled: false,
216
+ });
217
+
218
+ let testruns = client.uploader.readUploadedFiles(runId);
219
+ const numTotalArtifacts = testruns.length;
220
+
221
+ debug('Found testruns:', testruns);
222
+
223
+ if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
224
+
225
+ if (!testruns.length) {
226
+ console.log(APP_PREFIX, '🗄️ Total artifacts:', numTotalArtifacts);
227
+ if (numTotalArtifacts) {
228
+ console.log(APP_PREFIX, 'No new artifacts to upload');
229
+ console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
230
+ }
231
+ process.exit(0);
232
+ }
233
+
234
+ const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
235
+ if (!acc[rid]) {
236
+ acc[rid] = [];
237
+ }
238
+ if (!acc[rid].includes(file)) acc[rid].push(file);
239
+ return acc;
240
+ }, {});
241
+
242
+ await client.createRun();
243
+ client.uploader.checkEnabled();
244
+ client.uploader.disableLogStorage();
245
+
246
+ for (const rid in testrunsByRid) {
247
+ const files = testrunsByRid[rid];
248
+ await client.addTestRun(undefined, { rid, files });
249
+ }
250
+
251
+ console.log(APP_PREFIX, '🗄️', client.uploader.successfulUploads.length, 'artifacts 🟢uploaded');
252
+
253
+ if (client.uploader.successfulUploads.length) {
254
+ debug('\n', APP_PREFIX, `🗄️ ${client.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
255
+ const uploadedArtifacts = client.uploader.successfulUploads.map(file => ({
256
+ relativePath: file.path.replace(process.cwd(), ''),
257
+ link: file.link,
258
+ sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
259
+ }));
260
+
261
+ uploadedArtifacts.forEach(upload => {
262
+ debug(
263
+ `🟢Uploaded artifact`,
264
+ `${upload.relativePath},`,
265
+ 'size:',
266
+ `${upload.sizePretty},`,
267
+ 'link:',
268
+ `${upload.link}`,
269
+ );
270
+ });
271
+ }
272
+
273
+ const filesizeStrMaxLength = 7;
274
+
275
+ if (client.uploader.failedUploads.length) {
276
+ console.log(
277
+ '\n',
278
+ APP_PREFIX,
279
+ '🗄️',
280
+ client.uploader.failedUploads.length,
281
+ `artifacts 🔴${pc.bold('failed')} to upload`,
282
+ );
283
+
284
+ const failedUploads = client.uploader.failedUploads.map(({ path, size }) => ({
285
+ relativePath: path.replace(process.cwd(), ''),
286
+ sizePretty: prettyBytes(size, { round: 0 }).toString(),
287
+ }));
288
+
289
+ const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
290
+ failedUploads.forEach(upload => {
291
+ console.log(
292
+ ` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
293
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
294
+ )}`,
295
+ );
296
+ });
297
+ }
298
+ });
299
+
300
+ program
301
+ .command('replay')
302
+ .description('Replay test data from debug file and re-send to Testomat.io')
303
+ .argument('[debug-file]', 'Path to debug file (defaults to /tmp/testomatio.debug.latest.json)')
304
+ .option('--dry-run', 'Preview the data without sending to Testomat.io')
305
+ .action(async (debugFile, opts) => {
306
+ try {
307
+ const replayService = new Replay({
308
+ apiKey: config.TESTOMATIO,
309
+ dryRun: opts.dryRun,
310
+ onLog: (message) => console.log(APP_PREFIX, message),
311
+ onError: (message) => console.error(APP_PREFIX, '⚠️ ', message),
312
+ onProgress: ({ current, total }) => {
313
+ if (current % 10 === 0 || current === total) {
314
+ console.log(APP_PREFIX, `📊 Progress: ${current}/${total} tests processed`);
315
+ }
316
+ }
317
+ });
318
+
319
+ const result = await replayService.replay(debugFile);
320
+
321
+ if (result.dryRun) {
322
+ console.log(APP_PREFIX, '🔍 Dry run completed:');
323
+ console.log(APP_PREFIX, ` - Tests found: ${result.testsCount}`);
324
+ console.log(APP_PREFIX, ` - Environment variables: ${Object.keys(result.envVars).length}`);
325
+ console.log(APP_PREFIX, ` - Run parameters:`, result.runParams);
326
+ console.log(APP_PREFIX, ' Use without --dry-run to actually send the data');
327
+ } else {
328
+ console.log(APP_PREFIX, `✅ Successfully replayed ${result.successCount}/${result.testsCount} tests`);
329
+ if (result.failureCount > 0) {
330
+ console.log(APP_PREFIX, `⚠️ ${result.failureCount} tests failed to upload`);
331
+ }
332
+ }
333
+
334
+ process.exit(0);
335
+ } catch (err) {
336
+ console.error(APP_PREFIX, '❌ Error replaying debug data:', err.message);
337
+ if (err.message.includes('Debug file not found')) {
338
+ console.error(APP_PREFIX, '💡 Hint: Run tests with TESTOMATIO_DEBUG=1 to generate debug files');
339
+ }
340
+ process.exit(1);
341
+ }
342
+ });
343
+
344
+ program.parse(process.argv);
345
+
346
+ if (!process.argv.slice(2).length) {
347
+ program.outputHelp();
348
+ }
@@ -0,0 +1,77 @@
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 { APP_PREFIX } from '../constants.js';
7
+ import XmlReader from '../xmlReader.js';
8
+ import { getPackageVersion } from '../utils/utils.js';
9
+ import dotenv from 'dotenv';
10
+ import path from 'path';
11
+
12
+ const version = getPackageVersion();
13
+
14
+ const debug = createDebugMessages('@testomatio/reporter:xml-cli');
15
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
16
+ const program = new Command();
17
+
18
+ program
19
+ .arguments('<pattern>')
20
+ .option('-d, --dir <dir>', 'Project directory')
21
+ .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
22
+ .option('--lang <lang>', 'Language used (python, ruby, java)')
23
+ .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
24
+ .option('--env-file <envfile>', 'Load environment variables from env file')
25
+ .action(async (pattern, opts) => {
26
+ if (!pattern.endsWith('.xml')) {
27
+ pattern += '.xml';
28
+ }
29
+ let { javaTests, lang } = opts;
30
+ if (opts.envFile) {
31
+ console.log(APP_PREFIX, 'Loading env file:', opts.envFile);
32
+ debug('Loading env file: %s', opts.envFile);
33
+ dotenv.config({ path: opts.envFile });
34
+ }
35
+ lang = lang?.toLowerCase();
36
+ if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
37
+ const runReader = new XmlReader({ javaTests, lang });
38
+ const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
39
+ if (!files.length) {
40
+ console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
41
+ process.exitCode = 1;
42
+ return;
43
+ }
44
+
45
+ for (const file of files) {
46
+ console.log(APP_PREFIX, `Parsed ${file}`);
47
+ runReader.parse(file);
48
+ }
49
+
50
+ let timeoutTimer;
51
+ if (opts.timelimit) {
52
+ timeoutTimer = setTimeout(
53
+ () => {
54
+ console.log(
55
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
56
+ );
57
+ process.exit(0);
58
+ },
59
+ parseInt(opts.timelimit, 10) * 1000,
60
+ );
61
+ }
62
+
63
+ try {
64
+ await runReader.createRun();
65
+ await runReader.uploadData();
66
+ } catch (err) {
67
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
68
+ }
69
+
70
+ if (timeoutTimer) clearTimeout(timeoutTimer);
71
+ });
72
+
73
+ if (process.argv.length < 3) {
74
+ program.outputHelp();
75
+ }
76
+
77
+ program.parse(process.argv);
@@ -0,0 +1,124 @@
1
+ #!/usr/bin/env node
2
+ import { spawn } from 'cross-spawn';
3
+ import { Command } from 'commander';
4
+ import pc from 'picocolors';
5
+ import TestomatClient from '../client.js';
6
+ import { APP_PREFIX, STATUS } from '../constants.js';
7
+ import { getPackageVersion } from '../utils/utils.js';
8
+ import { config } from '../config.js';
9
+ import dotenv from 'dotenv';
10
+
11
+ const version = getPackageVersion();
12
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
13
+ const program = new Command();
14
+
15
+ program
16
+ .option('-c, --command <cmd>', 'Test runner command')
17
+ .option('--launch', 'Start a new run and return its ID')
18
+ .option('--finish', 'Finish Run by its ID')
19
+ .option('--env-file <envfile>', 'Load environment variables from env file')
20
+ .option('--filter <filter>', 'Additional execution filter')
21
+ .action(async opts => {
22
+ const { launch, finish, filter } = opts;
23
+ let { command } = opts;
24
+
25
+ if (opts.envFile) dotenv.config({ path: opts.envFile });
26
+
27
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
28
+ const title = process.env.TESTOMATIO_TITLE;
29
+
30
+ if (launch) {
31
+ console.log('Starting a new Run on Testomat.io...');
32
+ const client = new TestomatClient({ apiKey });
33
+
34
+ client.createRun().then(() => {
35
+ console.log(process.env.runId);
36
+ process.exit(0);
37
+ });
38
+ return;
39
+ }
40
+
41
+ if (finish) {
42
+ // TODO: add error in case of TESTOMATIO environment variable is not set
43
+ // because command is fine in console, but actually (on testomat.io) run is not finished
44
+ if (!process.env.TESTOMATIO_RUN) {
45
+ console.log('TESTOMATIO_RUN environment variable must be set.');
46
+ return process.exit(1);
47
+ }
48
+
49
+ console.log('Finishing Run on Testomat.io...');
50
+
51
+ const client = new TestomatClient({ apiKey });
52
+
53
+ // @ts-ignore
54
+ client.updateRunStatus(STATUS.FINISHED).then(() => {
55
+ console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
56
+ process.exit(0);
57
+ });
58
+ return;
59
+ }
60
+
61
+ let exitCode = 0;
62
+
63
+ if (!command.split) {
64
+ process.exitCode = 255;
65
+ console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
66
+ return;
67
+ }
68
+
69
+ const client = new TestomatClient({ apiKey, title, parallel: true });
70
+
71
+ if (filter) {
72
+ const [pipe, ...optsArray] = filter.split(':');
73
+ const pipeOptions = optsArray.join(':');
74
+
75
+ try {
76
+ const tests = await client.prepareRun({ pipe, pipeOptions });
77
+
78
+ if (!tests || tests.length === 0) {
79
+ return;
80
+ }
81
+
82
+ const grep = ` --grep (${tests.join('|')})`;
83
+ command += grep;
84
+ } catch (err) {
85
+ console.log(APP_PREFIX, err);
86
+ }
87
+ }
88
+
89
+ const testCmds = command.split(' ');
90
+ console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
91
+
92
+ if (!apiKey) {
93
+ const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
94
+
95
+ cmd.on('close', code => {
96
+ console.log(APP_PREFIX, '⚠️ ', `Runner exited with ${pc.bold(code)}, report is ignored`);
97
+
98
+ if (code > exitCode) exitCode = code;
99
+ process.exitCode = exitCode;
100
+ });
101
+
102
+ return;
103
+ }
104
+
105
+ client.createRun().then(() => {
106
+ const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
107
+
108
+ cmd.on('close', code => {
109
+ const emoji = code === 0 ? '🟢' : '🔴';
110
+ console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
111
+ const status = code === 0 ? 'passed' : 'failed';
112
+ client.updateRunStatus(status, true);
113
+
114
+ if (code > exitCode) exitCode = code;
115
+ process.exitCode = exitCode;
116
+ });
117
+ });
118
+ });
119
+
120
+ if (process.argv.length <= 2) {
121
+ program.outputHelp();
122
+ }
123
+
124
+ program.parse(process.argv);