@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.
- package/lib/adapter/codecept.d.ts +2 -0
- package/lib/adapter/codecept.js +31 -26
- package/lib/adapter/cucumber/current.d.ts +14 -0
- package/lib/adapter/cucumber/legacy.d.ts +0 -0
- package/lib/adapter/cucumber.d.ts +2 -0
- package/lib/adapter/cypress-plugin/index.d.ts +2 -0
- package/lib/adapter/cypress-plugin/index.js +10 -10
- package/lib/adapter/jasmine.d.ts +11 -0
- package/lib/adapter/jest.d.ts +13 -0
- package/lib/adapter/mocha.d.ts +2 -0
- package/lib/adapter/mocha.js +4 -4
- package/lib/adapter/nightwatch.d.ts +4 -0
- package/lib/adapter/nightwatch.js +80 -0
- package/lib/adapter/playwright.d.ts +14 -0
- package/lib/adapter/playwright.js +58 -33
- package/lib/adapter/vitest.d.ts +35 -0
- package/lib/adapter/vitest.js +6 -6
- package/lib/adapter/webdriver.d.ts +24 -0
- package/lib/adapter/webdriver.js +51 -14
- package/lib/bin/cli.d.ts +2 -0
- package/lib/bin/cli.js +250 -0
- package/lib/bin/reportXml.d.ts +2 -0
- package/lib/bin/reportXml.js +15 -11
- package/lib/bin/startTest.d.ts +2 -0
- package/lib/bin/startTest.js +12 -7
- package/lib/bin/uploadArtifacts.d.ts +2 -0
- package/lib/bin/uploadArtifacts.js +82 -0
- package/lib/client.d.ts +76 -0
- package/lib/client.js +128 -53
- package/lib/config.d.ts +1 -0
- package/lib/config.js +2 -2
- package/lib/constants.d.ts +25 -0
- package/lib/constants.js +5 -1
- package/lib/data-storage.d.ts +34 -0
- package/lib/data-storage.js +19 -9
- package/lib/junit-adapter/adapter.d.ts +9 -0
- package/lib/junit-adapter/csharp.d.ts +5 -0
- package/lib/junit-adapter/csharp.js +11 -1
- package/lib/junit-adapter/index.d.ts +3 -0
- package/lib/junit-adapter/java.d.ts +5 -0
- package/lib/junit-adapter/javascript.d.ts +4 -0
- package/lib/junit-adapter/python.d.ts +5 -0
- package/lib/junit-adapter/ruby.d.ts +4 -0
- package/lib/output.d.ts +11 -0
- package/lib/package.json +3 -1
- package/lib/pipe/bitbucket.d.ts +23 -0
- package/lib/pipe/bitbucket.js +19 -9
- package/lib/pipe/csv.d.ts +47 -0
- package/lib/pipe/csv.js +2 -2
- package/lib/pipe/debug.d.ts +29 -0
- package/lib/pipe/debug.js +108 -0
- package/lib/pipe/github.d.ts +30 -0
- package/lib/pipe/github.js +37 -5
- package/lib/pipe/gitlab.d.ts +23 -0
- package/lib/pipe/gitlab.js +2 -3
- package/lib/pipe/html.d.ts +35 -0
- package/lib/pipe/html.js +9 -4
- package/lib/pipe/index.d.ts +1 -0
- package/lib/pipe/index.js +20 -10
- package/lib/pipe/testomatio.d.ts +70 -0
- package/lib/pipe/testomatio.js +54 -39
- package/lib/reporter-functions.d.ts +34 -0
- package/lib/reporter-functions.js +17 -7
- package/lib/reporter.d.ts +232 -0
- package/lib/reporter.js +19 -33
- package/lib/services/artifacts.d.ts +33 -0
- package/lib/services/index.d.ts +9 -0
- package/lib/services/key-values.d.ts +27 -0
- package/lib/services/key-values.js +1 -1
- package/lib/services/logger.d.ts +64 -0
- package/lib/services/logger.js +1 -2
- package/lib/template/testomatio.hbs +651 -1366
- package/lib/uploader.d.ts +60 -0
- package/lib/uploader.js +312 -0
- package/lib/utils/pipe_utils.d.ts +41 -0
- package/lib/utils/pipe_utils.js +3 -5
- package/lib/utils/utils.d.ts +47 -0
- package/lib/utils/utils.js +99 -12
- package/lib/xmlReader.d.ts +92 -0
- package/lib/xmlReader.js +64 -25
- package/package.json +19 -13
- package/src/adapter/codecept.js +30 -26
- package/src/adapter/cypress-plugin/index.js +5 -5
- package/src/adapter/mocha.cjs +1 -1
- package/src/adapter/mocha.js +4 -4
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +59 -31
- package/src/adapter/vitest.js +6 -6
- package/src/adapter/webdriver.js +42 -12
- package/src/bin/cli.js +303 -0
- package/src/bin/reportXml.js +19 -9
- package/src/bin/startTest.js +9 -4
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +137 -57
- package/src/config.js +2 -2
- package/src/constants.js +5 -1
- package/src/data-storage.js +2 -2
- package/src/junit-adapter/csharp.js +13 -1
- package/src/pipe/bitbucket.js +2 -2
- package/src/pipe/csv.js +3 -3
- package/src/pipe/debug.js +104 -0
- package/src/pipe/github.js +3 -5
- package/src/pipe/gitlab.js +6 -7
- package/src/pipe/html.js +14 -7
- package/src/pipe/index.js +5 -7
- package/src/pipe/testomatio.js +75 -76
- package/src/reporter-functions.js +18 -7
- package/src/reporter.cjs_decprecated +21 -0
- package/src/reporter.js +20 -11
- package/src/services/key-values.js +1 -1
- package/src/services/logger.js +5 -4
- package/src/template/testomatio.hbs +651 -1366
- package/src/uploader.js +371 -0
- package/src/utils/pipe_utils.js +4 -12
- package/src/utils/utils.js +64 -15
- package/src/xmlReader.js +76 -26
- package/lib/adapter/jasmine/jasmine.js +0 -63
- package/lib/adapter/mocha/mocha.js +0 -125
- package/lib/fileUploader.js +0 -245
- package/lib/utils/chalk.js +0 -10
- package/src/fileUploader.js +0 -307
- package/src/reporter.cjs +0 -22
- 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
|
|
59
|
+
const testMeta = services.keyValues.get(fullTestTitle);
|
|
61
60
|
const rid = test.id || test.testId || uuidv4();
|
|
62
61
|
|
|
63
|
-
|
|
64
|
-
|
|
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:
|
|
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
|
-
|
|
98
|
-
|
|
99
|
-
|
|
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
|
|
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
|
-
|
|
116
|
-
|
|
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 =
|
|
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
|
|
235
|
+
function generateTmpFilepath(filename = '') {
|
|
236
|
+
filename = filename || `tmp.${crypto.randomBytes(16).toString('hex')}`;
|
|
194
237
|
const tmpdir = os.tmpdir();
|
|
195
|
-
return path.join(tmpdir,
|
|
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 };
|
package/src/adapter/vitest.js
CHANGED
|
@@ -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 {
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
|
|
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
|
+
}
|