@testomatio/reporter 2.0.0-beta-esm → 2.0.0-beta.1-xml

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 (123) hide show
  1. package/lib/adapter/codecept.d.ts +2 -0
  2. package/lib/adapter/codecept.js +31 -26
  3. package/lib/adapter/cucumber/current.d.ts +14 -0
  4. package/lib/adapter/cucumber/legacy.d.ts +0 -0
  5. package/lib/adapter/cucumber.d.ts +2 -0
  6. package/lib/adapter/cypress-plugin/index.d.ts +2 -0
  7. package/lib/adapter/cypress-plugin/index.js +10 -10
  8. package/lib/adapter/jasmine.d.ts +11 -0
  9. package/lib/adapter/jest.d.ts +13 -0
  10. package/lib/adapter/mocha.d.ts +2 -0
  11. package/lib/adapter/mocha.js +4 -4
  12. package/lib/adapter/nightwatch.d.ts +4 -0
  13. package/lib/adapter/nightwatch.js +80 -0
  14. package/lib/adapter/playwright.d.ts +14 -0
  15. package/lib/adapter/playwright.js +58 -33
  16. package/lib/adapter/vitest.d.ts +35 -0
  17. package/lib/adapter/vitest.js +6 -6
  18. package/lib/adapter/webdriver.d.ts +24 -0
  19. package/lib/adapter/webdriver.js +51 -14
  20. package/lib/bin/cli.d.ts +2 -0
  21. package/lib/bin/cli.js +250 -0
  22. package/lib/bin/reportXml.d.ts +2 -0
  23. package/lib/bin/reportXml.js +15 -11
  24. package/lib/bin/startTest.d.ts +2 -0
  25. package/lib/bin/startTest.js +12 -7
  26. package/lib/bin/uploadArtifacts.d.ts +2 -0
  27. package/lib/bin/uploadArtifacts.js +82 -0
  28. package/lib/client.d.ts +76 -0
  29. package/lib/client.js +128 -53
  30. package/lib/config.d.ts +1 -0
  31. package/lib/config.js +2 -2
  32. package/lib/constants.d.ts +25 -0
  33. package/lib/constants.js +5 -1
  34. package/lib/data-storage.d.ts +34 -0
  35. package/lib/data-storage.js +19 -9
  36. package/lib/junit-adapter/adapter.d.ts +9 -0
  37. package/lib/junit-adapter/csharp.d.ts +5 -0
  38. package/lib/junit-adapter/csharp.js +11 -1
  39. package/lib/junit-adapter/index.d.ts +3 -0
  40. package/lib/junit-adapter/java.d.ts +5 -0
  41. package/lib/junit-adapter/javascript.d.ts +4 -0
  42. package/lib/junit-adapter/python.d.ts +5 -0
  43. package/lib/junit-adapter/ruby.d.ts +4 -0
  44. package/lib/output.d.ts +11 -0
  45. package/lib/package.json +3 -1
  46. package/lib/pipe/bitbucket.d.ts +23 -0
  47. package/lib/pipe/bitbucket.js +19 -9
  48. package/lib/pipe/csv.d.ts +47 -0
  49. package/lib/pipe/csv.js +2 -2
  50. package/lib/pipe/debug.d.ts +29 -0
  51. package/lib/pipe/debug.js +108 -0
  52. package/lib/pipe/github.d.ts +30 -0
  53. package/lib/pipe/github.js +37 -5
  54. package/lib/pipe/gitlab.d.ts +23 -0
  55. package/lib/pipe/gitlab.js +2 -3
  56. package/lib/pipe/html.d.ts +35 -0
  57. package/lib/pipe/html.js +9 -4
  58. package/lib/pipe/index.d.ts +1 -0
  59. package/lib/pipe/index.js +20 -10
  60. package/lib/pipe/testomatio.d.ts +70 -0
  61. package/lib/pipe/testomatio.js +54 -39
  62. package/lib/reporter-functions.d.ts +34 -0
  63. package/lib/reporter-functions.js +17 -7
  64. package/lib/reporter.d.ts +232 -0
  65. package/lib/reporter.js +19 -33
  66. package/lib/services/artifacts.d.ts +33 -0
  67. package/lib/services/index.d.ts +9 -0
  68. package/lib/services/key-values.d.ts +27 -0
  69. package/lib/services/key-values.js +1 -1
  70. package/lib/services/logger.d.ts +64 -0
  71. package/lib/services/logger.js +1 -2
  72. package/lib/template/testomatio.hbs +651 -1366
  73. package/lib/uploader.d.ts +60 -0
  74. package/lib/uploader.js +312 -0
  75. package/lib/utils/pipe_utils.d.ts +41 -0
  76. package/lib/utils/pipe_utils.js +3 -5
  77. package/lib/utils/utils.d.ts +47 -0
  78. package/lib/utils/utils.js +99 -12
  79. package/lib/xmlReader.d.ts +92 -0
  80. package/lib/xmlReader.js +64 -25
  81. package/package.json +19 -13
  82. package/src/adapter/codecept.js +30 -26
  83. package/src/adapter/cypress-plugin/index.js +5 -5
  84. package/src/adapter/mocha.cjs +1 -1
  85. package/src/adapter/mocha.js +4 -4
  86. package/src/adapter/nightwatch.js +88 -0
  87. package/src/adapter/playwright.js +59 -31
  88. package/src/adapter/vitest.js +6 -6
  89. package/src/adapter/webdriver.js +42 -12
  90. package/src/bin/cli.js +303 -0
  91. package/src/bin/reportXml.js +19 -9
  92. package/src/bin/startTest.js +9 -4
  93. package/src/bin/uploadArtifacts.js +91 -0
  94. package/src/client.js +137 -57
  95. package/src/config.js +2 -2
  96. package/src/constants.js +5 -1
  97. package/src/data-storage.js +2 -2
  98. package/src/junit-adapter/csharp.js +13 -1
  99. package/src/pipe/bitbucket.js +2 -2
  100. package/src/pipe/csv.js +3 -3
  101. package/src/pipe/debug.js +104 -0
  102. package/src/pipe/github.js +3 -5
  103. package/src/pipe/gitlab.js +6 -7
  104. package/src/pipe/html.js +14 -7
  105. package/src/pipe/index.js +5 -7
  106. package/src/pipe/testomatio.js +75 -76
  107. package/src/reporter-functions.js +18 -7
  108. package/src/reporter.cjs_decprecated +21 -0
  109. package/src/reporter.js +20 -11
  110. package/src/services/key-values.js +1 -1
  111. package/src/services/logger.js +5 -4
  112. package/src/template/testomatio.hbs +651 -1366
  113. package/src/uploader.js +371 -0
  114. package/src/utils/pipe_utils.js +4 -12
  115. package/src/utils/utils.js +64 -15
  116. package/src/xmlReader.js +76 -26
  117. package/lib/adapter/jasmine/jasmine.js +0 -63
  118. package/lib/adapter/mocha/mocha.js +0 -125
  119. package/lib/fileUploader.js +0 -245
  120. package/lib/utils/chalk.js +0 -10
  121. package/src/fileUploader.js +0 -307
  122. package/src/reporter.cjs +0 -22
  123. package/src/utils/chalk.js +0 -13
