@testomatio/reporter 2.7.6 → 2.7.9-beta.1-markdown
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/bin/cli.js +1 -1
- package/lib/constants.d.ts +9 -1
- package/lib/constants.js +22 -4
- package/lib/pipe/coverage.js +1 -1
- package/lib/pipe/debug.d.ts +2 -0
- package/lib/pipe/debug.js +29 -13
- package/lib/pipe/html.js +21 -108
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/markdown.d.ts +25 -0
- package/lib/pipe/markdown.js +698 -0
- package/lib/pipe/testomatio.js +20 -17
- package/lib/replay.d.ts +2 -1
- package/lib/replay.js +20 -15
- package/lib/template/testomatio.hbs +0 -2
- package/lib/utils/debug.d.ts +12 -0
- package/lib/utils/debug.js +27 -0
- package/lib/xmlReader.js +25 -3
- package/package.json +1 -2
- package/src/bin/cli.js +2 -2
- package/src/constants.js +17 -2
- package/src/pipe/coverage.js +2 -2
- package/src/pipe/debug.js +27 -13
- package/src/pipe/html.js +18 -108
- package/src/pipe/index.js +2 -0
- package/src/pipe/markdown.js +743 -0
- package/src/pipe/testomatio.js +28 -14
- package/src/replay.js +23 -17
- package/src/template/testomatio.hbs +0 -2
- package/src/utils/debug.js +20 -0
- package/src/xmlReader.js +27 -3
- package/types/types.d.ts +5 -0
package/lib/pipe/testomatio.js
CHANGED
|
@@ -70,7 +70,7 @@ class TestomatioPipe {
|
|
|
70
70
|
// Create a new instance of gaxios with a custom config
|
|
71
71
|
this.client = new gaxios_1.Gaxios({
|
|
72
72
|
baseURL: `${this.url.trim()}`,
|
|
73
|
-
timeout: constants_js_1.
|
|
73
|
+
timeout: constants_js_1.REQUEST_TIMEOUT,
|
|
74
74
|
proxy: proxy ? proxy.toString() : undefined,
|
|
75
75
|
retry: true,
|
|
76
76
|
retryConfig: {
|
|
@@ -225,6 +225,7 @@ class TestomatioPipe {
|
|
|
225
225
|
method: 'PUT',
|
|
226
226
|
url: `/api/reporter/${this.runId}`,
|
|
227
227
|
data: runParams,
|
|
228
|
+
timeout: (0, constants_js_1.getCreateRunRequestTimeout)(),
|
|
228
229
|
responseType: 'json',
|
|
229
230
|
});
|
|
230
231
|
if (resp.data.artifacts)
|
|
@@ -246,6 +247,7 @@ class TestomatioPipe {
|
|
|
246
247
|
method: 'POST',
|
|
247
248
|
url: '/api/reporter',
|
|
248
249
|
data: runParams,
|
|
250
|
+
timeout: (0, constants_js_1.getCreateRunRequestTimeout)(),
|
|
249
251
|
maxContentLength: Infinity,
|
|
250
252
|
responseType: 'json',
|
|
251
253
|
});
|
|
@@ -262,17 +264,14 @@ class TestomatioPipe {
|
|
|
262
264
|
debug('Run created', this.runId);
|
|
263
265
|
}
|
|
264
266
|
catch (err) {
|
|
267
|
+
if (!this.apiKey)
|
|
268
|
+
console.error('Testomat.io API key is not set');
|
|
265
269
|
const errorText = err.response?.data?.message || err.message;
|
|
266
270
|
debug('Error creating run', err);
|
|
267
|
-
console.log(errorText || err);
|
|
271
|
+
console.log(constants_js_1.APP_PREFIX, errorText || err);
|
|
268
272
|
if (err.response?.status === 403)
|
|
269
273
|
this.#disablePipe();
|
|
270
|
-
|
|
271
|
-
console.error('Testomat.io API key is not set');
|
|
272
|
-
if (!this.apiKey?.startsWith('tstmt'))
|
|
273
|
-
console.error('Testomat.io API key is invalid');
|
|
274
|
-
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG)
|
|
275
|
-
this.#logFailedResponse(err);
|
|
274
|
+
this.#logFailedResponse(err);
|
|
276
275
|
console.error(constants_js_1.APP_PREFIX, 'Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report');
|
|
277
276
|
printCreateIssue();
|
|
278
277
|
}
|
|
@@ -477,9 +476,8 @@ class TestomatioPipe {
|
|
|
477
476
|
}
|
|
478
477
|
}
|
|
479
478
|
catch (err) {
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
this.#logFailedResponse(err);
|
|
479
|
+
console.log(constants_js_1.APP_PREFIX, 'Error updating status, skipping...', err);
|
|
480
|
+
this.#logFailedResponse(err);
|
|
483
481
|
printCreateIssue();
|
|
484
482
|
}
|
|
485
483
|
debug('Run finished');
|
|
@@ -501,18 +499,23 @@ class TestomatioPipe {
|
|
|
501
499
|
responseBody = '<empty>';
|
|
502
500
|
responseBody = hideTestomatioToken(responseBody);
|
|
503
501
|
const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
|
|
504
|
-
const method = error.response?.config
|
|
505
|
-
const url = error.response?.config
|
|
506
|
-
let message = picocolors_1.default.yellow('
|
|
507
|
-
message += picocolors_1.default.bold(`${picocolors_1.default.red(statusCode)} ${method} ${url}\n`);
|
|
502
|
+
const method = error.response?.config?.method || '<unknown method>';
|
|
503
|
+
const url = String(error.response?.config?.url || '<unknown url>');
|
|
504
|
+
let message = picocolors_1.default.yellow('⚠️ Request to Testomat.io failed:\n');
|
|
505
|
+
message += picocolors_1.default.bold(`${picocolors_1.default.red(statusCode)} ${method} ${picocolors_1.default.gray(url)}\n`);
|
|
506
|
+
if (statusCode === 403) {
|
|
507
|
+
message += `\t${picocolors_1.default.red('Please check your API token. It might be invalid or expired.')}\n`;
|
|
508
|
+
}
|
|
508
509
|
message += `\t${picocolors_1.default.bold('response: ')}${picocolors_1.default.gray(responseBody)}\n`;
|
|
509
510
|
const requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
|
|
510
|
-
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG) {
|
|
511
|
+
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
|
|
512
|
+
// full body
|
|
511
513
|
message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(requestBody)}\n`;
|
|
512
514
|
}
|
|
513
515
|
else {
|
|
516
|
+
// cut body
|
|
514
517
|
const requestBodyCut = requestBody.slice(0, 1000);
|
|
515
|
-
message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(`${requestBodyCut}
|
|
518
|
+
message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(`${requestBodyCut}...`)}\n`;
|
|
516
519
|
message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
|
|
517
520
|
}
|
|
518
521
|
console.log(message);
|
package/lib/replay.d.ts
CHANGED
|
@@ -6,7 +6,8 @@ export class Replay {
|
|
|
6
6
|
onLog: any;
|
|
7
7
|
onError: any;
|
|
8
8
|
/**
|
|
9
|
-
* Get the default debug file path
|
|
9
|
+
* Get the default debug file path.
|
|
10
|
+
* Returns ./testomatio.debug.json (actual file in CI, symlink to tmp file in local).
|
|
10
11
|
* @returns {string} Path to the latest debug file
|
|
11
12
|
*/
|
|
12
13
|
getDefaultDebugFile(): string;
|
package/lib/replay.js
CHANGED
|
@@ -6,7 +6,6 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
6
6
|
exports.Replay = void 0;
|
|
7
7
|
const fs_1 = __importDefault(require("fs"));
|
|
8
8
|
const path_1 = __importDefault(require("path"));
|
|
9
|
-
const os_1 = __importDefault(require("os"));
|
|
10
9
|
const client_js_1 = __importDefault(require("./client.js"));
|
|
11
10
|
const constants_js_1 = require("./constants.js");
|
|
12
11
|
const config_js_1 = require("./config.js");
|
|
@@ -19,11 +18,12 @@ class Replay {
|
|
|
19
18
|
this.onError = options.onError || console.error;
|
|
20
19
|
}
|
|
21
20
|
/**
|
|
22
|
-
* Get the default debug file path
|
|
21
|
+
* Get the default debug file path.
|
|
22
|
+
* Returns ./testomatio.debug.json (actual file in CI, symlink to tmp file in local).
|
|
23
23
|
* @returns {string} Path to the latest debug file
|
|
24
24
|
*/
|
|
25
25
|
getDefaultDebugFile() {
|
|
26
|
-
return path_1.default.join(
|
|
26
|
+
return path_1.default.join(process.cwd(), `${constants_js_1.DEBUG_FILE}.json`);
|
|
27
27
|
}
|
|
28
28
|
/**
|
|
29
29
|
* Parse a debug file and extract test data
|
|
@@ -125,8 +125,11 @@ class Replay {
|
|
|
125
125
|
testsWithoutRid.push({ ...test });
|
|
126
126
|
}
|
|
127
127
|
}
|
|
128
|
-
else if (logEntry.
|
|
128
|
+
else if (logEntry.action === 'finishRun') {
|
|
129
129
|
finishParams = logEntry.params || {};
|
|
130
|
+
if (logEntry.runId && !runId) {
|
|
131
|
+
runId = logEntry.runId;
|
|
132
|
+
}
|
|
130
133
|
}
|
|
131
134
|
}
|
|
132
135
|
catch (err) {
|
|
@@ -197,22 +200,25 @@ class Replay {
|
|
|
197
200
|
dryRun: true,
|
|
198
201
|
};
|
|
199
202
|
}
|
|
200
|
-
|
|
203
|
+
if (runId) {
|
|
204
|
+
this.onLog(`Using existing run ID: ${runId}`);
|
|
205
|
+
process.env.TESTOMATIO_RUN = runId;
|
|
206
|
+
}
|
|
207
|
+
else {
|
|
208
|
+
this.onLog('Publishing to run...');
|
|
209
|
+
}
|
|
210
|
+
process.env.TESTOMATIO_REPLAY = '1';
|
|
201
211
|
const client = new client_js_1.default({
|
|
202
212
|
apiKey: this.apiKey,
|
|
203
213
|
isBatchEnabled: true,
|
|
204
214
|
...runParams,
|
|
215
|
+
...(runId && { runId }),
|
|
205
216
|
});
|
|
206
|
-
// Use the stored runId if available, otherwise create a new run
|
|
207
217
|
if (runId) {
|
|
208
|
-
this.onLog(`Using existing run ID: ${runId}`);
|
|
209
218
|
client.runId = runId;
|
|
219
|
+
client.pipeStore.runId = runId;
|
|
210
220
|
}
|
|
211
|
-
|
|
212
|
-
this.onLog('Publishing to run...');
|
|
213
|
-
await client.createRun(runParams);
|
|
214
|
-
}
|
|
215
|
-
// Send each test result
|
|
221
|
+
await client.createRun(runParams);
|
|
216
222
|
let successCount = 0;
|
|
217
223
|
let failureCount = 0;
|
|
218
224
|
for (const [index, test] of tests.entries()) {
|
|
@@ -239,7 +245,8 @@ class Replay {
|
|
|
239
245
|
}
|
|
240
246
|
}
|
|
241
247
|
await client.updateRunStatus(finishParams.status || constants_js_1.STATUS.FINISHED);
|
|
242
|
-
|
|
248
|
+
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
249
|
+
return {
|
|
243
250
|
success: true,
|
|
244
251
|
testsCount: tests.length,
|
|
245
252
|
successCount,
|
|
@@ -249,8 +256,6 @@ class Replay {
|
|
|
249
256
|
envVars,
|
|
250
257
|
runId: runId || client.runId,
|
|
251
258
|
};
|
|
252
|
-
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
253
|
-
return result;
|
|
254
259
|
}
|
|
255
260
|
}
|
|
256
261
|
exports.Replay = Replay;
|
|
@@ -2242,7 +2242,6 @@
|
|
|
2242
2242
|
<div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
|
|
2243
2243
|
<div class='env-var-info'>
|
|
2244
2244
|
<div class='env-var-key'>{{@key}}</div>
|
|
2245
|
-
<div class='env-var-description'>{{this.description}}</div>
|
|
2246
2245
|
</div>
|
|
2247
2246
|
<div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
|
|
2248
2247
|
{{#if this.isSensitive}}
|
|
@@ -2267,7 +2266,6 @@
|
|
|
2267
2266
|
<div class='env-var-item {{#unless this.isSet}}env-var-unset{{/unless}}'>
|
|
2268
2267
|
<div class='env-var-info'>
|
|
2269
2268
|
<div class='env-var-key'>{{@key}}</div>
|
|
2270
|
-
<div class='env-var-description'>{{this.description}}</div>
|
|
2271
2269
|
</div>
|
|
2272
2270
|
<div class='env-var-value {{#unless this.isSet}}env-var-empty{{/unless}} {{#if this.isSensitive}}env-var-confidential{{/if}}'>
|
|
2273
2271
|
{{#if this.isSensitive}}
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Get the debug file path(s).
|
|
3
|
+
*
|
|
4
|
+
* Always creates a timestamped file in tmp dir and a symlink in project root.
|
|
5
|
+
*
|
|
6
|
+
* @param {string} [suffix] - Optional suffix appended to the base name (e.g. 'replay').
|
|
7
|
+
* @returns {{root: string, tmp: string}} root path (symlink), tmp path (actual file)
|
|
8
|
+
*/
|
|
9
|
+
export function getDebugFilePath(suffix?: string): {
|
|
10
|
+
root: string;
|
|
11
|
+
tmp: string;
|
|
12
|
+
};
|
|
@@ -0,0 +1,27 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
exports.getDebugFilePath = getDebugFilePath;
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const os_1 = __importDefault(require("os"));
|
|
9
|
+
const constants_js_1 = require("../constants.js");
|
|
10
|
+
/**
|
|
11
|
+
* Get the debug file path(s).
|
|
12
|
+
*
|
|
13
|
+
* Always creates a timestamped file in tmp dir and a symlink in project root.
|
|
14
|
+
*
|
|
15
|
+
* @param {string} [suffix] - Optional suffix appended to the base name (e.g. 'replay').
|
|
16
|
+
* @returns {{root: string, tmp: string}} root path (symlink), tmp path (actual file)
|
|
17
|
+
*/
|
|
18
|
+
function getDebugFilePath(suffix = '') {
|
|
19
|
+
const dateTime = new Date().toISOString().slice(0, 19).replace(/[:.]/g, '-');
|
|
20
|
+
const baseName = suffix ? `${constants_js_1.DEBUG_FILE}-${suffix}` : constants_js_1.DEBUG_FILE;
|
|
21
|
+
return {
|
|
22
|
+
root: path_1.default.join(process.cwd(), `${baseName}.json`),
|
|
23
|
+
tmp: path_1.default.join(os_1.default.tmpdir(), `${baseName}.${dateTime}.json`),
|
|
24
|
+
};
|
|
25
|
+
}
|
|
26
|
+
|
|
27
|
+
module.exports.getDebugFilePath = getDebugFilePath;
|
package/lib/xmlReader.js
CHANGED
|
@@ -22,15 +22,25 @@ const log_js_1 = require("./utils/log.js");
|
|
|
22
22
|
const debug = (0, debug_1.default)('@testomatio/reporter:xml');
|
|
23
23
|
const ridRunId = (0, crypto_1.randomUUID)();
|
|
24
24
|
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
25
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, TESTOMATIO_LEGACY_NUNIT, } = process.env;
|
|
25
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_MAX_STACK_TRACE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN, TESTOMATIO_MARK_DETACHED, TESTOMATIO_LEGACY_NUNIT, TESTOMATIO_MAX_ENTITY_EXPANSIONS, } = process.env;
|
|
26
|
+
const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
|
|
27
|
+
const MAX_ENTITY_EXPANSIONS = parseInt(TESTOMATIO_MAX_ENTITY_EXPANSIONS, 10) || 10000;
|
|
28
|
+
const ENTITY_EXPANSION_LIMIT_REGEXP = /Entity expansion limit exceeded/i;
|
|
26
29
|
const options = {
|
|
27
30
|
ignoreDeclaration: true,
|
|
28
31
|
ignoreAttributes: false,
|
|
29
32
|
alwaysCreateTextNode: false,
|
|
30
33
|
attributeNamePrefix: '',
|
|
31
34
|
parseTagValue: true,
|
|
35
|
+
processEntities: {
|
|
36
|
+
enabled: true,
|
|
37
|
+
maxEntitySize: 10000,
|
|
38
|
+
maxExpansionDepth: 10,
|
|
39
|
+
maxTotalExpansions: MAX_ENTITY_EXPANSIONS,
|
|
40
|
+
maxExpandedLength: 100000,
|
|
41
|
+
maxEntityCount: 10000,
|
|
42
|
+
},
|
|
32
43
|
};
|
|
33
|
-
const MAX_OUTPUT_LENGTH = parseInt(TESTOMATIO_MAX_STACK_TRACE, 10) || 10000;
|
|
34
44
|
const reduceOptions = {};
|
|
35
45
|
class XmlReader {
|
|
36
46
|
constructor(opts = {}) {
|
|
@@ -84,7 +94,19 @@ class XmlReader {
|
|
|
84
94
|
for (const regex of cutRegexes) {
|
|
85
95
|
xmlData = xmlData.replace(regex, (_, p1, p2, p3) => `${p1}${p2.substring(0, MAX_OUTPUT_LENGTH)}${p3}`);
|
|
86
96
|
}
|
|
87
|
-
|
|
97
|
+
let jsonResult;
|
|
98
|
+
try {
|
|
99
|
+
jsonResult = this.parser.parse(xmlData);
|
|
100
|
+
}
|
|
101
|
+
catch (error) {
|
|
102
|
+
if (ENTITY_EXPANSION_LIMIT_REGEXP.test(error.message)) {
|
|
103
|
+
throw new Error(`${error.message}\n\n` +
|
|
104
|
+
`XML report contains more entity references than the current limit (${MAX_ENTITY_EXPANSIONS}). ` +
|
|
105
|
+
'If this XML report is trusted, increase the limit with TESTOMATIO_MAX_ENTITY_EXPANSIONS, for example:\n' +
|
|
106
|
+
`TESTOMATIO_MAX_ENTITY_EXPANSIONS=${MAX_ENTITY_EXPANSIONS * 2} npx report-xml "{pattern}" --lang={lang}`);
|
|
107
|
+
}
|
|
108
|
+
throw error;
|
|
109
|
+
}
|
|
88
110
|
let jsonSuite;
|
|
89
111
|
if (jsonResult.testsuites) {
|
|
90
112
|
jsonSuite = jsonResult.testsuites;
|
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "2.7.
|
|
3
|
+
"version": "2.7.9-beta.1-markdown",
|
|
4
4
|
"description": "Testomatio Reporter Client",
|
|
5
5
|
"engines": {
|
|
6
6
|
"node": ">=18"
|
|
@@ -17,7 +17,6 @@
|
|
|
17
17
|
"@aws-sdk/lib-storage": "^3.279.0",
|
|
18
18
|
"@cucumber/cucumber": "^10.9.0",
|
|
19
19
|
"@octokit/rest": "^21.1.1",
|
|
20
|
-
"aws-sdk": "^2.1072.0",
|
|
21
20
|
"callsite-record": "^4.1.4",
|
|
22
21
|
"commander": "^12",
|
|
23
22
|
"cross-spawn": "^7.0.3",
|
package/src/bin/cli.js
CHANGED
|
@@ -6,7 +6,7 @@ import { glob } from 'glob';
|
|
|
6
6
|
import createDebugMessages from 'debug';
|
|
7
7
|
import TestomatClient from '../client.js';
|
|
8
8
|
import XmlReader from '../xmlReader.js';
|
|
9
|
-
import { APP_PREFIX, STATUS } from '../constants.js';
|
|
9
|
+
import { APP_PREFIX, STATUS, DEBUG_FILE } from '../constants.js';
|
|
10
10
|
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
11
11
|
import { config } from '../config.js';
|
|
12
12
|
import { readLatestRunId } from '../utils/utils.js';
|
|
@@ -370,7 +370,7 @@ program
|
|
|
370
370
|
program
|
|
371
371
|
.command('replay')
|
|
372
372
|
.description('Replay test data from debug file and re-send to Testomat.io')
|
|
373
|
-
.argument('[debug-file]',
|
|
373
|
+
.argument('[debug-file]', `Path to debug file. Defaults to ./${DEBUG_FILE}.json`)
|
|
374
374
|
.option('--dry-run', 'Preview the data without sending to Testomat.io')
|
|
375
375
|
.action(async (debugFile, opts) => {
|
|
376
376
|
try {
|
package/src/constants.js
CHANGED
|
@@ -8,7 +8,7 @@ const TESTOMATIO_REQUEST_TIMEOUT = parseInt(process.env.TESTOMATIO_REQUEST_TIMEO
|
|
|
8
8
|
if (TESTOMATIO_REQUEST_TIMEOUT) {
|
|
9
9
|
console.log(`${APP_PREFIX} Request timeout is set to ${TESTOMATIO_REQUEST_TIMEOUT / 1000}s`);
|
|
10
10
|
}
|
|
11
|
-
const
|
|
11
|
+
const REQUEST_TIMEOUT = TESTOMATIO_REQUEST_TIMEOUT || 20 * 1000;
|
|
12
12
|
const SCREENSHOTS_ON_STEPS = process.env.TESTOMATIO_SCREENSHOTS_ON_STEPS == null
|
|
13
13
|
|| transformEnvVarToBoolean(process.env.TESTOMATIO_SCREENSHOTS_ON_STEPS);
|
|
14
14
|
|
|
@@ -35,6 +35,12 @@ const HTML_REPORT = {
|
|
|
35
35
|
TEMPLATE_NAME: 'testomatio.hbs',
|
|
36
36
|
};
|
|
37
37
|
|
|
38
|
+
// markdown pipe var
|
|
39
|
+
const MARKDOWN_REPORT = {
|
|
40
|
+
FOLDER: 'md-report',
|
|
41
|
+
REPORT_DEFAULT_NAME: 'testomatio-report.md',
|
|
42
|
+
};
|
|
43
|
+
|
|
38
44
|
const testomatLogoURL = 'https://avatars.githubusercontent.com/u/59105116?s=36&v=4';
|
|
39
45
|
|
|
40
46
|
const REPORTER_REQUEST_RETRIES = {
|
|
@@ -44,14 +50,23 @@ const REPORTER_REQUEST_RETRIES = {
|
|
|
44
50
|
withinTimeSeconds: Number(process.env.TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS) || 60,
|
|
45
51
|
};
|
|
46
52
|
|
|
53
|
+
const DEBUG_FILE = 'testomatio.debug';
|
|
54
|
+
|
|
55
|
+
function getCreateRunRequestTimeout() {
|
|
56
|
+
return Math.max(REQUEST_TIMEOUT, 80 * 1000);
|
|
57
|
+
}
|
|
58
|
+
|
|
47
59
|
export {
|
|
48
60
|
APP_PREFIX,
|
|
49
61
|
TESTOMAT_TMP_STORAGE_DIR,
|
|
50
62
|
CSV_HEADERS,
|
|
51
63
|
STATUS,
|
|
52
64
|
HTML_REPORT,
|
|
53
|
-
|
|
65
|
+
MARKDOWN_REPORT,
|
|
66
|
+
REQUEST_TIMEOUT,
|
|
67
|
+
getCreateRunRequestTimeout,
|
|
54
68
|
testomatLogoURL,
|
|
55
69
|
REPORTER_REQUEST_RETRIES,
|
|
56
70
|
SCREENSHOTS_ON_STEPS,
|
|
71
|
+
DEBUG_FILE,
|
|
57
72
|
};
|
package/src/pipe/coverage.js
CHANGED
|
@@ -4,7 +4,7 @@ import yaml from 'js-yaml';
|
|
|
4
4
|
import { execSync } from 'child_process';
|
|
5
5
|
import { Gaxios } from 'gaxios';
|
|
6
6
|
import { minimatch } from 'minimatch';
|
|
7
|
-
import { APP_PREFIX,
|
|
7
|
+
import { APP_PREFIX, REQUEST_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
|
|
8
8
|
import { generateFilterRequestParams } from '../utils/pipe_utils.js';
|
|
9
9
|
import { parsePipeOptions } from '../utils/pipe_utils.js';
|
|
10
10
|
import { config } from '../config.js';
|
|
@@ -76,7 +76,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
76
76
|
// Create a new instance of gaxios with a custom config
|
|
77
77
|
this.client = new Gaxios({
|
|
78
78
|
baseURL: `${this.url.trim()}`,
|
|
79
|
-
timeout:
|
|
79
|
+
timeout: REQUEST_TIMEOUT,
|
|
80
80
|
proxy: proxy ? proxy.toString() : undefined,
|
|
81
81
|
retry: true,
|
|
82
82
|
retryConfig: {
|
package/src/pipe/debug.js
CHANGED
|
@@ -1,9 +1,9 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
|
-
import os from 'os';
|
|
4
3
|
import createDebugMessages from 'debug';
|
|
5
4
|
import prettyMs from 'pretty-ms';
|
|
6
5
|
import { log } from '../utils/log.js';
|
|
6
|
+
import { getDebugFilePath } from '../utils/debug.js';
|
|
7
7
|
|
|
8
8
|
const debug = createDebugMessages('@testomatio/reporter:pipe:debug');
|
|
9
9
|
|
|
@@ -21,23 +21,33 @@ export class DebugPipe {
|
|
|
21
21
|
tests: [],
|
|
22
22
|
batchIndex: 0,
|
|
23
23
|
};
|
|
24
|
-
|
|
24
|
+
const suffix = process.env.TESTOMATIO_REPLAY ? 'replay' : '';
|
|
25
|
+
const paths = getDebugFilePath(suffix);
|
|
26
|
+
this.logFilePath = paths.tmp;
|
|
27
|
+
this.rootPath = paths.root;
|
|
28
|
+
this.historyDir = path.dirname(paths.tmp);
|
|
25
29
|
|
|
26
30
|
debug('Creating debug file:', this.logFilePath);
|
|
27
31
|
fs.writeFileSync(this.logFilePath, '');
|
|
28
32
|
|
|
29
|
-
// Create symlink
|
|
30
|
-
|
|
33
|
+
// Create symlink in project root pointing to the timestamped debug file.
|
|
34
|
+
// Symlinks may fail on Windows without admin / on filesystems that don't support them;
|
|
35
|
+
// fall back to printing the actual tmp path so the user-facing log isn't misleading.
|
|
31
36
|
try {
|
|
32
|
-
//
|
|
33
|
-
|
|
34
|
-
|
|
37
|
+
// Use lstatSync (not existsSync) so we also detect dangling symlinks —
|
|
38
|
+
// existsSync follows links and returns false when the target is gone,
|
|
39
|
+
// which would leave a stale symlink in place and make symlinkSync fail with EEXIST.
|
|
40
|
+
try {
|
|
41
|
+
fs.lstatSync(paths.root);
|
|
42
|
+
fs.unlinkSync(paths.root);
|
|
43
|
+
} catch (e) {
|
|
44
|
+
if (e.code !== 'ENOENT') throw e;
|
|
35
45
|
}
|
|
36
|
-
|
|
37
|
-
|
|
38
|
-
debug('Created symlink:', symlinkPath, '->', this.logFilePath);
|
|
46
|
+
fs.symlinkSync(this.logFilePath, paths.root);
|
|
47
|
+
debug('Created symlink:', paths.root, '->', this.logFilePath);
|
|
39
48
|
} catch (err) {
|
|
40
|
-
debug('Failed to create symlink:', err.message);
|
|
49
|
+
debug('Failed to create symlink, using tmp path directly:', err.message);
|
|
50
|
+
this.rootPath = this.logFilePath;
|
|
41
51
|
}
|
|
42
52
|
|
|
43
53
|
log.info('🪲 Debug file created');
|
|
@@ -114,8 +124,12 @@ export class DebugPipe {
|
|
|
114
124
|
if (!this.isEnabled) return;
|
|
115
125
|
await this.sync();
|
|
116
126
|
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
117
|
-
|
|
118
|
-
|
|
127
|
+
const logData = { action: 'finishRun', params };
|
|
128
|
+
if (this.store.runId) logData.runId = this.store.runId;
|
|
129
|
+
this.logToFile(logData);
|
|
130
|
+
|
|
131
|
+
log.info(`🪲 Debug file: ${this.rootPath}`);
|
|
132
|
+
log.info(`History: ${this.historyDir}`);
|
|
119
133
|
}
|
|
120
134
|
|
|
121
135
|
async sync() {
|
package/src/pipe/html.js
CHANGED
|
@@ -906,121 +906,31 @@ function dropISayEcho(lines) {
|
|
|
906
906
|
return out;
|
|
907
907
|
}
|
|
908
908
|
|
|
909
|
-
|
|
910
|
-
* Collects all Testomatio and S3 environment variables
|
|
911
|
-
* Uses hardcoded list to avoid file system dependencies for end users
|
|
912
|
-
* @returns {Object} Object with TESTOMATIO_ and S3_ variables grouped
|
|
913
|
-
*/
|
|
914
|
-
function collectEnvironmentVariables() {
|
|
915
|
-
return getHardcodedEnvVars();
|
|
916
|
-
}
|
|
909
|
+
const SENSITIVE_ENV_PATTERNS = [/TOKEN/, /SECRET/, /PASSWORD/, /KEY/, /^TESTOMATIO$/];
|
|
917
910
|
|
|
918
|
-
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
* @param {Set} sensitiveVars - Set of sensitive variable names
|
|
922
|
-
* @returns {Object} Processed environment variables with metadata
|
|
923
|
-
*/
|
|
924
|
-
function processEnvironmentVariables(varConfigs, sensitiveVars) {
|
|
925
|
-
const result = {};
|
|
911
|
+
function isSensitiveEnvName(name) {
|
|
912
|
+
return SENSITIVE_ENV_PATTERNS.some(re => re.test(name));
|
|
913
|
+
}
|
|
926
914
|
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
const isSensitive = sensitiveVars.has(key);
|
|
915
|
+
function collectEnvironmentVariables() {
|
|
916
|
+
const groups = { testomatio: {}, s3: {} };
|
|
930
917
|
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
result[key] = { value: '***', description: config.description, isSet: true, isSensitive: true };
|
|
934
|
-
} else {
|
|
935
|
-
result[key] = { value: '', description: config.description, isSet: false, isSensitive: true };
|
|
936
|
-
}
|
|
937
|
-
} else {
|
|
938
|
-
if (value !== undefined) {
|
|
939
|
-
result[key] = { value, description: config.description, isSet: true };
|
|
940
|
-
} else {
|
|
941
|
-
result[key] = { value: '', description: config.description, isSet: false };
|
|
942
|
-
}
|
|
943
|
-
}
|
|
944
|
-
}
|
|
918
|
+
for (const [name, value] of Object.entries(process.env)) {
|
|
919
|
+
if (value === undefined) continue;
|
|
945
920
|
|
|
946
|
-
|
|
947
|
-
|
|
921
|
+
let group = null;
|
|
922
|
+
if (name === 'TESTOMATIO' || name.startsWith('TESTOMATIO_')) group = 'testomatio';
|
|
923
|
+
else if (name.startsWith('S3_')) group = 's3';
|
|
924
|
+
if (!group) continue;
|
|
948
925
|
|
|
949
|
-
|
|
950
|
-
|
|
951
|
-
|
|
952
|
-
* @returns {Object} Object with TESTOMATIO_ and S3_ variables
|
|
953
|
-
*/
|
|
954
|
-
function getHardcodedEnvVars() {
|
|
955
|
-
const allVars = {
|
|
956
|
-
testomatio: {
|
|
957
|
-
TESTOMATIO: { description: 'API Key for Testomat.io' },
|
|
958
|
-
TESTOMATIO_API_KEY: { description: 'API Key (alias for TESTOMATIO)' },
|
|
959
|
-
TESTOMATIO_CREATE: { description: 'Create new tests in Testomat.io' },
|
|
960
|
-
TESTOMATIO_DEBUG: { description: 'Enable debug mode' },
|
|
961
|
-
TESTOMATIO_DISABLE_BATCH_UPLOAD: { description: 'Disable batch upload' },
|
|
962
|
-
TESTOMATIO_ENV: { description: 'Environment label (e.g., "Windows, Chrome")' },
|
|
963
|
-
TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN: { description: 'Glob pattern to exclude files' },
|
|
964
|
-
TESTOMATIO_EXCLUDE_SKIPPED: { description: 'Exclude skipped tests from report' },
|
|
965
|
-
TESTOMATIO_FILENAME: { description: 'HTML report filename' },
|
|
966
|
-
TESTOMATIO_HTML_FILENAME: { description: 'HTML report filename' },
|
|
967
|
-
TESTOMATIO_HTML_REPORT_FOLDER: { description: 'Folder for HTML report' },
|
|
968
|
-
TESTOMATIO_HTML_REPORT_SAVE: { description: 'Save HTML report' },
|
|
969
|
-
TESTOMATIO_INTERCEPT_CONSOLE_LOGS: { description: 'Intercept console logs' },
|
|
970
|
-
TESTOMATIO_MARK_DETACHED: { description: 'Mark tests as detached' },
|
|
971
|
-
TESTOMATIO_MAX_REQUEST_FAILURES: { description: 'Max request failures' },
|
|
972
|
-
TESTOMATIO_MAX_REQUEST_FAILURES_COUNT: { description: 'Max request failures count' },
|
|
973
|
-
TESTOMATIO_MAX_REQUEST_RETRIES_WITHIN_TIME_SECONDS: { description: 'Max retries within time period' },
|
|
974
|
-
TESTOMATIO_NO_STEPS: { description: 'Disable steps reporting' },
|
|
975
|
-
TESTOMATIO_NO_TIMESTAMP: { description: 'Remove timestamps from logs' },
|
|
976
|
-
TESTOMATIO_PROCEED: { description: 'Proceed even if tests fail' },
|
|
977
|
-
TESTOMATIO_PUBLISH: { description: 'Publish results to Testomat.io' },
|
|
978
|
-
TESTOMATIO_REQUEST_TIMEOUT: { description: 'Request timeout in milliseconds' },
|
|
979
|
-
TESTOMATIO_RUN: { description: 'Run ID to report tests to' },
|
|
980
|
-
TESTOMATIO_RUNGROUP_TITLE: { description: 'Title for run group' },
|
|
981
|
-
TESTOMATIO_SHARED_RUN: { description: 'Share run for parallel execution' },
|
|
982
|
-
TESTOMATIO_SHARED_RUN_TIMEOUT: { description: 'Timeout for shared run (in seconds)' },
|
|
983
|
-
TESTOMATIO_STACK_ARTIFACTS: { description: 'Stack artifacts in report' },
|
|
984
|
-
TESTOMATIO_STACK_FILTER: { description: 'Filter stack traces' },
|
|
985
|
-
TESTOMATIO_STACK_PASSED: { description: 'Report stack for passed tests' },
|
|
986
|
-
TESTOMATIO_STEPS_PASSED: { description: 'Report steps for passed tests' },
|
|
987
|
-
TESTOMATIO_SUITE: { description: 'Suite ID for new tests' },
|
|
988
|
-
TESTOMATIO_TOKEN: { description: 'API Token (alias for TESTOMATIO)' },
|
|
989
|
-
TESTOMATIO_TITLE: { description: 'Title for the test run' },
|
|
990
|
-
TESTOMATIO_URL: { description: 'Testomat.io URL (custom instance)' },
|
|
991
|
-
TESTOMATIO_WORKDIR: { description: 'Working directory for relative paths' },
|
|
992
|
-
},
|
|
993
|
-
s3: {
|
|
994
|
-
S3_ACCESS_KEY_ID: { description: 'S3 access key ID' },
|
|
995
|
-
S3_BUCKET: { description: 'S3 bucket name' },
|
|
996
|
-
S3_ENDPOINT: { description: 'S3 endpoint URL' },
|
|
997
|
-
S3_FORCE_PATH_STYLE: { description: 'S3 force path style' },
|
|
998
|
-
S3_KEY: { description: 'S3 access key' },
|
|
999
|
-
S3_PREFIX: { description: 'S3 key prefix' },
|
|
1000
|
-
S3_REGION: { description: 'S3 region' },
|
|
1001
|
-
S3_SECRET: { description: 'S3 secret key' },
|
|
1002
|
-
S3_SECRET_ACCESS_KEY: { description: 'S3 secret access key' },
|
|
1003
|
-
S3_SESSION_TOKEN: { description: 'S3 session token' },
|
|
1004
|
-
},
|
|
1005
|
-
};
|
|
926
|
+
const isSensitive = isSensitiveEnvName(name);
|
|
927
|
+
let displayValue = value;
|
|
928
|
+
if (isSensitive) displayValue = '***';
|
|
1006
929
|
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
'TESTOMATIO_TOKEN',
|
|
1010
|
-
'TESTOMATIO_API_KEY',
|
|
1011
|
-
'S3_KEY',
|
|
1012
|
-
'S3_SECRET',
|
|
1013
|
-
'S3_ACCESS_KEY_ID',
|
|
1014
|
-
'S3_SECRET_ACCESS_KEY',
|
|
1015
|
-
'S3_SESSION_TOKEN',
|
|
1016
|
-
]);
|
|
1017
|
-
|
|
1018
|
-
const envVars = {
|
|
1019
|
-
testomatio: processEnvironmentVariables(allVars.testomatio, sensitiveVars),
|
|
1020
|
-
s3: processEnvironmentVariables(allVars.s3, sensitiveVars),
|
|
1021
|
-
};
|
|
930
|
+
groups[group][name] = { value: displayValue, isSet: true, isSensitive };
|
|
931
|
+
}
|
|
1022
932
|
|
|
1023
|
-
return
|
|
933
|
+
return groups;
|
|
1024
934
|
}
|
|
1025
935
|
|
|
1026
936
|
/**
|
package/src/pipe/index.js
CHANGED
|
@@ -6,6 +6,7 @@ import GitHubPipe from './github.js';
|
|
|
6
6
|
import GitLabPipe from './gitlab.js';
|
|
7
7
|
import CsvPipe from './csv.js';
|
|
8
8
|
import HtmlPipe from './html.js';
|
|
9
|
+
import MarkdownPipe from './markdown.js';
|
|
9
10
|
import CoveragePipe from './coverage.js';
|
|
10
11
|
import { BitbucketPipe } from './bitbucket.js';
|
|
11
12
|
import { DebugPipe } from './debug.js';
|
|
@@ -48,6 +49,7 @@ export async function pipesFactory(params, opts) {
|
|
|
48
49
|
new GitLabPipe(params, opts),
|
|
49
50
|
new CsvPipe(params, opts),
|
|
50
51
|
new HtmlPipe(params, opts),
|
|
52
|
+
new MarkdownPipe(params, opts),
|
|
51
53
|
new BitbucketPipe(params, opts),
|
|
52
54
|
new CoveragePipe(params, opts),
|
|
53
55
|
new DebugPipe(params, opts),
|