@testomatio/reporter 2.8.6-beta-fix-xml-batch → 2.9.1-beta.1-allure-chunking
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 +4 -2
- package/lib/adapter/codecept.js +19 -0
- package/lib/allureReader.d.ts +65 -0
- package/lib/allureReader.js +489 -0
- package/lib/bin/cli.js +28 -0
- package/lib/junit-adapter/index.js +4 -0
- package/lib/junit-adapter/kotlin.d.ts +5 -0
- package/lib/junit-adapter/kotlin.js +46 -0
- package/lib/pipe/coverage.js +18 -6
- package/lib/utils/pipe_utils.d.ts +17 -0
- package/lib/utils/pipe_utils.js +45 -0
- package/lib/utils/utils.js +9 -0
- package/lib/xmlReader.d.ts +0 -1
- package/lib/xmlReader.js +2 -38
- package/package.json +1 -1
- package/src/adapter/codecept.js +24 -2
- package/src/allureReader.js +574 -0
- package/src/bin/cli.js +35 -0
- package/src/junit-adapter/index.js +4 -0
- package/src/junit-adapter/kotlin.js +48 -0
- package/src/pipe/coverage.js +25 -7
- package/src/utils/pipe_utils.js +49 -0
- package/src/utils/utils.js +5 -0
- package/src/xmlReader.js +2 -47
|
@@ -0,0 +1,46 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const adapter_js_1 = __importDefault(require("./adapter.js"));
|
|
8
|
+
class KotlinAdapter extends adapter_js_1.default {
|
|
9
|
+
getFilePath(t) {
|
|
10
|
+
const fileName = namespaceToFileName(t.suite_title);
|
|
11
|
+
return this.opts.javaTests + path_1.default.sep + fileName;
|
|
12
|
+
}
|
|
13
|
+
formatTest(t) {
|
|
14
|
+
const fileParts = t.suite_title.split('.');
|
|
15
|
+
t.file = namespaceToFileName(t.suite_title);
|
|
16
|
+
t.title = t.title.split('(')[0];
|
|
17
|
+
// detect params
|
|
18
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
19
|
+
if (paramMatches) {
|
|
20
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
21
|
+
if (params.length === 1)
|
|
22
|
+
params[0] = 'param';
|
|
23
|
+
let paramIndex = 0;
|
|
24
|
+
t.title = t.title.replace(/: \[(.*?)\]/g, () => {
|
|
25
|
+
if (params.length < 2)
|
|
26
|
+
return `\${param}`;
|
|
27
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
28
|
+
paramIndex++;
|
|
29
|
+
return `\${${paramName}}`;
|
|
30
|
+
});
|
|
31
|
+
const example = {};
|
|
32
|
+
paramMatches.forEach((match, index) => {
|
|
33
|
+
example[params[index]] = match.replace(/[[\]]/g, '');
|
|
34
|
+
});
|
|
35
|
+
t.example = example;
|
|
36
|
+
}
|
|
37
|
+
t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ');
|
|
38
|
+
return t;
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
function namespaceToFileName(fileName) {
|
|
42
|
+
const fileParts = fileName.split('.');
|
|
43
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
44
|
+
return `${fileParts.join(path_1.default.sep)}.kt`;
|
|
45
|
+
}
|
|
46
|
+
module.exports = KotlinAdapter;
|
package/lib/pipe/coverage.js
CHANGED
|
@@ -221,9 +221,10 @@ class CoveragePipe {
|
|
|
221
221
|
*/
|
|
222
222
|
#getChangedFilesFromGit(cmd) {
|
|
223
223
|
try {
|
|
224
|
+
// Capture stderr (instead of ignoring it) so Git's actual error is available for diagnostics
|
|
224
225
|
const result = (0, child_process_1.execSync)(cmd, {
|
|
225
226
|
encoding: 'utf-8',
|
|
226
|
-
stdio: ['pipe', 'pipe', '
|
|
227
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
227
228
|
});
|
|
228
229
|
return result
|
|
229
230
|
.split('\n')
|
|
@@ -231,15 +232,26 @@ class CoveragePipe {
|
|
|
231
232
|
.filter(Boolean);
|
|
232
233
|
}
|
|
233
234
|
catch (err) {
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
// Prefer Git's own stderr output, fall back to the generic error message
|
|
236
|
+
const gitOutput = (err.stderr || '').toString().trim();
|
|
237
|
+
const errorMessage = gitOutput || err.message || '';
|
|
238
|
+
// Git edge: Not a git repository
|
|
236
239
|
if (errorMessage.includes('Not a git repository')) {
|
|
237
240
|
log_js_1.log.error('❌ Error: This folder is not a Git repository.');
|
|
241
|
+
return [];
|
|
238
242
|
}
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
// Git edge: the branch/ref to diff against is not available locally.
|
|
244
|
+
// This is common in CI, where a shallow checkout fetches only the current branch.
|
|
245
|
+
if (errorMessage.includes('unknown revision') ||
|
|
246
|
+
errorMessage.includes('ambiguous argument') ||
|
|
247
|
+
errorMessage.includes('bad revision')) {
|
|
248
|
+
log_js_1.log.error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
249
|
+
log_js_1.log.error(`🔍 Branch "${this.branch}" was not found locally. ` +
|
|
250
|
+
`In CI this usually means a shallow checkout — fetch full history first, e.g. ` +
|
|
251
|
+
`actions/checkout with "fetch-depth: 0", or run "git fetch origin ${this.branch}:${this.branch}".`);
|
|
252
|
+
return [];
|
|
241
253
|
}
|
|
242
|
-
|
|
254
|
+
throw new Error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
243
255
|
}
|
|
244
256
|
}
|
|
245
257
|
/**
|
|
@@ -64,3 +64,20 @@ export function parsePipeOptions(optionsStr?: string): any;
|
|
|
64
64
|
* @returns {string} Empty string if no ids; otherwise the formatted output.
|
|
65
65
|
*/
|
|
66
66
|
export function formatFilterListIds(ids: string[], format: "grep" | "json" | "newline" | "ids"): string;
|
|
67
|
+
/**
|
|
68
|
+
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
69
|
+
* @param {Object} data - Data to measure
|
|
70
|
+
* @returns {number} Size in bytes
|
|
71
|
+
*/
|
|
72
|
+
export function getObjectSize(data: any): number;
|
|
73
|
+
/**
|
|
74
|
+
* Split a tests array into chunks bounded by serialized size.
|
|
75
|
+
* Used by the XML and Allure readers so both upload tests with identical batching:
|
|
76
|
+
* each chunk becomes a single batch request (manual batch mode), which keeps requests
|
|
77
|
+
* under the size limit and guarantees every test is sent exactly once.
|
|
78
|
+
*
|
|
79
|
+
* @param {Array} tests - Array of tests to split
|
|
80
|
+
* @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
|
|
81
|
+
* @returns {Array<Array>} Array of test chunks
|
|
82
|
+
*/
|
|
83
|
+
export function splitTestsIntoChunks(tests: any[], maxSizeBytes?: number): Array<any[]>;
|
package/lib/utils/pipe_utils.js
CHANGED
|
@@ -8,6 +8,8 @@ exports.statusEmoji = statusEmoji;
|
|
|
8
8
|
exports.fullName = fullName;
|
|
9
9
|
exports.parsePipeOptions = parsePipeOptions;
|
|
10
10
|
exports.formatFilterListIds = formatFilterListIds;
|
|
11
|
+
exports.getObjectSize = getObjectSize;
|
|
12
|
+
exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
|
11
13
|
const log_js_1 = require("./log.js");
|
|
12
14
|
/**
|
|
13
15
|
* Set S3 credentials from the provided artifacts object.
|
|
@@ -162,6 +164,45 @@ function parsePipeOptions(optionsStr) {
|
|
|
162
164
|
}
|
|
163
165
|
return options;
|
|
164
166
|
}
|
|
167
|
+
/**
|
|
168
|
+
* Calculate the approximate size of data in bytes (JSON stringified, UTF-8 encoded length).
|
|
169
|
+
* @param {Object} data - Data to measure
|
|
170
|
+
* @returns {number} Size in bytes
|
|
171
|
+
*/
|
|
172
|
+
function getObjectSize(data) {
|
|
173
|
+
const body = JSON.stringify(data);
|
|
174
|
+
return new TextEncoder().encode(body).length;
|
|
175
|
+
}
|
|
176
|
+
/**
|
|
177
|
+
* Split a tests array into chunks bounded by serialized size.
|
|
178
|
+
* Used by the XML and Allure readers so both upload tests with identical batching:
|
|
179
|
+
* each chunk becomes a single batch request (manual batch mode), which keeps requests
|
|
180
|
+
* under the size limit and guarantees every test is sent exactly once.
|
|
181
|
+
*
|
|
182
|
+
* @param {Array} tests - Array of tests to split
|
|
183
|
+
* @param {number} [maxSizeBytes=1048576] - Maximum serialized size per chunk (default 1MB)
|
|
184
|
+
* @returns {Array<Array>} Array of test chunks
|
|
185
|
+
*/
|
|
186
|
+
function splitTestsIntoChunks(tests, maxSizeBytes = 1 * 1024 * 1024) {
|
|
187
|
+
const chunks = [];
|
|
188
|
+
let currentChunk = [];
|
|
189
|
+
let currentChunkSize = 0;
|
|
190
|
+
for (const test of tests) {
|
|
191
|
+
const testSize = getObjectSize(test);
|
|
192
|
+
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
193
|
+
if (wouldExceedSize && currentChunk.length > 0) {
|
|
194
|
+
chunks.push(currentChunk);
|
|
195
|
+
currentChunk = [];
|
|
196
|
+
currentChunkSize = 0;
|
|
197
|
+
}
|
|
198
|
+
currentChunk.push(test);
|
|
199
|
+
currentChunkSize += testSize;
|
|
200
|
+
}
|
|
201
|
+
if (currentChunk.length > 0) {
|
|
202
|
+
chunks.push(currentChunk);
|
|
203
|
+
}
|
|
204
|
+
return chunks;
|
|
205
|
+
}
|
|
165
206
|
/**
|
|
166
207
|
* Format a list of test IDs for `--filter-list` machine-readable output.
|
|
167
208
|
* Used when the CLI `--format` option is passed,
|
|
@@ -198,3 +239,7 @@ module.exports.fullName = fullName;
|
|
|
198
239
|
module.exports.parsePipeOptions = parsePipeOptions;
|
|
199
240
|
|
|
200
241
|
module.exports.formatFilterListIds = formatFilterListIds;
|
|
242
|
+
|
|
243
|
+
module.exports.getObjectSize = getObjectSize;
|
|
244
|
+
|
|
245
|
+
module.exports.splitTestsIntoChunks = splitTestsIntoChunks;
|
package/lib/utils/utils.js
CHANGED
|
@@ -324,6 +324,15 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
324
324
|
if (lineIndex === -1)
|
|
325
325
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
326
326
|
}
|
|
327
|
+
else if (opts.lang === 'kotlin') {
|
|
328
|
+
lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
|
|
329
|
+
if (lineIndex === -1)
|
|
330
|
+
lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
|
|
331
|
+
if (lineIndex === -1)
|
|
332
|
+
lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
|
|
333
|
+
if (lineIndex === -1)
|
|
334
|
+
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
335
|
+
}
|
|
327
336
|
else if (opts.lang === 'csharp') {
|
|
328
337
|
// Find the method declaration line
|
|
329
338
|
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
package/lib/xmlReader.d.ts
CHANGED
package/lib/xmlReader.js
CHANGED
|
@@ -14,6 +14,7 @@ const url_1 = require("url");
|
|
|
14
14
|
const nunit_parser_js_1 = require("./junit-adapter/nunit-parser.js");
|
|
15
15
|
const utils_js_1 = require("./utils/utils.js");
|
|
16
16
|
const index_js_1 = require("./pipe/index.js");
|
|
17
|
+
const pipe_utils_js_1 = require("./utils/pipe_utils.js");
|
|
17
18
|
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
18
19
|
const config_js_1 = require("./config.js");
|
|
19
20
|
const uploader_js_1 = require("./uploader.js");
|
|
@@ -474,43 +475,6 @@ class XmlReader {
|
|
|
474
475
|
this.uploader.checkEnabled();
|
|
475
476
|
return run;
|
|
476
477
|
}
|
|
477
|
-
/**
|
|
478
|
-
* Calculate the approximate size of data in bytes (JSON stringified length)
|
|
479
|
-
* @param {Object} data - Data to measure
|
|
480
|
-
* @returns {number} Size in bytes
|
|
481
|
-
*/
|
|
482
|
-
#getObjectSize(data) {
|
|
483
|
-
const body = JSON.stringify(data);
|
|
484
|
-
return new TextEncoder().encode(body).length;
|
|
485
|
-
}
|
|
486
|
-
/**
|
|
487
|
-
* Split tests array into chunks based on data size
|
|
488
|
-
* @param {Array} tests - Array of tests to split
|
|
489
|
-
* @returns {Array<Array>} Array of test chunks
|
|
490
|
-
*/
|
|
491
|
-
#splitTestsIntoChunks(tests) {
|
|
492
|
-
const maxSizeBytes = 1 * 1024 * 1024;
|
|
493
|
-
const chunks = [];
|
|
494
|
-
let currentChunk = [];
|
|
495
|
-
let currentChunkSize = 0;
|
|
496
|
-
for (const test of tests) {
|
|
497
|
-
const testSize = this.#getObjectSize(test);
|
|
498
|
-
const wouldExceedSize = currentChunkSize + testSize > maxSizeBytes;
|
|
499
|
-
if (wouldExceedSize) {
|
|
500
|
-
if (currentChunk.length > 0) {
|
|
501
|
-
chunks.push(currentChunk);
|
|
502
|
-
}
|
|
503
|
-
currentChunk = [];
|
|
504
|
-
currentChunkSize = 0;
|
|
505
|
-
}
|
|
506
|
-
currentChunk.push(test);
|
|
507
|
-
currentChunkSize += testSize;
|
|
508
|
-
}
|
|
509
|
-
if (currentChunk.length > 0) {
|
|
510
|
-
chunks.push(currentChunk);
|
|
511
|
-
}
|
|
512
|
-
return chunks;
|
|
513
|
-
}
|
|
514
478
|
async uploadData() {
|
|
515
479
|
await this.uploadArtifacts();
|
|
516
480
|
this.calculateStats();
|
|
@@ -531,7 +495,7 @@ class XmlReader {
|
|
|
531
495
|
};
|
|
532
496
|
return Promise.all(this.pipes.map(p => p.finishRun(finishData)));
|
|
533
497
|
}
|
|
534
|
-
const testChunks =
|
|
498
|
+
const testChunks = (0, pipe_utils_js_1.splitTestsIntoChunks)(this.tests);
|
|
535
499
|
const totalChunks = testChunks.length;
|
|
536
500
|
const totalTests = this.tests.length;
|
|
537
501
|
debug(`Split ${totalTests} tests into ${totalChunks} chunks (max 1MB per chunk)`);
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -48,6 +48,7 @@ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
|
|
|
48
48
|
|
|
49
49
|
function CodeceptReporter(config) {
|
|
50
50
|
const failedTests = [];
|
|
51
|
+
const reportedTestUids = new Set();
|
|
51
52
|
let videos = [];
|
|
52
53
|
let traces = [];
|
|
53
54
|
const reportTestPromises = [];
|
|
@@ -161,6 +162,7 @@ function CodeceptReporter(config) {
|
|
|
161
162
|
const error = hook?.ctx?.currentTest?.err;
|
|
162
163
|
|
|
163
164
|
for (const test of suite.tests) {
|
|
165
|
+
reportedTestUids.add(test.uid);
|
|
164
166
|
const reportTestPromise = client.addTestRun('failed', {
|
|
165
167
|
...stripExampleFromTitle(test.title),
|
|
166
168
|
rid: test.uid,
|
|
@@ -197,9 +199,29 @@ function CodeceptReporter(config) {
|
|
|
197
199
|
});
|
|
198
200
|
});
|
|
199
201
|
|
|
202
|
+
event.dispatcher.on(event.test.skipped, test => {
|
|
203
|
+
const { uid, tags, title } = test.simplify();
|
|
204
|
+
|
|
205
|
+
if (uid && reportedTestUids.has(uid)) return;
|
|
206
|
+
|
|
207
|
+
services.setContext(null);
|
|
208
|
+
|
|
209
|
+
const reportTestPromise = client.addTestRun(STATUS.SKIPPED, {
|
|
210
|
+
...stripExampleFromTitle(title),
|
|
211
|
+
rid: uid,
|
|
212
|
+
test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
|
|
213
|
+
suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
|
|
214
|
+
time: test.duration,
|
|
215
|
+
meta: test.meta,
|
|
216
|
+
});
|
|
217
|
+
reportTestPromises.push(reportTestPromise);
|
|
218
|
+
reportedTestUids.add(uid);
|
|
219
|
+
});
|
|
220
|
+
|
|
200
221
|
event.dispatcher.on(event.test.after, test => {
|
|
201
222
|
const { uid, tags, title, artifacts } = test.simplify();
|
|
202
223
|
const error = test.err || null;
|
|
224
|
+
reportedTestUids.add(uid);
|
|
203
225
|
failedTests.push(uid || title);
|
|
204
226
|
const testObj = getTestAndMessage(title);
|
|
205
227
|
const files = buildArtifactFiles(artifacts);
|
|
@@ -211,8 +233,8 @@ function CodeceptReporter(config) {
|
|
|
211
233
|
|
|
212
234
|
// Build step hierarchy with screenshot from screenshotOnFail
|
|
213
235
|
const stepHierarchy = buildUnifiedStepHierarchy(
|
|
214
|
-
test.steps,
|
|
215
|
-
hookSteps,
|
|
236
|
+
test.steps,
|
|
237
|
+
hookSteps,
|
|
216
238
|
screenshotOnFailPath
|
|
217
239
|
);
|
|
218
240
|
|