@testomatio/reporter 2.18.0 ā 2.19.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/constants.d.ts +1 -0
- package/lib/constants.js +7 -1
- package/lib/pipe/bitbucket.js +5 -1
- package/lib/pipe/github.js +5 -1
- package/lib/pipe/gitlab.js +5 -1
- package/lib/pipe/testomatio.d.ts +2 -0
- package/lib/pipe/testomatio.js +53 -2
- package/lib/utils/pipe_utils.d.ts +16 -0
- package/lib/utils/pipe_utils.js +37 -0
- package/package.json +1 -1
- package/src/constants.js +5 -0
- package/src/pipe/bitbucket.js +7 -1
- package/src/pipe/github.js +7 -1
- package/src/pipe/gitlab.js +7 -1
- package/src/pipe/testomatio.js +56 -2
- package/src/utils/pipe_utils.js +40 -0
package/lib/constants.d.ts
CHANGED
package/lib/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEBUG_FILE = exports.SCREENSHOTS_ON_STEPS = exports.REPORTER_REQUEST_RETRIES = exports.testomatLogoURL = exports.REQUEST_TIMEOUT = exports.MARKDOWN_REPORT = exports.HTML_REPORT = exports.BATCH_MODE = exports.STATUS = exports.CSV_HEADERS = exports.TESTOMAT_TMP_STORAGE_DIR = exports.APP_PREFIX = void 0;
|
|
6
|
+
exports.S3_CREDENTIALS_REFRESH_INTERVAL_MS = exports.DEBUG_FILE = exports.SCREENSHOTS_ON_STEPS = exports.REPORTER_REQUEST_RETRIES = exports.testomatLogoURL = exports.REQUEST_TIMEOUT = exports.MARKDOWN_REPORT = exports.HTML_REPORT = exports.BATCH_MODE = exports.STATUS = exports.CSV_HEADERS = exports.TESTOMAT_TMP_STORAGE_DIR = exports.APP_PREFIX = void 0;
|
|
7
7
|
exports.getCreateRunRequestTimeout = getCreateRunRequestTimeout;
|
|
8
8
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
@@ -69,6 +69,10 @@ const REPORTER_REQUEST_RETRIES = {
|
|
|
69
69
|
exports.REPORTER_REQUEST_RETRIES = REPORTER_REQUEST_RETRIES;
|
|
70
70
|
const DEBUG_FILE = 'testomatio.debug';
|
|
71
71
|
exports.DEBUG_FILE = DEBUG_FILE;
|
|
72
|
+
// temporary S3 tokens live ~1 hour; refresh credentials well before that so artifact
|
|
73
|
+
// uploads keep working on long runs
|
|
74
|
+
const S3_CREDENTIALS_REFRESH_INTERVAL_MS = 40 * 60 * 1000;
|
|
75
|
+
exports.S3_CREDENTIALS_REFRESH_INTERVAL_MS = S3_CREDENTIALS_REFRESH_INTERVAL_MS;
|
|
72
76
|
function getCreateRunRequestTimeout() {
|
|
73
77
|
return Math.max(REQUEST_TIMEOUT, 80 * 1000);
|
|
74
78
|
}
|
|
@@ -98,3 +102,5 @@ module.exports.testomatLogoURL = testomatLogoURL;
|
|
|
98
102
|
module.exports.REPORTER_REQUEST_RETRIES = REPORTER_REQUEST_RETRIES;
|
|
99
103
|
|
|
100
104
|
module.exports.DEBUG_FILE = DEBUG_FILE;
|
|
105
|
+
|
|
106
|
+
module.exports.S3_CREDENTIALS_REFRESH_INTERVAL_MS = S3_CREDENTIALS_REFRESH_INTERVAL_MS;
|
package/lib/pipe/bitbucket.js
CHANGED
|
@@ -175,7 +175,8 @@ class BitbucketPipe {
|
|
|
175
175
|
});
|
|
176
176
|
let body = summary;
|
|
177
177
|
if (this.description) {
|
|
178
|
-
|
|
178
|
+
// Bitbucket strips HTML, so no <details> here
|
|
179
|
+
body += `\n\n**š Description**\n\n${(0, utils_js_1.truncate)(this.description, 1024)}\n`;
|
|
179
180
|
}
|
|
180
181
|
if (failures.length) {
|
|
181
182
|
body += `\nš„ **Failures (${failures.length})**\n\n* ${failures.join('\n* ')}\n`;
|
|
@@ -183,6 +184,9 @@ class BitbucketPipe {
|
|
|
183
184
|
body += `\n> Notice: Only the first 10 failures are shown.`;
|
|
184
185
|
}
|
|
185
186
|
}
|
|
187
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
188
|
+
body += `\n\n**${title}**\n\n${list.join('\n')}`;
|
|
189
|
+
}
|
|
186
190
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
187
191
|
body += `\n\n**š¢ Slowest Tests**\n\n`;
|
|
188
192
|
body += this.tests
|
package/lib/pipe/github.js
CHANGED
|
@@ -163,7 +163,8 @@ class GitHubPipe {
|
|
|
163
163
|
});
|
|
164
164
|
let body = summary;
|
|
165
165
|
if (this.description) {
|
|
166
|
-
body +=
|
|
166
|
+
body += '\n\n<details>\n<summary><h3>š Description</h3></summary>\n\n';
|
|
167
|
+
body += `${(0, utils_js_1.truncate)(this.description, 1024)}\n\n</details>`;
|
|
167
168
|
}
|
|
168
169
|
const coverageConfiguration = this.store?.coverageConfiguration;
|
|
169
170
|
const isManualRun = this.store?.runKind === 'manual';
|
|
@@ -187,6 +188,9 @@ class GitHubPipe {
|
|
|
187
188
|
}
|
|
188
189
|
body += '\n\n</details>';
|
|
189
190
|
}
|
|
191
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
192
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
193
|
+
}
|
|
190
194
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
191
195
|
body += '\n<details>\n<summary><h3>š¢ Slowest Tests</h3></summary>\n\n';
|
|
192
196
|
body += this.tests
|
package/lib/pipe/gitlab.js
CHANGED
|
@@ -122,7 +122,8 @@ class GitLabPipe {
|
|
|
122
122
|
});
|
|
123
123
|
let body = summary;
|
|
124
124
|
if (this.description) {
|
|
125
|
-
body +=
|
|
125
|
+
body += '\n\n<details>\n<summary><h3>š Description</h3></summary>\n\n';
|
|
126
|
+
body += `${(0, utils_js_1.truncate)(this.description, 1024)}\n\n</details>`;
|
|
126
127
|
}
|
|
127
128
|
if (failures.length) {
|
|
128
129
|
body += `\n<details>\n<summary><h3>š„ Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
@@ -131,6 +132,9 @@ class GitLabPipe {
|
|
|
131
132
|
}
|
|
132
133
|
body += '\n\n</details>';
|
|
133
134
|
}
|
|
135
|
+
for (const { title, list } of (0, pipe_utils_js_1.smallRunSections)(this.tests)) {
|
|
136
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
137
|
+
}
|
|
134
138
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
135
139
|
body += '\n<details>\n<summary><h3>š¢ Slowest Tests</h3></summary>\n\n';
|
|
136
140
|
body += this.tests
|
package/lib/pipe/testomatio.d.ts
CHANGED
|
@@ -30,6 +30,8 @@ declare class TestomatioPipe implements Pipe {
|
|
|
30
30
|
retriesTimestamps: any[];
|
|
31
31
|
reportingCanceledDueToReqFailures: boolean;
|
|
32
32
|
notReportedTestsCount: number;
|
|
33
|
+
/** @type {NodeJS.Timeout | null} */
|
|
34
|
+
s3CredentialsRefreshTimer: NodeJS.Timeout | null;
|
|
33
35
|
isEnabled: boolean;
|
|
34
36
|
url: any;
|
|
35
37
|
apiKey: any;
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -63,6 +63,8 @@ class TestomatioPipe {
|
|
|
63
63
|
this.retriesTimestamps = [];
|
|
64
64
|
this.reportingCanceledDueToReqFailures = false;
|
|
65
65
|
this.notReportedTestsCount = 0;
|
|
66
|
+
/** @type {NodeJS.Timeout | null} */
|
|
67
|
+
this.s3CredentialsRefreshTimer = null;
|
|
66
68
|
this.isEnabled = false;
|
|
67
69
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
68
70
|
this.apiKey = params.apiKey || config_js_1.config.TESTOMATIO;
|
|
@@ -311,8 +313,10 @@ class TestomatioPipe {
|
|
|
311
313
|
timeout: (0, constants_js_1.getCreateRunRequestTimeout)(),
|
|
312
314
|
responseType: 'json',
|
|
313
315
|
});
|
|
314
|
-
if (resp.data.artifacts)
|
|
316
|
+
if (resp.data.artifacts) {
|
|
315
317
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
318
|
+
this.#startS3CredentialsRefresh(runParams);
|
|
319
|
+
}
|
|
316
320
|
if (resp.data.url) {
|
|
317
321
|
const respUrl = new URL(resp.data.url);
|
|
318
322
|
this.runUrl = `${this.url}${respUrl.pathname}`;
|
|
@@ -337,8 +341,10 @@ class TestomatioPipe {
|
|
|
337
341
|
this.runId = resp.data.uid;
|
|
338
342
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
339
343
|
this.runPublicUrl = resp.data.public_url;
|
|
340
|
-
if (resp.data.artifacts)
|
|
344
|
+
if (resp.data.artifacts) {
|
|
341
345
|
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
346
|
+
this.#startS3CredentialsRefresh(runParams);
|
|
347
|
+
}
|
|
342
348
|
this.store.runUrl = this.runUrl;
|
|
343
349
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
344
350
|
this.store.runId = this.runId;
|
|
@@ -363,6 +369,49 @@ class TestomatioPipe {
|
|
|
363
369
|
}
|
|
364
370
|
debug('"createRun" function finished');
|
|
365
371
|
}
|
|
372
|
+
/**
|
|
373
|
+
* Starts periodic refreshing of S3 credentials for long runs.
|
|
374
|
+
* Temporary S3 tokens (STS) expire after ~1 hour, so without refreshing,
|
|
375
|
+
* artifact uploads fail for every test reported later in the run.
|
|
376
|
+
* The same run params are re-sent on refresh (same as updating an existing run),
|
|
377
|
+
* and fresh credentials are taken from the response's `artifacts` field.
|
|
378
|
+
* @param {Record<string, any>} runParams
|
|
379
|
+
*/
|
|
380
|
+
#startS3CredentialsRefresh(runParams) {
|
|
381
|
+
if (this.s3CredentialsRefreshTimer)
|
|
382
|
+
return;
|
|
383
|
+
this.s3CredentialsRefreshTimer = setInterval(() => {
|
|
384
|
+
this.#refreshS3Credentials(runParams);
|
|
385
|
+
}, constants_js_1.S3_CREDENTIALS_REFRESH_INTERVAL_MS);
|
|
386
|
+
this.s3CredentialsRefreshTimer.unref?.();
|
|
387
|
+
debug(`S3 credentials refresh started (every ${constants_js_1.S3_CREDENTIALS_REFRESH_INTERVAL_MS / 60000} min)`);
|
|
388
|
+
}
|
|
389
|
+
#refreshS3Credentials = async (runParams) => {
|
|
390
|
+
if (!this.isEnabled || !this.runId)
|
|
391
|
+
return;
|
|
392
|
+
const { ci, access_event, ...refreshParams } = runParams;
|
|
393
|
+
try {
|
|
394
|
+
const resp = await this.client.request({
|
|
395
|
+
method: 'PUT',
|
|
396
|
+
url: `/api/reporter/${this.runId}`,
|
|
397
|
+
data: { ...refreshParams, api_key: this.apiKey },
|
|
398
|
+
responseType: 'json',
|
|
399
|
+
});
|
|
400
|
+
if (resp.data?.artifacts) {
|
|
401
|
+
(0, pipe_utils_js_1.setS3Credentials)(resp.data.artifacts);
|
|
402
|
+
debug('S3 credentials refreshed');
|
|
403
|
+
}
|
|
404
|
+
}
|
|
405
|
+
catch (err) {
|
|
406
|
+
debug('Failed to refresh S3 credentials:', err.message || err);
|
|
407
|
+
}
|
|
408
|
+
};
|
|
409
|
+
#stopS3CredentialsRefresh() {
|
|
410
|
+
if (this.s3CredentialsRefreshTimer) {
|
|
411
|
+
clearInterval(this.s3CredentialsRefreshTimer);
|
|
412
|
+
this.s3CredentialsRefreshTimer = null;
|
|
413
|
+
}
|
|
414
|
+
}
|
|
366
415
|
/**
|
|
367
416
|
* Decides whether to skip test reporting in case of too many request failures
|
|
368
417
|
* @returns {boolean}
|
|
@@ -506,6 +555,7 @@ class TestomatioPipe {
|
|
|
506
555
|
async finishRun(params) {
|
|
507
556
|
if (!this.isEnabled)
|
|
508
557
|
return;
|
|
558
|
+
this.#stopS3CredentialsRefresh();
|
|
509
559
|
await this.#batchUpload();
|
|
510
560
|
if (this.batch.intervalFunction) {
|
|
511
561
|
clearInterval(this.batch.intervalFunction);
|
|
@@ -577,6 +627,7 @@ class TestomatioPipe {
|
|
|
577
627
|
#disablePipe() {
|
|
578
628
|
this.isEnabled = false;
|
|
579
629
|
this.apiKey = null;
|
|
630
|
+
this.#stopS3CredentialsRefresh();
|
|
580
631
|
// clear interval function, otherwise the proccess will continue indefinitely
|
|
581
632
|
if (this.batch.intervalFunction) {
|
|
582
633
|
clearInterval(this.batch.intervalFunction);
|
|
@@ -72,6 +72,22 @@ export function runSummary(tests: Array<{
|
|
|
72
72
|
export function totalDuration(tests: Array<{
|
|
73
73
|
run_time?: number;
|
|
74
74
|
}>): string;
|
|
75
|
+
/**
|
|
76
|
+
* Sections listing the passed, skipped and not yet run tests of a small run; failures have their own
|
|
77
|
+
* section. Not yet run tests (no status, or `pending`) are listed without any status label.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array<{status?: string, title?: string, suite_title?: string, run_time?: number}>} tests
|
|
80
|
+
* @returns {Array<{title: string, list: string[]}>} Non-empty sections; none if the run is not small.
|
|
81
|
+
*/
|
|
82
|
+
export function smallRunSections(tests: Array<{
|
|
83
|
+
status?: string;
|
|
84
|
+
title?: string;
|
|
85
|
+
suite_title?: string;
|
|
86
|
+
run_time?: number;
|
|
87
|
+
}>): Array<{
|
|
88
|
+
title: string;
|
|
89
|
+
list: string[];
|
|
90
|
+
}>;
|
|
75
91
|
/**
|
|
76
92
|
* Describe the scope of a run that was prepared but not executed yet. Prefers the server's count;
|
|
77
93
|
* without it, falls back to the scoped ids, which mix tests (`Tā¦`) and suites (`Sā¦`).
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -12,6 +12,7 @@ exports.fullName = fullName;
|
|
|
12
12
|
exports.markdownTable = markdownTable;
|
|
13
13
|
exports.runSummary = runSummary;
|
|
14
14
|
exports.totalDuration = totalDuration;
|
|
15
|
+
exports.smallRunSections = smallRunSections;
|
|
15
16
|
exports.plannedTestsLabel = plannedTestsLabel;
|
|
16
17
|
exports.parsePipeOptions = parsePipeOptions;
|
|
17
18
|
exports.formatFilterListIds = formatFilterListIds;
|
|
@@ -135,6 +136,13 @@ function statusEmoji(status) {
|
|
|
135
136
|
return 'š';
|
|
136
137
|
return '';
|
|
137
138
|
}
|
|
139
|
+
/**
|
|
140
|
+
* @param {string} str
|
|
141
|
+
* @returns {string} The string with its first letter in upper case, e.g. `passed` ā `Passed`.
|
|
142
|
+
*/
|
|
143
|
+
function capitalize(str) {
|
|
144
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
145
|
+
}
|
|
138
146
|
/**
|
|
139
147
|
* Generate a full name string based on the provided test object.
|
|
140
148
|
* @param {object} t - The test object.
|
|
@@ -262,6 +270,33 @@ function totalDuration(tests) {
|
|
|
262
270
|
const milliseconds = tests.reduce((total, t) => total + (t.run_time || 0), 0);
|
|
263
271
|
return (0, humanize_duration_1.default)(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
264
272
|
}
|
|
273
|
+
/**
|
|
274
|
+
* Runs with fewer tests than this list every test in the report, not only the failed ones.
|
|
275
|
+
*/
|
|
276
|
+
const SMALL_RUN_TESTS_LIMIT = 10;
|
|
277
|
+
/**
|
|
278
|
+
* Sections listing the passed, skipped and not yet run tests of a small run; failures have their own
|
|
279
|
+
* section. Not yet run tests (no status, or `pending`) are listed without any status label.
|
|
280
|
+
*
|
|
281
|
+
* @param {Array<{status?: string, title?: string, suite_title?: string, run_time?: number}>} tests
|
|
282
|
+
* @returns {Array<{title: string, list: string[]}>} Non-empty sections; none if the run is not small.
|
|
283
|
+
*/
|
|
284
|
+
function smallRunSections(tests) {
|
|
285
|
+
if (tests.length >= SMALL_RUN_TESTS_LIMIT)
|
|
286
|
+
return [];
|
|
287
|
+
const sections = ['passed', 'skipped'].map(status => {
|
|
288
|
+
const list = tests
|
|
289
|
+
.filter(t => t.status === status)
|
|
290
|
+
.map(t => {
|
|
291
|
+
const runTime = t.run_time ? ` (${(0, humanize_duration_1.default)(t.run_time)})` : '';
|
|
292
|
+
return `* ${statusEmoji(status)} ${fullName(t)}${runTime}`;
|
|
293
|
+
});
|
|
294
|
+
return { title: `${statusEmoji(status)} ${capitalize(status)} (${list.length})`, list };
|
|
295
|
+
});
|
|
296
|
+
const notRun = tests.filter(t => !t.status || t.status === 'pending').map(t => `* ${fullName(t)}`);
|
|
297
|
+
sections.push({ title: `š Tests (${notRun.length})`, list: notRun });
|
|
298
|
+
return sections.filter(section => section.list.length);
|
|
299
|
+
}
|
|
265
300
|
/**
|
|
266
301
|
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
267
302
|
* need no surrounding `if`.
|
|
@@ -384,6 +419,8 @@ module.exports.runSummary = runSummary;
|
|
|
384
419
|
|
|
385
420
|
module.exports.totalDuration = totalDuration;
|
|
386
421
|
|
|
422
|
+
module.exports.smallRunSections = smallRunSections;
|
|
423
|
+
|
|
387
424
|
module.exports.plannedTestsLabel = plannedTestsLabel;
|
|
388
425
|
|
|
389
426
|
module.exports.parsePipeOptions = parsePipeOptions;
|
package/package.json
CHANGED
package/src/constants.js
CHANGED
|
@@ -60,6 +60,10 @@ const REPORTER_REQUEST_RETRIES = {
|
|
|
60
60
|
|
|
61
61
|
const DEBUG_FILE = 'testomatio.debug';
|
|
62
62
|
|
|
63
|
+
// temporary S3 tokens live ~1 hour; refresh credentials well before that so artifact
|
|
64
|
+
// uploads keep working on long runs
|
|
65
|
+
const S3_CREDENTIALS_REFRESH_INTERVAL_MS = 40 * 60 * 1000;
|
|
66
|
+
|
|
63
67
|
function getCreateRunRequestTimeout() {
|
|
64
68
|
return Math.max(REQUEST_TIMEOUT, 80 * 1000);
|
|
65
69
|
}
|
|
@@ -78,4 +82,5 @@ export {
|
|
|
78
82
|
REPORTER_REQUEST_RETRIES,
|
|
79
83
|
SCREENSHOTS_ON_STEPS,
|
|
80
84
|
DEBUG_FILE,
|
|
85
|
+
S3_CREDENTIALS_REFRESH_INTERVAL_MS,
|
|
81
86
|
};
|
package/src/pipe/bitbucket.js
CHANGED
|
@@ -7,6 +7,7 @@ import {
|
|
|
7
7
|
markdownTable,
|
|
8
8
|
runSummary,
|
|
9
9
|
totalDuration,
|
|
10
|
+
smallRunSections,
|
|
10
11
|
} from '../utils/pipe_utils.js';
|
|
11
12
|
import { Gaxios } from 'gaxios';
|
|
12
13
|
import pc from 'picocolors';
|
|
@@ -174,7 +175,8 @@ export class BitbucketPipe {
|
|
|
174
175
|
let body = summary;
|
|
175
176
|
|
|
176
177
|
if (this.description) {
|
|
177
|
-
|
|
178
|
+
// Bitbucket strips HTML, so no <details> here
|
|
179
|
+
body += `\n\n**š Description**\n\n${truncate(this.description, 1024)}\n`;
|
|
178
180
|
}
|
|
179
181
|
|
|
180
182
|
if (failures.length) {
|
|
@@ -184,6 +186,10 @@ export class BitbucketPipe {
|
|
|
184
186
|
}
|
|
185
187
|
}
|
|
186
188
|
|
|
189
|
+
for (const { title, list } of smallRunSections(this.tests)) {
|
|
190
|
+
body += `\n\n**${title}**\n\n${list.join('\n')}`;
|
|
191
|
+
}
|
|
192
|
+
|
|
187
193
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
188
194
|
body += `\n\n**š¢ Slowest Tests**\n\n`;
|
|
189
195
|
body += this.tests
|
package/src/pipe/github.js
CHANGED
|
@@ -12,6 +12,7 @@ import {
|
|
|
12
12
|
markdownTable,
|
|
13
13
|
runSummary,
|
|
14
14
|
totalDuration,
|
|
15
|
+
smallRunSections,
|
|
15
16
|
} from '../utils/pipe_utils.js';
|
|
16
17
|
import { log } from '../utils/log.js';
|
|
17
18
|
|
|
@@ -147,7 +148,8 @@ class GitHubPipe {
|
|
|
147
148
|
|
|
148
149
|
let body = summary;
|
|
149
150
|
if (this.description) {
|
|
150
|
-
body +=
|
|
151
|
+
body += '\n\n<details>\n<summary><h3>š Description</h3></summary>\n\n';
|
|
152
|
+
body += `${truncate(this.description, 1024)}\n\n</details>`;
|
|
151
153
|
}
|
|
152
154
|
const coverageConfiguration = this.store?.coverageConfiguration;
|
|
153
155
|
const isManualRun = this.store?.runKind === 'manual';
|
|
@@ -172,6 +174,10 @@ class GitHubPipe {
|
|
|
172
174
|
body += '\n\n</details>';
|
|
173
175
|
}
|
|
174
176
|
|
|
177
|
+
for (const { title, list } of smallRunSections(this.tests)) {
|
|
178
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
179
|
+
}
|
|
180
|
+
|
|
175
181
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
176
182
|
body += '\n<details>\n<summary><h3>š¢ Slowest Tests</h3></summary>\n\n';
|
|
177
183
|
body += this.tests
|
package/src/pipe/gitlab.js
CHANGED
|
@@ -13,6 +13,7 @@ import {
|
|
|
13
13
|
markdownTable,
|
|
14
14
|
runSummary,
|
|
15
15
|
totalDuration,
|
|
16
|
+
smallRunSections,
|
|
16
17
|
} from '../utils/pipe_utils.js';
|
|
17
18
|
import { log } from '../utils/log.js';
|
|
18
19
|
|
|
@@ -148,7 +149,8 @@ class GitLabPipe {
|
|
|
148
149
|
let body = summary;
|
|
149
150
|
|
|
150
151
|
if (this.description) {
|
|
151
|
-
body +=
|
|
152
|
+
body += '\n\n<details>\n<summary><h3>š Description</h3></summary>\n\n';
|
|
153
|
+
body += `${truncate(this.description, 1024)}\n\n</details>`;
|
|
152
154
|
}
|
|
153
155
|
|
|
154
156
|
if (failures.length) {
|
|
@@ -159,6 +161,10 @@ class GitLabPipe {
|
|
|
159
161
|
body += '\n\n</details>';
|
|
160
162
|
}
|
|
161
163
|
|
|
164
|
+
for (const { title, list } of smallRunSections(this.tests)) {
|
|
165
|
+
body += `\n<details>\n<summary><h3>${title}</h3></summary>\n\n${list.join('\n')}\n</details>`;
|
|
166
|
+
}
|
|
167
|
+
|
|
162
168
|
if (this.tests.length > 0 && !isPendingRun) {
|
|
163
169
|
body += '\n<details>\n<summary><h3>š¢ Slowest Tests</h3></summary>\n\n';
|
|
164
170
|
body += this.tests
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -9,6 +9,7 @@ import {
|
|
|
9
9
|
REQUEST_TIMEOUT,
|
|
10
10
|
getCreateRunRequestTimeout,
|
|
11
11
|
REPORTER_REQUEST_RETRIES,
|
|
12
|
+
S3_CREDENTIALS_REFRESH_INTERVAL_MS,
|
|
12
13
|
} from '../constants.js';
|
|
13
14
|
import {
|
|
14
15
|
isValidUrl,
|
|
@@ -72,6 +73,8 @@ class TestomatioPipe {
|
|
|
72
73
|
this.retriesTimestamps = [];
|
|
73
74
|
this.reportingCanceledDueToReqFailures = false;
|
|
74
75
|
this.notReportedTestsCount = 0;
|
|
76
|
+
/** @type {NodeJS.Timeout | null} */
|
|
77
|
+
this.s3CredentialsRefreshTimer = null;
|
|
75
78
|
|
|
76
79
|
this.isEnabled = false;
|
|
77
80
|
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
@@ -346,7 +349,10 @@ class TestomatioPipe {
|
|
|
346
349
|
timeout: getCreateRunRequestTimeout(),
|
|
347
350
|
responseType: 'json',
|
|
348
351
|
});
|
|
349
|
-
if (resp.data.artifacts)
|
|
352
|
+
if (resp.data.artifacts) {
|
|
353
|
+
setS3Credentials(resp.data.artifacts);
|
|
354
|
+
this.#startS3CredentialsRefresh(runParams);
|
|
355
|
+
}
|
|
350
356
|
if (resp.data.url) {
|
|
351
357
|
const respUrl = new URL(resp.data.url);
|
|
352
358
|
this.runUrl = `${this.url}${respUrl.pathname}`;
|
|
@@ -374,7 +380,10 @@ class TestomatioPipe {
|
|
|
374
380
|
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
375
381
|
this.runPublicUrl = resp.data.public_url;
|
|
376
382
|
|
|
377
|
-
if (resp.data.artifacts)
|
|
383
|
+
if (resp.data.artifacts) {
|
|
384
|
+
setS3Credentials(resp.data.artifacts);
|
|
385
|
+
this.#startS3CredentialsRefresh(runParams);
|
|
386
|
+
}
|
|
378
387
|
|
|
379
388
|
this.store.runUrl = this.runUrl;
|
|
380
389
|
this.store.runPublicUrl = this.runPublicUrl;
|
|
@@ -401,6 +410,49 @@ class TestomatioPipe {
|
|
|
401
410
|
debug('"createRun" function finished');
|
|
402
411
|
}
|
|
403
412
|
|
|
413
|
+
/**
|
|
414
|
+
* Starts periodic refreshing of S3 credentials for long runs.
|
|
415
|
+
* Temporary S3 tokens (STS) expire after ~1 hour, so without refreshing,
|
|
416
|
+
* artifact uploads fail for every test reported later in the run.
|
|
417
|
+
* The same run params are re-sent on refresh (same as updating an existing run),
|
|
418
|
+
* and fresh credentials are taken from the response's `artifacts` field.
|
|
419
|
+
* @param {Record<string, any>} runParams
|
|
420
|
+
*/
|
|
421
|
+
#startS3CredentialsRefresh(runParams) {
|
|
422
|
+
if (this.s3CredentialsRefreshTimer) return;
|
|
423
|
+
this.s3CredentialsRefreshTimer = setInterval(() => {
|
|
424
|
+
this.#refreshS3Credentials(runParams);
|
|
425
|
+
}, S3_CREDENTIALS_REFRESH_INTERVAL_MS);
|
|
426
|
+
this.s3CredentialsRefreshTimer.unref?.();
|
|
427
|
+
debug(`S3 credentials refresh started (every ${S3_CREDENTIALS_REFRESH_INTERVAL_MS / 60000} min)`);
|
|
428
|
+
}
|
|
429
|
+
|
|
430
|
+
#refreshS3Credentials = async runParams => {
|
|
431
|
+
if (!this.isEnabled || !this.runId) return;
|
|
432
|
+
const { ci, access_event, ...refreshParams } = runParams;
|
|
433
|
+
try {
|
|
434
|
+
const resp = await this.client.request({
|
|
435
|
+
method: 'PUT',
|
|
436
|
+
url: `/api/reporter/${this.runId}`,
|
|
437
|
+
data: { ...refreshParams, api_key: this.apiKey },
|
|
438
|
+
responseType: 'json',
|
|
439
|
+
});
|
|
440
|
+
if (resp.data?.artifacts) {
|
|
441
|
+
setS3Credentials(resp.data.artifacts);
|
|
442
|
+
debug('S3 credentials refreshed');
|
|
443
|
+
}
|
|
444
|
+
} catch (err) {
|
|
445
|
+
debug('Failed to refresh S3 credentials:', err.message || err);
|
|
446
|
+
}
|
|
447
|
+
};
|
|
448
|
+
|
|
449
|
+
#stopS3CredentialsRefresh() {
|
|
450
|
+
if (this.s3CredentialsRefreshTimer) {
|
|
451
|
+
clearInterval(this.s3CredentialsRefreshTimer);
|
|
452
|
+
this.s3CredentialsRefreshTimer = null;
|
|
453
|
+
}
|
|
454
|
+
}
|
|
455
|
+
|
|
404
456
|
/**
|
|
405
457
|
* Decides whether to skip test reporting in case of too many request failures
|
|
406
458
|
* @returns {boolean}
|
|
@@ -545,6 +597,7 @@ class TestomatioPipe {
|
|
|
545
597
|
async finishRun(params) {
|
|
546
598
|
if (!this.isEnabled) return;
|
|
547
599
|
|
|
600
|
+
this.#stopS3CredentialsRefresh();
|
|
548
601
|
await this.#batchUpload();
|
|
549
602
|
if (this.batch.intervalFunction) {
|
|
550
603
|
clearInterval(this.batch.intervalFunction);
|
|
@@ -637,6 +690,7 @@ class TestomatioPipe {
|
|
|
637
690
|
this.isEnabled = false;
|
|
638
691
|
this.apiKey = null;
|
|
639
692
|
|
|
693
|
+
this.#stopS3CredentialsRefresh();
|
|
640
694
|
// clear interval function, otherwise the proccess will continue indefinitely
|
|
641
695
|
if (this.batch.intervalFunction) {
|
|
642
696
|
clearInterval(this.batch.intervalFunction);
|
package/src/utils/pipe_utils.js
CHANGED
|
@@ -122,6 +122,14 @@ function statusEmoji(status) {
|
|
|
122
122
|
return '';
|
|
123
123
|
}
|
|
124
124
|
|
|
125
|
+
/**
|
|
126
|
+
* @param {string} str
|
|
127
|
+
* @returns {string} The string with its first letter in upper case, e.g. `passed` ā `Passed`.
|
|
128
|
+
*/
|
|
129
|
+
function capitalize(str) {
|
|
130
|
+
return str.charAt(0).toUpperCase() + str.slice(1);
|
|
131
|
+
}
|
|
132
|
+
|
|
125
133
|
/**
|
|
126
134
|
* Generate a full name string based on the provided test object.
|
|
127
135
|
* @param {object} t - The test object.
|
|
@@ -260,6 +268,37 @@ function totalDuration(tests) {
|
|
|
260
268
|
return humanizeDuration(Math.trunc(milliseconds), { maxDecimalPoints: 0 });
|
|
261
269
|
}
|
|
262
270
|
|
|
271
|
+
/**
|
|
272
|
+
* Runs with fewer tests than this list every test in the report, not only the failed ones.
|
|
273
|
+
*/
|
|
274
|
+
const SMALL_RUN_TESTS_LIMIT = 10;
|
|
275
|
+
|
|
276
|
+
/**
|
|
277
|
+
* Sections listing the passed, skipped and not yet run tests of a small run; failures have their own
|
|
278
|
+
* section. Not yet run tests (no status, or `pending`) are listed without any status label.
|
|
279
|
+
*
|
|
280
|
+
* @param {Array<{status?: string, title?: string, suite_title?: string, run_time?: number}>} tests
|
|
281
|
+
* @returns {Array<{title: string, list: string[]}>} Non-empty sections; none if the run is not small.
|
|
282
|
+
*/
|
|
283
|
+
function smallRunSections(tests) {
|
|
284
|
+
if (tests.length >= SMALL_RUN_TESTS_LIMIT) return [];
|
|
285
|
+
|
|
286
|
+
const sections = ['passed', 'skipped'].map(status => {
|
|
287
|
+
const list = tests
|
|
288
|
+
.filter(t => t.status === status)
|
|
289
|
+
.map(t => {
|
|
290
|
+
const runTime = t.run_time ? ` (${humanizeDuration(t.run_time)})` : '';
|
|
291
|
+
return `* ${statusEmoji(status)} ${fullName(t)}${runTime}`;
|
|
292
|
+
});
|
|
293
|
+
return { title: `${statusEmoji(status)} ${capitalize(status)} (${list.length})`, list };
|
|
294
|
+
});
|
|
295
|
+
|
|
296
|
+
const notRun = tests.filter(t => !t.status || t.status === 'pending').map(t => `* ${fullName(t)}`);
|
|
297
|
+
sections.push({ title: `š Tests (${notRun.length})`, list: notRun });
|
|
298
|
+
|
|
299
|
+
return sections.filter(section => section.list.length);
|
|
300
|
+
}
|
|
301
|
+
|
|
263
302
|
/**
|
|
264
303
|
* Render a two-column markdown table. Rows with an empty value are skipped, so optional rows
|
|
265
304
|
* need no surrounding `if`.
|
|
@@ -368,6 +407,7 @@ export {
|
|
|
368
407
|
markdownTable,
|
|
369
408
|
runSummary,
|
|
370
409
|
totalDuration,
|
|
410
|
+
smallRunSections,
|
|
371
411
|
plannedTestsLabel,
|
|
372
412
|
parsePipeOptions,
|
|
373
413
|
formatFilterListIds,
|