@testomatio/reporter 2.5.1 → 2.6.0-beta.1.allure
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/playwright.d.ts +3 -2
- package/lib/adapter/playwright.js +23 -17
- package/lib/allureReader.d.ts +65 -0
- package/lib/allureReader.js +448 -0
- package/lib/bin/cli.js +28 -0
- package/lib/data-storage.d.ts +1 -1
- 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/debug.js +2 -1
- package/lib/pipe/testomatio.js +1 -1
- package/lib/reporter.d.ts +30 -78
- 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/utils/log-formatter.d.ts +2 -1
- package/lib/utils/utils.js +9 -0
- package/package.json +1 -1
- package/src/adapter/playwright.js +24 -18
- package/src/allureReader.js +523 -0
- package/src/bin/cli.js +38 -4
- package/src/junit-adapter/index.js +4 -0
- package/src/junit-adapter/kotlin.js +48 -0
- package/src/pipe/debug.js +2 -1
- package/src/pipe/testomatio.js +1 -1
- package/src/utils/utils.js +5 -0
package/README.md
CHANGED
|
@@ -69,6 +69,7 @@ yarn add @testomatio/reporter --dev
|
|
|
69
69
|
| [TestCafe](./docs/frameworks.md#TestCafe) | [Detox](./docs/frameworks.md#Detox) | [Codeception](https://github.com/testomatio/php-reporter) |
|
|
70
70
|
| [Newman (Postman)](./docs/frameworks.md#Newman) | [JUnit](./docs/junit.md#junit) | [NUnit](./docs/junit.md#nunit) |
|
|
71
71
|
| [PyTest](./docs/junit.md#pytest) | [PHPUnit](./docs/junit.md#phpunit) | [Protractor](./docs/frameworks.md#protractor) |
|
|
72
|
+
| [Allure](./docs/allure.md) | | |
|
|
72
73
|
|
|
73
74
|
or **any [other via JUnit](./docs/junit.md)** report....
|
|
74
75
|
|
|
@@ -129,9 +130,10 @@ Bring this reporter on CI and never lose test results again!
|
|
|
129
130
|
- [CSV](./docs/pipes/csv.md)
|
|
130
131
|
- [HTML report](./docs/pipes/html.md)
|
|
131
132
|
- [Bitbucket](./docs/pipes/bitbucket.md)
|
|
132
|
-
-
|
|
133
|
-
- 📓 [JUnit](./docs/junit.md)
|
|
133
|
+
- 📓 [JUnit Reports](./docs/junit.md)
|
|
134
134
|
- 🗄️ [Artifacts](./docs/artifacts.md)
|
|
135
|
+
- 🔬 [Allure Reports](./docs/allure.md)
|
|
136
|
+
- 🔗 [Linking Tests](./docs/linking-tests.md)
|
|
135
137
|
- 🔂 [Workflows](./docs/workflows.md)
|
|
136
138
|
- 🖊️ [Logger](./docs/logger.md)
|
|
137
139
|
- 🪲 [Debug File Format](./docs/debug-file-format.md)
|
|
@@ -12,9 +12,10 @@ declare class PlaywrightReporter {
|
|
|
12
12
|
#private;
|
|
13
13
|
}
|
|
14
14
|
/**
|
|
15
|
-
* Extracts
|
|
15
|
+
* Extracts tags from test title, test options, and suite level
|
|
16
|
+
* Identifies duplicate tags (case-insensitive)
|
|
16
17
|
* @param {*} test - testInfo object from Playwright
|
|
17
|
-
* @returns {string[]} - array of normalized tags
|
|
18
|
+
* @returns {string[]} - array of normalized tags with @ prefix
|
|
18
19
|
*/
|
|
19
20
|
export function extractTags(test: any): string[];
|
|
20
21
|
import TestomatioClient from '../client.js';
|
|
@@ -92,7 +92,7 @@ class PlaywrightReporter {
|
|
|
92
92
|
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags.join(' ')}`),
|
|
93
93
|
suite_title,
|
|
94
94
|
title,
|
|
95
|
-
tags,
|
|
95
|
+
tags: tags.map(tag => tag.replace('@', '')),
|
|
96
96
|
steps: steps.length ? steps : undefined,
|
|
97
97
|
time: duration,
|
|
98
98
|
logs,
|
|
@@ -223,27 +223,33 @@ function generateTmpFilepath(filename = '') {
|
|
|
223
223
|
return path_1.default.join(tmpdir, filename);
|
|
224
224
|
}
|
|
225
225
|
/**
|
|
226
|
-
* Extracts
|
|
226
|
+
* Extracts tags from test title, test options, and suite level
|
|
227
|
+
* Identifies duplicate tags (case-insensitive)
|
|
227
228
|
* @param {*} test - testInfo object from Playwright
|
|
228
|
-
* @returns {string[]} - array of normalized tags
|
|
229
|
+
* @returns {string[]} - array of normalized tags with @ prefix
|
|
229
230
|
*/
|
|
230
231
|
function extractTags(test) {
|
|
231
|
-
const
|
|
232
|
-
|
|
233
|
-
|
|
234
|
-
|
|
235
|
-
|
|
236
|
-
|
|
237
|
-
|
|
232
|
+
const tagsMap = new Map(); // key: lowercase tag, value: original case tag
|
|
233
|
+
function addTag(tag) {
|
|
234
|
+
if (typeof tag !== 'string')
|
|
235
|
+
return;
|
|
236
|
+
const trimmed = tag.trim();
|
|
237
|
+
if (!trimmed)
|
|
238
|
+
return;
|
|
239
|
+
const normalizedTag = trimmed.startsWith('@') ? trimmed : `@${trimmed}`;
|
|
240
|
+
const lowercaseTag = normalizedTag.toLowerCase();
|
|
241
|
+
if (!tagsMap.has(lowercaseTag)) {
|
|
242
|
+
tagsMap.set(lowercaseTag, normalizedTag);
|
|
243
|
+
}
|
|
238
244
|
}
|
|
239
|
-
// Extract tags from test
|
|
240
|
-
|
|
241
|
-
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
245
|
+
// Extract tags from test title (@tag format); only test title is considered
|
|
246
|
+
const titleTagsMatch = test.title.match(/@[A-Za-z0-9_-]+/g) || [];
|
|
247
|
+
titleTagsMatch.forEach(addTag);
|
|
248
|
+
// Extract tags from test.tags (Playwright built-in tags); ignore parents
|
|
249
|
+
if (Array.isArray(test.tags)) {
|
|
250
|
+
test.tags.forEach(addTag);
|
|
245
251
|
}
|
|
246
|
-
return Array.from(
|
|
252
|
+
return Array.from(tagsMap.values());
|
|
247
253
|
}
|
|
248
254
|
/**
|
|
249
255
|
* Returns filename + test title
|
|
@@ -0,0 +1,65 @@
|
|
|
1
|
+
export default AllureReader;
|
|
2
|
+
declare class AllureReader {
|
|
3
|
+
constructor(opts?: {});
|
|
4
|
+
requestParams: {
|
|
5
|
+
apiKey: any;
|
|
6
|
+
url: any;
|
|
7
|
+
title: string;
|
|
8
|
+
env: string;
|
|
9
|
+
group_title: string;
|
|
10
|
+
isBatchEnabled: boolean;
|
|
11
|
+
};
|
|
12
|
+
runId: any;
|
|
13
|
+
opts: {};
|
|
14
|
+
withPackage: any;
|
|
15
|
+
store: {};
|
|
16
|
+
pipesPromise: Promise<any[]>;
|
|
17
|
+
_tests: any[];
|
|
18
|
+
stats: {};
|
|
19
|
+
suites: {};
|
|
20
|
+
uploader: S3Uploader;
|
|
21
|
+
version: any;
|
|
22
|
+
set tests(value: any[]);
|
|
23
|
+
get tests(): any[];
|
|
24
|
+
createRun(): Promise<any[]>;
|
|
25
|
+
pipes: any;
|
|
26
|
+
parse(resultsPattern: any): {};
|
|
27
|
+
parseContainerFiles(containerFiles: any): void;
|
|
28
|
+
processAllureResult(result: any, resultsDir: any): {
|
|
29
|
+
rid: any;
|
|
30
|
+
title: any;
|
|
31
|
+
status: any;
|
|
32
|
+
suite_title: any;
|
|
33
|
+
file: string;
|
|
34
|
+
run_time: number;
|
|
35
|
+
steps: any;
|
|
36
|
+
message: any;
|
|
37
|
+
stack: any;
|
|
38
|
+
meta: {};
|
|
39
|
+
links: {
|
|
40
|
+
label: string;
|
|
41
|
+
}[];
|
|
42
|
+
artifacts: any[];
|
|
43
|
+
create: boolean;
|
|
44
|
+
overwrite: boolean;
|
|
45
|
+
};
|
|
46
|
+
mapStatus(status: any): any;
|
|
47
|
+
extractSuiteTitle(result: any): any;
|
|
48
|
+
stripNamespace(suiteName: any): any;
|
|
49
|
+
extractFile(result: any): string;
|
|
50
|
+
getFileExtension(result: any): any;
|
|
51
|
+
extractMeta(result: any): {};
|
|
52
|
+
extractLinks(result: any): {
|
|
53
|
+
label: string;
|
|
54
|
+
}[];
|
|
55
|
+
convertSteps(steps: any, depth?: number): any;
|
|
56
|
+
calculateRunTime(item: any): number;
|
|
57
|
+
convertParameters(parameters: any): {};
|
|
58
|
+
combineRetryAttempts(attempts: any): any;
|
|
59
|
+
calculateStats(): {};
|
|
60
|
+
fetchSourceCode(): void;
|
|
61
|
+
getLanguage(): any;
|
|
62
|
+
uploadArtifacts(): Promise<void>;
|
|
63
|
+
uploadData(): Promise<any[]>;
|
|
64
|
+
}
|
|
65
|
+
import { S3Uploader } from './uploader.js';
|
|
@@ -0,0 +1,448 @@
|
|
|
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 debug_1 = __importDefault(require("debug"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
|
+
const fs_1 = __importDefault(require("fs"));
|
|
10
|
+
const glob_1 = require("glob");
|
|
11
|
+
const constants_js_1 = require("./constants.js");
|
|
12
|
+
const crypto_1 = require("crypto");
|
|
13
|
+
const url_1 = require("url");
|
|
14
|
+
const config_js_1 = require("./config.js");
|
|
15
|
+
const uploader_js_1 = require("./uploader.js");
|
|
16
|
+
const index_js_1 = require("./pipe/index.js");
|
|
17
|
+
const utils_js_1 = require("./utils/utils.js");
|
|
18
|
+
const index_js_2 = __importDefault(require("./junit-adapter/index.js"));
|
|
19
|
+
// @ts-ignore
|
|
20
|
+
const debug = (0, debug_1.default)('@testomatio/reporter:allure');
|
|
21
|
+
const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
22
|
+
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
|
|
23
|
+
class AllureReader {
|
|
24
|
+
constructor(opts = {}) {
|
|
25
|
+
this.requestParams = {
|
|
26
|
+
apiKey: opts.apiKey || config_js_1.config.TESTOMATIO,
|
|
27
|
+
url: opts.url || TESTOMATIO_URL,
|
|
28
|
+
title: TESTOMATIO_TITLE,
|
|
29
|
+
env: TESTOMATIO_ENV,
|
|
30
|
+
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
31
|
+
isBatchEnabled: true,
|
|
32
|
+
};
|
|
33
|
+
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
34
|
+
this.opts = opts || {};
|
|
35
|
+
this.withPackage = opts.withPackage || false;
|
|
36
|
+
this.store = {};
|
|
37
|
+
this.pipesPromise = (0, index_js_1.pipesFactory)(opts, this.store);
|
|
38
|
+
this._tests = [];
|
|
39
|
+
this.stats = {};
|
|
40
|
+
this.suites = {};
|
|
41
|
+
this.uploader = new uploader_js_1.S3Uploader();
|
|
42
|
+
const packageJsonPath = path_1.default.resolve(__dirname, '..', 'package.json');
|
|
43
|
+
this.version = JSON.parse(fs_1.default.readFileSync(packageJsonPath).toString()).version;
|
|
44
|
+
console.log(constants_js_1.APP_PREFIX, `Testomatio Reporter v${this.version}`);
|
|
45
|
+
}
|
|
46
|
+
get tests() {
|
|
47
|
+
return this._tests;
|
|
48
|
+
}
|
|
49
|
+
set tests(value) {
|
|
50
|
+
this._tests = value;
|
|
51
|
+
}
|
|
52
|
+
async createRun() {
|
|
53
|
+
const runParams = {
|
|
54
|
+
api_key: this.requestParams.apiKey,
|
|
55
|
+
title: this.requestParams.title,
|
|
56
|
+
env: this.requestParams.env,
|
|
57
|
+
group_title: this.requestParams.group_title,
|
|
58
|
+
isBatchEnabled: this.requestParams.isBatchEnabled,
|
|
59
|
+
};
|
|
60
|
+
debug('Run', runParams);
|
|
61
|
+
this.pipes = this.pipes || (await this.pipesPromise);
|
|
62
|
+
const run = await Promise.all(this.pipes.map(p => p.createRun(runParams)));
|
|
63
|
+
this.uploader.checkEnabled();
|
|
64
|
+
return run;
|
|
65
|
+
}
|
|
66
|
+
parse(resultsPattern) {
|
|
67
|
+
this._tests = [];
|
|
68
|
+
let pattern = resultsPattern;
|
|
69
|
+
// Auto-append wildcard if pattern refers to a directory (like XML command does)
|
|
70
|
+
if (!pattern.endsWith('.json') && !pattern.includes('*')) {
|
|
71
|
+
if (pattern.endsWith('/') || (fs_1.default.existsSync(pattern) && fs_1.default.statSync(pattern).isDirectory())) {
|
|
72
|
+
pattern = pattern.replace(/\/+$/, '') + '/*-result.json';
|
|
73
|
+
}
|
|
74
|
+
else {
|
|
75
|
+
pattern += '*-result.json';
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
const resultsDir = path_1.default.dirname(pattern);
|
|
79
|
+
console.log(constants_js_1.APP_PREFIX, `Scanning for Allure results in: ${resultsDir}`);
|
|
80
|
+
console.log(constants_js_1.APP_PREFIX, `Using pattern: ${pattern}`);
|
|
81
|
+
const resultFiles = glob_1.glob.sync(pattern);
|
|
82
|
+
const containerFiles = glob_1.glob.sync(pattern.replace('*-result.json', '*-container.json'));
|
|
83
|
+
if (resultFiles.length === 0 && containerFiles.length === 0) {
|
|
84
|
+
throw new Error(`No Allure result files found matching pattern: ${pattern}`);
|
|
85
|
+
}
|
|
86
|
+
console.log(constants_js_1.APP_PREFIX, `Found ${resultFiles.length} result files and ${containerFiles.length} container files`);
|
|
87
|
+
this.parseContainerFiles(containerFiles);
|
|
88
|
+
// Store all tests temporarily for deduplication by historyId
|
|
89
|
+
const allTests = [];
|
|
90
|
+
for (const file of resultFiles) {
|
|
91
|
+
const fullPath = file;
|
|
92
|
+
const fileDir = path_1.default.dirname(file);
|
|
93
|
+
try {
|
|
94
|
+
const resultData = JSON.parse(fs_1.default.readFileSync(fullPath, 'utf8'));
|
|
95
|
+
const test = this.processAllureResult(resultData, fileDir);
|
|
96
|
+
if (test) {
|
|
97
|
+
test._historyId = resultData.historyId;
|
|
98
|
+
test._stop = resultData.stop || 0;
|
|
99
|
+
allTests.push(test);
|
|
100
|
+
}
|
|
101
|
+
}
|
|
102
|
+
catch (err) {
|
|
103
|
+
console.warn(constants_js_1.APP_PREFIX, `Failed to parse ${file}:`, err.message);
|
|
104
|
+
debug('Parse error:', err);
|
|
105
|
+
}
|
|
106
|
+
}
|
|
107
|
+
const attemptsMap = new Map();
|
|
108
|
+
for (const test of allTests) {
|
|
109
|
+
const historyId = test._historyId || test.rid;
|
|
110
|
+
if (!attemptsMap.has(historyId)) {
|
|
111
|
+
attemptsMap.set(historyId, []);
|
|
112
|
+
}
|
|
113
|
+
attemptsMap.get(historyId).push(test);
|
|
114
|
+
}
|
|
115
|
+
const uniqueTestsMap = new Map();
|
|
116
|
+
for (const [historyId, attempts] of attemptsMap) {
|
|
117
|
+
uniqueTestsMap.set(historyId, this.combineRetryAttempts(attempts));
|
|
118
|
+
}
|
|
119
|
+
// Convert map to array and clean up internal fields
|
|
120
|
+
this._tests = Array.from(uniqueTestsMap.values()).map(t => {
|
|
121
|
+
delete t._historyId;
|
|
122
|
+
delete t._stop;
|
|
123
|
+
return t;
|
|
124
|
+
});
|
|
125
|
+
console.log(constants_js_1.APP_PREFIX, `Processed ${this._tests.length} unique tests (from ${allTests.length} result files)`);
|
|
126
|
+
return this.calculateStats();
|
|
127
|
+
}
|
|
128
|
+
parseContainerFiles(containerFiles) {
|
|
129
|
+
for (const file of containerFiles) {
|
|
130
|
+
try {
|
|
131
|
+
const data = JSON.parse(fs_1.default.readFileSync(file, 'utf8'));
|
|
132
|
+
if (data.name && data.children) {
|
|
133
|
+
data.children.forEach(uuid => {
|
|
134
|
+
this.suites[uuid] = data.name;
|
|
135
|
+
});
|
|
136
|
+
}
|
|
137
|
+
}
|
|
138
|
+
catch (err) {
|
|
139
|
+
debug('Failed to parse container file:', file, err.message);
|
|
140
|
+
}
|
|
141
|
+
}
|
|
142
|
+
debug('Parsed suites:', this.suites);
|
|
143
|
+
}
|
|
144
|
+
processAllureResult(result, resultsDir) {
|
|
145
|
+
const test = {
|
|
146
|
+
rid: result.uuid || (0, crypto_1.randomUUID)(),
|
|
147
|
+
title: result.name || 'Unknown test',
|
|
148
|
+
status: this.mapStatus(result.status),
|
|
149
|
+
suite_title: this.extractSuiteTitle(result),
|
|
150
|
+
file: this.extractFile(result),
|
|
151
|
+
run_time: this.calculateRunTime(result),
|
|
152
|
+
steps: this.convertSteps(result.steps || []),
|
|
153
|
+
message: result.statusDetails?.message || '',
|
|
154
|
+
stack: result.statusDetails?.trace || '',
|
|
155
|
+
meta: this.extractMeta(result),
|
|
156
|
+
links: this.extractLinks(result),
|
|
157
|
+
artifacts: [],
|
|
158
|
+
create: true,
|
|
159
|
+
overwrite: true,
|
|
160
|
+
};
|
|
161
|
+
// Add description if present
|
|
162
|
+
if (result.description) {
|
|
163
|
+
test.description = result.description;
|
|
164
|
+
}
|
|
165
|
+
if (result.parameters && result.parameters.length > 0) {
|
|
166
|
+
test.example = this.convertParameters(result.parameters);
|
|
167
|
+
}
|
|
168
|
+
if (result.attachments && result.attachments.length > 0) {
|
|
169
|
+
const attachments = result.attachments
|
|
170
|
+
.map(att => {
|
|
171
|
+
const fullPath = path_1.default.join(resultsDir, att.source);
|
|
172
|
+
if (fs_1.default.existsSync(fullPath)) {
|
|
173
|
+
return fullPath;
|
|
174
|
+
}
|
|
175
|
+
debug('Attachment file not found:', fullPath);
|
|
176
|
+
return null;
|
|
177
|
+
})
|
|
178
|
+
.filter(Boolean);
|
|
179
|
+
if (attachments.length > 0) {
|
|
180
|
+
test.files = attachments;
|
|
181
|
+
}
|
|
182
|
+
}
|
|
183
|
+
return test;
|
|
184
|
+
}
|
|
185
|
+
mapStatus(status) {
|
|
186
|
+
const statusMap = {
|
|
187
|
+
passed: 'passed',
|
|
188
|
+
failed: 'failed',
|
|
189
|
+
broken: 'failed',
|
|
190
|
+
skipped: 'skipped',
|
|
191
|
+
pending: 'skipped',
|
|
192
|
+
};
|
|
193
|
+
return statusMap[status] || 'failed';
|
|
194
|
+
}
|
|
195
|
+
extractSuiteTitle(result) {
|
|
196
|
+
const labels = result.labels || [];
|
|
197
|
+
// Only use suite label for suite_title
|
|
198
|
+
// Epic and Feature are sent as separate labels in meta
|
|
199
|
+
const suiteLabel = labels.find(l => l.name === 'suite')?.value;
|
|
200
|
+
if (suiteLabel) {
|
|
201
|
+
return this.stripNamespace(suiteLabel);
|
|
202
|
+
}
|
|
203
|
+
// Fallback to parentSuite or subSuite if no suite label
|
|
204
|
+
const parentSuite = labels.find(l => l.name === 'parentSuite')?.value;
|
|
205
|
+
const subSuite = labels.find(l => l.name === 'subSuite')?.value;
|
|
206
|
+
if (parentSuite && subSuite) {
|
|
207
|
+
return `${parentSuite} / ${subSuite}`;
|
|
208
|
+
}
|
|
209
|
+
if (parentSuite)
|
|
210
|
+
return parentSuite;
|
|
211
|
+
if (subSuite)
|
|
212
|
+
return subSuite;
|
|
213
|
+
return 'Default Suite';
|
|
214
|
+
}
|
|
215
|
+
stripNamespace(suiteName) {
|
|
216
|
+
if (suiteName && suiteName.includes('.')) {
|
|
217
|
+
return suiteName.split('.').pop();
|
|
218
|
+
}
|
|
219
|
+
return suiteName;
|
|
220
|
+
}
|
|
221
|
+
extractFile(result) {
|
|
222
|
+
const labels = result.labels || [];
|
|
223
|
+
const packageLabel = labels.find(l => l.name === 'package')?.value;
|
|
224
|
+
const testClassLabel = labels.find(l => l.name === 'testClass')?.value;
|
|
225
|
+
if (!packageLabel) {
|
|
226
|
+
return null;
|
|
227
|
+
}
|
|
228
|
+
const ext = this.getFileExtension(result);
|
|
229
|
+
let className;
|
|
230
|
+
if (testClassLabel) {
|
|
231
|
+
className = testClassLabel.split('.').pop();
|
|
232
|
+
}
|
|
233
|
+
else if (result.fullName) {
|
|
234
|
+
const fullNameParts = result.fullName.split('.');
|
|
235
|
+
className = fullNameParts[fullNameParts.length - 2] || fullNameParts[fullNameParts.length - 1];
|
|
236
|
+
}
|
|
237
|
+
else {
|
|
238
|
+
return null;
|
|
239
|
+
}
|
|
240
|
+
if (this.withPackage) {
|
|
241
|
+
const parts = packageLabel.split('.');
|
|
242
|
+
return `${parts.join('/')}/${className}.${ext}`;
|
|
243
|
+
}
|
|
244
|
+
return `${className}.${ext}`;
|
|
245
|
+
}
|
|
246
|
+
getFileExtension(result) {
|
|
247
|
+
const labels = result.labels || [];
|
|
248
|
+
const languageLabel = labels.find(l => l.name === 'language')?.value;
|
|
249
|
+
const extMap = {
|
|
250
|
+
java: 'java',
|
|
251
|
+
kotlin: 'kt',
|
|
252
|
+
javascript: 'js',
|
|
253
|
+
typescript: 'ts',
|
|
254
|
+
python: 'py',
|
|
255
|
+
ruby: 'rb',
|
|
256
|
+
'c#': 'cs',
|
|
257
|
+
php: 'php',
|
|
258
|
+
};
|
|
259
|
+
return extMap[languageLabel?.toLowerCase()] || 'java';
|
|
260
|
+
}
|
|
261
|
+
extractMeta(result) {
|
|
262
|
+
const labels = result.labels || [];
|
|
263
|
+
const excludedLabels = [
|
|
264
|
+
'suite', 'package', 'parentSuite', 'subSuite',
|
|
265
|
+
'testClass', 'testMethod', 'epic', 'feature',
|
|
266
|
+
];
|
|
267
|
+
const meta = {};
|
|
268
|
+
labels.forEach(label => {
|
|
269
|
+
if (!excludedLabels.includes(label.name)) {
|
|
270
|
+
meta[label.name] = label.value;
|
|
271
|
+
}
|
|
272
|
+
});
|
|
273
|
+
return meta;
|
|
274
|
+
}
|
|
275
|
+
extractLinks(result) {
|
|
276
|
+
const labels = result.labels || [];
|
|
277
|
+
const links = [];
|
|
278
|
+
const epicLabel = labels.find(l => l.name === 'epic');
|
|
279
|
+
const featureLabel = labels.find(l => l.name === 'feature');
|
|
280
|
+
if (epicLabel?.value) {
|
|
281
|
+
links.push({ label: `epic:${epicLabel.value}` });
|
|
282
|
+
}
|
|
283
|
+
if (featureLabel?.value) {
|
|
284
|
+
links.push({ label: `feature:${featureLabel.value}` });
|
|
285
|
+
}
|
|
286
|
+
return links.length > 0 ? links : undefined;
|
|
287
|
+
}
|
|
288
|
+
convertSteps(steps, depth = 0) {
|
|
289
|
+
if (depth >= 10)
|
|
290
|
+
return null;
|
|
291
|
+
return steps
|
|
292
|
+
.map(step => {
|
|
293
|
+
const convertedStep = {
|
|
294
|
+
category: 'user',
|
|
295
|
+
title: step.name || step.title || 'Unknown step',
|
|
296
|
+
duration: this.calculateRunTime(step),
|
|
297
|
+
steps: this.convertSteps(step.steps || [], depth + 1),
|
|
298
|
+
};
|
|
299
|
+
if (convertedStep.steps && convertedStep.steps.length === 0) {
|
|
300
|
+
delete convertedStep.steps;
|
|
301
|
+
}
|
|
302
|
+
if (convertedStep.duration === 0) {
|
|
303
|
+
delete convertedStep.duration;
|
|
304
|
+
}
|
|
305
|
+
return convertedStep;
|
|
306
|
+
})
|
|
307
|
+
.filter(Boolean);
|
|
308
|
+
}
|
|
309
|
+
calculateRunTime(item) {
|
|
310
|
+
if (item.start && item.stop) {
|
|
311
|
+
const durationMs = item.stop - item.start;
|
|
312
|
+
return durationMs / 1000;
|
|
313
|
+
}
|
|
314
|
+
return null;
|
|
315
|
+
}
|
|
316
|
+
convertParameters(parameters) {
|
|
317
|
+
const example = {};
|
|
318
|
+
parameters.forEach(param => {
|
|
319
|
+
if (param.name) {
|
|
320
|
+
example[param.name] = param.value;
|
|
321
|
+
}
|
|
322
|
+
});
|
|
323
|
+
return example;
|
|
324
|
+
}
|
|
325
|
+
combineRetryAttempts(attempts) {
|
|
326
|
+
attempts.sort((a, b) => (a._stop || 0) - (b._stop || 0));
|
|
327
|
+
const finalTest = attempts[attempts.length - 1];
|
|
328
|
+
const retryCount = attempts.length - 1;
|
|
329
|
+
if (retryCount > 0) {
|
|
330
|
+
finalTest.retries = retryCount;
|
|
331
|
+
}
|
|
332
|
+
const failedAttempts = attempts.filter(t => t.status === 'failed');
|
|
333
|
+
if (failedAttempts.length === 0) {
|
|
334
|
+
return finalTest;
|
|
335
|
+
}
|
|
336
|
+
const failureMessages = [];
|
|
337
|
+
const failureStacks = [];
|
|
338
|
+
for (const failed of failedAttempts) {
|
|
339
|
+
const attemptNum = attempts.indexOf(failed) + 1;
|
|
340
|
+
if (failed.message) {
|
|
341
|
+
failureMessages.push(`[Attempt ${attemptNum}] ${failed.message}`);
|
|
342
|
+
}
|
|
343
|
+
if (failed.stack) {
|
|
344
|
+
failureStacks.push(`\n--- Attempt ${attemptNum} ---\n${failed.stack}`);
|
|
345
|
+
}
|
|
346
|
+
}
|
|
347
|
+
if (failureMessages.length > 0) {
|
|
348
|
+
finalTest.message = failureMessages.join('\n');
|
|
349
|
+
}
|
|
350
|
+
if (failureStacks.length > 0) {
|
|
351
|
+
finalTest.stack = failureStacks.join('\n');
|
|
352
|
+
}
|
|
353
|
+
if (finalTest.status === 'passed') {
|
|
354
|
+
finalTest.status = 'failed';
|
|
355
|
+
const retryMsg = `Test passed after ${failedAttempts.length} retries. Previous failures:\n`;
|
|
356
|
+
finalTest.message = retryMsg + finalTest.message;
|
|
357
|
+
}
|
|
358
|
+
return finalTest;
|
|
359
|
+
}
|
|
360
|
+
calculateStats() {
|
|
361
|
+
this.stats = {
|
|
362
|
+
create_tests: true,
|
|
363
|
+
tests_count: this._tests.length,
|
|
364
|
+
passed_count: 0,
|
|
365
|
+
failed_count: 0,
|
|
366
|
+
skipped_count: 0,
|
|
367
|
+
duration: 0,
|
|
368
|
+
status: 'passed',
|
|
369
|
+
tests: this._tests,
|
|
370
|
+
};
|
|
371
|
+
this._tests.forEach(t => {
|
|
372
|
+
if (t.status === 'passed')
|
|
373
|
+
this.stats.passed_count++;
|
|
374
|
+
if (t.status === 'failed')
|
|
375
|
+
this.stats.failed_count++;
|
|
376
|
+
if (t.status === 'skipped')
|
|
377
|
+
this.stats.skipped_count++;
|
|
378
|
+
});
|
|
379
|
+
this.stats.duration = this._tests.reduce((acc, t) => acc + (t.run_time || 0), 0);
|
|
380
|
+
if (this.stats.failed_count)
|
|
381
|
+
this.stats.status = 'failed';
|
|
382
|
+
debug('Stats:', this.stats);
|
|
383
|
+
return this.stats;
|
|
384
|
+
}
|
|
385
|
+
fetchSourceCode() {
|
|
386
|
+
const adapter = this.adapter || (0, index_js_2.default)(this.getLanguage(), this.opts);
|
|
387
|
+
this._tests.forEach(t => {
|
|
388
|
+
try {
|
|
389
|
+
let filePath = t.file;
|
|
390
|
+
if (adapter && adapter.getFilePath) {
|
|
391
|
+
filePath = adapter.getFilePath(t);
|
|
392
|
+
}
|
|
393
|
+
if (!filePath)
|
|
394
|
+
return;
|
|
395
|
+
if (!fs_1.default.existsSync(filePath)) {
|
|
396
|
+
debug('Source file not found:', filePath);
|
|
397
|
+
return;
|
|
398
|
+
}
|
|
399
|
+
const contents = fs_1.default.readFileSync(filePath).toString();
|
|
400
|
+
const code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.getLanguage() });
|
|
401
|
+
if (code) {
|
|
402
|
+
t.code = code;
|
|
403
|
+
debug('Fetched code for test %s', t.title);
|
|
404
|
+
}
|
|
405
|
+
const testId = (0, utils_js_1.fetchIdFromCode)(contents, { lang: this.getLanguage() });
|
|
406
|
+
if (testId) {
|
|
407
|
+
t.test_id = testId;
|
|
408
|
+
debug('Fetched test id %s for test %s', testId, t.title);
|
|
409
|
+
}
|
|
410
|
+
}
|
|
411
|
+
catch (err) {
|
|
412
|
+
debug('Failed to fetch source code:', err.message);
|
|
413
|
+
}
|
|
414
|
+
});
|
|
415
|
+
}
|
|
416
|
+
getLanguage() {
|
|
417
|
+
if (this._tests.length === 0)
|
|
418
|
+
return null;
|
|
419
|
+
return this._tests[0].meta?.language || this.opts.lang;
|
|
420
|
+
}
|
|
421
|
+
async uploadArtifacts() {
|
|
422
|
+
for (const test of this._tests.filter(t => t.files && t.files.length > 0)) {
|
|
423
|
+
const runId = this.runId || this.store.runId || Date.now().toString();
|
|
424
|
+
const artifacts = await Promise.all(test.files.map(f => this.uploader.uploadFileByPath(f, [runId, test.rid, path_1.default.basename(f)])));
|
|
425
|
+
test.artifacts = artifacts.filter(a => a && a.link).map(a => a.link);
|
|
426
|
+
delete test.files;
|
|
427
|
+
if (test.artifacts.length > 0) {
|
|
428
|
+
console.log(constants_js_1.APP_PREFIX, `🗄️ Uploaded ${picocolors_1.default.bold(`${test.artifacts.length} artifacts`)} for test ${test.title}`);
|
|
429
|
+
}
|
|
430
|
+
}
|
|
431
|
+
}
|
|
432
|
+
async uploadData() {
|
|
433
|
+
await this.uploadArtifacts();
|
|
434
|
+
this.calculateStats();
|
|
435
|
+
this.fetchSourceCode();
|
|
436
|
+
const dataString = {
|
|
437
|
+
...this.stats,
|
|
438
|
+
api_key: this.requestParams.apiKey,
|
|
439
|
+
status: 'finished',
|
|
440
|
+
duration: this.stats.duration,
|
|
441
|
+
tests: this._tests,
|
|
442
|
+
};
|
|
443
|
+
debug('Uploading data', dataString);
|
|
444
|
+
this.pipes = this.pipes || (await this.pipesPromise);
|
|
445
|
+
return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
|
|
446
|
+
}
|
|
447
|
+
}
|
|
448
|
+
module.exports = AllureReader;
|
package/lib/bin/cli.js
CHANGED
|
@@ -10,6 +10,7 @@ const glob_1 = require("glob");
|
|
|
10
10
|
const debug_1 = __importDefault(require("debug"));
|
|
11
11
|
const client_js_1 = __importDefault(require("../client.js"));
|
|
12
12
|
const xmlReader_js_1 = __importDefault(require("../xmlReader.js"));
|
|
13
|
+
const allureReader_js_1 = __importDefault(require("../allureReader.js"));
|
|
13
14
|
const constants_js_1 = require("../constants.js");
|
|
14
15
|
const utils_js_1 = require("../utils/utils.js");
|
|
15
16
|
const config_js_1 = require("../config.js");
|
|
@@ -208,6 +209,33 @@ program
|
|
|
208
209
|
if (timeoutTimer)
|
|
209
210
|
clearTimeout(timeoutTimer);
|
|
210
211
|
});
|
|
212
|
+
program
|
|
213
|
+
.command('allure')
|
|
214
|
+
.description('Parse Allure result files and upload to Testomat.io')
|
|
215
|
+
.argument('<pattern>', 'Allure result directory pattern')
|
|
216
|
+
.option('-d, --dir <dir>', 'Project directory')
|
|
217
|
+
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
218
|
+
.option('--with-package', 'Keep full package path in file names (default: strip package prefix)')
|
|
219
|
+
.action(async (pattern, opts) => {
|
|
220
|
+
const runReader = new allureReader_js_1.default({ withPackage: opts.withPackage });
|
|
221
|
+
let timeoutTimer;
|
|
222
|
+
if (opts.timelimit) {
|
|
223
|
+
timeoutTimer = setTimeout(() => {
|
|
224
|
+
console.log(`⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`);
|
|
225
|
+
process.exit(0);
|
|
226
|
+
}, parseInt(opts.timelimit, 10) * 1000);
|
|
227
|
+
}
|
|
228
|
+
try {
|
|
229
|
+
await runReader.parse(pattern);
|
|
230
|
+
await runReader.createRun();
|
|
231
|
+
await runReader.uploadData();
|
|
232
|
+
}
|
|
233
|
+
catch (err) {
|
|
234
|
+
console.log(constants_js_1.APP_PREFIX, 'Error uploading Allure results:', err);
|
|
235
|
+
}
|
|
236
|
+
if (timeoutTimer)
|
|
237
|
+
clearTimeout(timeoutTimer);
|
|
238
|
+
});
|
|
211
239
|
program
|
|
212
240
|
.command('upload-artifacts')
|
|
213
241
|
.description('Upload artifacts to Testomat.io')
|
package/lib/data-storage.d.ts
CHANGED
|
@@ -9,6 +9,7 @@ const java_js_1 = __importDefault(require("./java.js"));
|
|
|
9
9
|
const python_js_1 = __importDefault(require("./python.js"));
|
|
10
10
|
const ruby_js_1 = __importDefault(require("./ruby.js"));
|
|
11
11
|
const csharp_js_1 = __importDefault(require("./csharp.js"));
|
|
12
|
+
const kotlin_js_1 = __importDefault(require("./kotlin.js"));
|
|
12
13
|
function AdapterFactory(lang, opts) {
|
|
13
14
|
if (lang === 'java') {
|
|
14
15
|
return new java_js_1.default(opts);
|
|
@@ -25,6 +26,9 @@ function AdapterFactory(lang, opts) {
|
|
|
25
26
|
if (lang === 'c#' || lang === 'csharp') {
|
|
26
27
|
return new csharp_js_1.default(opts);
|
|
27
28
|
}
|
|
29
|
+
if (lang === 'kotlin') {
|
|
30
|
+
return new kotlin_js_1.default(opts);
|
|
31
|
+
}
|
|
28
32
|
return new adapter_js_1.default(opts);
|
|
29
33
|
}
|
|
30
34
|
module.exports = AdapterFactory;
|