@@ -0,0 +1,88 @@
1
+ import TestomatClient from '../client.js';
2
+ import { config } from '../config.js';
3
+ import { STATUS } from '../constants.js';
4
+ import { getTestomatIdFromTestTitle } from '../utils/utils.js';
5
+
6
+ const apiKey = config.TESTOMATIO;
7
+ const client = new TestomatClient({ apiKey });
8
+
9
+ export default {
10
+ write: async (results, options, done) => {
11
+ await client.createRun();
12
+
13
+ const testFiles = results.modules;
14
+
15
+ for (const fileName in testFiles) {
16
+ // in nightwatch: object containing tests from a single file
17
+ const testModule = testFiles[fileName];
18
+
19
+ // passed and failed tests (tests with assertions)
20
+ const completedTests = testModule.completed;
21
+
22
+ // skipped tests (skipped by user or tests without assertions)
23
+ const skippedTests = testModule.skipped;
24
+
25
+ const tags = testModule.tags || [];
26
+
27
+ // if test file contains multiple suites, the last suite name is used as a name 🤷‍♂️
28
+ // no other places which contain suite name (even inside test object)
29
+ const suiteTitle = testModule.name;
30
+
31
+ for (const testTitle in completedTests) {
32
+ const test = completedTests[testTitle];
33
+ let status;
34
+ switch (test.status) {
35
+ case 'pass':
36
+ status = STATUS.PASSED;
37
+ break;
38
+ case 'fail':
39
+ status = STATUS.FAILED;
40
+ break;
41
+ // probably not required (because skipped tests are in separate array), but just in case
42
+ case 'skip':
43
+ status = STATUS.SKIPPED;
44
+ console.info('Skipped test is in completed tests array:', test, 'Not expected behavior.');
45
+ break;
46
+ default:
47
+ console.error('Test status processing error:', test.status);
48
+ }
49
+
50
+ const testId = getTestomatIdFromTestTitle(testTitle);
51
+
52
+ client.addTestRun(status, {
53
+ error: { name: test.assertions?.[0]?.name, message: test.assertions?.[0]?.message, stack: test.stackTrace },
54
+ file: testModule.modulePath?.replace(process.cwd(), ''),
55
+ message: test.assertions?.[0]?.message,
56
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
57
+ stack: test.stackTrace,
58
+ suite_title: suiteTitle,
59
+ tags,
60
+ test_id: testId,
61
+ time: test.timeMs,
62
+ title: testTitle,
63
+ });
64
+ }
65
+
66
+ // just array with skipped tests titles, no any other info
67
+ for (const testTitle of skippedTests) {
68
+ client.addTestRun(STATUS.SKIPPED, {
69
+ suite_title: suiteTitle,
70
+ tags,
71
+ rid: `${testModule.uuid || ''}_${testTitle || ''}`,
72
+ title: testTitle,
73
+ });
74
+ }
75
+ }
76
+
77
+ /**
78
+ * @type {'passed' | 'failed' | 'finished'}
79
+ */
80
+ let runStatus = 'finished';
81
+ if (results.failed) runStatus = 'failed';
82
+ else if (results.passed) runStatus = 'passed';
83
+
84
+ await client.updateRunStatus(runStatus);
85
+
86
+ done();
87
+ },
88
+ };
@@ -6,7 +6,6 @@ import { v4 as uuidv4 } from 'uuid';
6
6
  import fs from 'fs';
