@testomatio/reporter 2.15.0-beta.1-json-output → 2.15.0
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.js +11 -1
- package/lib/bin/cli.js +12 -6
- package/lib/client.js +4 -4
- package/lib/pipe/csv.js +5 -4
- package/lib/pipe/html.js +9 -8
- package/lib/pipe/testomatio.js +41 -23
- package/lib/utils/hide_token.d.ts +8 -0
- package/lib/utils/hide_token.js +17 -0
- package/lib/utils/log.d.ts +15 -0
- package/lib/utils/log.js +75 -7
- package/package.json +1 -1
- package/src/adapter/codecept.js +13 -1
- package/src/bin/cli.js +12 -5
- package/src/client.js +4 -4
- package/src/pipe/csv.js +5 -4
- package/src/pipe/html.js +9 -8
- package/src/pipe/testomatio.js +40 -25
- package/src/utils/hide_token.js +12 -0
- package/src/utils/log.js +73 -7
package/lib/adapter/codecept.js
CHANGED
|
@@ -259,7 +259,7 @@ function CodeceptReporter(config) {
|
|
|
259
259
|
processArtifactsForUpload(artifacts, uid, title, videos, traces);
|
|
260
260
|
});
|
|
261
261
|
event.dispatcher.on(event.step.started, step => {
|
|
262
|
-
const stepText = `${repeat(output.stepShift)} ${
|
|
262
|
+
const stepText = `${repeat(output.stepShift)} ${formatStepLog(step)}`;
|
|
263
263
|
data_storage_js_1.dataStorage.putData('log', stepText);
|
|
264
264
|
});
|
|
265
265
|
event.dispatcher.on(event.step.finished, step => {
|
|
@@ -316,6 +316,16 @@ function stripTagsFromTitle(title) {
|
|
|
316
316
|
function repeat(num) {
|
|
317
317
|
return ''.padStart(num, ' ');
|
|
318
318
|
}
|
|
319
|
+
function formatStepLog(step) {
|
|
320
|
+
if (typeof step?.toCliStyled === 'function')
|
|
321
|
+
return step.toCliStyled();
|
|
322
|
+
if (typeof step?.toString === 'function' && step.toString !== Object.prototype.toString) {
|
|
323
|
+
return step.toString();
|
|
324
|
+
}
|
|
325
|
+
const title = step?.title || '';
|
|
326
|
+
const args = Array.isArray(step?.args) ? step.args.join(', ') : '';
|
|
327
|
+
return `${title}${args ? ` ${args}` : ''}`.trim();
|
|
328
|
+
}
|
|
319
329
|
// Helper functions for cleaner event handling
|
|
320
330
|
function initializeTestDataStore() {
|
|
321
331
|
if (!global.testomatioDataStore)
|
package/lib/bin/cli.js
CHANGED
|
@@ -45,6 +45,9 @@ program
|
|
|
45
45
|
if (subOpts.filterList || subOpts.format) {
|
|
46
46
|
process.env.TESTOMATIO_LOG_STDERR = '1';
|
|
47
47
|
process.env.TESTOMATIO_LOG_LEVEL ||= 'WARN';
|
|
48
|
+
// with --format json the logs are machine-readable too: one JSON object per line on stderr
|
|
49
|
+
if (subOpts.format === 'json')
|
|
50
|
+
process.env.TESTOMATIO_LOG_JSON = '1';
|
|
48
51
|
}
|
|
49
52
|
else {
|
|
50
53
|
console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
@@ -80,7 +83,7 @@ program
|
|
|
80
83
|
};
|
|
81
84
|
}
|
|
82
85
|
await client.createRun(createRunParams);
|
|
83
|
-
const runId = client.pipeStore.runId
|
|
86
|
+
const runId = client.pipeStore.runId;
|
|
84
87
|
if (!runId) {
|
|
85
88
|
log_js_1.log.error(picocolors_1.default.red('Failed to create run on Testomat.io.'));
|
|
86
89
|
process.exit(1);
|
|
@@ -90,7 +93,7 @@ program
|
|
|
90
93
|
const plannedTests = (client.pipeStore.preparedTestIds || []).map(id => ({ test_id: id, title: id }));
|
|
91
94
|
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
92
95
|
// stdout carries ONLY the run data so it can be captured: RUN_ID=$(reporter start)
|
|
93
|
-
console.log((0, pipe_utils_js_1.formatRunOutput)(
|
|
96
|
+
console.log((0, pipe_utils_js_1.formatRunOutput)(client.pipeStore, opts.format));
|
|
94
97
|
process.exit(0);
|
|
95
98
|
});
|
|
96
99
|
program
|
|
@@ -226,16 +229,19 @@ program
|
|
|
226
229
|
}
|
|
227
230
|
if (apiKey) {
|
|
228
231
|
await client.createRun(createRunParams);
|
|
229
|
-
const runId =
|
|
232
|
+
const runId = client.pipeStore.runId;
|
|
233
|
+
if (!runId) {
|
|
234
|
+
log_js_1.log.error(picocolors_1.default.red('Failed to create run on Testomat.io.'));
|
|
235
|
+
process.exit(1);
|
|
236
|
+
}
|
|
230
237
|
if (client.pipeStore.runUrl)
|
|
231
238
|
log_js_1.log.info(`📊 Report URL: ${picocolors_1.default.magenta(client.pipeStore.runUrl)}`);
|
|
232
239
|
if (opts.kind !== 'manual') {
|
|
233
240
|
log_js_1.log.info(`No command passed, so you need to run tests yourself:`);
|
|
234
241
|
log_js_1.log.info(`TESTOMATIO_RUN=${runId} <command>`);
|
|
235
242
|
}
|
|
236
|
-
|
|
237
|
-
|
|
238
|
-
console.log(runOutput);
|
|
243
|
+
if (opts.format)
|
|
244
|
+
console.log((0, pipe_utils_js_1.formatRunOutput)(client.pipeStore, opts.format));
|
|
239
245
|
}
|
|
240
246
|
else {
|
|
241
247
|
log_js_1.log.info('⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
|
package/lib/client.js
CHANGED
|
@@ -175,7 +175,7 @@ class Client {
|
|
|
175
175
|
}
|
|
176
176
|
}
|
|
177
177
|
catch (err) {
|
|
178
|
-
|
|
178
|
+
log_js_1.log.error('Error in uploadStepArtifacts for testRid', testRid, ':', err.message || err);
|
|
179
179
|
throw err;
|
|
180
180
|
}
|
|
181
181
|
}
|
|
@@ -209,7 +209,7 @@ class Client {
|
|
|
209
209
|
await this.uploadStepArtifacts(steps, rid);
|
|
210
210
|
}
|
|
211
211
|
catch (err) {
|
|
212
|
-
|
|
212
|
+
log_js_1.log.error('Failed to upload step artifacts:', err.message || err);
|
|
213
213
|
}
|
|
214
214
|
const uploadedFiles = [];
|
|
215
215
|
const stackArtifactsEnabled = (0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
@@ -357,7 +357,7 @@ class Client {
|
|
|
357
357
|
}));
|
|
358
358
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
359
359
|
failedUploads.forEach(upload => {
|
|
360
|
-
|
|
360
|
+
log_js_1.log.info(` ${picocolors_1.default.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${picocolors_1.default.gray(`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`)}`);
|
|
361
361
|
});
|
|
362
362
|
}
|
|
363
363
|
if (this.uploader.skippedUploads.length) {
|
|
@@ -368,7 +368,7 @@ class Client {
|
|
|
368
368
|
}));
|
|
369
369
|
const pathPadding = Math.max(...skippedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
370
370
|
skippedUploads.forEach(upload => {
|
|
371
|
-
|
|
371
|
+
log_js_1.log.info(` ${picocolors_1.default.gray('|')} 🟡 ${upload.relativePath.padEnd(pathPadding)} ${picocolors_1.default.gray(`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`)}`);
|
|
372
372
|
});
|
|
373
373
|
}
|
|
374
374
|
if (this.uploader.skippedUploads.length || this.uploader.failedUploads.length) {
|
package/lib/pipe/csv.js
CHANGED
|
@@ -11,6 +11,7 @@ const picocolors_1 = __importDefault(require("picocolors"));
|
|
|
11
11
|
const lodash_merge_1 = __importDefault(require("lodash.merge"));
|
|
12
12
|
const utils_js_1 = require("../utils/utils.js");
|
|
13
13
|
const constants_js_1 = require("../constants.js");
|
|
14
|
+
const log_js_1 = require("../utils/log.js");
|
|
14
15
|
const debug = (0, debug_1.default)('@testomatio/reporter:pipe:csv');
|
|
15
16
|
/**
|
|
16
17
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
@@ -68,10 +69,10 @@ class CsvPipe {
|
|
|
68
69
|
// First, we check whether the export directory exists: if yes - OK, no - create it.
|
|
69
70
|
this.checkExportDir();
|
|
70
71
|
if (!this.outputFile) {
|
|
71
|
-
|
|
72
|
+
log_js_1.log.warn(picocolors_1.default.yellow(`⚠️ CSV file is not set, ignoring`));
|
|
72
73
|
return;
|
|
73
74
|
}
|
|
74
|
-
|
|
75
|
+
log_js_1.log.info(picocolors_1.default.yellow(`⏳ The test results will be added to the csv. It will take some time...`));
|
|
75
76
|
try {
|
|
76
77
|
// Create csv writer object
|
|
77
78
|
const writer = (0, csv_writer_1.createObjectCsvWriter)({
|
|
@@ -82,7 +83,7 @@ class CsvPipe {
|
|
|
82
83
|
return await writer.writeRecords(data);
|
|
83
84
|
}
|
|
84
85
|
catch (e) {
|
|
85
|
-
|
|
86
|
+
log_js_1.log.error('Unknown csv error: ', e);
|
|
86
87
|
}
|
|
87
88
|
}
|
|
88
89
|
/**
|
|
@@ -122,7 +123,7 @@ class CsvPipe {
|
|
|
122
123
|
// Save results based on the default headers
|
|
123
124
|
if (this.isEnabled) {
|
|
124
125
|
await this.saveToCsv(this.results, constants_js_1.CSV_HEADERS);
|
|
125
|
-
|
|
126
|
+
log_js_1.log.info(picocolors_1.default.green(`🗃️ Recording completed! You can check the result in file = ${this.outputFile}`));
|
|
126
127
|
}
|
|
127
128
|
}
|
|
128
129
|
toString() {
|
package/lib/pipe/html.js
CHANGED
|
@@ -13,6 +13,7 @@ const marked_1 = require("marked");
|
|
|
13
13
|
const file_url_1 = __importDefault(require("file-url"));
|
|
14
14
|
const utils_js_1 = require("../utils/utils.js");
|
|
15
15
|
const constants_js_1 = require("../constants.js");
|
|
16
|
+
const log_js_1 = require("../utils/log.js");
|
|
16
17
|
const node_url_1 = require("node:url");
|
|
17
18
|
const debug = (0, debug_1.default)('@testomatio/reporter:pipe:html');
|
|
18
19
|
const HTML_ARTIFACTS_DIR = 'artifacts';
|
|
@@ -120,12 +121,12 @@ class HtmlPipe {
|
|
|
120
121
|
const { runParams, tests, outputPath, templatePath, warningMsg: msg } = opts;
|
|
121
122
|
debug('HTML tests data:', tests);
|
|
122
123
|
if (!outputPath) {
|
|
123
|
-
|
|
124
|
+
log_js_1.log.warn(picocolors_1.default.yellow(`🚨 HTML export path is not set, ignoring...`));
|
|
124
125
|
return;
|
|
125
126
|
}
|
|
126
|
-
|
|
127
|
+
log_js_1.log.info(picocolors_1.default.yellow(`⏳ The test results will be added to the HTML report. It will take some time...`));
|
|
127
128
|
if (msg) {
|
|
128
|
-
|
|
129
|
+
log_js_1.log.info(picocolors_1.default.blue(msg));
|
|
129
130
|
}
|
|
130
131
|
const aggregatedTests = aggregateTestRetries(tests);
|
|
131
132
|
const copyLocalArtifacts = resolveHtmlCopyArtifacts(this.htmlCopyArtifacts);
|
|
@@ -237,10 +238,10 @@ class HtmlPipe {
|
|
|
237
238
|
// Convert the file path to a file URL
|
|
238
239
|
const fileUrlPath = (0, file_url_1.default)(absolutePath, { resolve: true });
|
|
239
240
|
debug('HTML tests data:', fileUrlPath);
|
|
240
|
-
|
|
241
|
+
log_js_1.log.info(picocolors_1.default.green(`📊 The HTML report was successfully generated. Full filepath: ${fileUrlPath}`));
|
|
241
242
|
}
|
|
242
243
|
else {
|
|
243
|
-
|
|
244
|
+
log_js_1.log.error(picocolors_1.default.red(`🚨 Failed to generate the HTML report.`));
|
|
244
245
|
}
|
|
245
246
|
}
|
|
246
247
|
/**
|
|
@@ -251,7 +252,7 @@ class HtmlPipe {
|
|
|
251
252
|
*/
|
|
252
253
|
#generateHTMLReport(data, templatePath = '') {
|
|
253
254
|
if (!templatePath) {
|
|
254
|
-
|
|
255
|
+
log_js_1.log.error(picocolors_1.default.red(`🚨 HTML template not found. Report generation is impossible!`));
|
|
255
256
|
return;
|
|
256
257
|
}
|
|
257
258
|
const templateSource = fs_1.default.readFileSync(templatePath, 'utf8');
|
|
@@ -261,8 +262,8 @@ class HtmlPipe {
|
|
|
261
262
|
return template(data);
|
|
262
263
|
}
|
|
263
264
|
catch (e) {
|
|
264
|
-
|
|
265
|
-
|
|
265
|
+
log_js_1.log.error(picocolors_1.default.red('❌ Oops! An unknown error occurred when generating an HTML report'));
|
|
266
|
+
log_js_1.log.error(picocolors_1.default.red(e));
|
|
266
267
|
}
|
|
267
268
|
}
|
|
268
269
|
#loadReportHelpers() {
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -10,6 +10,7 @@ const json_cycle_1 = __importDefault(require("json-cycle"));
|
|
|
10
10
|
const constants_js_1 = require("../constants.js");
|
|
11
11
|
const utils_js_1 = require("../utils/utils.js");
|
|
12
12
|
const pipe_utils_js_1 = require("../utils/pipe_utils.js");
|
|
13
|
+
const hide_token_js_1 = require("../utils/hide_token.js");
|
|
13
14
|
const config_js_1 = require("../config.js");
|
|
14
15
|
const log_js_1 = require("../utils/log.js");
|
|
15
16
|
const debug = (0, debug_1.default)('@testomatio/reporter:pipe:testomatio');
|
|
@@ -346,14 +347,14 @@ class TestomatioPipe {
|
|
|
346
347
|
}
|
|
347
348
|
catch (err) {
|
|
348
349
|
if (!this.apiKey)
|
|
349
|
-
|
|
350
|
+
log_js_1.log.error('Testomat.io API key is not set');
|
|
350
351
|
const errorText = err.response?.data?.message || err.message;
|
|
351
352
|
debug('Error creating run', err);
|
|
352
|
-
|
|
353
|
+
log_js_1.log.error(errorText || err);
|
|
353
354
|
if (err.response?.status === 403)
|
|
354
355
|
this.#disablePipe();
|
|
355
356
|
this.#logFailedResponse(err);
|
|
356
|
-
|
|
357
|
+
log_js_1.log.error('Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report');
|
|
357
358
|
printCreateIssue();
|
|
358
359
|
}
|
|
359
360
|
debug('"createRun" function finished');
|
|
@@ -370,7 +371,7 @@ class TestomatioPipe {
|
|
|
370
371
|
this.reportingCanceledDueToReqFailures = true;
|
|
371
372
|
let errorMessage = `⚠️ ${process.env.TESTOMATIO_MAX_REQUEST_FAILURES}`;
|
|
372
373
|
errorMessage += ' requests were failed, reporting to Testomat aborted.';
|
|
373
|
-
|
|
374
|
+
log_js_1.log.warn(picocolors_1.default.yellow(errorMessage));
|
|
374
375
|
}
|
|
375
376
|
return cancelReporting;
|
|
376
377
|
}
|
|
@@ -513,7 +514,7 @@ class TestomatioPipe {
|
|
|
513
514
|
debug('Finishing run...');
|
|
514
515
|
if (this.reportingCanceledDueToReqFailures) {
|
|
515
516
|
const errorMessage = picocolors_1.default.red(`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`);
|
|
516
|
-
|
|
517
|
+
log_js_1.log.warn(errorMessage);
|
|
517
518
|
}
|
|
518
519
|
const { status } = params;
|
|
519
520
|
// a pending run was just created: nothing to update here, only other pipes report it
|
|
@@ -561,7 +562,7 @@ class TestomatioPipe {
|
|
|
561
562
|
}
|
|
562
563
|
}
|
|
563
564
|
catch (err) {
|
|
564
|
-
|
|
565
|
+
log_js_1.log.error('Error updating status, skipping...', err.message || err);
|
|
565
566
|
this.#logFailedResponse(err);
|
|
566
567
|
printCreateIssue();
|
|
567
568
|
}
|
|
@@ -582,7 +583,7 @@ class TestomatioPipe {
|
|
|
582
583
|
let responseBody = stringify(error.response?.data ?? error.response ?? error, { pretty: true });
|
|
583
584
|
if (!responseBody)
|
|
584
585
|
responseBody = '<empty>';
|
|
585
|
-
responseBody = hideTestomatioToken(responseBody);
|
|
586
|
+
responseBody = (0, hide_token_js_1.hideTestomatioToken)(responseBody);
|
|
586
587
|
const statusCode = error.status || error.code || error.response?.status || '<unknown status code>';
|
|
587
588
|
const method = error.response?.config?.method || '<unknown method>';
|
|
588
589
|
const url = String(error.response?.config?.url || '<unknown url>');
|
|
@@ -600,18 +601,32 @@ class TestomatioPipe {
|
|
|
600
601
|
message += `\t${picocolors_1.default.red(statusText)}\n`;
|
|
601
602
|
}
|
|
602
603
|
message += `\t${picocolors_1.default.bold('response: ')}${picocolors_1.default.gray(responseBody)}\n`;
|
|
603
|
-
|
|
604
|
+
let requestBody = (0, hide_token_js_1.hideTestomatioToken)(stringify(error.response?.config?.data));
|
|
605
|
+
let requestTruncated = false;
|
|
604
606
|
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
|
|
605
607
|
// full body
|
|
606
608
|
message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(requestBody)}\n`;
|
|
607
609
|
}
|
|
608
610
|
else {
|
|
609
611
|
// cut body
|
|
610
|
-
|
|
611
|
-
|
|
612
|
+
requestTruncated = true;
|
|
613
|
+
requestBody = `${requestBody.slice(0, 1000)}...`;
|
|
614
|
+
message += `\t${picocolors_1.default.bold('request: ')}${picocolors_1.default.gray(requestBody)}\n`;
|
|
612
615
|
message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
|
|
613
616
|
}
|
|
614
|
-
|
|
617
|
+
// the JSON line is built from the same values as the text message, with the token already hidden
|
|
618
|
+
const fields = {
|
|
619
|
+
status: statusCode,
|
|
620
|
+
method,
|
|
621
|
+
url,
|
|
622
|
+
error: apiMessage || statusText || undefined,
|
|
623
|
+
response: parseIfJson(responseBody),
|
|
624
|
+
request: parseIfJson(requestBody),
|
|
625
|
+
};
|
|
626
|
+
// a cut body is no longer valid JSON, so consumers are told why `request` is a string
|
|
627
|
+
if (requestTruncated)
|
|
628
|
+
fields.requestTruncated = true;
|
|
629
|
+
log_js_1.log.errorWithFields(fields, message);
|
|
615
630
|
if (error.response?.data?.message?.includes('could not be matched')) {
|
|
616
631
|
this.hasUnmatchedTests = true;
|
|
617
632
|
}
|
|
@@ -626,20 +641,9 @@ function printCreateIssue() {
|
|
|
626
641
|
return;
|
|
627
642
|
registeredErrorHints = true;
|
|
628
643
|
process.on('exit', () => {
|
|
629
|
-
|
|
644
|
+
log_js_1.log.error('There was an error reporting to Testomat.io.\n', picocolors_1.default.yellow('If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new.'), picocolors_1.default.yellow('Provide the logs from above'));
|
|
630
645
|
});
|
|
631
646
|
}
|
|
632
|
-
/**
|
|
633
|
-
* Removes Testomatio token from string data
|
|
634
|
-
*
|
|
635
|
-
* @param {string} data
|
|
636
|
-
* @returns {string}
|
|
637
|
-
*/
|
|
638
|
-
function hideTestomatioToken(data) {
|
|
639
|
-
return (typeof data === 'string' ? data : '')
|
|
640
|
-
.replace(/"api_key"\s*:\s*"[^"]+"/g, '"api_key": "<hidden>"')
|
|
641
|
-
.replace(/"(tstmt_[^"]+)"/g, '"tstmt_***"');
|
|
642
|
-
}
|
|
643
647
|
/**
|
|
644
648
|
* Stringifies provided data
|
|
645
649
|
*
|
|
@@ -650,4 +654,18 @@ function hideTestomatioToken(data) {
|
|
|
650
654
|
function stringify(anything, opts = { pretty: false }) {
|
|
651
655
|
return typeof anything === 'string' ? anything : JSON.stringify(anything, null, opts.pretty ? 2 : undefined);
|
|
652
656
|
}
|
|
657
|
+
/**
|
|
658
|
+
* Turn a JSON string back into an object for structured logs; keeps the string if it is not JSON.
|
|
659
|
+
*
|
|
660
|
+
* @param {string} data
|
|
661
|
+
* @returns {any}
|
|
662
|
+
*/
|
|
663
|
+
function parseIfJson(data) {
|
|
664
|
+
try {
|
|
665
|
+
return JSON.parse(data);
|
|
666
|
+
}
|
|
667
|
+
catch {
|
|
668
|
+
return data;
|
|
669
|
+
}
|
|
670
|
+
}
|
|
653
671
|
module.exports = TestomatioPipe;
|
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hides the Testomat.io API token in any data which is about to be printed or logged.
|
|
3
|
+
* Applied at the logger level, so a raw error object with a request body can't leak the token.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} data
|
|
6
|
+
* @returns {string} The data with every token replaced, empty string if data is not a string.
|
|
7
|
+
*/
|
|
8
|
+
export function hideTestomatioToken(data: string): string;
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
3
|
+
exports.hideTestomatioToken = hideTestomatioToken;
|
|
4
|
+
/**
|
|
5
|
+
* Hides the Testomat.io API token in any data which is about to be printed or logged.
|
|
6
|
+
* Applied at the logger level, so a raw error object with a request body can't leak the token.
|
|
7
|
+
*
|
|
8
|
+
* @param {string} data
|
|
9
|
+
* @returns {string} The data with every token replaced, empty string if data is not a string.
|
|
10
|
+
*/
|
|
11
|
+
function hideTestomatioToken(data) {
|
|
12
|
+
if (typeof data !== 'string')
|
|
13
|
+
return '';
|
|
14
|
+
return data.replace(/"api_key"\s*:\s*"[^"]+"/g, '"api_key": "<hidden>"').replace(/tstmt_[\w-]+/g, 'tstmt_***');
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
module.exports.hideTestomatioToken = hideTestomatioToken;
|
package/lib/utils/log.d.ts
CHANGED
|
@@ -12,6 +12,12 @@ export function getLogLevel(): number;
|
|
|
12
12
|
* @returns {boolean} True if the message should be logged
|
|
13
13
|
*/
|
|
14
14
|
export function shouldLog(messageLevel: number): boolean;
|
|
15
|
+
/**
|
|
16
|
+
* Check if logs should be printed as JSON lines instead of [TESTOMATIO] prefixed text.
|
|
17
|
+
* Enabled by the CLI for `--format json` so the whole output is machine-readable.
|
|
18
|
+
* @returns {boolean}
|
|
19
|
+
*/
|
|
20
|
+
export function isJsonOutput(): boolean;
|
|
15
21
|
/**
|
|
16
22
|
* Log an info message with [TESTOMATIO] prefix.
|
|
17
23
|
* Only logs when TESTOMATIO_LOG_LEVEL is INFO.
|
|
@@ -30,6 +36,13 @@ export function warn(...args: any[]): void;
|
|
|
30
36
|
* @param {...any} args - Arguments to log
|
|
31
37
|
*/
|
|
32
38
|
export function error(...args: any[]): void;
|
|
39
|
+
/**
|
|
40
|
+
* Log an error which carries structured data, e.g. a failed API request.
|
|
41
|
+
* The fields are added to the JSON line; in text mode only the message is printed.
|
|
42
|
+
* @param {Object} fields - Extra fields to add to the JSON object
|
|
43
|
+
* @param {...any} args - Arguments to log as text
|
|
44
|
+
*/
|
|
45
|
+
export function errorWithFields(fields: any, ...args: any[]): void;
|
|
33
46
|
export namespace LOG_LEVELS {
|
|
34
47
|
let ERROR: number;
|
|
35
48
|
let WARN: number;
|
|
@@ -39,6 +52,8 @@ export namespace log {
|
|
|
39
52
|
export { info };
|
|
40
53
|
export { warn };
|
|
41
54
|
export { error };
|
|
55
|
+
export { errorWithFields };
|
|
56
|
+
export { isJsonOutput };
|
|
42
57
|
export { getLogLevel };
|
|
43
58
|
export { shouldLog };
|
|
44
59
|
export { LOG_LEVELS };
|
package/lib/utils/log.js
CHANGED
|
@@ -3,10 +3,14 @@ Object.defineProperty(exports, "__esModule", { value: true });
|
|
|
3
3
|
exports.log = exports.LOG_LEVELS = void 0;
|
|
4
4
|
exports.getLogLevel = getLogLevel;
|
|
5
5
|
exports.shouldLog = shouldLog;
|
|
6
|
+
exports.isJsonOutput = isJsonOutput;
|
|
6
7
|
exports.info = info;
|
|
7
8
|
exports.warn = warn;
|
|
8
9
|
exports.error = error;
|
|
10
|
+
exports.errorWithFields = errorWithFields;
|
|
11
|
+
const util_1 = require("util");
|
|
9
12
|
const constants_js_1 = require("../constants.js");
|
|
13
|
+
const hide_token_js_1 = require("./hide_token.js");
|
|
10
14
|
/**
|
|
11
15
|
* Log levels for the Testomat.io reporter.
|
|
12
16
|
* A message is logged if its level is <= the current log level.
|
|
@@ -39,16 +43,51 @@ function getLogLevel() {
|
|
|
39
43
|
function shouldLog(messageLevel) {
|
|
40
44
|
return messageLevel <= getLogLevel() || !!process.env.TESTOMATIO_DEBUG;
|
|
41
45
|
}
|
|
46
|
+
/**
|
|
47
|
+
* Check if logs should be printed as JSON lines instead of [TESTOMATIO] prefixed text.
|
|
48
|
+
* Enabled by the CLI for `--format json` so the whole output is machine-readable.
|
|
49
|
+
* @returns {boolean}
|
|
50
|
+
*/
|
|
51
|
+
function isJsonOutput() {
|
|
52
|
+
return process.env.TESTOMATIO_LOG_JSON === '1';
|
|
53
|
+
}
|
|
54
|
+
/**
|
|
55
|
+
* Render a log message as a single JSON line, e.g. `{"level":"error","message":"..."}`.
|
|
56
|
+
* Colors are stripped, so the message stays readable after parsing.
|
|
57
|
+
* @param {string} level - Log level name
|
|
58
|
+
* @param {any[]} args - Arguments as passed to the log function
|
|
59
|
+
* @param {Object} [fields] - Extra fields to add to the JSON object
|
|
60
|
+
* @returns {string}
|
|
61
|
+
*/
|
|
62
|
+
function jsonLine(level, args, fields = {}) {
|
|
63
|
+
const message = (0, util_1.stripVTControlCharacters)((0, util_1.format)(...args)).trim();
|
|
64
|
+
return (0, hide_token_js_1.hideTestomatioToken)(JSON.stringify({ ...fields, level, message }));
|
|
65
|
+
}
|
|
66
|
+
/**
|
|
67
|
+
* Render the arguments of a log function as text, with the API token hidden.
|
|
68
|
+
* Errors and other objects are formatted the way console does it.
|
|
69
|
+
* @param {any[]} args - Arguments as passed to the log function
|
|
70
|
+
* @returns {string}
|
|
71
|
+
*/
|
|
72
|
+
function textLine(args) {
|
|
73
|
+
return (0, hide_token_js_1.hideTestomatioToken)((0, util_1.format)(...args));
|
|
74
|
+
}
|
|
42
75
|
/**
|
|
43
76
|
* Log an info message with [TESTOMATIO] prefix.
|
|
44
77
|
* Only logs when TESTOMATIO_LOG_LEVEL is INFO.
|
|
45
78
|
* @param {...any} args - Arguments to log
|
|
46
79
|
*/
|
|
47
80
|
function info(...args) {
|
|
48
|
-
if (shouldLog(exports.LOG_LEVELS.INFO))
|
|
49
|
-
|
|
50
|
-
|
|
81
|
+
if (!shouldLog(exports.LOG_LEVELS.INFO))
|
|
82
|
+
return;
|
|
83
|
+
let fn = console.log;
|
|
84
|
+
if (process.env.TESTOMATIO_LOG_STDERR === '1')
|
|
85
|
+
fn = console.error;
|
|
86
|
+
if (isJsonOutput()) {
|
|
87
|
+
fn(jsonLine('info', args));
|
|
88
|
+
return;
|
|
51
89
|
}
|
|
90
|
+
fn(constants_js_1.APP_PREFIX, textLine(args));
|
|
52
91
|
}
|
|
53
92
|
/**
|
|
54
93
|
* Log a warning message with [TESTOMATIO] prefix.
|
|
@@ -56,9 +95,13 @@ function info(...args) {
|
|
|
56
95
|
* @param {...any} args - Arguments to log
|
|
57
96
|
*/
|
|
58
97
|
function warn(...args) {
|
|
59
|
-
if (shouldLog(exports.LOG_LEVELS.WARN))
|
|
60
|
-
|
|
98
|
+
if (!shouldLog(exports.LOG_LEVELS.WARN))
|
|
99
|
+
return;
|
|
100
|
+
if (isJsonOutput()) {
|
|
101
|
+
console.warn(jsonLine('warn', args));
|
|
102
|
+
return;
|
|
61
103
|
}
|
|
104
|
+
console.warn(constants_js_1.APP_PREFIX, textLine(args));
|
|
62
105
|
}
|
|
63
106
|
/**
|
|
64
107
|
* Log an error message with [TESTOMATIO] prefix.
|
|
@@ -66,9 +109,28 @@ function warn(...args) {
|
|
|
66
109
|
* @param {...any} args - Arguments to log
|
|
67
110
|
*/
|
|
68
111
|
function error(...args) {
|
|
69
|
-
if (shouldLog(exports.LOG_LEVELS.ERROR))
|
|
70
|
-
|
|
112
|
+
if (!shouldLog(exports.LOG_LEVELS.ERROR))
|
|
113
|
+
return;
|
|
114
|
+
if (isJsonOutput()) {
|
|
115
|
+
console.error(jsonLine('error', args));
|
|
116
|
+
return;
|
|
71
117
|
}
|
|
118
|
+
console.error(constants_js_1.APP_PREFIX, textLine(args));
|
|
119
|
+
}
|
|
120
|
+
/**
|
|
121
|
+
* Log an error which carries structured data, e.g. a failed API request.
|
|
122
|
+
* The fields are added to the JSON line; in text mode only the message is printed.
|
|
123
|
+
* @param {Object} fields - Extra fields to add to the JSON object
|
|
124
|
+
* @param {...any} args - Arguments to log as text
|
|
125
|
+
*/
|
|
126
|
+
function errorWithFields(fields, ...args) {
|
|
127
|
+
if (!shouldLog(exports.LOG_LEVELS.ERROR))
|
|
128
|
+
return;
|
|
129
|
+
if (isJsonOutput()) {
|
|
130
|
+
console.error(jsonLine('error', args, fields));
|
|
131
|
+
return;
|
|
132
|
+
}
|
|
133
|
+
console.error(constants_js_1.APP_PREFIX, textLine(args));
|
|
72
134
|
}
|
|
73
135
|
/**
|
|
74
136
|
* Logging utility for Testomat.io reporter.
|
|
@@ -83,6 +145,8 @@ exports.log = {
|
|
|
83
145
|
info,
|
|
84
146
|
warn,
|
|
85
147
|
error,
|
|
148
|
+
errorWithFields,
|
|
149
|
+
isJsonOutput,
|
|
86
150
|
getLogLevel,
|
|
87
151
|
shouldLog,
|
|
88
152
|
LOG_LEVELS: exports.LOG_LEVELS,
|
|
@@ -92,8 +156,12 @@ module.exports.getLogLevel = getLogLevel;
|
|
|
92
156
|
|
|
93
157
|
module.exports.shouldLog = shouldLog;
|
|
94
158
|
|
|
159
|
+
module.exports.isJsonOutput = isJsonOutput;
|
|
160
|
+
|
|
95
161
|
module.exports.info = info;
|
|
96
162
|
|
|
97
163
|
module.exports.warn = warn;
|
|
98
164
|
|
|
99
165
|
module.exports.error = error;
|
|
166
|
+
|
|
167
|
+
module.exports.errorWithFields = errorWithFields;
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -301,7 +301,7 @@ function CodeceptReporter(config) {
|
|
|
301
301
|
});
|
|
302
302
|
|
|
303
303
|
event.dispatcher.on(event.step.started, step => {
|
|
304
|
-
const stepText = `${repeat(output.stepShift)} ${
|
|
304
|
+
const stepText = `${repeat(output.stepShift)} ${formatStepLog(step)}`;
|
|
305
305
|
dataStorage.putData('log', stepText);
|
|
306
306
|
});
|
|
307
307
|
|
|
@@ -369,6 +369,18 @@ function repeat(num) {
|
|
|
369
369
|
return ''.padStart(num, ' ');
|
|
370
370
|
}
|
|
371
371
|
|
|
372
|
+
function formatStepLog(step) {
|
|
373
|
+
if (typeof step?.toCliStyled === 'function') return step.toCliStyled();
|
|
374
|
+
|
|
375
|
+
if (typeof step?.toString === 'function' && step.toString !== Object.prototype.toString) {
|
|
376
|
+
return step.toString();
|
|
377
|
+
}
|
|
378
|
+
|
|
379
|
+
const title = step?.title || '';
|
|
380
|
+
const args = Array.isArray(step?.args) ? step.args.join(', ') : '';
|
|
381
|
+
return `${title}${args ? ` ${args}` : ''}`.trim();
|
|
382
|
+
}
|
|
383
|
+
|
|
372
384
|
// Helper functions for cleaner event handling
|
|
373
385
|
function initializeTestDataStore() {
|
|
374
386
|
if (!global.testomatioDataStore) global.testomatioDataStore = {};
|
package/src/bin/cli.js
CHANGED
|
@@ -43,6 +43,8 @@ program
|
|
|
43
43
|
if (subOpts.filterList || subOpts.format) {
|
|
44
44
|
process.env.TESTOMATIO_LOG_STDERR = '1';
|
|
45
45
|
process.env.TESTOMATIO_LOG_LEVEL ||= 'WARN';
|
|
46
|
+
// with --format json the logs are machine-readable too: one JSON object per line on stderr
|
|
47
|
+
if (subOpts.format === 'json') process.env.TESTOMATIO_LOG_JSON = '1';
|
|
46
48
|
} else {
|
|
47
49
|
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
48
50
|
}
|
|
@@ -82,7 +84,7 @@ program
|
|
|
82
84
|
|
|
83
85
|
await client.createRun(createRunParams);
|
|
84
86
|
|
|
85
|
-
const runId = client.pipeStore.runId
|
|
87
|
+
const runId = client.pipeStore.runId;
|
|
86
88
|
if (!runId) {
|
|
87
89
|
log.error(pc.red('Failed to create run on Testomat.io.'));
|
|
88
90
|
process.exit(1);
|
|
@@ -94,7 +96,7 @@ program
|
|
|
94
96
|
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
95
97
|
|
|
96
98
|
// stdout carries ONLY the run data so it can be captured: RUN_ID=$(reporter start)
|
|
97
|
-
console.log(formatRunOutput(
|
|
99
|
+
console.log(formatRunOutput(client.pipeStore, opts.format));
|
|
98
100
|
process.exit(0);
|
|
99
101
|
});
|
|
100
102
|
|
|
@@ -250,15 +252,20 @@ program
|
|
|
250
252
|
|
|
251
253
|
if (apiKey) {
|
|
252
254
|
await client.createRun(createRunParams);
|
|
253
|
-
|
|
255
|
+
|
|
256
|
+
const runId = client.pipeStore.runId;
|
|
257
|
+
if (!runId) {
|
|
258
|
+
log.error(pc.red('Failed to create run on Testomat.io.'));
|
|
259
|
+
process.exit(1);
|
|
260
|
+
}
|
|
261
|
+
|
|
254
262
|
if (client.pipeStore.runUrl) log.info( `📊 Report URL: ${pc.magenta(client.pipeStore.runUrl)}`);
|
|
255
263
|
|
|
256
264
|
if (opts.kind !== 'manual') {
|
|
257
265
|
log.info( `No command passed, so you need to run tests yourself:`);
|
|
258
266
|
log.info( `TESTOMATIO_RUN=${runId} <command>`);
|
|
259
267
|
}
|
|
260
|
-
|
|
261
|
-
if (opts.format && runOutput) console.log(runOutput);
|
|
268
|
+
if (opts.format) console.log(formatRunOutput(client.pipeStore, opts.format));
|
|
262
269
|
} else {
|
|
263
270
|
log.info( '⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
|
|
264
271
|
process.exit(1);
|
package/src/client.js
CHANGED
|
@@ -187,7 +187,7 @@ class Client {
|
|
|
187
187
|
}
|
|
188
188
|
|
|
189
189
|
} catch (err) {
|
|
190
|
-
|
|
190
|
+
log.error('Error in uploadStepArtifacts for testRid', testRid, ':', err.message || err);
|
|
191
191
|
throw err;
|
|
192
192
|
}
|
|
193
193
|
}
|
|
@@ -225,7 +225,7 @@ class Client {
|
|
|
225
225
|
try {
|
|
226
226
|
await this.uploadStepArtifacts(steps, rid);
|
|
227
227
|
} catch (err) {
|
|
228
|
-
|
|
228
|
+
log.error('Failed to upload step artifacts:', err.message || err);
|
|
229
229
|
}
|
|
230
230
|
|
|
231
231
|
const uploadedFiles = [];
|
|
@@ -423,7 +423,7 @@ class Client {
|
|
|
423
423
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
424
424
|
|
|
425
425
|
failedUploads.forEach(upload => {
|
|
426
|
-
|
|
426
|
+
log.info(
|
|
427
427
|
` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
|
|
428
428
|
`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
|
|
429
429
|
)}`,
|
|
@@ -439,7 +439,7 @@ class Client {
|
|
|
439
439
|
}));
|
|
440
440
|
const pathPadding = Math.max(...skippedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
441
441
|
skippedUploads.forEach(upload => {
|
|
442
|
-
|
|
442
|
+
log.info(
|
|
443
443
|
` ${pc.gray('|')} 🟡 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
|
|
444
444
|
`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
|
|
445
445
|
)}`,
|
package/src/pipe/csv.js
CHANGED
|
@@ -6,6 +6,7 @@ import pc from 'picocolors';
|
|
|
6
6
|
import merge from 'lodash.merge';
|
|
7
7
|
import { isSameTest, getCurrentDateTime, ansiRegExp } from '../utils/utils.js';
|
|
8
8
|
import { CSV_HEADERS } from '../constants.js';
|
|
9
|
+
import { log } from '../utils/log.js';
|
|
9
10
|
|
|
10
11
|
const debug = createDebugMessages('@testomatio/reporter:pipe:csv');
|
|
11
12
|
/**
|
|
@@ -76,11 +77,11 @@ class CsvPipe {
|
|
|
76
77
|
this.checkExportDir();
|
|
77
78
|
|
|
78
79
|
if (!this.outputFile) {
|
|
79
|
-
|
|
80
|
+
log.warn(pc.yellow(`⚠️ CSV file is not set, ignoring`));
|
|
80
81
|
return;
|
|
81
82
|
}
|
|
82
83
|
|
|
83
|
-
|
|
84
|
+
log.info(pc.yellow(`⏳ The test results will be added to the csv. It will take some time...`));
|
|
84
85
|
|
|
85
86
|
try {
|
|
86
87
|
// Create csv writer object
|
|
@@ -91,7 +92,7 @@ class CsvPipe {
|
|
|
91
92
|
// Save csv file based on the current data
|
|
92
93
|
return await writer.writeRecords(data);
|
|
93
94
|
} catch (e) {
|
|
94
|
-
|
|
95
|
+
log.error('Unknown csv error: ', e);
|
|
95
96
|
}
|
|
96
97
|
}
|
|
97
98
|
|
|
@@ -135,7 +136,7 @@ class CsvPipe {
|
|
|
135
136
|
// Save results based on the default headers
|
|
136
137
|
if (this.isEnabled) {
|
|
137
138
|
await this.saveToCsv(this.results, CSV_HEADERS);
|
|
138
|
-
|
|
139
|
+
log.info(pc.green(`🗃️ Recording completed! You can check the result in file = ${this.outputFile}`));
|
|
139
140
|
}
|
|
140
141
|
}
|
|
141
142
|
|
package/src/pipe/html.js
CHANGED
|
@@ -8,6 +8,7 @@ import { marked } from 'marked';
|
|
|
8
8
|
import fileUrl from 'file-url';
|
|
9
9
|
import { fileSystem, isSameTest, ansiRegExp, formatStep, transformEnvVarToBoolean } from '../utils/utils.js';
|
|
10
10
|
import { HTML_REPORT } from '../constants.js';
|
|
11
|
+
import { log } from '../utils/log.js';
|
|
11
12
|
import { fileURLToPath } from 'node:url';
|
|
12
13
|
|
|
13
14
|
const debug = createDebugMessages('@testomatio/reporter:pipe:html');
|
|
@@ -144,14 +145,14 @@ class HtmlPipe {
|
|
|
144
145
|
debug('HTML tests data:', tests);
|
|
145
146
|
|
|
146
147
|
if (!outputPath) {
|
|
147
|
-
|
|
148
|
+
log.warn(pc.yellow(`🚨 HTML export path is not set, ignoring...`));
|
|
148
149
|
return;
|
|
149
150
|
}
|
|
150
151
|
|
|
151
|
-
|
|
152
|
+
log.info(pc.yellow(`⏳ The test results will be added to the HTML report. It will take some time...`));
|
|
152
153
|
|
|
153
154
|
if (msg) {
|
|
154
|
-
|
|
155
|
+
log.info(pc.blue(msg));
|
|
155
156
|
}
|
|
156
157
|
|
|
157
158
|
const aggregatedTests = aggregateTestRetries(tests);
|
|
@@ -295,9 +296,9 @@ class HtmlPipe {
|
|
|
295
296
|
|
|
296
297
|
debug('HTML tests data:', fileUrlPath);
|
|
297
298
|
|
|
298
|
-
|
|
299
|
+
log.info(pc.green(`📊 The HTML report was successfully generated. Full filepath: ${fileUrlPath}`));
|
|
299
300
|
} else {
|
|
300
|
-
|
|
301
|
+
log.error(pc.red(`🚨 Failed to generate the HTML report.`));
|
|
301
302
|
}
|
|
302
303
|
}
|
|
303
304
|
|
|
@@ -309,7 +310,7 @@ class HtmlPipe {
|
|
|
309
310
|
*/
|
|
310
311
|
#generateHTMLReport(data, templatePath = '') {
|
|
311
312
|
if (!templatePath) {
|
|
312
|
-
|
|
313
|
+
log.error(pc.red(`🚨 HTML template not found. Report generation is impossible!`));
|
|
313
314
|
return;
|
|
314
315
|
}
|
|
315
316
|
|
|
@@ -320,8 +321,8 @@ class HtmlPipe {
|
|
|
320
321
|
|
|
321
322
|
return template(data);
|
|
322
323
|
} catch (e) {
|
|
323
|
-
|
|
324
|
-
|
|
324
|
+
log.error(pc.red('❌ Oops! An unknown error occurred when generating an HTML report'));
|
|
325
|
+
log.error(pc.red(e));
|
|
325
326
|
}
|
|
326
327
|
}
|
|
327
328
|
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -18,6 +18,7 @@ import {
|
|
|
18
18
|
getGitCommitSha,
|
|
19
19
|
} from '../utils/utils.js';
|
|
20
20
|
import { parseFilterParams, generateFilterRequestParams, setS3Credentials } from '../utils/pipe_utils.js';
|
|
21
|
+
import { hideTestomatioToken } from '../utils/hide_token.js';
|
|
21
22
|
import { config } from '../config.js';
|
|
22
23
|
import { log } from '../utils/log.js';
|
|
23
24
|
|
|
@@ -380,16 +381,15 @@ class TestomatioPipe {
|
|
|
380
381
|
process.env.runId = this.runId;
|
|
381
382
|
debug('Run created', this.runId);
|
|
382
383
|
} catch (err) {
|
|
383
|
-
if (!this.apiKey)
|
|
384
|
+
if (!this.apiKey) log.error('Testomat.io API key is not set');
|
|
384
385
|
const errorText = err.response?.data?.message || err.message;
|
|
385
386
|
debug('Error creating run', err);
|
|
386
|
-
|
|
387
|
+
log.error(errorText || err);
|
|
387
388
|
if (err.response?.status === 403) this.#disablePipe();
|
|
388
389
|
|
|
389
390
|
this.#logFailedResponse(err);
|
|
390
391
|
|
|
391
|
-
|
|
392
|
-
APP_PREFIX,
|
|
392
|
+
log.error(
|
|
393
393
|
'Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report',
|
|
394
394
|
);
|
|
395
395
|
printCreateIssue();
|
|
@@ -409,7 +409,7 @@ class TestomatioPipe {
|
|
|
409
409
|
this.reportingCanceledDueToReqFailures = true;
|
|
410
410
|
let errorMessage = `⚠️ ${process.env.TESTOMATIO_MAX_REQUEST_FAILURES}`;
|
|
411
411
|
errorMessage += ' requests were failed, reporting to Testomat aborted.';
|
|
412
|
-
|
|
412
|
+
log.warn(pc.yellow(errorMessage));
|
|
413
413
|
}
|
|
414
414
|
return cancelReporting;
|
|
415
415
|
}
|
|
@@ -557,7 +557,7 @@ class TestomatioPipe {
|
|
|
557
557
|
const errorMessage = pc.red(
|
|
558
558
|
`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
|
|
559
559
|
);
|
|
560
|
-
|
|
560
|
+
log.warn(errorMessage);
|
|
561
561
|
}
|
|
562
562
|
|
|
563
563
|
const { status } = params;
|
|
@@ -621,7 +621,7 @@ class TestomatioPipe {
|
|
|
621
621
|
);
|
|
622
622
|
}
|
|
623
623
|
} catch (err) {
|
|
624
|
-
|
|
624
|
+
log.error('Error updating status, skipping...', err.message || err);
|
|
625
625
|
this.#logFailedResponse(err);
|
|
626
626
|
printCreateIssue();
|
|
627
627
|
}
|
|
@@ -665,18 +665,32 @@ class TestomatioPipe {
|
|
|
665
665
|
|
|
666
666
|
message += `\t${pc.bold('response: ')}${pc.gray(responseBody)}\n`;
|
|
667
667
|
|
|
668
|
-
|
|
668
|
+
let requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
|
|
669
|
+
let requestTruncated = false;
|
|
669
670
|
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
|
|
670
671
|
// full body
|
|
671
672
|
message += `\t${pc.bold('request: ')}${pc.gray(requestBody)}\n`;
|
|
672
673
|
} else {
|
|
673
674
|
// cut body
|
|
674
|
-
|
|
675
|
-
|
|
675
|
+
requestTruncated = true;
|
|
676
|
+
requestBody = `${requestBody.slice(0, 1000)}...`;
|
|
677
|
+
message += `\t${pc.bold('request: ')}${pc.gray(requestBody)}\n`;
|
|
676
678
|
message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
|
|
677
679
|
}
|
|
678
680
|
|
|
679
|
-
|
|
681
|
+
// the JSON line is built from the same values as the text message, with the token already hidden
|
|
682
|
+
const fields = {
|
|
683
|
+
status: statusCode,
|
|
684
|
+
method,
|
|
685
|
+
url,
|
|
686
|
+
error: apiMessage || statusText || undefined,
|
|
687
|
+
response: parseIfJson(responseBody),
|
|
688
|
+
request: parseIfJson(requestBody),
|
|
689
|
+
};
|
|
690
|
+
// a cut body is no longer valid JSON, so consumers are told why `request` is a string
|
|
691
|
+
if (requestTruncated) fields.requestTruncated = true;
|
|
692
|
+
|
|
693
|
+
log.errorWithFields(fields, message);
|
|
680
694
|
|
|
681
695
|
if (error.response?.data?.message?.includes('could not be matched')) {
|
|
682
696
|
this.hasUnmatchedTests = true;
|
|
@@ -693,8 +707,7 @@ function printCreateIssue() {
|
|
|
693
707
|
if (registeredErrorHints) return;
|
|
694
708
|
registeredErrorHints = true;
|
|
695
709
|
process.on('exit', () => {
|
|
696
|
-
|
|
697
|
-
APP_PREFIX,
|
|
710
|
+
log.error(
|
|
698
711
|
'There was an error reporting to Testomat.io.\n',
|
|
699
712
|
pc.yellow(
|
|
700
713
|
'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new.',
|
|
@@ -704,18 +717,6 @@ function printCreateIssue() {
|
|
|
704
717
|
});
|
|
705
718
|
}
|
|
706
719
|
|
|
707
|
-
/**
|
|
708
|
-
* Removes Testomatio token from string data
|
|
709
|
-
*
|
|
710
|
-
* @param {string} data
|
|
711
|
-
* @returns {string}
|
|
712
|
-
*/
|
|
713
|
-
function hideTestomatioToken(data) {
|
|
714
|
-
return (typeof data === 'string' ? data : '')
|
|
715
|
-
.replace(/"api_key"\s*:\s*"[^"]+"/g, '"api_key": "<hidden>"')
|
|
716
|
-
.replace(/"(tstmt_[^"]+)"/g, '"tstmt_***"');
|
|
717
|
-
}
|
|
718
|
-
|
|
719
720
|
/**
|
|
720
721
|
* Stringifies provided data
|
|
721
722
|
*
|
|
@@ -727,4 +728,18 @@ function stringify(anything, opts = { pretty: false }) {
|
|
|
727
728
|
return typeof anything === 'string' ? anything : JSON.stringify(anything, null, opts.pretty ? 2 : undefined);
|
|
728
729
|
}
|
|
729
730
|
|
|
731
|
+
/**
|
|
732
|
+
* Turn a JSON string back into an object for structured logs; keeps the string if it is not JSON.
|
|
733
|
+
*
|
|
734
|
+
* @param {string} data
|
|
735
|
+
* @returns {any}
|
|
736
|
+
*/
|
|
737
|
+
function parseIfJson(data) {
|
|
738
|
+
try {
|
|
739
|
+
return JSON.parse(data);
|
|
740
|
+
} catch {
|
|
741
|
+
return data;
|
|
742
|
+
}
|
|
743
|
+
}
|
|
744
|
+
|
|
730
745
|
export default TestomatioPipe;
|
|
@@ -0,0 +1,12 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Hides the Testomat.io API token in any data which is about to be printed or logged.
|
|
3
|
+
* Applied at the logger level, so a raw error object with a request body can't leak the token.
|
|
4
|
+
*
|
|
5
|
+
* @param {string} data
|
|
6
|
+
* @returns {string} The data with every token replaced, empty string if data is not a string.
|
|
7
|
+
*/
|
|
8
|
+
export function hideTestomatioToken(data) {
|
|
9
|
+
if (typeof data !== 'string') return '';
|
|
10
|
+
|
|
11
|
+
return data.replace(/"api_key"\s*:\s*"[^"]+"/g, '"api_key": "<hidden>"').replace(/tstmt_[\w-]+/g, 'tstmt_***');
|
|
12
|
+
}
|
package/src/utils/log.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
+
import { format as formatArgs, stripVTControlCharacters } from 'util';
|
|
1
2
|
import { APP_PREFIX } from '../constants.js';
|
|
3
|
+
import { hideTestomatioToken } from './hide_token.js';
|
|
2
4
|
|
|
3
5
|
/**
|
|
4
6
|
* Log levels for the Testomat.io reporter.
|
|
@@ -35,16 +37,54 @@ export function shouldLog(messageLevel) {
|
|
|
35
37
|
return messageLevel <= getLogLevel() || !!process.env.TESTOMATIO_DEBUG;
|
|
36
38
|
}
|
|
37
39
|
|
|
40
|
+
/**
|
|
41
|
+
* Check if logs should be printed as JSON lines instead of [TESTOMATIO] prefixed text.
|
|
42
|
+
* Enabled by the CLI for `--format json` so the whole output is machine-readable.
|
|
43
|
+
* @returns {boolean}
|
|
44
|
+
*/
|
|
45
|
+
export function isJsonOutput() {
|
|
46
|
+
return process.env.TESTOMATIO_LOG_JSON === '1';
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Render a log message as a single JSON line, e.g. `{"level":"error","message":"..."}`.
|
|
51
|
+
* Colors are stripped, so the message stays readable after parsing.
|
|
52
|
+
* @param {string} level - Log level name
|
|
53
|
+
* @param {any[]} args - Arguments as passed to the log function
|
|
54
|
+
* @param {Object} [fields] - Extra fields to add to the JSON object
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function jsonLine(level, args, fields = {}) {
|
|
58
|
+
const message = stripVTControlCharacters(formatArgs(...args)).trim();
|
|
59
|
+
return hideTestomatioToken(JSON.stringify({ ...fields, level, message }));
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
/**
|
|
63
|
+
* Render the arguments of a log function as text, with the API token hidden.
|
|
64
|
+
* Errors and other objects are formatted the way console does it.
|
|
65
|
+
* @param {any[]} args - Arguments as passed to the log function
|
|
66
|
+
* @returns {string}
|
|
67
|
+
*/
|
|
68
|
+
function textLine(args) {
|
|
69
|
+
return hideTestomatioToken(formatArgs(...args));
|
|
70
|
+
}
|
|
71
|
+
|
|
38
72
|
/**
|
|
39
73
|
* Log an info message with [TESTOMATIO] prefix.
|
|
40
74
|
* Only logs when TESTOMATIO_LOG_LEVEL is INFO.
|
|
41
75
|
* @param {...any} args - Arguments to log
|
|
42
76
|
*/
|
|
43
77
|
export function info(...args) {
|
|
44
|
-
if (shouldLog(LOG_LEVELS.INFO))
|
|
45
|
-
|
|
46
|
-
|
|
78
|
+
if (!shouldLog(LOG_LEVELS.INFO)) return;
|
|
79
|
+
|
|
80
|
+
let fn = console.log;
|
|
81
|
+
if (process.env.TESTOMATIO_LOG_STDERR === '1') fn = console.error;
|
|
82
|
+
|
|
83
|
+
if (isJsonOutput()) {
|
|
84
|
+
fn(jsonLine('info', args));
|
|
85
|
+
return;
|
|
47
86
|
}
|
|
87
|
+
fn(APP_PREFIX, textLine(args));
|
|
48
88
|
}
|
|
49
89
|
|
|
50
90
|
/**
|
|
@@ -53,9 +93,13 @@ export function info(...args) {
|
|
|
53
93
|
* @param {...any} args - Arguments to log
|
|
54
94
|
*/
|
|
55
95
|
export function warn(...args) {
|
|
56
|
-
if (shouldLog(LOG_LEVELS.WARN))
|
|
57
|
-
|
|
96
|
+
if (!shouldLog(LOG_LEVELS.WARN)) return;
|
|
97
|
+
|
|
98
|
+
if (isJsonOutput()) {
|
|
99
|
+
console.warn(jsonLine('warn', args));
|
|
100
|
+
return;
|
|
58
101
|
}
|
|
102
|
+
console.warn(APP_PREFIX, textLine(args));
|
|
59
103
|
}
|
|
60
104
|
|
|
61
105
|
/**
|
|
@@ -64,9 +108,29 @@ export function warn(...args) {
|
|
|
64
108
|
* @param {...any} args - Arguments to log
|
|
65
109
|
*/
|
|
66
110
|
export function error(...args) {
|
|
67
|
-
if (shouldLog(LOG_LEVELS.ERROR))
|
|
68
|
-
|
|
111
|
+
if (!shouldLog(LOG_LEVELS.ERROR)) return;
|
|
112
|
+
|
|
113
|
+
if (isJsonOutput()) {
|
|
114
|
+
console.error(jsonLine('error', args));
|
|
115
|
+
return;
|
|
116
|
+
}
|
|
117
|
+
console.error(APP_PREFIX, textLine(args));
|
|
118
|
+
}
|
|
119
|
+
|
|
120
|
+
/**
|
|
121
|
+
* Log an error which carries structured data, e.g. a failed API request.
|
|
122
|
+
* The fields are added to the JSON line; in text mode only the message is printed.
|
|
123
|
+
* @param {Object} fields - Extra fields to add to the JSON object
|
|
124
|
+
* @param {...any} args - Arguments to log as text
|
|
125
|
+
*/
|
|
126
|
+
export function errorWithFields(fields, ...args) {
|
|
127
|
+
if (!shouldLog(LOG_LEVELS.ERROR)) return;
|
|
128
|
+
|
|
129
|
+
if (isJsonOutput()) {
|
|
130
|
+
console.error(jsonLine('error', args, fields));
|
|
131
|
+
return;
|
|
69
132
|
}
|
|
133
|
+
console.error(APP_PREFIX, textLine(args));
|
|
70
134
|
}
|
|
71
135
|
|
|
72
136
|
/**
|
|
@@ -82,6 +146,8 @@ export const log = {
|
|
|
82
146
|
info,
|
|
83
147
|
warn,
|
|
84
148
|
error,
|
|
149
|
+
errorWithFields,
|
|
150
|
+
isJsonOutput,
|
|
85
151
|
getLogLevel,
|
|
86
152
|
shouldLog,
|
|
87
153
|
LOG_LEVELS,
|