@testomatio/reporter 2.3.9-beta-bin-fix → 2.4.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/README.md +3 -2
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +40 -11
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +57 -152
- package/lib/data-storage.d.ts +1 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +4 -0
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +43 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +433 -0
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/coverage.d.ts +82 -0
- package/lib/pipe/coverage.js +373 -0
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +44 -18
- package/lib/reporter-functions.js +14 -12
- package/lib/reporter.d.ts +31 -21
- package/lib/reporter.js +40 -5
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- package/lib/uploader.js +4 -0
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/utils/pipe_utils.d.ts +15 -0
- package/lib/utils/pipe_utils.js +44 -2
- package/lib/utils/utils.d.ts +6 -0
- package/lib/utils/utils.js +260 -25
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +12 -7
- package/src/adapter/codecept.js +19 -19
- package/src/adapter/mocha.js +1 -1
- package/src/adapter/playwright.js +2 -2
- package/src/bin/cli.js +51 -13
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +69 -130
- package/src/helpers.js +1 -0
- package/src/junit-adapter/csharp.js +48 -6
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/pipe/bitbucket.js +5 -5
- package/src/pipe/coverage.js +440 -0
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/index.js +2 -0
- package/src/pipe/testomatio.js +109 -85
- package/src/reporter-functions.js +15 -12
- package/src/reporter.js +6 -4
- package/src/services/links.js +1 -1
- package/src/uploader.js +5 -0
- package/src/utils/log-formatter.js +113 -0
- package/src/utils/pipe_utils.js +52 -3
- package/src/utils/utils.js +277 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
package/src/pipe/testomatio.js
CHANGED
|
@@ -3,7 +3,12 @@ import pc from 'picocolors';
|
|
|
3
3
|
import { Gaxios } from 'gaxios';
|
|
4
4
|
import JsonCycle from 'json-cycle';
|
|
5
5
|
import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
|
|
6
|
-
import { isValidUrl,
|
|
6
|
+
import { isValidUrl,
|
|
7
|
+
foundedTestLog,
|
|
8
|
+
readLatestRunId,
|
|
9
|
+
transformEnvVarToBoolean,
|
|
10
|
+
getGitCommitSha
|
|
11
|
+
} from '../utils/utils.js';
|
|
7
12
|
import { parseFilterParams, generateFilterRequestParams, setS3Credentials } from '../utils/pipe_utils.js';
|
|
8
13
|
import { config } from '../config.js';
|
|
9
14
|
|
|
@@ -46,7 +51,25 @@ class TestomatioPipe {
|
|
|
46
51
|
this.store = store || {};
|
|
47
52
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
48
53
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
49
|
-
this.sharedRunTimeout =
|
|
54
|
+
this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
|
|
55
|
+
? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
|
|
56
|
+
: undefined;
|
|
57
|
+
|
|
58
|
+
if (this.sharedRunTimeout && !this.sharedRun) {
|
|
59
|
+
debug('Auto-enabling sharedRun because sharedRunTimeout is set');
|
|
60
|
+
this.sharedRun = true;
|
|
61
|
+
}
|
|
62
|
+
|
|
63
|
+
if (!this.title && (this.sharedRun || this.sharedRunTimeout)) {
|
|
64
|
+
const sha = getGitCommitSha();
|
|
65
|
+
if (sha) {
|
|
66
|
+
this.title = `Shared Run - ${sha}`;
|
|
67
|
+
console.log(APP_PREFIX, `🔄 Auto-generated title for shared run: ${this.title}`);
|
|
68
|
+
} else {
|
|
69
|
+
console.log(APP_PREFIX, pc.red('Failed to resolve git commit SHA for shared run title.'));
|
|
70
|
+
console.log(APP_PREFIX, 'Please run the tests inside a Git repository or set TESTOMATIO_TITLE explicitly.');
|
|
71
|
+
}
|
|
72
|
+
}
|
|
50
73
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
51
74
|
this.env = process.env.TESTOMATIO_ENV;
|
|
52
75
|
this.label = process.env.TESTOMATIO_LABEL;
|
|
@@ -60,8 +83,8 @@ class TestomatioPipe {
|
|
|
60
83
|
retryConfig: {
|
|
61
84
|
retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
62
85
|
retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
63
|
-
httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
|
|
64
|
-
shouldRetry:
|
|
86
|
+
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
87
|
+
shouldRetry: error => {
|
|
65
88
|
if (!error.response) return false;
|
|
66
89
|
switch (error.response?.status) {
|
|
67
90
|
case 400: // Bad request (probably wrong API key)
|
|
@@ -73,8 +96,8 @@ class TestomatioPipe {
|
|
|
73
96
|
break;
|
|
74
97
|
}
|
|
75
98
|
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
76
|
-
}
|
|
77
|
-
}
|
|
99
|
+
},
|
|
100
|
+
},
|
|
78
101
|
});
|
|
79
102
|
|
|
80
103
|
this.isEnabled = true;
|
|
@@ -104,7 +127,6 @@ class TestomatioPipe {
|
|
|
104
127
|
// add test ID + run ID
|
|
105
128
|
if (data.rid) data.rid = `${this.runId}-${data.rid}`;
|
|
106
129
|
|
|
107
|
-
|
|
108
130
|
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
109
131
|
data.stack = null;
|
|
110
132
|
}
|
|
@@ -120,7 +142,6 @@ class TestomatioPipe {
|
|
|
120
142
|
return data;
|
|
121
143
|
}
|
|
122
144
|
|
|
123
|
-
|
|
124
145
|
/**
|
|
125
146
|
* Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
|
|
126
147
|
* @param {Object} opts - The options for preparing the test grepList.
|
|
@@ -131,17 +152,23 @@ class TestomatioPipe {
|
|
|
131
152
|
async prepareRun(opts) {
|
|
132
153
|
if (!this.isEnabled) return [];
|
|
133
154
|
|
|
134
|
-
const
|
|
155
|
+
const clearOptions = parseFilterParams(opts);
|
|
156
|
+
|
|
157
|
+
if (!clearOptions) {
|
|
158
|
+
return [];
|
|
159
|
+
}
|
|
160
|
+
|
|
161
|
+
const { type, id } = clearOptions;
|
|
135
162
|
|
|
136
163
|
try {
|
|
137
164
|
const q = generateFilterRequestParams({
|
|
138
165
|
type,
|
|
139
166
|
id,
|
|
140
|
-
apiKey: this
|
|
167
|
+
apiKey: this?.apiKey?.trim(),
|
|
141
168
|
});
|
|
142
169
|
|
|
143
170
|
if (!q) {
|
|
144
|
-
return;
|
|
171
|
+
return [];
|
|
145
172
|
}
|
|
146
173
|
|
|
147
174
|
const resp = await this.client.request({
|
|
@@ -163,7 +190,7 @@ class TestomatioPipe {
|
|
|
163
190
|
|
|
164
191
|
/**
|
|
165
192
|
* Creates a new run on Testomat.io
|
|
166
|
-
* @param {{isBatchEnabled?: boolean}} params
|
|
193
|
+
* @param {{isBatchEnabled?: boolean, kind?: string}} params
|
|
167
194
|
* @returns Promise<void>
|
|
168
195
|
*/
|
|
169
196
|
async createRun(params = {}) {
|
|
@@ -204,6 +231,7 @@ class TestomatioPipe {
|
|
|
204
231
|
label: this.label,
|
|
205
232
|
shared_run: this.sharedRun,
|
|
206
233
|
shared_run_timeout: this.sharedRunTimeout,
|
|
234
|
+
kind: params.kind,
|
|
207
235
|
}).filter(([, value]) => !!value),
|
|
208
236
|
);
|
|
209
237
|
debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
|
|
@@ -215,7 +243,7 @@ class TestomatioPipe {
|
|
|
215
243
|
method: 'PUT',
|
|
216
244
|
url: `/api/reporter/${this.runId}`,
|
|
217
245
|
data: runParams,
|
|
218
|
-
responseType: 'json'
|
|
246
|
+
responseType: 'json',
|
|
219
247
|
});
|
|
220
248
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
221
249
|
return;
|
|
@@ -228,7 +256,7 @@ class TestomatioPipe {
|
|
|
228
256
|
url: '/api/reporter',
|
|
229
257
|
data: runParams,
|
|
230
258
|
maxContentLength: Infinity,
|
|
231
|
-
responseType: 'json'
|
|
259
|
+
responseType: 'json',
|
|
232
260
|
});
|
|
233
261
|
|
|
234
262
|
this.runId = resp.data.uid;
|
|
@@ -287,44 +315,44 @@ class TestomatioPipe {
|
|
|
287
315
|
|
|
288
316
|
debug('Adding test', json);
|
|
289
317
|
|
|
290
|
-
return this.client
|
|
291
|
-
|
|
292
|
-
|
|
293
|
-
|
|
294
|
-
|
|
295
|
-
|
|
296
|
-
|
|
297
|
-
|
|
298
|
-
|
|
299
|
-
|
|
300
|
-
|
|
301
|
-
|
|
302
|
-
|
|
303
|
-
|
|
318
|
+
return this.client
|
|
319
|
+
.request({
|
|
320
|
+
method: 'POST',
|
|
321
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
322
|
+
data: json,
|
|
323
|
+
headers: {
|
|
324
|
+
'Content-Type': 'application/json',
|
|
325
|
+
},
|
|
326
|
+
maxContentLength: Infinity,
|
|
327
|
+
})
|
|
328
|
+
.catch(err => {
|
|
329
|
+
this.requestFailures++;
|
|
330
|
+
this.notReportedTestsCount++;
|
|
331
|
+
if (err.response) {
|
|
332
|
+
if (err.response.status >= 400) {
|
|
333
|
+
const responseData = err.response.data || { message: '' };
|
|
334
|
+
console.log(
|
|
335
|
+
APP_PREFIX,
|
|
336
|
+
pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
337
|
+
pc.gray(data?.title || ''),
|
|
338
|
+
);
|
|
339
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
340
|
+
this.hasUnmatchedTests = true;
|
|
341
|
+
}
|
|
342
|
+
return;
|
|
343
|
+
}
|
|
304
344
|
console.log(
|
|
305
345
|
APP_PREFIX,
|
|
306
|
-
pc.yellow(`Warning: ${
|
|
307
|
-
|
|
346
|
+
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
347
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
308
348
|
);
|
|
309
|
-
|
|
310
|
-
|
|
311
|
-
|
|
312
|
-
return;
|
|
349
|
+
printCreateIssue(err);
|
|
350
|
+
} else {
|
|
351
|
+
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
313
352
|
}
|
|
314
|
-
|
|
315
|
-
APP_PREFIX,
|
|
316
|
-
pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
|
|
317
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
318
|
-
);
|
|
319
|
-
printCreateIssue(err);
|
|
320
|
-
} else {
|
|
321
|
-
console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
|
|
322
|
-
}
|
|
323
|
-
});
|
|
353
|
+
});
|
|
324
354
|
};
|
|
325
355
|
|
|
326
|
-
|
|
327
|
-
|
|
328
356
|
/**
|
|
329
357
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
330
358
|
*/
|
|
@@ -349,43 +377,42 @@ class TestomatioPipe {
|
|
|
349
377
|
const testsToSend = this.batch.tests.splice(0);
|
|
350
378
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
351
379
|
|
|
352
|
-
return this.client
|
|
353
|
-
|
|
354
|
-
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
358
|
-
|
|
359
|
-
|
|
360
|
-
|
|
361
|
-
|
|
362
|
-
|
|
363
|
-
|
|
364
|
-
|
|
365
|
-
|
|
366
|
-
|
|
367
|
-
|
|
368
|
-
|
|
369
|
-
|
|
380
|
+
return this.client
|
|
381
|
+
.request({
|
|
382
|
+
method: 'POST',
|
|
383
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
384
|
+
data: {
|
|
385
|
+
api_key: this.apiKey,
|
|
386
|
+
tests: testsToSend,
|
|
387
|
+
batch_index: this.batch.batchIndex,
|
|
388
|
+
},
|
|
389
|
+
headers: {
|
|
390
|
+
'Content-Type': 'application/json',
|
|
391
|
+
},
|
|
392
|
+
maxContentLength: Infinity,
|
|
393
|
+
})
|
|
394
|
+
.catch(err => {
|
|
395
|
+
this.requestFailures++;
|
|
396
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
397
|
+
if (err.response) {
|
|
398
|
+
if (err.response.status >= 400) {
|
|
399
|
+
const responseData = err.response.data || { message: '' };
|
|
400
|
+
console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
|
|
401
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
402
|
+
this.hasUnmatchedTests = true;
|
|
403
|
+
}
|
|
404
|
+
return;
|
|
405
|
+
}
|
|
370
406
|
console.log(
|
|
371
407
|
APP_PREFIX,
|
|
372
|
-
pc.yellow(`Warning:
|
|
408
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
409
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
373
410
|
);
|
|
374
|
-
|
|
375
|
-
|
|
376
|
-
|
|
377
|
-
return;
|
|
411
|
+
printCreateIssue(err);
|
|
412
|
+
} else {
|
|
413
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
378
414
|
}
|
|
379
|
-
|
|
380
|
-
APP_PREFIX,
|
|
381
|
-
pc.yellow(`Warning: (${err.response?.status})`),
|
|
382
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
383
|
-
);
|
|
384
|
-
printCreateIssue(err);
|
|
385
|
-
} else {
|
|
386
|
-
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
387
|
-
}
|
|
388
|
-
});
|
|
415
|
+
});
|
|
389
416
|
};
|
|
390
417
|
|
|
391
418
|
/**
|
|
@@ -408,9 +435,9 @@ class TestomatioPipe {
|
|
|
408
435
|
else this.batch.tests.push(data);
|
|
409
436
|
|
|
410
437
|
// if test is added after run which is already finished
|
|
411
|
-
|
|
438
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
412
439
|
|
|
413
|
-
|
|
440
|
+
// return promise to be able to wait for it
|
|
414
441
|
return uploading;
|
|
415
442
|
}
|
|
416
443
|
|
|
@@ -459,7 +486,7 @@ class TestomatioPipe {
|
|
|
459
486
|
status_event,
|
|
460
487
|
detach: params.detach,
|
|
461
488
|
tests: params.tests,
|
|
462
|
-
}
|
|
489
|
+
},
|
|
463
490
|
});
|
|
464
491
|
|
|
465
492
|
if (this.runUrl) {
|
|
@@ -525,9 +552,6 @@ function printCreateIssue(err) {
|
|
|
525
552
|
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
526
553
|
console.log('```');
|
|
527
554
|
});
|
|
528
|
-
|
|
529
555
|
}
|
|
530
556
|
|
|
531
|
-
|
|
532
|
-
|
|
533
557
|
export default TestomatioPipe;
|
|
@@ -1,3 +1,4 @@
|
|
|
1
|
+
import { isPlaywright } from './helpers.js';
|
|
1
2
|
import { services } from './services/index.js';
|
|
2
3
|
|
|
3
4
|
/**
|
|
@@ -7,9 +8,7 @@ import { services } from './services/index.js';
|
|
|
7
8
|
* @returns {void}
|
|
8
9
|
*/
|
|
9
10
|
function saveArtifact(data, context = null) {
|
|
10
|
-
|
|
11
|
-
throw new Error(`This function is not available in Playwright framework.
|
|
12
|
-
/Playwright supports artifacts out of the box`);
|
|
11
|
+
showPlaywrightWarning('artifact', 'Playwright supports artifacts out of the box.');
|
|
13
12
|
if (!data) return;
|
|
14
13
|
services.artifacts.put(data, context);
|
|
15
14
|
}
|
|
@@ -20,7 +19,6 @@ function saveArtifact(data, context = null) {
|
|
|
20
19
|
* @returns {void}
|
|
21
20
|
*/
|
|
22
21
|
function logMessage(...args) {
|
|
23
|
-
if (process.env.IS_PLAYWRIGHT) throw new Error('This function is not available in Playwright framework');
|
|
24
22
|
services.logger._templateLiteralLog(...args);
|
|
25
23
|
}
|
|
26
24
|
|
|
@@ -30,10 +28,10 @@ function logMessage(...args) {
|
|
|
30
28
|
* @returns {void}
|
|
31
29
|
*/
|
|
32
30
|
function addStep(message) {
|
|
33
|
-
if (process.env.IS_PLAYWRIGHT)
|
|
34
|
-
throw new Error('This function is not available in Playwright framework. Use playwright steps');
|
|
35
|
-
|
|
36
31
|
services.logger.step(message);
|
|
32
|
+
// this is done because Playwright reporter intercepts console logs and then we gather them and show on Testomat
|
|
33
|
+
// if not console.log, the step message will be lost from reporter
|
|
34
|
+
if (isPlaywright) console.log(`Step: ${message}`);
|
|
37
35
|
}
|
|
38
36
|
|
|
39
37
|
/**
|
|
@@ -43,8 +41,7 @@ function addStep(message) {
|
|
|
43
41
|
* @returns {void}
|
|
44
42
|
*/
|
|
45
43
|
function setKeyValue(keyValue, value = null) {
|
|
46
|
-
|
|
47
|
-
throw new Error('This function is not available in Playwright framework. Use test tag instead.');
|
|
44
|
+
showPlaywrightWarning('meta', 'Use test annotations instead.');
|
|
48
45
|
|
|
49
46
|
if (typeof keyValue === 'string') {
|
|
50
47
|
keyValue = { [keyValue]: value };
|
|
@@ -59,12 +56,12 @@ function setKeyValue(keyValue, value = null) {
|
|
|
59
56
|
* @returns {void}
|
|
60
57
|
*/
|
|
61
58
|
function setLabel(key, value = null) {
|
|
59
|
+
showPlaywrightWarning('label', 'Use test tag instead.');
|
|
62
60
|
if (Array.isArray(value)) {
|
|
63
61
|
return value.forEach(label => setLabel(key, label));
|
|
64
62
|
}
|
|
65
|
-
const labelObject =
|
|
66
|
-
? { label: `${key}:${value}` }
|
|
67
|
-
: { label: key };
|
|
63
|
+
const labelObject =
|
|
64
|
+
value !== null && value !== undefined && value !== '' ? { label: `${key}:${value}` } : { label: key };
|
|
68
65
|
services.links.put([labelObject]);
|
|
69
66
|
}
|
|
70
67
|
|
|
@@ -88,6 +85,12 @@ function linkJira(...jiraIds) {
|
|
|
88
85
|
services.links.put(links);
|
|
89
86
|
}
|
|
90
87
|
|
|
88
|
+
function showPlaywrightWarning(functionName, recommendation) {
|
|
89
|
+
if (isPlaywright) {
|
|
90
|
+
console.warn(`[TESTOMATIO] '${functionName}' function is not supported for Playwright. ${recommendation}`);
|
|
91
|
+
}
|
|
92
|
+
}
|
|
93
|
+
|
|
91
94
|
export default {
|
|
92
95
|
artifact: saveArtifact,
|
|
93
96
|
log: logMessage,
|
package/src/reporter.js
CHANGED
|
@@ -1,8 +1,10 @@
|
|
|
1
|
-
|
|
2
|
-
|
|
1
|
+
import Client from './client.js';
|
|
2
|
+
import * as TestomatioConstants from './constants.js';
|
|
3
3
|
import { services } from './services/index.js';
|
|
4
4
|
import reporterFunctions from './reporter-functions.js';
|
|
5
5
|
|
|
6
|
+
export { Client };
|
|
7
|
+
export const STATUS = TestomatioConstants.STATUS;
|
|
6
8
|
export const artifact = reporterFunctions.artifact;
|
|
7
9
|
export const log = reporterFunctions.log;
|
|
8
10
|
export const logger = services.logger;
|
|
@@ -35,6 +37,6 @@ export default {
|
|
|
35
37
|
linkTest: reporterFunctions.linkTest,
|
|
36
38
|
linkJira: reporterFunctions.linkJira,
|
|
37
39
|
|
|
38
|
-
|
|
39
|
-
|
|
40
|
+
TestomatioClient: Client,
|
|
41
|
+
STATUS,
|
|
40
42
|
};
|
package/src/services/links.js
CHANGED
package/src/uploader.js
CHANGED
|
@@ -194,6 +194,11 @@ export class S3Uploader {
|
|
|
194
194
|
filePath = path.join(process.cwd(), filePath);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
+
// Normalize path separators for cross-platform compatibility
|
|
198
|
+
if (typeof filePath === 'string') {
|
|
199
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
200
|
+
}
|
|
201
|
+
|
|
197
202
|
const data = { rid, file: filePath, uploaded };
|
|
198
203
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
199
204
|
fs.appendFileSync(tempFilePath, jsonLine);
|
|
@@ -0,0 +1,113 @@
|
|
|
1
|
+
import createCallsiteRecord from 'callsite-record';
|
|
2
|
+
import { minimatch } from 'minimatch';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { stripVTControlCharacters } from 'util';
|
|
5
|
+
import { sep } from 'path';
|
|
6
|
+
import { formatStep, truncate } from './utils.js';
|
|
7
|
+
|
|
8
|
+
const stripColors = stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
12
|
+
* @param {Object} params - Parameters for formatting logs
|
|
13
|
+
* @param {string} params.error - Error message
|
|
14
|
+
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
15
|
+
* @param {string} params.logs - Test logs
|
|
16
|
+
* @returns {string}
|
|
17
|
+
*/
|
|
18
|
+
export function formatLogs({ error, steps, logs }) {
|
|
19
|
+
error = error?.trim();
|
|
20
|
+
logs = logs
|
|
21
|
+
?.trim()
|
|
22
|
+
.split('\n')
|
|
23
|
+
.map(l => truncate(l))
|
|
24
|
+
.join('\n');
|
|
25
|
+
|
|
26
|
+
if (Array.isArray(steps)) {
|
|
27
|
+
steps = steps
|
|
28
|
+
.map(step => formatStep(step))
|
|
29
|
+
.flat()
|
|
30
|
+
.join('\n');
|
|
31
|
+
} else {
|
|
32
|
+
steps = null;
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
let testLogs = '';
|
|
36
|
+
if (steps) testLogs += `${pc.bold(pc.blue('################[ Steps ]################'))}\n${steps}\n\n`;
|
|
37
|
+
if (logs) testLogs += `${pc.bold(pc.gray('################[ Logs ]################'))}\n${logs}\n\n`;
|
|
38
|
+
if (error) testLogs += `${pc.bold(pc.red('################[ Failure ]################'))}\n${error}`;
|
|
39
|
+
return testLogs;
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
/**
|
|
43
|
+
* Formats an error with stack trace and diff information
|
|
44
|
+
* @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
|
|
45
|
+
* The error object to format
|
|
46
|
+
* @param {string} [message] - Optional error message override
|
|
47
|
+
* @returns {string}
|
|
48
|
+
*/
|
|
49
|
+
export function formatError(error, message) {
|
|
50
|
+
if (!message) message = error.message;
|
|
51
|
+
// @ts-ignore - inspect is a custom property added by some testing frameworks
|
|
52
|
+
if (error.inspect) message = error.inspect() || '';
|
|
53
|
+
|
|
54
|
+
let stack = '';
|
|
55
|
+
if (error.name) stack += `${pc.red(error.name)}`;
|
|
56
|
+
// @ts-ignore - operator is a custom property added by assertion libraries
|
|
57
|
+
if (error.operator) stack += ` (${pc.red(error.operator)})`;
|
|
58
|
+
// add new line if something was added to stack
|
|
59
|
+
if (stack) stack += ': ';
|
|
60
|
+
|
|
61
|
+
stack += `${message}\n`;
|
|
62
|
+
|
|
63
|
+
// @ts-ignore - diff is a custom property added by vitest
|
|
64
|
+
if (error.diff) {
|
|
65
|
+
// diff for vitest
|
|
66
|
+
stack += error.diff;
|
|
67
|
+
stack += '\n\n';
|
|
68
|
+
} else if (error.actual && error.expected && error.actual !== error.expected) {
|
|
69
|
+
// diffs for mocha, cypress, codeceptjs style
|
|
70
|
+
stack += `\n\n${pc.bold(pc.green('+ expected'))} ${pc.bold(pc.red('- actual'))}`;
|
|
71
|
+
stack += `\n${pc.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
|
|
72
|
+
stack += `\n${pc.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
|
|
73
|
+
stack += '\n\n';
|
|
74
|
+
}
|
|
75
|
+
|
|
76
|
+
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
77
|
+
|
|
78
|
+
try {
|
|
79
|
+
let hasFrame = false;
|
|
80
|
+
const record = createCallsiteRecord({
|
|
81
|
+
forError: error,
|
|
82
|
+
isCallsiteFrame: frame => {
|
|
83
|
+
if (customFilter && minimatch(frame.fileName, customFilter)) return false;
|
|
84
|
+
if (hasFrame) return false;
|
|
85
|
+
if (isNotInternalFrame(frame)) hasFrame = true;
|
|
86
|
+
return hasFrame;
|
|
87
|
+
},
|
|
88
|
+
});
|
|
89
|
+
// @ts-ignore
|
|
90
|
+
if (record && !record.filename.startsWith('http')) {
|
|
91
|
+
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
92
|
+
}
|
|
93
|
+
return stack;
|
|
94
|
+
} catch (e) {
|
|
95
|
+
console.log(e);
|
|
96
|
+
}
|
|
97
|
+
}
|
|
98
|
+
|
|
99
|
+
/**
|
|
100
|
+
* Checks if a stack frame is not an internal frame (node_modules or internal)
|
|
101
|
+
* @param {Object} frame - Stack frame object
|
|
102
|
+
* @returns {boolean}
|
|
103
|
+
*/
|
|
104
|
+
function isNotInternalFrame(frame) {
|
|
105
|
+
return (
|
|
106
|
+
frame.getFileName() &&
|
|
107
|
+
frame.getFileName().includes(sep) &&
|
|
108
|
+
!frame.getFileName().includes('node_modules') &&
|
|
109
|
+
!frame.getFileName().includes('internal')
|
|
110
|
+
);
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
export { stripColors };
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -15,6 +15,7 @@ function setS3Credentials(artifacts) {
|
|
|
15
15
|
if (artifacts.BUCKET) process.env.S3_BUCKET = artifacts.BUCKET;
|
|
16
16
|
if (artifacts.SESSION_TOKEN) process.env.S3_SESSION_TOKEN = artifacts.SESSION_TOKEN;
|
|
17
17
|
if (artifacts.presign) process.env.TESTOMATIO_PRIVATE_ARTIFACTS = '1';
|
|
18
|
+
if (artifacts.stack_artifacts) process.env.TESTOMATIO_STACK_ARTIFACTS = '1';
|
|
18
19
|
// endpoint is not received from the server; and shuld be empty if IAM used (credentails obtained from the testomat)
|
|
19
20
|
process.env.S3_ENDPOINT = artifacts.ENDPOINT || '';
|
|
20
21
|
}
|
|
@@ -25,6 +26,12 @@ function setS3Credentials(artifacts) {
|
|
|
25
26
|
* @returns {Object|null} - An object containing the generated request parameters, or null if the type is invalid.
|
|
26
27
|
*/
|
|
27
28
|
function generateFilterRequestParams(params) {
|
|
29
|
+
// Defensive check: ensure params is an object
|
|
30
|
+
if (!params || typeof params !== 'object') {
|
|
31
|
+
console.error(APP_PREFIX, `Invalid parameters provided. Expected an object, got: ${typeof params}`);
|
|
32
|
+
return;
|
|
33
|
+
}
|
|
34
|
+
|
|
28
35
|
const { type, id, apiKey } = params;
|
|
29
36
|
|
|
30
37
|
if (!type) {
|
|
@@ -53,9 +60,13 @@ function generateFilterRequestParams(params) {
|
|
|
53
60
|
* The object has properties "type" and "id".
|
|
54
61
|
*/
|
|
55
62
|
function parseFilterParams(opts) {
|
|
56
|
-
const [type,
|
|
63
|
+
const [type, ...idParts] = opts.split('=');
|
|
64
|
+
const id = idParts.join('=');
|
|
65
|
+
|
|
57
66
|
const validType = updateFilterType(type);
|
|
58
67
|
|
|
68
|
+
if (!validType) return undefined;
|
|
69
|
+
|
|
59
70
|
return {
|
|
60
71
|
type: validType,
|
|
61
72
|
id,
|
|
@@ -69,6 +80,8 @@ function parseFilterParams(opts) {
|
|
|
69
80
|
* Returns undefined if the type is not valid.
|
|
70
81
|
*/
|
|
71
82
|
function updateFilterType(type) {
|
|
83
|
+
if (!type || typeof type !== 'string') return;
|
|
84
|
+
|
|
72
85
|
let typeLowerCase = type.toLowerCase();
|
|
73
86
|
|
|
74
87
|
const filterTypes = ['tag-name', 'plan', 'label', 'jira-ticket'];
|
|
@@ -86,7 +99,7 @@ function updateFilterType(type) {
|
|
|
86
99
|
];
|
|
87
100
|
|
|
88
101
|
if (!filterTypes.includes(typeLowerCase)) {
|
|
89
|
-
console.log(APP_PREFIX, `❗❗❗ Invalid "
|
|
102
|
+
console.log(APP_PREFIX, `❗❗❗ Invalid filter: "${type}" start settings! Available option list: ${filterTypes}`);
|
|
90
103
|
return;
|
|
91
104
|
}
|
|
92
105
|
|
|
@@ -120,4 +133,40 @@ function fullName(t) {
|
|
|
120
133
|
return line;
|
|
121
134
|
}
|
|
122
135
|
|
|
123
|
-
|
|
136
|
+
/**
|
|
137
|
+
* Parses a comma-separated list of key-value pairs into an options object.
|
|
138
|
+
*
|
|
139
|
+
* The input string should be formatted as `"key1=value1,key2=value2,..."`.
|
|
140
|
+
* Whitespace around keys and values is trimmed. If the input is empty or undefined,
|
|
141
|
+
* an empty object is returned.
|
|
142
|
+
*
|
|
143
|
+
* @param {string} [optionsStr] - A comma-separated string of key=value pairs.
|
|
144
|
+
* @returns {Object} An object mapping option keys to their string values.
|
|
145
|
+
*
|
|
146
|
+
* @example
|
|
147
|
+
* parsePipeOptions('foo=bar,baz=qux');
|
|
148
|
+
* => Returns: { foo: 'bar', baz: 'qux' }
|
|
149
|
+
*/
|
|
150
|
+
function parsePipeOptions(optionsStr) {
|
|
151
|
+
const options = {};
|
|
152
|
+
if (!optionsStr) return options;
|
|
153
|
+
|
|
154
|
+
const pairs = optionsStr.split(',');
|
|
155
|
+
for (const pair of pairs) {
|
|
156
|
+
const [key, value] = pair.split('=');
|
|
157
|
+
if (key && value) {
|
|
158
|
+
options[key.trim()] = value.trim();
|
|
159
|
+
}
|
|
160
|
+
}
|
|
161
|
+
return options;
|
|
162
|
+
}
|
|
163
|
+
|
|
164
|
+
export {
|
|
165
|
+
updateFilterType,
|
|
166
|
+
parseFilterParams,
|
|
167
|
+
generateFilterRequestParams,
|
|
168
|
+
setS3Credentials,
|
|
169
|
+
statusEmoji,
|
|
170
|
+
fullName,
|
|
171
|
+
parsePipeOptions
|
|
172
|
+
};
|