7
7
  import { APP_PREFIX, STATUS as Status, TESTOMAT_TMP_STORAGE_DIR } from '../constants.js';
8
8
  import TestomatioClient from '../client.js';
9
- import { upload } from '../fileUploader.js';
10
9
  import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
11
10
  import { services } from '../services/index.js';
12
11
  import { dataStorage } from '../data-storage.js';
@@ -57,11 +56,37 @@ class PlaywrightReporter {
57
56
  logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${result.stdout.join('')}`;
58
57
  }
59
58
  const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
60
- const keyValues = services.keyValues.get(fullTestTitle);
59
+ const testMeta = services.keyValues.get(fullTestTitle);
61
60
  const rid = test.id || test.testId || uuidv4();
62
61
 
63
- const reportTestPromise = this.client.addTestRun(checkStatus(result.status), {
64
- rid,
62
+ /**
63
+ * @type {{
64
+ * browser?: string,
65
+ * dependencies: string[],
66
+ * isMobile?: boolean
67
+ * metadata: Record<string, any>,
68
+ * name: string,
69
+ * }}
70
+ */
71
+ const project = {
72
+ browser: test.parent.project().use.defaultBrowserType,
73
+ dependencies: test.parent.project().dependencies,
74
+ isMobile: test.parent.project().use.isMobile,
75
+ metadata: test.parent.project().metadata,
76
+ name: test.parent.project().name,
77
+ };
78
+
79
+ let status = result.status;
80
+ // process test.fail() annotation
81
+ if (test.expectedStatus === 'failed') {
82
+ // actual status = expected
83
+ if (result.status === 'failed') status = 'passed';
84
+ // actual status != expected
85
+ if (result.status === 'passed') status = 'failed';
86
+ }
87
+
88
+ const reportTestPromise = this.client.addTestRun(checkStatus(status), {
89
+ rid: `${rid}-${project.name}`,
65
90
  error,
66
91
  test_id: getTestomatIdFromTestTitle(`${title} ${test.tags?.join(' ')}`),
67
92
  suite_title,
@@ -70,12 +95,19 @@ class PlaywrightReporter {
70
95
  time: duration,
71
96
  logs,
72
97
  manuallyAttachedArtifacts,
73
- meta: keyValues,
98
+ meta: {
99
+ browser: project.browser,
100
+ isMobile: project.isMobile,
101
+ project: project.name,
102
+ projectDependencies: project.dependencies?.length ? project.dependencies : null,
103
+ ...testMeta,
104
+ ...project.metadata, // metadata has any type (in playwright), but we will stringify it in client.js
105
+ },
74
106
  file: test.location?.file,
75
107
  });
76
108
 
77
109
  this.uploads.push({
78
- rid,
110
+ rid: `${rid}-${project.name}`,
79
111
  title: test.title,
80
112
  files: result.attachments.filter(a => a.body || a.path),
81
113
  file: test.location?.file,
@@ -94,9 +126,13 @@ class PlaywrightReporter {
94
126
  }
95
127
 
96
128
  if (artifact.body) {
97
- const fileName = tmpFile();
98
- fs.writeFileSync(fileName, artifact.body);
99
- return fileName;
129
+ let filePath = generateTmpFilepath(artifact.name);
130
+
131
+ const extension = artifact.contentType?.split('/')[1]?.replace('jpeg', 'jpg');
132
+ if (extension) filePath += `.${extension}`;
133
+
134
+ fs.writeFileSync(filePath, artifact.body);
135
+ return filePath;
100
136
  }
101
137
 
102
138
  return null;
@@ -107,20 +143,26 @@ class PlaywrightReporter {
107
143
 
108
144
  await Promise.all(reportTestPromises);
109
145
 
110
- if (this.uploads.length && upload.isArtifactsEnabled()) {
111
- console.log(APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
146
+ if (this.uploads.length) {
147
+ if (this.client.uploader.isEnabled) console.log(APP_PREFIX, `🎞️ Uploading ${this.uploads.length} files...`);
112
148
 
113
149
  const promises = [];
114
150
 
115
- for (const anUpload of this.uploads) {
116
- const { rid, file, title } = anUpload;
151
+ // ? possible move to addTestRun (needs investigation if files are ready)
152
+ for (const upload of this.uploads) {
153
+ const { rid, file, title } = upload;
117
154
 
118
- const files = anUpload.files.map(attachment => ({
155
+ const files = upload.files.map(attachment => ({
119
156
  path: this.#getArtifactPath(attachment),
120
157
  title,
121
158
  type: attachment.contentType,
122
159
  }));
123
160
 
161
+ if (!this.client.uploader.isEnabled) {
162
+ files.forEach(f => this.client.uploader.storeUploadedFile(f, this.client.runId, rid, false));
163
+ continue;
164
+ }
165
+
124
166
  promises.push(
125
167
  this.client.addTestRun(undefined, {
126
168
  rid,
@@ -190,9 +232,10 @@ function appendStep(step, shift = 0) {
190
232
  return resultStep;
191
233
  }
192
234
 
193
- function tmpFile(prefix = 'tmp.') {
235
+ function generateTmpFilepath(filename = '') {
236
+ filename = filename || `tmp.${crypto.randomBytes(16).toString('hex')}`;
194
237
  const tmpdir = os.tmpdir();
195
- return path.join(tmpdir, prefix + crypto.randomBytes(16).toString('hex'));
238
+ return path.join(tmpdir, filename);
196
239
  }
197
240
 
198
241
  /**
@@ -204,19 +247,4 @@ function getTestContextName(test) {
204
247
  return `${test._requireFile || ''}_${test.title}`;
205
248
  }
206
249
 
207
- function initPlaywrightForStorage() {
208
- try {
209
- // @ts-ignore-next-line
210
- // eslint-disable-next-line import/no-extraneous-dependencies
211
- const { test } = require('@playwright/test');
212
- // eslint-disable-next-line no-empty-pattern
213
- test.beforeEach(async ({}, testInfo) => {
214
- global.testomatioTestTitle = `${testInfo.file || ''}_${testInfo.title}`;
215
- });
216
- } catch (e) {
217
- // ignore
218
- }
219
- }
220
-
221
250
  export default PlaywrightReporter;
222
- export { initPlaywrightForStorage };
@@ -7,13 +7,13 @@ import createDebugMessages from 'debug';
7
7
  const debug = createDebugMessages('@testomatio/reporter:adapter-jest');
8
8
 
9
9
  /**
10
- * @typedef {import('../../types').VitestTest} VitestTest
11
- * @typedef {import('../../types').VitestTestFile} VitestTestFile
12
- * @typedef {import('../../types').VitestSuite} VitestSuite
13
- * @typedef {import('../../types').VitestTestLogs} VitestTestLogs
14
- * @typedef {import('../../vitest.types').ErrorWithDiff} ErrorWithDiff
10
+ * @typedef {import('../../types/types.js').VitestTest} VitestTest
11
+ * @typedef {import('../../types/types.js').VitestTestFile} VitestTestFile
12
+ * @typedef {import('../../types/types.js').VitestSuite} VitestSuite
13
+ * @typedef {import('../../types/types.js').VitestTestLogs} VitestTestLogs
14
+ * @typedef {import('../../types/vitest.types.js').ErrorWithDiff} ErrorWithDiff
15
15
  * @typedef {typeof import('../constants.js').STATUS} STATUS
16
- * @typedef {import('../../types').TestData} TestData
16
+ * @typedef {import('../../types/types.js').TestData} TestData
17
17
  */
18
18
 
19
19
  class VitestReporter {
@@ -1,8 +1,8 @@
1
- // eslint-disable-next-line
2
- import WDIOReporter, { RunnerStats } from '@wdio/reporter';
3
-
1
+ import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
4
2
  import TestomatClient from '../client.js';
5
- import { getTestomatIdFromTestTitle } from '../utils/utils.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
6
 
7
7
  class WebdriverReporter extends WDIOReporter {
8
8
  constructor(options) {
@@ -23,8 +23,8 @@ class WebdriverReporter extends WDIOReporter {
23
23
  }
24
24
 
25
25
  /**
26
- *
27
- * @param {RunnerStats} runData
26
+ *
27
+ * @param {RunnerStats} runData
28
28
  */
29
29
  async onRunnerEnd(runData) {
30
30
  this._isSynchronising = true;
@@ -33,20 +33,35 @@ class WebdriverReporter extends WDIOReporter {
33
33
 
34
34
  this._isSynchronising = false;
35
35
 
36
- // NOTE: new functionality; may break everything
36
+ // NOTE: new functionality; may break everything
37
37
  // also this may require additional status mapping
38
38
  await this.client.updateRunStatus(runData.failures ? 'failed' : 'passed');
39
39
  }
40
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
+
41
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
+
42
61
  this._addTestPromises.push(this.addTest(test));
43
62
  }
44
63
 
45
64
  // wdio-cucumber does not trigger onTestEnd hook, thus, using this one
46
- /**
47
- *
48
- * @returns
49
- */
50
65
  onSuiteEnd(scerario) {
51
66
  if (scerario.type === 'scenario') {
52
67
  this._addTestPromises.push(this.addBddScenario(scerario));
@@ -66,7 +81,10 @@ class WebdriverReporter extends WDIOReporter {
66
81
  .map(el => Buffer.from(el.result.value, 'base64'));
67
82
 
68
83
  await this.client.addTestRun(state, {
84
+ manuallyAttachedArtifacts: test.artifacts,
69
85
  error,
86
+ logs: test.logs,
87
+ meta: test.meta,
70
88
  title,
71
89
  test_id: testId,
72
90
  time: duration,
@@ -75,7 +93,7 @@ class WebdriverReporter extends WDIOReporter {
75
93
  }
76
94
 
77
95
  /**
78
- * @param {import('../../types').WebdriverIOScenario} scenario
96
+ * @param {import('../../types/types.js').WebdriverIOScenario} scenario
79
97
  */
80
98
  addBddScenario(scenario) {
81
99
  if (!this.client) return;
@@ -108,4 +126,16 @@ class WebdriverReporter extends WDIOReporter {
108
126
  }
109
127
  }
110
128
 
129
+ /**
130
+ *
131
+ * @param {*} fullTestTitle
132
+ * @returns string
133
+ */
134
+ function getTestLogs(fullTestTitle) {
135
+ const logsArr = services.logger.getLogs(fullTestTitle);
136
+ // remove duplicates (for some reason, logs are duplicated several times)
137
+ const logs = logsArr ? Array.from(new Set(logsArr)).join('\n').trim() : '';
138
+ return logs;
139
+ }
140
+
111
141
  export default WebdriverReporter;
package/src/bin/cli.js ADDED
@@ -0,0 +1,303 @@
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
+
17
+ const debug = createDebugMessages('@testomatio/reporter:xml-cli');
18
+ const version = getPackageVersion();
19
+ console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
20
+ const program = new Command();
21
+
22
+ program
23
+ .version(version)
24
+ .option('--env-file <envfile>', 'Load environment variables from env file')
25
+ .hook('preAction', thisCommand => {
26
+ const opts = thisCommand.opts();
27
+ if (opts.envFile) {
28
+ dotenv.config({ path: opts.envFile });
29
+ } else {
30
+ dotenv.config();
31
+ }
32
+ });
33
+
34
+ program
35
+ .command('start')
36
+ .description('Start a new run and return its ID')
37
+ .action(async () => {
38
+ console.log('Starting a new Run on Testomat.io...');
39
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
40
+ const client = new TestomatClient({ apiKey });
41
+
42
+ client.createRun().then(() => {
43
+ console.log(process.env.runId);
44
+ process.exit(0);
45
+ });
46
+ });
47
+
48
+ program
49
+ .command('finish')
50
+ .description('Finish Run by its ID')
51
+ .action(async () => {
52
+ process.env.TESTOMATIO_RUN ||= readLatestRunId();
53
+
54
+ if (!process.env.TESTOMATIO_RUN) {
55
+ console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
56
+ return process.exit(1);
57
+ }
58
+
59
+ console.log('Finishing Run on Testomat.io...');
60
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
61
+ const client = new TestomatClient({ apiKey });
62
+
63
+ // @ts-ignore
64
+ client.updateRunStatus(STATUS.FINISHED).then(() => {
65
+ console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
66
+ process.exit(0);
67
+ });
68
+ });
69
+
70
+ program
71
+ .command('run')
72
+ .description('Run tests with the specified command')
73
+ .argument('<command>', 'Test runner command')
74
+ .option('--filter <filter>', 'Additional execution filter')
75
+ .action(async (command, opts) => {
76
+ const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
77
+ const title = process.env.TESTOMATIO_TITLE;
78
+
79
+ if (!command || !command.split) {
80
+ console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
81
+ return process.exit(255);
82
+ }
83
+
84
+ const client = new TestomatClient({ apiKey, title, parallel: true });
85
+
86
+ if (opts.filter) {
87
+ const [pipe, ...optsArray] = opts.filter.split(':');
88
+ const pipeOptions = optsArray.join(':');
89
+
90
+ try {
91
+ const tests = await client.prepareRun({ pipe, pipeOptions });
92
+ if (tests && tests.length > 0) {
93
+ command += ` --grep (${tests.join('|')})`;
94
+ }
95
+ } catch (err) {
96
+ console.log(APP_PREFIX, err);
97
+ }
98
+ }
99
+
100
+ console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
101
+
102
+ const runTests = () => {
103
+ const testCmds = command.split(' ');
104
+ const cmd = spawn(testCmds[0], testCmds.slice(1), { stdio: 'inherit' });
105
+
106
+ cmd.on('close', code => {
107
+ const emoji = code === 0 ? '🟢' : '🔴';
108
+ console.log(APP_PREFIX, emoji, `Runner exited with ${pc.bold(code)}`);
109
+ if (apiKey) {
110
+ const status = code === 0 ? 'passed' : 'failed';
111
+ client.updateRunStatus(status, true);
112
+ }
113
+ process.exit(code);
114
+ });
115
+ };
116
+
117
+ if (apiKey) {
118
+ client.createRun().then(runTests);
119
+ } else {
120
+ runTests();
121
+ }
122
+ });
123
+
124
+ // program
125
+ // .command('xml')
126
+ // .description('Parse XML reports and upload to Testomat.io')
127
+ // .argument('<pattern>', 'XML file pattern')
128
+ // .option('-d, --dir <dir>', 'Project directory')
129
+ // .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
130
+ // .option('--lang <lang>', 'Language used (python, ruby, java)')
131
+ // .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
132
+ // .action(async (pattern, opts) => {
133
+ // if (!pattern.endsWith('.xml')) {
134
+ // pattern += '.xml';
135
+ // }
136
+ // let { javaTests, lang } = opts;
137
+ // if (javaTests === true) javaTests = 'src/test/java';
138
+ // lang = lang?.toLowerCase();
139
+ // const runReader = new XmlReader({ javaTests, lang });
140
+ // const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
141
+ // if (!files.length) {
142
+ // console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
143
+ // process.exit(1);
144
+ // }
145
+
146
+ program
147
+ .command('xml')
148
+ .description('Parse XML reports and upload to Testomat.io')
149
+ .argument('<pattern>', 'XML file pattern')
150
+ .option('-d, --dir <dir>', 'Project directory')
151
+ .option('--java-tests [java-path]', 'Load Java tests from path, by default: src/test/java')
152
+ .option('--lang <lang>', 'Language used (python, ruby, java)')
153
+ .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
154
+ .action(async (pattern, opts) => {
155
+ if (!pattern.endsWith('.xml')) {
156
+ pattern += '.xml';
157
+ }
158
+ let { javaTests, lang } = opts;
159
+ if (javaTests === true) javaTests = 'src/test/java';
160
+ lang = lang?.toLowerCase();
161
+ const runReader = new XmlReader({ javaTests, lang });
162
+ const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
163
+ if (!files.length) {
164
+ console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
165
+ process.exit(1);
166
+ }
167
+
168
+ for (const file of files) {
169
+ console.log(APP_PREFIX, `Parsed ${file}`);
170
+ runReader.parse(file);
171
+ }
172
+
173
+ let timeoutTimer;
174
+ if (opts.timelimit) {
175
+ timeoutTimer = setTimeout(
176
+ () => {
177
+ console.log(
178
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
179
+ );
180
+ process.exit(0);
181
+ },
182
+ parseInt(opts.timelimit, 10) * 1000,
183
+ );
184
+ }
185
+
186
+ try {
187
+ await runReader.createRun();
188
+ await runReader.uploadData();
189
+ } catch (err) {
190
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
191
+ }
192
+
193
+ if (timeoutTimer) clearTimeout(timeoutTimer);
194
+ });
195
+
196
+ program
197
+ .command('upload-artifacts')
198
+ .description('Upload artifacts to Testomat.io')
199
+ .option('--force', 'Re-upload artifacts even if they were uploaded before')
200
+ .action(async opts => {
201
+ const apiKey = config.TESTOMATIO;
202
+
203
+ process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
204
+ const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
205
+
206
+ if (!runId) {
207
+ console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
208
+ return process.exit(1);
209
+ }
210
+
211
+ const client = new TestomatClient({
212
+ apiKey,
213
+ runId,
214
+ isBatchEnabled: false,
215
+ });
216
+
217
+ let testruns = client.uploader.readUploadedFiles(runId);
218
+ const numTotalArtifacts = testruns.length;
219
+
220
+ debug('Found testruns:', testruns);
221
+
222
+ if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
223
+
224
+ if (!testruns.length) {
225
+ console.log(APP_PREFIX, '🗄️ Total artifacts:', numTotalArtifacts);
226
+ if (numTotalArtifacts) {
227
+ console.log(APP_PREFIX, 'No new artifacts to upload');
228
+ console.log(APP_PREFIX, 'To re-upload artifacts run this command with --force flag');
229
+ }
230
+ process.exit(0);
231
+ }
232
+
233
+ const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
234
+ if (!acc[rid]) {
235
+ acc[rid] = [];
236
+ }
237
+ if (!acc[rid].includes(file)) acc[rid].push(file);
238
+ return acc;
239
+ }, {});
240
+
241
+ await client.createRun();
242
+ client.uploader.checkEnabled();
243
+ client.uploader.disableLogStorage();
244
+
245
+ for (const rid in testrunsByRid) {
246
+ const files = testrunsByRid[rid];
247
+ await client.addTestRun(undefined, { rid, files });
248
+ }
249
+
250
+ console.log(APP_PREFIX, '🗄️', client.uploader.successfulUploads.length, 'artifacts 🟢uploaded');
251
+
252
+ if (client.uploader.successfulUploads.length) {
253
+ debug('\n', APP_PREFIX, `🗄️ ${client.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
254
+ const uploadedArtifacts = client.uploader.successfulUploads.map(file => ({
255
+ relativePath: file.path.replace(process.cwd(), ''),
256
+ link: file.link,
257
+ sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
258
+ }));
259
+
260
+ uploadedArtifacts.forEach(upload => {
261
+ debug(
262
+ `🟢Uploaded artifact`,
263
+ `${upload.relativePath},`,
264
+ 'size:',
265
+ `${upload.sizePretty},`,
266
+ 'link:',
267
+ `${upload.link}`,
268
+ );
269
+ });
270
+ }
271
+
272
+ const filesizeStrMaxLength = 7;
273
+
274
+ if (client.uploader.failedUploads.length) {
275
+ console.log(
276
+ '\n',
277
+ APP_PREFIX,
278
+ '🗄️',
279
+ client.uploader.failedUploads.length,
280
+ `artifacts 🔴${pc.bold('failed')} to upload`,
281
+ );
282
+
283
+ const failedUploads = client.uploader.failedUploads.map(({ path, size }) => ({
284
+ relativePath: path.replace(process.cwd(), ''),
285
+ sizePretty: prettyBytes(size, { round: 0 }).toString(),
286
+ }));
287
+
288
+ const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
289
+ failedUploads.forEach(upload => {
290
+ console.log(
291
+ ` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
292
+ `| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
293
+ )}`,
294
+ );
295
+ });
296
+ }
297
+ });
298
+
299
+ program.parse(process.argv);
300
+
301
+ if (!process.argv.slice(2).length) {
302
+ program.outputHelp();
303
+ }