@testomatio/reporter 2.5.1 → 2.6.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/playwright.d.ts +15 -2
- package/lib/adapter/playwright.js +108 -21
- package/lib/bin/cli.js +35 -7
- package/lib/pipe/coverage.js +63 -5
- package/lib/pipe/github.js +15 -0
- package/lib/pipe/testomatio.js +71 -35
- package/lib/reporter-functions.js +8 -0
- package/lib/services/links.d.ts +4 -2
- package/lib/services/links.js +1 -1
- package/package.json +1 -1
- package/src/adapter/playwright.js +119 -23
- package/src/bin/cli.js +37 -11
- package/src/pipe/coverage.js +90 -32
- package/src/pipe/github.js +14 -0
- package/src/pipe/testomatio.js +86 -52
- package/src/reporter-functions.js +8 -0
- package/src/services/links.js +1 -1
- package/types/types.d.ts +1 -1
|
@@ -1,4 +1,4 @@
|
|
|
1
|
-
import
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
2
|
import crypto from 'crypto';
|
|
3
3
|
import os from 'os';
|
|
4
4
|
import path from 'path';
|
|
@@ -10,8 +10,10 @@ import { getTestomatIdFromTestTitle, fileSystem } from '../utils/utils.js';
|
|
|
10
10
|
import { services } from '../services/index.js';
|
|
11
11
|
import { dataStorage } from '../data-storage.js';
|
|
12
12
|
import { extensionMap } from '../utils/constants.js';
|
|
13
|
+
import pc from 'picocolors';
|
|
13
14
|
|
|
14
15
|
const reportTestPromises = [];
|
|
16
|
+
const debug = createDebugMessages('@testomatio/reporter:adapter-playwright');
|
|
15
17
|
|
|
16
18
|
class PlaywrightReporter {
|
|
17
19
|
constructor(config = {}) {
|
|
@@ -55,13 +57,26 @@ class PlaywrightReporter {
|
|
|
55
57
|
const tags = extractTags(test);
|
|
56
58
|
|
|
57
59
|
const fullTestTitle = getTestContextName(test);
|
|
60
|
+
|
|
58
61
|
let logs = '';
|
|
59
|
-
|
|
60
|
-
|
|
62
|
+
// get links along with filtered logs (liks related logs removed)
|
|
63
|
+
const { stdout: filteredStdout, links } = fetchLinksFromLogs(result.stdout);
|
|
64
|
+
if (filteredStdout?.length || result.stderr?.length) {
|
|
65
|
+
logs = `\n\n${pc.bold('Logs:')}\n${pc.red(result.stderr.join(''))}\n${filteredStdout.join('')}`;
|
|
61
66
|
}
|
|
67
|
+
|
|
68
|
+
/*
|
|
69
|
+
All services fucntions work different for Playwright.
|
|
70
|
+
We don't have access to test title (as result, to test id) when calling this functions inside a test.
|
|
71
|
+
Thus, when user calls services functions inside a test, we just log this data to console.
|
|
72
|
+
Playwright intercepts the console.log on it's end and we just get this data from it.
|
|
73
|
+
Thus, we have a tiny drawback: all data from services functions inside a test will be logged to console.
|
|
74
|
+
And this requires a condition to be added for each service function – if its Playwright, then log to console.
|
|
75
|
+
|
|
76
|
+
"get" method of services will not return data for Playwright, we should parse stdout.
|
|
77
|
+
*/
|
|
62
78
|
const manuallyAttachedArtifacts = services.artifacts.get(fullTestTitle);
|
|
63
79
|
const testMeta = services.keyValues.get(fullTestTitle);
|
|
64
|
-
const links = services.links.get(fullTestTitle);
|
|
65
80
|
const rid = test.id || test.testId || uuidv4();
|
|
66
81
|
|
|
67
82
|
/**
|
|
@@ -96,7 +111,7 @@ class PlaywrightReporter {
|
|
|
96
111
|
test_id: getTestomatIdFromTestTitle(`${title} ${tags.join(' ')}`),
|
|
97
112
|
suite_title,
|
|
98
113
|
title,
|
|
99
|
-
tags,
|
|
114
|
+
tags: tags.map(tag => tag.replace('@', '')),
|
|
100
115
|
steps: steps.length ? steps : undefined,
|
|
101
116
|
time: duration,
|
|
102
117
|
logs,
|
|
@@ -248,29 +263,35 @@ function generateTmpFilepath(filename = '') {
|
|
|
248
263
|
}
|
|
249
264
|
|
|
250
265
|
/**
|
|
251
|
-
* Extracts
|
|
266
|
+
* Extracts tags from test title, test options, and suite level
|
|
267
|
+
* Identifies duplicate tags (case-insensitive)
|
|
252
268
|
* @param {*} test - testInfo object from Playwright
|
|
253
|
-
* @returns {string[]} - array of normalized tags
|
|
269
|
+
* @returns {string[]} - array of normalized tags with @ prefix
|
|
254
270
|
*/
|
|
255
271
|
function extractTags(test) {
|
|
256
|
-
const
|
|
257
|
-
|
|
258
|
-
|
|
259
|
-
|
|
260
|
-
|
|
261
|
-
|
|
262
|
-
|
|
263
|
-
|
|
272
|
+
const tagsMap = new Map(); // key: lowercase tag, value: original case tag
|
|
273
|
+
|
|
274
|
+
function addTag(tag) {
|
|
275
|
+
if (typeof tag !== 'string') return;
|
|
276
|
+
const trimmed = tag.trim();
|
|
277
|
+
if (!trimmed) return;
|
|
278
|
+
const normalizedTag = trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
|
|
279
|
+
const lowercaseTag = normalizedTag.toLowerCase();
|
|
280
|
+
if (!tagsMap.has(lowercaseTag)) {
|
|
281
|
+
tagsMap.set(lowercaseTag, normalizedTag);
|
|
282
|
+
}
|
|
264
283
|
}
|
|
265
284
|
|
|
266
|
-
// Extract tags from test
|
|
267
|
-
|
|
268
|
-
|
|
269
|
-
|
|
270
|
-
|
|
271
|
-
|
|
285
|
+
// Extract tags from test title (@tag format); only test title is considered
|
|
286
|
+
const titleTagsMatch = test.title.match(/@[A-Za-z0-9_-]+/g) || [];
|
|
287
|
+
titleTagsMatch.forEach(addTag);
|
|
288
|
+
|
|
289
|
+
// Extract tags from test.tags (Playwright built-in tags); ignore parents
|
|
290
|
+
if (Array.isArray(test.tags)) {
|
|
291
|
+
test.tags.forEach(addTag);
|
|
272
292
|
}
|
|
273
|
-
|
|
293
|
+
|
|
294
|
+
return Array.from(tagsMap.values());
|
|
274
295
|
}
|
|
275
296
|
|
|
276
297
|
/**
|
|
@@ -282,5 +303,80 @@ function getTestContextName(test) {
|
|
|
282
303
|
return `${test._requireFile || ''}_${test.title}`;
|
|
283
304
|
}
|
|
284
305
|
|
|
306
|
+
/**
|
|
307
|
+
* Fetches links from stdout. Returns links and filtered stdout (without data containing markers)
|
|
308
|
+
*
|
|
309
|
+
* @param {(string | Buffer)[]} stdout
|
|
310
|
+
* @returns {{ links: { [key: 'test' | 'jira']: string }[], stdout: (string | Buffer)[] }}
|
|
311
|
+
*/
|
|
312
|
+
function fetchLinksFromLogs(stdout) {
|
|
313
|
+
const links = [];
|
|
314
|
+
|
|
315
|
+
const markers = [
|
|
316
|
+
{ key: '[TESTOMATIO-LINK-TESTS]', type: 'test' },
|
|
317
|
+
{ key: '[TESTOMATIO-LINK-JIRA]', type: 'jira' },
|
|
318
|
+
];
|
|
319
|
+
|
|
320
|
+
const filteredStdout = [];
|
|
321
|
+
|
|
322
|
+
stdout.forEach(entry => {
|
|
323
|
+
if (typeof entry !== 'string') {
|
|
324
|
+
filteredStdout.push(entry);
|
|
325
|
+
return;
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
// check if entry contains any of markers
|
|
329
|
+
if (!markers.some(m => entry.includes(m.key))) {
|
|
330
|
+
filteredStdout.push(entry);
|
|
331
|
+
return;
|
|
332
|
+
}
|
|
333
|
+
|
|
334
|
+
const newEntryLines = [];
|
|
335
|
+
entry.split('\n').forEach(line => {
|
|
336
|
+
line = line.trim();
|
|
337
|
+
let hasMarker = false;
|
|
338
|
+
for (const marker of markers) {
|
|
339
|
+
if (line.includes(marker.key)) {
|
|
340
|
+
hasMarker = true;
|
|
341
|
+
try {
|
|
342
|
+
const rawJson = line.split(marker.key)[1]?.trim();
|
|
343
|
+
if (!rawJson) continue;
|
|
344
|
+
|
|
345
|
+
// smart JSON extraction: take until the last ']', otherwise take the whole string
|
|
346
|
+
const lastBracketIndex = rawJson.lastIndexOf(']');
|
|
347
|
+
const jsonStr = lastBracketIndex !== -1 ? rawJson.substring(0, lastBracketIndex + 1) : rawJson;
|
|
348
|
+
|
|
349
|
+
// test ids or jira ids
|
|
350
|
+
const ids = JSON.parse(jsonStr);
|
|
351
|
+
links.push(
|
|
352
|
+
...ids
|
|
353
|
+
// filter non-truthy ids
|
|
354
|
+
.filter(id => !!id)
|
|
355
|
+
.map(id => ({
|
|
356
|
+
// marker type is either 'test' or 'jira'
|
|
357
|
+
[marker.type]: id,
|
|
358
|
+
})),
|
|
359
|
+
);
|
|
360
|
+
} catch (e) {
|
|
361
|
+
debug('Error parsing links from string:', line, '\n', e);
|
|
362
|
+
}
|
|
363
|
+
}
|
|
364
|
+
}
|
|
365
|
+
if (!hasMarker && line) {
|
|
366
|
+
newEntryLines.push(line);
|
|
367
|
+
}
|
|
368
|
+
});
|
|
369
|
+
|
|
370
|
+
if (newEntryLines.length) {
|
|
371
|
+
filteredStdout.push(newEntryLines.join('\n'));
|
|
372
|
+
}
|
|
373
|
+
});
|
|
374
|
+
|
|
375
|
+
return {
|
|
376
|
+
stdout: filteredStdout,
|
|
377
|
+
links,
|
|
378
|
+
};
|
|
379
|
+
}
|
|
380
|
+
|
|
285
381
|
export default PlaywrightReporter;
|
|
286
|
-
export { extractTags };
|
|
382
|
+
export { extractTags, fetchLinksFromLogs };
|
package/src/bin/cli.js
CHANGED
|
@@ -79,22 +79,17 @@ program
|
|
|
79
79
|
.command('run')
|
|
80
80
|
.alias('test')
|
|
81
81
|
.description('Run tests with the specified command')
|
|
82
|
-
.argument('
|
|
82
|
+
.argument('[command]', 'Test runner command')
|
|
83
83
|
.option('--filter <filter>', 'Additional execution filter')
|
|
84
84
|
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
85
85
|
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
86
86
|
.action(async (command, opts) => {
|
|
87
87
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
88
88
|
const title = process.env.TESTOMATIO_TITLE;
|
|
89
|
-
|
|
90
|
-
if (!command || !command.split) {
|
|
91
|
-
console.log(APP_PREFIX, `No command provided. Use -c option to launch a test runner.`);
|
|
92
|
-
return process.exit(255);
|
|
93
|
-
}
|
|
94
|
-
|
|
95
89
|
const client = new TestomatClient({ apiKey, title });
|
|
96
90
|
|
|
97
91
|
if (opts.filter || opts.filterList) {
|
|
92
|
+
console.log(APP_PREFIX,'Filtering tests...');
|
|
98
93
|
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
|
|
99
94
|
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
|
|
100
95
|
// Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
|
|
@@ -102,6 +97,9 @@ program
|
|
|
102
97
|
const pipeOptions = optsArray.join(':');
|
|
103
98
|
|
|
104
99
|
const prepareRunParams = { pipe, pipeOptions };
|
|
100
|
+
if (opts.filterList) {
|
|
101
|
+
client.pipeStore.filterList = true;
|
|
102
|
+
}
|
|
105
103
|
|
|
106
104
|
try {
|
|
107
105
|
const tests = await client.prepareRun(prepareRunParams);
|
|
@@ -118,18 +116,46 @@ program
|
|
|
118
116
|
|
|
119
117
|
if(opts.filterList) {
|
|
120
118
|
console.log(APP_PREFIX, pc.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
|
|
121
|
-
console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
|
|
119
|
+
if (command) console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
|
|
122
120
|
return;
|
|
123
121
|
}
|
|
124
|
-
|
|
125
|
-
command
|
|
126
|
-
|
|
122
|
+
|
|
123
|
+
if (command && command.split) {
|
|
124
|
+
command = filteredCommand;
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
127
|
catch (err) {
|
|
128
128
|
console.log(APP_PREFIX, err.message || err);
|
|
129
129
|
return;
|
|
130
130
|
}
|
|
131
131
|
}
|
|
132
132
|
|
|
133
|
+
// just create a run (wich tests which match filters) without executing tests
|
|
134
|
+
if (!command || !command.split) {
|
|
135
|
+
const createRunParams = {};
|
|
136
|
+
if (title) {
|
|
137
|
+
createRunParams.title = title;
|
|
138
|
+
}
|
|
139
|
+
if (opts.kind) {
|
|
140
|
+
createRunParams.kind = opts.kind;
|
|
141
|
+
}
|
|
142
|
+
|
|
143
|
+
if (apiKey) {
|
|
144
|
+
await client.createRun(createRunParams);
|
|
145
|
+
const runId = process.env.TESTOMATIO_RUN || process.env.runId;
|
|
146
|
+
if (client.pipeStore.runUrl) console.log(APP_PREFIX, `📊 Report URL: ${pc.magenta(client.pipeStore.runUrl)}`);
|
|
147
|
+
|
|
148
|
+
if (opts.kind !== 'manual') {
|
|
149
|
+
console.log(APP_PREFIX, `No command passed, so you need to run tests yourself:`);
|
|
150
|
+
console.log(APP_PREFIX, `TESTOMATIO_RUN=${runId} <command>`);
|
|
151
|
+
}
|
|
152
|
+
} else {
|
|
153
|
+
console.log(APP_PREFIX, '⚠️ No API key provided. Cannot create run without TESTOMATIO key.');
|
|
154
|
+
process.exit(1);
|
|
155
|
+
}
|
|
156
|
+
return process.exit(0);
|
|
157
|
+
}
|
|
158
|
+
|
|
133
159
|
console.log(APP_PREFIX, `🚀 Running`, pc.green(command));
|
|
134
160
|
|
|
135
161
|
const runTests = async () => {
|
package/src/pipe/coverage.js
CHANGED
|
@@ -54,7 +54,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
54
54
|
|
|
55
55
|
this.branch = options?.diff || process.env.COVERAGE_BRANCH || this.#GIT.default_branch;
|
|
56
56
|
this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
|
|
57
|
-
|
|
57
|
+
|
|
58
58
|
if (this.isBranchDefault) {
|
|
59
59
|
console.log(
|
|
60
60
|
APP_PREFIX,
|
|
@@ -98,7 +98,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
98
98
|
}
|
|
99
99
|
});
|
|
100
100
|
|
|
101
|
-
// In case if we have all needed data
|
|
101
|
+
// In case if we have all needed data
|
|
102
102
|
this.isEnabled = true;
|
|
103
103
|
|
|
104
104
|
debug('Coverage Pipe initialized', {
|
|
@@ -108,7 +108,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
108
108
|
|
|
109
109
|
this.parsedCoverage = {};
|
|
110
110
|
this.changedFiles = [];
|
|
111
|
-
this.matchedLines = new Set();
|
|
111
|
+
this.matchedLines = new Set();
|
|
112
112
|
this.tests = new Set();
|
|
113
113
|
this.suiteIds = new Set();
|
|
114
114
|
this.tagLabels = new Set();
|
|
@@ -118,12 +118,15 @@ class CoveragePipe { // or Changes for the future???
|
|
|
118
118
|
debug(`Coverage Pipe: is Enabled = ${this.isEnabled}`);
|
|
119
119
|
}
|
|
120
120
|
|
|
121
|
-
async prepareRun(opts) {
|
|
121
|
+
async prepareRun(opts) {
|
|
122
122
|
// Reset internal mutable state for isolation
|
|
123
123
|
this.tests.clear();
|
|
124
124
|
this.suiteIds.clear();
|
|
125
125
|
this.tagLabels.clear();
|
|
126
126
|
this.results = [];
|
|
127
|
+
if (this.store) {
|
|
128
|
+
this.store.coverageConfiguration = undefined;
|
|
129
|
+
}
|
|
127
130
|
|
|
128
131
|
if (!this.isEnabled) return [];
|
|
129
132
|
|
|
@@ -132,6 +135,9 @@ class CoveragePipe { // or Changes for the future???
|
|
|
132
135
|
|
|
133
136
|
// Step 2: Extract all available tests and compare with coverage file
|
|
134
137
|
const lines = await this.extractRelevantTestsFromChanges();
|
|
138
|
+
if (this.store?.filterList && lines.size > 0) {
|
|
139
|
+
console.log(APP_PREFIX, `Matched files: ${[...lines].join(', ')}`);
|
|
140
|
+
}
|
|
135
141
|
|
|
136
142
|
if (lines.size === 0) {
|
|
137
143
|
console.log(APP_PREFIX, 'ℹ️ No matching entries in coverage file for provided Git changes.');
|
|
@@ -148,22 +154,33 @@ class CoveragePipe { // or Changes for the future???
|
|
|
148
154
|
if (!tests) return [];
|
|
149
155
|
|
|
150
156
|
console.log(
|
|
151
|
-
APP_PREFIX,
|
|
157
|
+
APP_PREFIX,
|
|
152
158
|
`✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
|
|
153
159
|
' in Testomat.io service side.'
|
|
154
160
|
);
|
|
155
|
-
|
|
161
|
+
|
|
156
162
|
tests.forEach(testId => this.tests.add(testId));
|
|
157
|
-
}
|
|
163
|
+
}
|
|
158
164
|
}
|
|
159
165
|
|
|
160
|
-
if (this.tests.size === 0) {
|
|
161
|
-
console.log(APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
|
|
166
|
+
if (this.tests.size === 0 && this.suiteIds.size === 0) {
|
|
167
|
+
console.log(APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
|
|
162
168
|
return [];
|
|
163
169
|
}
|
|
164
170
|
|
|
165
|
-
this.results = [...this.tests];
|
|
166
|
-
|
|
171
|
+
this.results = [...this.tests, ...this.suiteIds];
|
|
172
|
+
if (this.store) {
|
|
173
|
+
this.store.coverageConfiguration = {
|
|
174
|
+
tests: [...this.tests],
|
|
175
|
+
suites: [...this.suiteIds],
|
|
176
|
+
};
|
|
177
|
+
this.store.coverageDescription = this.#buildRunDescription({
|
|
178
|
+
matchedLines: lines,
|
|
179
|
+
testsCount: this.tests.size,
|
|
180
|
+
suitesCount: this.suiteIds.size,
|
|
181
|
+
});
|
|
182
|
+
}
|
|
183
|
+
|
|
167
184
|
return this.results;
|
|
168
185
|
}
|
|
169
186
|
|
|
@@ -214,18 +231,18 @@ class CoveragePipe { // or Changes for the future???
|
|
|
214
231
|
|
|
215
232
|
if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
|
|
216
233
|
console.log(APP_PREFIX, `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
|
|
217
|
-
|
|
234
|
+
|
|
218
235
|
return undefined;
|
|
219
236
|
}
|
|
220
237
|
|
|
221
238
|
return resp.data.tests;
|
|
222
|
-
}
|
|
239
|
+
}
|
|
223
240
|
catch (err) {
|
|
224
241
|
console.error(
|
|
225
242
|
APP_PREFIX,
|
|
226
243
|
`🚩 Error getting available tests from the Testomat.io by "test_grep" option: ${err}`
|
|
227
244
|
);
|
|
228
|
-
|
|
245
|
+
|
|
229
246
|
return undefined;
|
|
230
247
|
}
|
|
231
248
|
}
|
|
@@ -243,48 +260,48 @@ class CoveragePipe { // or Changes for the future???
|
|
|
243
260
|
encoding: 'utf-8',
|
|
244
261
|
stdio: ['pipe', 'pipe', 'ignore']
|
|
245
262
|
});
|
|
246
|
-
|
|
263
|
+
|
|
247
264
|
return result
|
|
248
265
|
.split('\n')
|
|
249
266
|
.map(f => f.trim())
|
|
250
267
|
.filter(Boolean);
|
|
251
|
-
}
|
|
268
|
+
}
|
|
252
269
|
catch (err) {
|
|
253
270
|
const errorMessage = err.message || '';
|
|
254
271
|
// Git edge: Not a git repository or other error
|
|
255
272
|
if (errorMessage.includes('Not a git repository')) {
|
|
256
273
|
console.error(APP_PREFIX, '❌ Error: This folder is not a Git repository.');
|
|
257
|
-
}
|
|
274
|
+
}
|
|
258
275
|
else {
|
|
259
276
|
throw new Error(`❌ Git command failed ("${cmd}"):\n`, errorMessage);
|
|
260
277
|
}
|
|
261
|
-
|
|
278
|
+
|
|
262
279
|
return [];
|
|
263
280
|
}
|
|
264
281
|
}
|
|
265
282
|
|
|
266
283
|
/**
|
|
267
|
-
* Builds a Git command string to list file changes between the current state
|
|
284
|
+
* Builds a Git command string to list file changes between the current state
|
|
268
285
|
* and a specified Git branch using `git diff --name-only`.
|
|
269
|
-
*
|
|
286
|
+
*
|
|
270
287
|
* Private pipe function
|
|
271
288
|
* @throws {Error} Throws an error if `this.branch` is not defined.
|
|
272
289
|
* @returns {string} A Git command string, e.g., 'git diff <branch> --name-only'.
|
|
273
290
|
*/
|
|
274
291
|
#buildGitCommand() {
|
|
275
292
|
if (!this.branch) throw new Error(`❌ Invalid changes option for setted branch!`);
|
|
276
|
-
|
|
277
|
-
return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
|
|
293
|
+
|
|
294
|
+
return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
|
|
278
295
|
}
|
|
279
296
|
|
|
280
297
|
/**
|
|
281
|
-
* Retrieves the list of files changed in the current Git working directory
|
|
298
|
+
* Retrieves the list of files changed in the current Git working directory
|
|
282
299
|
* compared to a specified branch.
|
|
283
300
|
*
|
|
284
|
-
* This method builds a Git diff command and attempts to retrieve the changed
|
|
301
|
+
* This method builds a Git diff command and attempts to retrieve the changed
|
|
285
302
|
* files using that command. It logs helpful information and errors during the process.
|
|
286
303
|
*
|
|
287
|
-
* If no changed files are found, or an error occurs at any stage, the method logs
|
|
304
|
+
* If no changed files are found, or an error occurs at any stage, the method logs
|
|
288
305
|
* the issue and returns `undefined`.
|
|
289
306
|
*
|
|
290
307
|
* @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
|
|
@@ -295,12 +312,12 @@ class CoveragePipe { // or Changes for the future???
|
|
|
295
312
|
|
|
296
313
|
try {
|
|
297
314
|
cmd = this.#buildGitCommand();
|
|
298
|
-
}
|
|
315
|
+
}
|
|
299
316
|
catch (err) {
|
|
300
317
|
console.error(APP_PREFIX, err.message);
|
|
301
318
|
return undefined;
|
|
302
319
|
}
|
|
303
|
-
|
|
320
|
+
|
|
304
321
|
console.error(APP_PREFIX, `ℹ️ We will use '${cmd}' Git command.`);
|
|
305
322
|
|
|
306
323
|
try {
|
|
@@ -325,9 +342,9 @@ class CoveragePipe { // or Changes for the future???
|
|
|
325
342
|
console.error(APP_PREFIX, err.message);
|
|
326
343
|
console.error(APP_PREFIX, "🔍 Pls, check this Git command manually to understand the original problem.");
|
|
327
344
|
return undefined;
|
|
328
|
-
}
|
|
345
|
+
}
|
|
329
346
|
|
|
330
|
-
console.log(APP_PREFIX, `📑 GIT changed files:\n - ${this.changedFiles.join('\n - ')}`);
|
|
347
|
+
console.log(APP_PREFIX, `📑 GIT changed files:\n - ${this.changedFiles.join('\n - ')}`);
|
|
331
348
|
return this;
|
|
332
349
|
}
|
|
333
350
|
|
|
@@ -412,7 +429,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
412
429
|
for (const [pattern, ids] of Object.entries(this.parsedCoverage)) {
|
|
413
430
|
if (minimatch(changedFile, pattern)) {
|
|
414
431
|
this.matchedLines.add(changedFile);
|
|
415
|
-
|
|
432
|
+
|
|
416
433
|
ids.forEach(id => {
|
|
417
434
|
// Example: "@Tt74099t1"
|
|
418
435
|
if (id.startsWith('@T')) {
|
|
@@ -420,7 +437,7 @@ class CoveragePipe { // or Changes for the future???
|
|
|
420
437
|
}
|
|
421
438
|
// Example: "@Sd74099c1"
|
|
422
439
|
else if (id.startsWith('@S')) {
|
|
423
|
-
this.
|
|
440
|
+
this.suiteIds.add(id.slice(1));
|
|
424
441
|
}
|
|
425
442
|
// Example: "tag:@TestSmoke"
|
|
426
443
|
else if (id.startsWith('tag')) {
|
|
@@ -435,6 +452,47 @@ class CoveragePipe { // or Changes for the future???
|
|
|
435
452
|
|
|
436
453
|
return this.matchedLines;
|
|
437
454
|
}
|
|
455
|
+
|
|
456
|
+
#buildRunDescription({ matchedLines, testsCount, suitesCount }) {
|
|
457
|
+
const sourceBranch =
|
|
458
|
+
process.env.GITHUB_HEAD_REF ||
|
|
459
|
+
process.env.GITHUB_REF_NAME ||
|
|
460
|
+
process.env.CI_COMMIT_REF_NAME ||
|
|
461
|
+
this.#getCurrentGitBranch() ||
|
|
462
|
+
'current branch';
|
|
463
|
+
const targetBranch = this.branch || 'target branch';
|
|
464
|
+
const coverageFile = this.coverageFilePath ? path.basename(this.coverageFilePath) : 'coverage.yml';
|
|
465
|
+
const updatedFiles = matchedLines && matchedLines.size > 0 ? [...matchedLines] : this.changedFiles;
|
|
466
|
+
|
|
467
|
+
let description = `Changes to **${updatedFiles.length}** files in ${sourceBranch} to ${targetBranch}.\n\n`;
|
|
468
|
+
if (suitesCount > 0 || testsCount > 0) {
|
|
469
|
+
const affectedItems = [];
|
|
470
|
+
if (suitesCount > 0) affectedItems.push(`**${suitesCount} suites**`);
|
|
471
|
+
if (testsCount > 0) affectedItems.push(`**${testsCount} individual tests**`);
|
|
472
|
+
description += `May affect ${affectedItems.join(' and ')} which are recommended to be tested for regression.\n\n`; // eslint-disable-line
|
|
473
|
+
}
|
|
474
|
+
description += 'Updated source files:\n';
|
|
475
|
+
if (updatedFiles.length) {
|
|
476
|
+
description += updatedFiles.map(file => `* \`${file}\``).join('\n');
|
|
477
|
+
description += '\n\n';
|
|
478
|
+
} else {
|
|
479
|
+
description += '* No matched files found\n\n';
|
|
480
|
+
}
|
|
481
|
+
description += `Mapping source files to tests set via \`${coverageFile}\` file.`;
|
|
482
|
+
return description;
|
|
483
|
+
}
|
|
484
|
+
|
|
485
|
+
#getCurrentGitBranch() {
|
|
486
|
+
try {
|
|
487
|
+
const branch = execSync('git rev-parse --abbrev-ref HEAD', {
|
|
488
|
+
encoding: 'utf-8',
|
|
489
|
+
stdio: ['pipe', 'pipe', 'ignore'],
|
|
490
|
+
}).trim();
|
|
491
|
+
return branch || undefined;
|
|
492
|
+
} catch (err) {
|
|
493
|
+
return undefined;
|
|
494
|
+
}
|
|
495
|
+
}
|
|
438
496
|
}
|
|
439
497
|
|
|
440
|
-
export default CoveragePipe;
|
|
498
|
+
export default CoveragePipe;
|
package/src/pipe/github.js
CHANGED
|
@@ -142,6 +142,20 @@ class GitHubPipe {
|
|
|
142
142
|
});
|
|
143
143
|
|
|
144
144
|
let body = summary;
|
|
145
|
+
const coverageConfiguration = this.store?.coverageConfiguration;
|
|
146
|
+
const isManualRun = this.store?.runKind === 'manual';
|
|
147
|
+
if (isManualRun && coverageConfiguration) {
|
|
148
|
+
const testsCount = coverageConfiguration.tests?.length || 0;
|
|
149
|
+
const suitesCount = coverageConfiguration.suites?.length || 0;
|
|
150
|
+
body += '\n\n<details>\n<summary><h3>🧭 Coverage Scope</h3></summary>\n\n';
|
|
151
|
+
if (!testsCount && !suitesCount) {
|
|
152
|
+
body += '- No tests were affected, run disabled\n';
|
|
153
|
+
} else {
|
|
154
|
+
body += `- Suites: ${suitesCount}\n`;
|
|
155
|
+
body += `- Tests: ${testsCount}\n`;
|
|
156
|
+
}
|
|
157
|
+
body += '\n</details>';
|
|
158
|
+
}
|
|
145
159
|
|
|
146
160
|
if (failures.length) {
|
|
147
161
|
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|