@testomatio/reporter 2.0.0-beta.2-gaxios → 2.0.0-beta.2-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/package.json +6 -5
- package/src/adapter/nightwatch.js +1 -1
- package/src/adapter/webdriver.js +1 -1
- package/src/bin/cli.js +2 -1
- package/src/bin/reportXml.js +4 -1
- package/src/bin/startTest.js +2 -1
- package/src/bin/uploadArtifacts.js +2 -1
- package/src/junit-adapter/csharp.js +13 -1
- package/src/pipe/bitbucket.js +24 -22
- package/src/pipe/gitlab.js +8 -27
- package/src/pipe/testomatio.js +95 -95
- package/src/utils/utils.js +14 -1
- package/src/xmlReader.js +47 -11
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.0.0-beta.2-
|
|
3
|
+
"version": "2.0.0-beta.2-xml",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -16,7 +16,8 @@
|
|
|
16
16
|
"@cucumber/cucumber": "^10.9.0",
|
|
17
17
|
"@octokit/rest": "^21.1.1",
|
|
18
18
|
"aws-sdk": "^2.1072.0",
|
|
19
|
-
"
|
|
19
|
+
"axios": "^1.6.2",
|
|
20
|
+
"axios-retry": "^3.9.1",
|
|
20
21
|
"callsite-record": "^4.1.4",
|
|
21
22
|
"commander": "^12",
|
|
22
23
|
"cross-spawn": "^7.0.3",
|
|
@@ -48,7 +49,6 @@
|
|
|
48
49
|
"testcafe"
|
|
49
50
|
],
|
|
50
51
|
"scripts": {
|
|
51
|
-
"@cucumber/cucumber": "^10.9.0",
|
|
52
52
|
"clear-exportdir": "rm -rf export/",
|
|
53
53
|
"pretty": "npx prettier --check .",
|
|
54
54
|
"pretty:fix": "prettier --write .",
|
|
@@ -67,8 +67,9 @@
|
|
|
67
67
|
"test:adapter:playwright:example": "npx playwright test --config='./tests/adapter/examples/playwright/playwright.config.ts'",
|
|
68
68
|
"test:adapter:vitest:example": "npx vitest --config='./tests/adapter/examples/vitest/vitest.config.ts'",
|
|
69
69
|
"test:storage": "npx mocha tests-storage/artifact-storage.test.js && npx mocha tests-storage/data-storage.test.js && TESTOMATIO_INTERCEPT_CONSOLE_LOGS=true npx mocha tests-storage/logger.test.js && npx mocha tests-storage/logger-2.test.js && npx mocha tests-storage/reporter-functions.test.js",
|
|
70
|
-
"
|
|
71
|
-
"build": "rm -rf ./cjs &&
|
|
70
|
+
"build": "rm -rf ./cjs && tsc --module commonjs && npx tsx build/scripts/edit-js-files.js && npx tsx build/scripts/edit-package-json.js && chmod +x ./build/scripts/copy-tesmplate.sh && ./build/scripts/copy-tesmplate.sh",
|
|
71
|
+
"build:bun": "rm -rf ./cjs && bun build ./src/reporter.js ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node",
|
|
72
|
+
"build:watch:bun": "rm -rf ./cjs && bun build ./src/bin/reportXml.js ./src/bin/startTest.js ./src/bin/uploadArtifacts.js --outdir ./cjs --target node --watch --onSuccess \"build/scripts/post-build.js\""
|
|
72
73
|
},
|
|
73
74
|
"devDependencies": {
|
|
74
75
|
"@playwright/test": "^1.49.1",
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
import TestomatClient from '../client.js';
|
|
2
2
|
import { config } from '../config.js';
|
|
3
|
-
import { STATUS } from '../constants';
|
|
3
|
+
import { STATUS } from '../constants.js';
|
|
4
4
|
import { getTestomatIdFromTestTitle } from '../utils/utils.js';
|
|
5
5
|
|
|
6
6
|
const apiKey = config.TESTOMATIO;
|
package/src/adapter/webdriver.js
CHANGED
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import WDIOReporter,
|
|
1
|
+
import { default as WDIOReporter, RunnerStats } from '@wdio/reporter';
|
|
2
2
|
import TestomatClient from '../client.js';
|
|
3
3
|
import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
4
4
|
import { services } from '../services/index.js';
|
package/src/bin/cli.js
CHANGED
|
@@ -7,7 +7,7 @@ import createDebugMessages from 'debug';
|
|
|
7
7
|
import TestomatClient from '../client.js';
|
|
8
8
|
import XmlReader from '../xmlReader.js';
|
|
9
9
|
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
10
|
-
import {
|
|
10
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
11
11
|
import { config } from '../config.js';
|
|
12
12
|
import { readLatestRunId } from '../utils/utils.js';
|
|
13
13
|
import pc from 'picocolors';
|
|
@@ -15,6 +15,7 @@ import { filesize as prettyBytes } from 'filesize';
|
|
|
15
15
|
import dotenv from 'dotenv';
|
|
16
16
|
|
|
17
17
|
const debug = createDebugMessages('@testomatio/reporter:xml-cli');
|
|
18
|
+
const version = getPackageVersion();
|
|
18
19
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
19
20
|
const program = new Command();
|
|
20
21
|
|
package/src/bin/reportXml.js
CHANGED
|
@@ -5,8 +5,11 @@ import { glob } from 'glob';
|
|
|
5
5
|
import createDebugMessages from 'debug';
|
|
6
6
|
import { APP_PREFIX } from '../constants.js';
|
|
7
7
|
import XmlReader from '../xmlReader.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
|
+
import path from 'path';
|
|
11
|
+
|
|
12
|
+
const version = getPackageVersion();
|
|
10
13
|
|
|
11
14
|
const debug = createDebugMessages('@testomatio/reporter:xml-cli');
|
|
12
15
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io XML Reporter v${version}`)));
|
package/src/bin/startTest.js
CHANGED
|
@@ -4,10 +4,11 @@ import { Command } from 'commander';
|
|
|
4
4
|
import pc from 'picocolors';
|
|
5
5
|
import TestomatClient from '../client.js';
|
|
6
6
|
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
7
|
-
import {
|
|
7
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
8
8
|
import { config } from '../config.js';
|
|
9
9
|
import dotenv from 'dotenv';
|
|
10
10
|
|
|
11
|
+
const version = getPackageVersion();
|
|
11
12
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
12
13
|
const program = new Command();
|
|
13
14
|
|
|
@@ -5,12 +5,13 @@ import pc from 'picocolors';
|
|
|
5
5
|
import createDebugMessages from 'debug';
|
|
6
6
|
import TestomatClient from '../client.js';
|
|
7
7
|
import { APP_PREFIX } from '../constants.js';
|
|
8
|
-
import {
|
|
8
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
9
9
|
import { config } from '../config.js';
|
|
10
10
|
import { readLatestRunId } from '../utils/utils.js';
|
|
11
11
|
import dotenv from 'dotenv';
|
|
12
12
|
|
|
13
13
|
const debug = createDebugMessages('@testomatio/reporter:upload-cli');
|
|
14
|
+
const version = getPackageVersion();
|
|
14
15
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
15
16
|
const program = new Command();
|
|
16
17
|
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import path from 'path';
|
|
1
2
|
import Adapter from './adapter.js';
|
|
2
3
|
|
|
3
4
|
class CSharpAdapter extends Adapter {
|
|
@@ -7,10 +8,21 @@ class CSharpAdapter extends Adapter {
|
|
|
7
8
|
if (example) t.example = { ...example[1].split(',') };
|
|
8
9
|
const suite = t.suite_title.split('.');
|
|
9
10
|
t.suite_title = suite.pop();
|
|
10
|
-
t.file =
|
|
11
|
+
t.file = namespaceToFileName(t.file);
|
|
11
12
|
t.title = title.trim();
|
|
12
13
|
return t;
|
|
13
14
|
}
|
|
15
|
+
|
|
16
|
+
getFilePath(t) {
|
|
17
|
+
const fileName = namespaceToFileName(t.file);
|
|
18
|
+
return fileName;
|
|
19
|
+
}
|
|
14
20
|
}
|
|
15
21
|
|
|
16
22
|
export default CSharpAdapter;
|
|
23
|
+
|
|
24
|
+
function namespaceToFileName(fileName) {
|
|
25
|
+
const fileParts = fileName.split('.');
|
|
26
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
27
|
+
return `${fileParts.join(path.sep)}.cs`;
|
|
28
|
+
}
|
package/src/pipe/bitbucket.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import { APP_PREFIX, testomatLogoURL } from '../constants.js';
|
|
2
2
|
import { ansiRegExp, isSameTest } from '../utils/utils.js';
|
|
3
3
|
import { statusEmoji, fullName } from '../utils/pipe_utils.js';
|
|
4
|
-
import
|
|
4
|
+
import axios from 'axios';
|
|
5
5
|
import pc from 'picocolors';
|
|
6
6
|
import humanizeDuration from 'humanize-duration';
|
|
7
7
|
import merge from 'lodash.merge';
|
|
@@ -40,13 +40,6 @@ export class BitbucketPipe {
|
|
|
40
40
|
}
|
|
41
41
|
|
|
42
42
|
this.isEnabled = true;
|
|
43
|
-
this.client = new Gaxios({
|
|
44
|
-
baseURL: 'https://api.bitbucket.org/2.0',
|
|
45
|
-
headers: {
|
|
46
|
-
'Content-Type': 'application/json',
|
|
47
|
-
'Authorization': `Bearer ${this.token}`
|
|
48
|
-
}
|
|
49
|
-
});
|
|
50
43
|
|
|
51
44
|
debug('Bitbucket Pipe: Enabled');
|
|
52
45
|
}
|
|
@@ -173,21 +166,26 @@ export class BitbucketPipe {
|
|
|
173
166
|
|
|
174
167
|
// Construct Bitbucket API URL for comments
|
|
175
168
|
// eslint-disable-next-line max-len
|
|
176
|
-
const commentsRequestURL =
|
|
169
|
+
const commentsRequestURL = `https://api.bitbucket.org/2.0/repositories/${this.ENV.BITBUCKET_WORKSPACE}/${this.ENV.BITBUCKET_REPO_SLUG}/pullrequests/${this.ENV.BITBUCKET_PR_ID}/comments`;
|
|
177
170
|
|
|
178
171
|
// Delete previous report
|
|
179
|
-
await deletePreviousReport(
|
|
172
|
+
await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
|
|
180
173
|
|
|
181
174
|
// Add current report
|
|
182
175
|
debug(`Adding comment via URL: ${commentsRequestURL}`);
|
|
183
176
|
debug(`Final Bitbucket API call body: ${body}`);
|
|
184
177
|
|
|
185
178
|
try {
|
|
186
|
-
const addCommentResponse = await
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
179
|
+
const addCommentResponse = await axios.post(
|
|
180
|
+
commentsRequestURL,
|
|
181
|
+
{ content: { raw: body } },
|
|
182
|
+
{
|
|
183
|
+
headers: {
|
|
184
|
+
Authorization: `Bearer ${this.token}`,
|
|
185
|
+
'Content-Type': 'application/json',
|
|
186
|
+
},
|
|
187
|
+
},
|
|
188
|
+
);
|
|
191
189
|
|
|
192
190
|
const commentID = addCommentResponse.data.id;
|
|
193
191
|
// eslint-disable-next-line max-len
|
|
@@ -212,16 +210,18 @@ export class BitbucketPipe {
|
|
|
212
210
|
updateRun() {}
|
|
213
211
|
}
|
|
214
212
|
|
|
215
|
-
async function deletePreviousReport(
|
|
213
|
+
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
216
214
|
if (process.env.BITBUCKET_KEEP_OUTDATED_REPORTS) return;
|
|
217
215
|
|
|
218
216
|
// Get comments
|
|
219
217
|
let comments = [];
|
|
220
218
|
|
|
221
219
|
try {
|
|
222
|
-
const response = await
|
|
223
|
-
|
|
224
|
-
|
|
220
|
+
const response = await axiosInstance.get(commentsRequestURL, {
|
|
221
|
+
headers: {
|
|
222
|
+
Authorization: `Bearer ${token}`,
|
|
223
|
+
'Content-Type': 'application/json',
|
|
224
|
+
},
|
|
225
225
|
});
|
|
226
226
|
comments = response.data.values;
|
|
227
227
|
} catch (e) {
|
|
@@ -236,9 +236,11 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
|
|
|
236
236
|
try {
|
|
237
237
|
// Delete previous comment
|
|
238
238
|
const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
|
|
239
|
-
await
|
|
240
|
-
|
|
241
|
-
|
|
239
|
+
await axiosInstance.delete(deleteCommentURL, {
|
|
240
|
+
headers: {
|
|
241
|
+
Authorization: `Bearer ${token}`,
|
|
242
|
+
'Content-Type': 'application/json',
|
|
243
|
+
},
|
|
242
244
|
});
|
|
243
245
|
} catch (e) {
|
|
244
246
|
console.warn(`Can't delete previously added comment with testomat.io report. Ignored.`);
|
package/src/pipe/gitlab.js
CHANGED
|
@@ -1,5 +1,5 @@
|
|
|
1
1
|
import createDebugMessages from 'debug';
|
|
2
|
-
import
|
|
2
|
+
import axios from 'axios';
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import humanizeDuration from 'humanize-duration';
|
|
5
5
|
import merge from 'lodash.merge';
|
|
@@ -45,12 +45,6 @@ class GitLabPipe {
|
|
|
45
45
|
}
|
|
46
46
|
|
|
47
47
|
this.isEnabled = true;
|
|
48
|
-
this.client = new Gaxios({
|
|
49
|
-
baseURL: 'https://gitlab.com/api/v4',
|
|
50
|
-
headers: {
|
|
51
|
-
'Content-Type': 'application/json',
|
|
52
|
-
}
|
|
53
|
-
});
|
|
54
48
|
|
|
55
49
|
debug('GitLab Pipe: Enabled');
|
|
56
50
|
}
|
|
@@ -163,21 +157,16 @@ class GitLabPipe {
|
|
|
163
157
|
}
|
|
164
158
|
|
|
165
159
|
// eslint-disable-next-line max-len
|
|
166
|
-
const commentsRequestURL =
|
|
160
|
+
const commentsRequestURL = `https://gitlab.com/api/v4/projects/${this.ENV.CI_PROJECT_ID}/merge_requests/${this.ENV.CI_MERGE_REQUEST_IID}/notes`;
|
|
167
161
|
|
|
168
162
|
// delete previous report
|
|
169
|
-
await deletePreviousReport(
|
|
163
|
+
await deletePreviousReport(axios, commentsRequestURL, this.hiddenCommentData, this.token);
|
|
170
164
|
|
|
171
165
|
// add current report
|
|
172
166
|
debug(`Adding comment via url: ${commentsRequestURL}`);
|
|
173
167
|
|
|
174
168
|
try {
|
|
175
|
-
const addCommentResponse = await
|
|
176
|
-
method: 'POST',
|
|
177
|
-
url: commentsRequestURL,
|
|
178
|
-
params: { access_token: this.token },
|
|
179
|
-
data: { body }
|
|
180
|
-
});
|
|
169
|
+
const addCommentResponse = await axios.post(`${commentsRequestURL}?access_token=${this.token}`, { body });
|
|
181
170
|
|
|
182
171
|
const commentID = addCommentResponse.data.id;
|
|
183
172
|
// eslint-disable-next-line max-len
|
|
@@ -202,18 +191,14 @@ class GitLabPipe {
|
|
|
202
191
|
updateRun() {}
|
|
203
192
|
}
|
|
204
193
|
|
|
205
|
-
async function deletePreviousReport(
|
|
194
|
+
async function deletePreviousReport(axiosInstance, commentsRequestURL, hiddenCommentData, token) {
|
|
206
195
|
if (process.env.GITLAB_KEEP_OUTDATED_REPORTS) return;
|
|
207
196
|
|
|
208
197
|
// get comments
|
|
209
198
|
let comments = [];
|
|
210
199
|
|
|
211
200
|
try {
|
|
212
|
-
const response = await
|
|
213
|
-
method: 'GET',
|
|
214
|
-
url: commentsRequestURL,
|
|
215
|
-
params: { access_token: token }
|
|
216
|
-
});
|
|
201
|
+
const response = await axiosInstance.get(`${commentsRequestURL}?access_token=${token}`);
|
|
217
202
|
comments = response.data;
|
|
218
203
|
} catch (e) {
|
|
219
204
|
console.error('Error while attempt to retrieve comments on GitLab Merge Request:\n', e);
|
|
@@ -226,12 +211,8 @@ async function deletePreviousReport(client, commentsRequestURL, hiddenCommentDat
|
|
|
226
211
|
if (comment.body.includes(hiddenCommentData)) {
|
|
227
212
|
try {
|
|
228
213
|
// delete previous comment
|
|
229
|
-
const deleteCommentURL = `${commentsRequestURL}/${comment.id}`;
|
|
230
|
-
await
|
|
231
|
-
method: 'DELETE',
|
|
232
|
-
url: deleteCommentURL,
|
|
233
|
-
params: { access_token: token }
|
|
234
|
-
});
|
|
214
|
+
const deleteCommentURL = `${commentsRequestURL}/${comment.id}?access_token=${token}`;
|
|
215
|
+
await axiosInstance.delete(deleteCommentURL);
|
|
235
216
|
} catch (e) {
|
|
236
217
|
console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
|
|
237
218
|
}
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -1,6 +1,12 @@
|
|
|
1
1
|
import createDebugMessages from 'debug';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
|
|
3
|
+
|
|
4
|
+
// Retry interceptor function
|
|
5
|
+
import axiosRetry from 'axios-retry';
|
|
6
|
+
|
|
7
|
+
// Default axios instance
|
|
8
|
+
import axios from 'axios';
|
|
9
|
+
|
|
4
10
|
import JsonCycle from 'json-cycle';
|
|
5
11
|
import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
|
|
6
12
|
import { isValidUrl, foundedTestLog } from '../utils/utils.js';
|
|
@@ -51,31 +57,43 @@ class TestomatioPipe {
|
|
|
51
57
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
52
58
|
this.env = process.env.TESTOMATIO_ENV;
|
|
53
59
|
this.label = process.env.TESTOMATIO_LABEL;
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
this.client = new Gaxios({
|
|
60
|
+
// Create a new instance of axios with a custom config
|
|
61
|
+
this.axios = axios.create({
|
|
57
62
|
baseURL: `${this.url.trim()}`,
|
|
58
63
|
timeout: AXIOS_TIMEOUT,
|
|
59
|
-
proxy: proxy
|
|
60
|
-
|
|
61
|
-
|
|
62
|
-
|
|
63
|
-
|
|
64
|
-
retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
65
|
-
shouldRetry: (error) => {
|
|
66
|
-
if (!error.response) return false;
|
|
67
|
-
switch (error.response?.status) {
|
|
68
|
-
case 400: // Bad request (probably wrong API key)
|
|
69
|
-
case 404: // Test not matched
|
|
70
|
-
case 429: // Rate limit exceeded
|
|
71
|
-
case 500: // Internal server error
|
|
72
|
-
return false;
|
|
73
|
-
default:
|
|
74
|
-
break;
|
|
64
|
+
proxy: proxy
|
|
65
|
+
? {
|
|
66
|
+
host: proxy.hostname,
|
|
67
|
+
port: parseInt(proxy.port, 10),
|
|
68
|
+
protocol: proxy.protocol,
|
|
75
69
|
}
|
|
76
|
-
|
|
70
|
+
: false,
|
|
71
|
+
});
|
|
72
|
+
|
|
73
|
+
// Pass the axios instance to the retry function
|
|
74
|
+
axiosRetry(this.axios, {
|
|
75
|
+
// do not use retries for unit tests
|
|
76
|
+
retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
|
|
77
|
+
shouldResetTimeout: true,
|
|
78
|
+
retryCondition: error => {
|
|
79
|
+
if (!error.response) return false;
|
|
80
|
+
switch (error.response?.status) {
|
|
81
|
+
case 400: // Bad request (probably wrong API key)
|
|
82
|
+
case 404: // Test not matched
|
|
83
|
+
case 429: // Rate limit exceeded
|
|
84
|
+
case 500: // Internal server error
|
|
85
|
+
return false;
|
|
86
|
+
default:
|
|
87
|
+
break;
|
|
77
88
|
}
|
|
78
|
-
|
|
89
|
+
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
90
|
+
},
|
|
91
|
+
retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
|
|
92
|
+
onRetry: async (retryCount, error) => {
|
|
93
|
+
this.retriesTimestamps.push(Date.now());
|
|
94
|
+
|
|
95
|
+
debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
|
|
96
|
+
},
|
|
79
97
|
});
|
|
80
98
|
|
|
81
99
|
this.isEnabled = true;
|
|
@@ -116,15 +134,12 @@ class TestomatioPipe {
|
|
|
116
134
|
return;
|
|
117
135
|
}
|
|
118
136
|
|
|
119
|
-
const resp = await this.
|
|
120
|
-
|
|
121
|
-
url: '/api/test_grep',
|
|
122
|
-
params: q
|
|
123
|
-
});
|
|
137
|
+
const resp = await this.axios.get('/api/test_grep', q);
|
|
138
|
+
const { data } = resp;
|
|
124
139
|
|
|
125
|
-
if (Array.isArray(
|
|
126
|
-
foundedTestLog(APP_PREFIX,
|
|
127
|
-
return
|
|
140
|
+
if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
|
|
141
|
+
foundedTestLog(APP_PREFIX, data.tests);
|
|
142
|
+
return data.tests;
|
|
128
143
|
}
|
|
129
144
|
|
|
130
145
|
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
@@ -148,6 +163,7 @@ class TestomatioPipe {
|
|
|
148
163
|
|
|
149
164
|
// GitHub Actions Url
|
|
150
165
|
if (!buildUrl && process.env.GITHUB_RUN_ID) {
|
|
166
|
+
// eslint-disable-next-line max-len
|
|
151
167
|
buildUrl = `${process.env.GITHUB_SERVER_URL}/${process.env.GITHUB_REPOSITORY}/actions/runs/${process.env.GITHUB_RUN_ID}`;
|
|
152
168
|
}
|
|
153
169
|
|
|
@@ -183,23 +199,16 @@ class TestomatioPipe {
|
|
|
183
199
|
if (this.runId) {
|
|
184
200
|
this.store.runId = this.runId;
|
|
185
201
|
debug(`Run with id ${this.runId} already created, updating...`);
|
|
186
|
-
const resp = await this.
|
|
187
|
-
method: 'PUT',
|
|
188
|
-
url: `/api/reporter/${this.runId}`,
|
|
189
|
-
data: runParams
|
|
190
|
-
});
|
|
202
|
+
const resp = await this.axios.put(`/api/reporter/${this.runId}`, runParams);
|
|
191
203
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
192
204
|
return;
|
|
193
205
|
}
|
|
194
206
|
|
|
195
207
|
debug('Creating run...');
|
|
196
208
|
try {
|
|
197
|
-
const resp = await this.
|
|
198
|
-
method: 'POST',
|
|
199
|
-
url: '/api/reporter',
|
|
200
|
-
data: runParams,
|
|
209
|
+
const resp = await this.axios.post(`/api/reporter`, runParams, {
|
|
201
210
|
maxContentLength: Infinity,
|
|
202
|
-
|
|
211
|
+
maxBodyLength: Infinity,
|
|
203
212
|
});
|
|
204
213
|
|
|
205
214
|
this.runId = resp.data.uid;
|
|
@@ -216,7 +225,6 @@ class TestomatioPipe {
|
|
|
216
225
|
debug('Run created', this.runId);
|
|
217
226
|
} catch (err) {
|
|
218
227
|
const errorText = err.response?.data?.message || err.message;
|
|
219
|
-
debug('Error creating run', err);
|
|
220
228
|
console.log(errorText || err);
|
|
221
229
|
if (!this.apiKey) console.error('Testomat.io API key is not set');
|
|
222
230
|
if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
|
|
@@ -263,15 +271,7 @@ class TestomatioPipe {
|
|
|
263
271
|
|
|
264
272
|
debug('Adding test', json);
|
|
265
273
|
|
|
266
|
-
return this.
|
|
267
|
-
method: 'POST',
|
|
268
|
-
url: `/api/reporter/${this.runId}/testrun`,
|
|
269
|
-
data: json,
|
|
270
|
-
headers: {
|
|
271
|
-
'Content-Type': 'application/json',
|
|
272
|
-
},
|
|
273
|
-
maxContentLength: Infinity
|
|
274
|
-
}).catch(err => {
|
|
274
|
+
return this.axios.post(`/api/reporter/${this.runId}/testrun`, json, axiosAddTestrunRequestConfig).catch(err => {
|
|
275
275
|
this.requestFailures++;
|
|
276
276
|
this.notReportedTestsCount++;
|
|
277
277
|
if (err.response) {
|
|
@@ -323,43 +323,38 @@ class TestomatioPipe {
|
|
|
323
323
|
const testsToSend = this.batch.tests.splice(0);
|
|
324
324
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
325
325
|
|
|
326
|
-
return this.
|
|
327
|
-
|
|
328
|
-
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
326
|
+
return this.axios
|
|
327
|
+
.post(
|
|
328
|
+
`/api/reporter/${this.runId}/testrun`,
|
|
329
|
+
{ api_key: this.apiKey, tests: testsToSend, batch_index: this.batch.batchIndex },
|
|
330
|
+
axiosAddTestrunRequestConfig,
|
|
331
|
+
)
|
|
332
|
+
.catch(err => {
|
|
333
|
+
this.requestFailures++;
|
|
334
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
335
|
+
if (err.response) {
|
|
336
|
+
if (err.response.status >= 400) {
|
|
337
|
+
const responseData = err.response.data || { message: '' };
|
|
338
|
+
console.log(
|
|
339
|
+
APP_PREFIX,
|
|
340
|
+
pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
341
|
+
// pc.grey(data?.title || ''),
|
|
342
|
+
);
|
|
343
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
344
|
+
this.hasUnmatchedTests = true;
|
|
345
|
+
}
|
|
346
|
+
return;
|
|
347
|
+
}
|
|
344
348
|
console.log(
|
|
345
349
|
APP_PREFIX,
|
|
346
|
-
pc.yellow(`Warning:
|
|
350
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
351
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
347
352
|
);
|
|
348
|
-
|
|
349
|
-
|
|
350
|
-
|
|
351
|
-
return;
|
|
353
|
+
printCreateIssue(err);
|
|
354
|
+
} else {
|
|
355
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
352
356
|
}
|
|
353
|
-
|
|
354
|
-
APP_PREFIX,
|
|
355
|
-
pc.yellow(`Warning: (${err.response?.status})`),
|
|
356
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
357
|
-
);
|
|
358
|
-
printCreateIssue(err);
|
|
359
|
-
} else {
|
|
360
|
-
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
361
|
-
}
|
|
362
|
-
});
|
|
357
|
+
});
|
|
363
358
|
};
|
|
364
359
|
|
|
365
360
|
/**
|
|
@@ -418,16 +413,12 @@ class TestomatioPipe {
|
|
|
418
413
|
|
|
419
414
|
try {
|
|
420
415
|
if (this.runId && !this.proceed) {
|
|
421
|
-
await this.
|
|
422
|
-
|
|
423
|
-
|
|
424
|
-
|
|
425
|
-
|
|
426
|
-
|
|
427
|
-
status_event,
|
|
428
|
-
detach: params.detach,
|
|
429
|
-
tests: params.tests,
|
|
430
|
-
}
|
|
416
|
+
await this.axios.put(`/api/reporter/${this.runId}`, {
|
|
417
|
+
api_key: this.apiKey,
|
|
418
|
+
duration: params.duration,
|
|
419
|
+
status_event,
|
|
420
|
+
detach: params.detach,
|
|
421
|
+
tests: params.tests,
|
|
431
422
|
});
|
|
432
423
|
if (this.runUrl) {
|
|
433
424
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
|
|
@@ -487,11 +478,20 @@ function printCreateIssue(err) {
|
|
|
487
478
|
if (!err.config) return;
|
|
488
479
|
|
|
489
480
|
const time = new Date().toUTCString();
|
|
490
|
-
const {
|
|
481
|
+
const { data, url, baseURL, method } = err?.config || {};
|
|
491
482
|
console.log('```js');
|
|
492
|
-
console.log({
|
|
483
|
+
console.log({ data: data?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
493
484
|
console.log('```');
|
|
494
485
|
});
|
|
495
486
|
}
|
|
496
487
|
|
|
488
|
+
const axiosAddTestrunRequestConfig = {
|
|
489
|
+
maxContentLength: Infinity,
|
|
490
|
+
maxBodyLength: Infinity,
|
|
491
|
+
headers: {
|
|
492
|
+
// Overwrite Axios's automatically set Content-Type
|
|
493
|
+
'Content-Type': 'application/json',
|
|
494
|
+
},
|
|
495
|
+
};
|
|
496
|
+
|
|
497
497
|
export default TestomatioPipe;
|
package/src/utils/utils.js
CHANGED
|
@@ -5,9 +5,13 @@ import fs from 'fs';
|
|
|
5
5
|
import isValid from 'is-valid-path';
|
|
6
6
|
import createDebugMessages from 'debug';
|
|
7
7
|
import os from 'os';
|
|
8
|
+
import { fileURLToPath } from 'url';
|
|
8
9
|
|
|
9
10
|
const debug = createDebugMessages('@testomatio/reporter:util');
|
|
10
11
|
|
|
12
|
+
// Use __dirname directly since we're compiling to CommonJS
|
|
13
|
+
const __dirname = path.resolve();
|
|
14
|
+
|
|
11
15
|
/**
|
|
12
16
|
* @param {String} testTitle - Test title
|
|
13
17
|
*
|
|
@@ -107,7 +111,7 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
107
111
|
.join('\n');
|
|
108
112
|
};
|
|
109
113
|
|
|
110
|
-
const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
114
|
+
export const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
111
115
|
|
|
112
116
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
113
117
|
const comments = code
|
|
@@ -150,6 +154,9 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
150
154
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
151
155
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
152
156
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
157
|
+
} else if (opts.lang === 'csharp') {
|
|
158
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
159
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
153
160
|
} else {
|
|
154
161
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
155
162
|
}
|
|
@@ -353,6 +360,12 @@ function formatStep(step, shift = 0) {
|
|
|
353
360
|
return lines;
|
|
354
361
|
}
|
|
355
362
|
|
|
363
|
+
export function getPackageVersion() {
|
|
364
|
+
const packageJsonPath = path.resolve(__dirname, '../../package.json');
|
|
365
|
+
const packageJson = JSON.parse(fs.readFileSync(packageJsonPath, 'utf8'));
|
|
366
|
+
return packageJson.version;
|
|
367
|
+
}
|
|
368
|
+
|
|
356
369
|
export {
|
|
357
370
|
ansiRegExp,
|
|
358
371
|
isSameTest,
|
package/src/xmlReader.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
fetchSourceCodeFromStackTrace,
|
|
14
14
|
fetchIdFromCode,
|
|
15
15
|
humanize,
|
|
16
|
+
TEST_ID_REGEX,
|
|
16
17
|
} from './utils/utils.js';
|
|
17
18
|
import { pipesFactory } from './pipe/index.js';
|
|
18
19
|
import adapterFactory from './junit-adapter/index.js';
|
|
@@ -26,8 +27,9 @@ const debug = createDebugMessages('@testomatio/reporter:xml');
|
|
|
26
27
|
const ridRunId = randomUUID();
|
|
27
28
|
|
|
28
29
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
29
|
-
const { TESTOMATIO_RUNGROUP_TITLE,
|
|
30
|
-
|
|
30
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE,
|
|
31
|
+
TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV,
|
|
32
|
+
TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED } = process.env;
|
|
31
33
|
|
|
32
34
|
const options = {
|
|
33
35
|
ignoreDeclaration: true,
|
|
@@ -37,6 +39,8 @@ const options = {
|
|
|
37
39
|
parseTagValue: true,
|
|
38
40
|
};
|
|
39
41
|
|
|
42
|
+
const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
|
|
43
|
+
|
|
40
44
|
const reduceOptions = {};
|
|
41
45
|
|
|
42
46
|
class XmlReader {
|
|
@@ -91,7 +95,7 @@ class XmlReader {
|
|
|
91
95
|
];
|
|
92
96
|
|
|
93
97
|
for (const regex of cutRegexes) {
|
|
94
|
-
xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0,
|
|
98
|
+
xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
|
|
95
99
|
}
|
|
96
100
|
|
|
97
101
|
const jsonResult = this.parser.parse(xmlData);
|
|
@@ -341,6 +345,7 @@ class XmlReader {
|
|
|
341
345
|
if (file.endsWith('.rb')) this.stats.language = 'ruby';
|
|
342
346
|
if (file.endsWith('.js')) this.stats.language = 'js';
|
|
343
347
|
if (file.endsWith('.ts')) this.stats.language = 'ts';
|
|
348
|
+
if (file.endsWith('.cs')) this.stats.language = 'csharp';
|
|
344
349
|
}
|
|
345
350
|
|
|
346
351
|
if (!fs.existsSync(file)) {
|
|
@@ -394,13 +399,14 @@ class XmlReader {
|
|
|
394
399
|
async uploadArtifacts() {
|
|
395
400
|
for (const test of this.tests.filter(t => !!t.stack)) {
|
|
396
401
|
let files = [];
|
|
397
|
-
if (test.files?.length)
|
|
398
|
-
|
|
402
|
+
if (!test.files?.length) continue;
|
|
403
|
+
|
|
404
|
+
files = test.files.map(f => path.isAbsolute(f) ? f : path.join(process.cwd(), f));
|
|
399
405
|
|
|
400
406
|
if (!files.length) continue;
|
|
401
407
|
|
|
402
408
|
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
403
|
-
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId])));
|
|
409
|
+
test.artifacts = await Promise.all(files.map(f => this.uploader.uploadFileByPath(f, [runId, path.basename(f)])));
|
|
404
410
|
console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${files.length} artifacts`)} for test ${test.title}`);
|
|
405
411
|
}
|
|
406
412
|
}
|
|
@@ -471,7 +477,7 @@ function reduceTestCases(prev, item) {
|
|
|
471
477
|
testCases
|
|
472
478
|
.filter(t => !!t)
|
|
473
479
|
.forEach(testCaseItem => {
|
|
474
|
-
const file = testCaseItem.file || item.filepath || '';
|
|
480
|
+
const file = testCaseItem.file || item.filepath || item.fullname || '';
|
|
475
481
|
|
|
476
482
|
let stack = '';
|
|
477
483
|
let message = '';
|
|
@@ -505,15 +511,38 @@ function reduceTestCases(prev, item) {
|
|
|
505
511
|
stack = `${
|
|
506
512
|
testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''
|
|
507
513
|
}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
|
|
508
|
-
|
|
514
|
+
let testId = fetchIdFromOutput(stack);
|
|
515
|
+
|
|
516
|
+
if (tags?.length && !testId) {
|
|
517
|
+
testId = tags.filter(t => t.startsWith('T')).map(t => `@${t}`).find(t => t.match(TEST_ID_REGEX))?.slice(2);
|
|
518
|
+
}
|
|
509
519
|
|
|
510
520
|
let status = STATUS.PASSED.toString();
|
|
511
521
|
if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
|
|
512
522
|
if ('skipped' in testCaseItem) status = STATUS.SKIPPED;
|
|
523
|
+
if (testCaseItem.result && Object.values(STATUS).includes(testCaseItem.result.toLowerCase())) {
|
|
524
|
+
status = testCaseItem.result.toLowerCase();
|
|
525
|
+
}
|
|
513
526
|
|
|
514
527
|
let rid = null;
|
|
515
528
|
if (testCaseItem.id) rid = `${ridRunId}-${testCaseItem.id}`;
|
|
516
529
|
|
|
530
|
+
// Extract attachments
|
|
531
|
+
let files = [];
|
|
532
|
+
if (testCaseItem.attachments) {
|
|
533
|
+
const attachments = Array.isArray(testCaseItem.attachments.attachment)
|
|
534
|
+
? testCaseItem.attachments.attachment
|
|
535
|
+
: [testCaseItem.attachments.attachment];
|
|
536
|
+
|
|
537
|
+
files = attachments
|
|
538
|
+
.filter(a => a && a.filePath)
|
|
539
|
+
.map(a => a.filePath);
|
|
540
|
+
}
|
|
541
|
+
|
|
542
|
+
// Extract files from stack trace using existing utility
|
|
543
|
+
const stackFiles = fetchFilesFromStackTrace(stack);
|
|
544
|
+
files = [...new Set([...files, ...stackFiles])]; // Remove duplicates
|
|
545
|
+
|
|
517
546
|
prev.push({
|
|
518
547
|
rid,
|
|
519
548
|
file,
|
|
@@ -528,7 +557,9 @@ function reduceTestCases(prev, item) {
|
|
|
528
557
|
run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
|
|
529
558
|
status,
|
|
530
559
|
title,
|
|
560
|
+
root_suite_id: TESTOMATIO_SUITE,
|
|
531
561
|
suite_title: suiteTitle,
|
|
562
|
+
files,
|
|
532
563
|
});
|
|
533
564
|
});
|
|
534
565
|
return prev;
|
|
@@ -555,10 +586,15 @@ function fetchProperties(item) {
|
|
|
555
586
|
|
|
556
587
|
if (!item.properties) return {};
|
|
557
588
|
|
|
558
|
-
|
|
589
|
+
// Handle both single property and array of properties
|
|
590
|
+
const properties = Array.isArray(item.properties.property)
|
|
591
|
+
? item.properties.property
|
|
592
|
+
: [item.properties.property].filter(Boolean);
|
|
593
|
+
|
|
594
|
+
const prop = properties.find(p => p.name === 'Description');
|
|
559
595
|
if (prop) title = prop.value;
|
|
560
|
-
|
|
561
|
-
|
|
596
|
+
|
|
597
|
+
properties
|
|
562
598
|
.filter(p => p.name === 'Category')
|
|
563
599
|
.forEach(p => tags.push(p.value));
|
|
564
600
|
return { title, tags };
|