testomatio-reporter-cli 2.15.0-beta.1-json-output → 2.16.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/package.json +1 -1
- package/src/adapter/codecept.js +36 -8
- 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 +47 -27
- package/src/utils/hide_token.js +12 -0
- package/src/utils/log.js +73 -7
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -34,7 +34,8 @@ const HOOK_EXECUTION_ORDER = {
|
|
|
34
34
|
// codeceptjs workers are self-contained
|
|
35
35
|
dataStorage.isFileStorage = false;
|
|
36
36
|
|
|
37
|
-
|
|
37
|
+
// CodeceptJS appends the serialized data row of a data-driven test to its title
|
|
38
|
+
const DATA_REGEXP = / \| (\{.*\}|\[.*\]|null|"(?:\\.|[^"\\])*")((?:\s+@[a-zA-Z0-9-_]+)*)$/;
|
|
38
39
|
|
|
39
40
|
if (MAJOR_VERSION < 3) {
|
|
40
41
|
console.log('🔴 This reporter works with CodeceptJS 3+, please update your tests');
|
|
@@ -301,7 +302,7 @@ function CodeceptReporter(config) {
|
|
|
301
302
|
});
|
|
302
303
|
|
|
303
304
|
event.dispatcher.on(event.step.started, step => {
|
|
304
|
-
const stepText = `${repeat(output.stepShift)} ${
|
|
305
|
+
const stepText = `${repeat(output.stepShift)} ${formatStepLog(step)}`;
|
|
305
306
|
dataStorage.putData('log', stepText);
|
|
306
307
|
});
|
|
307
308
|
|
|
@@ -349,15 +350,30 @@ function stripExampleFromTitle(title) {
|
|
|
349
350
|
const res = title.match(DATA_REGEXP);
|
|
350
351
|
if (!res) return { title, example: null };
|
|
351
352
|
|
|
353
|
+
let example = null;
|
|
354
|
+
let exampleParsed = false;
|
|
352
355
|
try {
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
return { title, example };
|
|
356
|
+
example = JSON.parse(res[1]);
|
|
357
|
+
exampleParsed = true;
|
|
356
358
|
} catch (e) {
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
359
|
+
try {
|
|
360
|
+
example = JSON.parse(res[1].slice(1, -1));
|
|
361
|
+
exampleParsed = true;
|
|
362
|
+
} catch (e2) {
|
|
363
|
+
debug('Failed to parse example from title:', res[1]);
|
|
364
|
+
}
|
|
365
|
+
}
|
|
366
|
+
|
|
367
|
+
let baseTitle = title.slice(0, res.index).trim();
|
|
368
|
+
if (exampleParsed && baseTitle.includes('${current}')) {
|
|
369
|
+
baseTitle = baseTitle.replaceAll('${current}', formatExample(example));
|
|
360
370
|
}
|
|
371
|
+
return { title: `${baseTitle}${res[2]}`, example };
|
|
372
|
+
}
|
|
373
|
+
|
|
374
|
+
function formatExample(example) {
|
|
375
|
+
if (example !== null && typeof example === 'object') return JSON.stringify(example);
|
|
376
|
+
return String(example);
|
|
361
377
|
}
|
|
362
378
|
|
|
363
379
|
function stripTagsFromTitle(title) {
|
|
@@ -369,6 +385,18 @@ function repeat(num) {
|
|
|
369
385
|
return ''.padStart(num, ' ');
|
|
370
386
|
}
|
|
371
387
|
|
|
388
|
+
function formatStepLog(step) {
|
|
389
|
+
if (typeof step?.toCliStyled === 'function') return step.toCliStyled();
|
|
390
|
+
|
|
391
|
+
if (typeof step?.toString === 'function' && step.toString !== Object.prototype.toString) {
|
|
392
|
+
return step.toString();
|
|
393
|
+
}
|
|
394
|
+
|
|
395
|
+
const title = step?.title || '';
|
|
396
|
+
const args = Array.isArray(step?.args) ? step.args.join(', ') : '';
|
|
397
|
+
return `${title}${args ? ` ${args}` : ''}`.trim();
|
|
398
|
+
}
|
|
399
|
+
|
|
372
400
|
// Helper functions for cleaner event handling
|
|
373
401
|
function initializeTestDataStore() {
|
|
374
402
|
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
|
|
|
@@ -87,12 +88,15 @@ class TestomatioPipe {
|
|
|
87
88
|
this.store = store || {};
|
|
88
89
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
89
90
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
91
|
+
this.sharedRunShards = process.env.TESTOMATIO_SHARDS
|
|
92
|
+
? parseInt(process.env.TESTOMATIO_SHARDS, 10) || undefined
|
|
93
|
+
: undefined;
|
|
90
94
|
this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
|
|
91
95
|
? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
|
|
92
96
|
: undefined;
|
|
93
97
|
|
|
94
|
-
if (this.sharedRunTimeout && !this.sharedRun) {
|
|
95
|
-
debug('Auto-enabling sharedRun because sharedRunTimeout is set');
|
|
98
|
+
if ((this.sharedRunTimeout || this.sharedRunShards) && !this.sharedRun) {
|
|
99
|
+
debug('Auto-enabling sharedRun because sharedRunTimeout or sharedRunShards is set');
|
|
96
100
|
this.sharedRun = true;
|
|
97
101
|
}
|
|
98
102
|
|
|
@@ -322,6 +326,7 @@ class TestomatioPipe {
|
|
|
322
326
|
label: this.label,
|
|
323
327
|
shared_run: this.sharedRun,
|
|
324
328
|
shared_run_timeout: this.sharedRunTimeout,
|
|
329
|
+
shared_run_shards: this.sharedRunShards,
|
|
325
330
|
kind: params.kind,
|
|
326
331
|
status: params.status,
|
|
327
332
|
configuration,
|
|
@@ -380,16 +385,15 @@ class TestomatioPipe {
|
|
|
380
385
|
process.env.runId = this.runId;
|
|
381
386
|
debug('Run created', this.runId);
|
|
382
387
|
} catch (err) {
|
|
383
|
-
if (!this.apiKey)
|
|
388
|
+
if (!this.apiKey) log.error('Testomat.io API key is not set');
|
|
384
389
|
const errorText = err.response?.data?.message || err.message;
|
|
385
390
|
debug('Error creating run', err);
|
|
386
|
-
|
|
391
|
+
log.error(errorText || err);
|
|
387
392
|
if (err.response?.status === 403) this.#disablePipe();
|
|
388
393
|
|
|
389
394
|
this.#logFailedResponse(err);
|
|
390
395
|
|
|
391
|
-
|
|
392
|
-
APP_PREFIX,
|
|
396
|
+
log.error(
|
|
393
397
|
'Error creating Testomat.io report (see details above), please check if your API key is valid. Skipping report',
|
|
394
398
|
);
|
|
395
399
|
printCreateIssue();
|
|
@@ -409,7 +413,7 @@ class TestomatioPipe {
|
|
|
409
413
|
this.reportingCanceledDueToReqFailures = true;
|
|
410
414
|
let errorMessage = `⚠️ ${process.env.TESTOMATIO_MAX_REQUEST_FAILURES}`;
|
|
411
415
|
errorMessage += ' requests were failed, reporting to Testomat aborted.';
|
|
412
|
-
|
|
416
|
+
log.warn(pc.yellow(errorMessage));
|
|
413
417
|
}
|
|
414
418
|
return cancelReporting;
|
|
415
419
|
}
|
|
@@ -557,7 +561,7 @@ class TestomatioPipe {
|
|
|
557
561
|
const errorMessage = pc.red(
|
|
558
562
|
`⚠️ Due to request failures, ${this.notReportedTestsCount} test(s) were not reported to Testomat.io`,
|
|
559
563
|
);
|
|
560
|
-
|
|
564
|
+
log.warn(errorMessage);
|
|
561
565
|
}
|
|
562
566
|
|
|
563
567
|
const { status } = params;
|
|
@@ -570,6 +574,7 @@ class TestomatioPipe {
|
|
|
570
574
|
if (status === STATUS.FINISHED) status_event = 'finish';
|
|
571
575
|
if (status === STATUS.PASSED) status_event = 'pass';
|
|
572
576
|
if (status === STATUS.FAILED) status_event = 'fail';
|
|
577
|
+
if (this.sharedRun) status_event = 'finish';
|
|
573
578
|
|
|
574
579
|
try {
|
|
575
580
|
if (this.runId && !this.proceed) {
|
|
@@ -621,7 +626,7 @@ class TestomatioPipe {
|
|
|
621
626
|
);
|
|
622
627
|
}
|
|
623
628
|
} catch (err) {
|
|
624
|
-
|
|
629
|
+
log.error('Error updating status, skipping...', err.message || err);
|
|
625
630
|
this.#logFailedResponse(err);
|
|
626
631
|
printCreateIssue();
|
|
627
632
|
}
|
|
@@ -665,18 +670,32 @@ class TestomatioPipe {
|
|
|
665
670
|
|
|
666
671
|
message += `\t${pc.bold('response: ')}${pc.gray(responseBody)}\n`;
|
|
667
672
|
|
|
668
|
-
|
|
673
|
+
let requestBody = hideTestomatioToken(stringify(error.response?.config?.data));
|
|
674
|
+
let requestTruncated = false;
|
|
669
675
|
if (process.env.DEBUG || process.env.TESTOMATIO_DEBUG || requestBody.length < 1000) {
|
|
670
676
|
// full body
|
|
671
677
|
message += `\t${pc.bold('request: ')}${pc.gray(requestBody)}\n`;
|
|
672
678
|
} else {
|
|
673
679
|
// cut body
|
|
674
|
-
|
|
675
|
-
|
|
680
|
+
requestTruncated = true;
|
|
681
|
+
requestBody = `${requestBody.slice(0, 1000)}...`;
|
|
682
|
+
message += `\t${pc.bold('request: ')}${pc.gray(requestBody)}\n`;
|
|
676
683
|
message += '\trequest body is cut, run with TESTOMATIO_DEBUG=1 to see full body\n';
|
|
677
684
|
}
|
|
678
685
|
|
|
679
|
-
|
|
686
|
+
// the JSON line is built from the same values as the text message, with the token already hidden
|
|
687
|
+
const fields = {
|
|
688
|
+
status: statusCode,
|
|
689
|
+
method,
|
|
690
|
+
url,
|
|
691
|
+
error: apiMessage || statusText || undefined,
|
|
692
|
+
response: parseIfJson(responseBody),
|
|
693
|
+
request: parseIfJson(requestBody),
|
|
694
|
+
};
|
|
695
|
+
// a cut body is no longer valid JSON, so consumers are told why `request` is a string
|
|
696
|
+
if (requestTruncated) fields.requestTruncated = true;
|
|
697
|
+
|
|
698
|
+
log.errorWithFields(fields, message);
|
|
680
699
|
|
|
681
700
|
if (error.response?.data?.message?.includes('could not be matched')) {
|
|
682
701
|
this.hasUnmatchedTests = true;
|
|
@@ -693,8 +712,7 @@ function printCreateIssue() {
|
|
|
693
712
|
if (registeredErrorHints) return;
|
|
694
713
|
registeredErrorHints = true;
|
|
695
714
|
process.on('exit', () => {
|
|
696
|
-
|
|
697
|
-
APP_PREFIX,
|
|
715
|
+
log.error(
|
|
698
716
|
'There was an error reporting to Testomat.io.\n',
|
|
699
717
|
pc.yellow(
|
|
700
718
|
'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new.',
|
|
@@ -704,18 +722,6 @@ function printCreateIssue() {
|
|
|
704
722
|
});
|
|
705
723
|
}
|
|
706
724
|
|
|
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
725
|
/**
|
|
720
726
|
* Stringifies provided data
|
|
721
727
|
*
|
|
@@ -727,4 +733,18 @@ function stringify(anything, opts = { pretty: false }) {
|
|
|
727
733
|
return typeof anything === 'string' ? anything : JSON.stringify(anything, null, opts.pretty ? 2 : undefined);
|
|
728
734
|
}
|
|
729
735
|
|
|
736
|
+
/**
|
|
737
|
+
* Turn a JSON string back into an object for structured logs; keeps the string if it is not JSON.
|
|
738
|
+
*
|
|
739
|
+
* @param {string} data
|
|
740
|
+
* @returns {any}
|
|
741
|
+
*/
|
|
742
|
+
function parseIfJson(data) {
|
|
743
|
+
try {
|
|
744
|
+
return JSON.parse(data);
|
|
745
|
+
} catch {
|
|
746
|
+
return data;
|
|
747
|
+
}
|
|
748
|
+
}
|
|
749
|
+
|
|
730
750
|
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,
|