@testomatio/reporter 2.3.9-beta-bin-fix → 2.3.9
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 +2 -1
- package/lib/adapter/codecept.js +12 -9
- package/lib/bin/cli.js +14 -4
- package/lib/bin/reportXml.js +5 -2
- package/lib/client.d.ts +1 -11
- package/lib/client.js +39 -142
- package/lib/junit-adapter/csharp.d.ts +0 -1
- package/lib/junit-adapter/csharp.js +43 -7
- package/lib/junit-adapter/nunit-parser.d.ts +82 -0
- package/lib/junit-adapter/nunit-parser.js +433 -0
- package/lib/pipe/bitbucket.js +5 -5
- package/lib/pipe/gitlab.js +4 -4
- package/lib/pipe/testomatio.d.ts +2 -1
- package/lib/pipe/testomatio.js +19 -14
- package/lib/reporter-functions.js +1 -3
- package/lib/reporter.d.ts +19 -9
- package/lib/reporter.js +40 -5
- package/lib/uploader.js +4 -0
- package/lib/utils/log-formatter.d.ts +28 -0
- package/lib/utils/log-formatter.js +127 -0
- package/lib/utils/utils.js +189 -24
- package/lib/xmlReader.d.ts +32 -26
- package/lib/xmlReader.js +121 -52
- package/package.json +8 -4
- package/src/adapter/codecept.js +19 -19
- package/src/adapter/mocha.js +1 -1
- package/src/adapter/playwright.js +2 -2
- package/src/bin/cli.js +16 -4
- package/src/bin/reportXml.js +5 -2
- package/src/client.js +47 -116
- package/src/junit-adapter/csharp.js +48 -6
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/pipe/bitbucket.js +5 -5
- package/src/pipe/debug.js +1 -2
- package/src/pipe/gitlab.js +4 -4
- package/src/pipe/testomatio.js +75 -80
- package/src/reporter-functions.js +2 -3
- package/src/reporter.js +6 -4
- package/src/services/links.js +1 -1
- package/src/uploader.js +5 -0
- package/src/utils/log-formatter.js +113 -0
- package/src/utils/utils.js +202 -22
- package/src/xmlReader.js +144 -46
- package/types/types.d.ts +364 -0
- package/types/vitest.types.d.ts +93 -0
package/lib/reporter.d.ts
CHANGED
|
@@ -1,3 +1,10 @@
|
|
|
1
|
+
export { Client };
|
|
2
|
+
export const STATUS: {
|
|
3
|
+
PASSED: string;
|
|
4
|
+
FAILED: string;
|
|
5
|
+
SKIPPED: string;
|
|
6
|
+
FINISHED: string;
|
|
7
|
+
};
|
|
1
8
|
export const artifact: (data: string | {
|
|
2
9
|
path: string;
|
|
3
10
|
type: string;
|
|
@@ -80,7 +87,7 @@ export const label: (key: string, value?: string | null) => void;
|
|
|
80
87
|
export const linkTest: (...testIds: string[]) => void;
|
|
81
88
|
export const linkJira: (...jiraIds: string[]) => void;
|
|
82
89
|
declare namespace _default {
|
|
83
|
-
let testomatioLogger: {
|
|
90
|
+
export let testomatioLogger: {
|
|
84
91
|
"__#13@#originalUserLogger": {
|
|
85
92
|
assert(condition?: boolean, ...data: any[]): void;
|
|
86
93
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
@@ -148,13 +155,13 @@ declare namespace _default {
|
|
|
148
155
|
}): void;
|
|
149
156
|
prettyObjects: boolean;
|
|
150
157
|
};
|
|
151
|
-
let artifact: (data: string | {
|
|
158
|
+
export let artifact: (data: string | {
|
|
152
159
|
path: string;
|
|
153
160
|
type: string;
|
|
154
161
|
name: string;
|
|
155
162
|
}, context?: any) => void;
|
|
156
|
-
let log: (...args: any[]) => void;
|
|
157
|
-
let logger: {
|
|
163
|
+
export let log: (...args: any[]) => void;
|
|
164
|
+
export let logger: {
|
|
158
165
|
"__#13@#originalUserLogger": {
|
|
159
166
|
assert(condition?: boolean, ...data: any[]): void;
|
|
160
167
|
assert(value: any, message?: string, ...optionalParams: any[]): void;
|
|
@@ -222,13 +229,15 @@ declare namespace _default {
|
|
|
222
229
|
}): void;
|
|
223
230
|
prettyObjects: boolean;
|
|
224
231
|
};
|
|
225
|
-
let meta: (keyValue: {
|
|
232
|
+
export let meta: (keyValue: {
|
|
226
233
|
[key: string]: string;
|
|
227
234
|
} | string, value?: string | null) => void;
|
|
228
|
-
let step: (message: string) => void;
|
|
229
|
-
let label: (key: string, value?: string | null) => void;
|
|
230
|
-
let linkTest: (...testIds: string[]) => void;
|
|
231
|
-
let linkJira: (...jiraIds: string[]) => void;
|
|
235
|
+
export let step: (message: string) => void;
|
|
236
|
+
export let label: (key: string, value?: string | null) => void;
|
|
237
|
+
export let linkTest: (...testIds: string[]) => void;
|
|
238
|
+
export let linkJira: (...jiraIds: string[]) => void;
|
|
239
|
+
export { Client as TestomatioClient };
|
|
240
|
+
export { STATUS };
|
|
232
241
|
}
|
|
233
242
|
export default _default;
|
|
234
243
|
export type ArtifactFunction = typeof import("./reporter-functions.js").default.artifact;
|
|
@@ -237,3 +246,4 @@ export type LoggerService = typeof import("./services/index.js").services.logger
|
|
|
237
246
|
export type MetaFunction = typeof import("./reporter-functions.js").default.keyValue;
|
|
238
247
|
export type StepFunction = typeof import("./reporter-functions.js").default.step;
|
|
239
248
|
export type LabelFunction = typeof import("./reporter-functions.js").default.label;
|
|
249
|
+
import Client from './client.js';
|
package/lib/reporter.js
CHANGED
|
@@ -1,13 +1,48 @@
|
|
|
1
1
|
"use strict";
|
|
2
|
+
var __createBinding = (this && this.__createBinding) || (Object.create ? (function(o, m, k, k2) {
|
|
3
|
+
if (k2 === undefined) k2 = k;
|
|
4
|
+
var desc = Object.getOwnPropertyDescriptor(m, k);
|
|
5
|
+
if (!desc || ("get" in desc ? !m.__esModule : desc.writable || desc.configurable)) {
|
|
6
|
+
desc = { enumerable: true, get: function() { return m[k]; } };
|
|
7
|
+
}
|
|
8
|
+
Object.defineProperty(o, k2, desc);
|
|
9
|
+
}) : (function(o, m, k, k2) {
|
|
10
|
+
if (k2 === undefined) k2 = k;
|
|
11
|
+
o[k2] = m[k];
|
|
12
|
+
}));
|
|
13
|
+
var __setModuleDefault = (this && this.__setModuleDefault) || (Object.create ? (function(o, v) {
|
|
14
|
+
Object.defineProperty(o, "default", { enumerable: true, value: v });
|
|
15
|
+
}) : function(o, v) {
|
|
16
|
+
o["default"] = v;
|
|
17
|
+
});
|
|
18
|
+
var __importStar = (this && this.__importStar) || (function () {
|
|
19
|
+
var ownKeys = function(o) {
|
|
20
|
+
ownKeys = Object.getOwnPropertyNames || function (o) {
|
|
21
|
+
var ar = [];
|
|
22
|
+
for (var k in o) if (Object.prototype.hasOwnProperty.call(o, k)) ar[ar.length] = k;
|
|
23
|
+
return ar;
|
|
24
|
+
};
|
|
25
|
+
return ownKeys(o);
|
|
26
|
+
};
|
|
27
|
+
return function (mod) {
|
|
28
|
+
if (mod && mod.__esModule) return mod;
|
|
29
|
+
var result = {};
|
|
30
|
+
if (mod != null) for (var k = ownKeys(mod), i = 0; i < k.length; i++) if (k[i] !== "default") __createBinding(result, mod, k[i]);
|
|
31
|
+
__setModuleDefault(result, mod);
|
|
32
|
+
return result;
|
|
33
|
+
};
|
|
34
|
+
})();
|
|
2
35
|
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
36
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
37
|
};
|
|
5
38
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.linkJira = exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = void 0;
|
|
7
|
-
|
|
8
|
-
|
|
39
|
+
exports.linkJira = exports.linkTest = exports.label = exports.step = exports.meta = exports.logger = exports.log = exports.artifact = exports.STATUS = exports.Client = void 0;
|
|
40
|
+
const client_js_1 = __importDefault(require("./client.js"));
|
|
41
|
+
exports.Client = client_js_1.default;
|
|
42
|
+
const TestomatioConstants = __importStar(require("./constants.js"));
|
|
9
43
|
const index_js_1 = require("./services/index.js");
|
|
10
44
|
const reporter_functions_js_1 = __importDefault(require("./reporter-functions.js"));
|
|
45
|
+
exports.STATUS = TestomatioConstants.STATUS;
|
|
11
46
|
exports.artifact = reporter_functions_js_1.default.artifact;
|
|
12
47
|
exports.log = reporter_functions_js_1.default.log;
|
|
13
48
|
exports.logger = index_js_1.services.logger;
|
|
@@ -37,6 +72,6 @@ module.exports = {
|
|
|
37
72
|
label: reporter_functions_js_1.default.label,
|
|
38
73
|
linkTest: reporter_functions_js_1.default.linkTest,
|
|
39
74
|
linkJira: reporter_functions_js_1.default.linkJira,
|
|
40
|
-
|
|
41
|
-
|
|
75
|
+
TestomatioClient: client_js_1.default,
|
|
76
|
+
STATUS: exports.STATUS,
|
|
42
77
|
};
|
package/lib/uploader.js
CHANGED
|
@@ -170,6 +170,10 @@ class S3Uploader {
|
|
|
170
170
|
if (typeof filePath === 'string' && !path_1.default.isAbsolute(filePath)) {
|
|
171
171
|
filePath = path_1.default.join(process.cwd(), filePath);
|
|
172
172
|
}
|
|
173
|
+
// Normalize path separators for cross-platform compatibility
|
|
174
|
+
if (typeof filePath === 'string') {
|
|
175
|
+
filePath = filePath.replace(/\\/g, '/');
|
|
176
|
+
}
|
|
173
177
|
const data = { rid, file: filePath, uploaded };
|
|
174
178
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
175
179
|
fs_1.default.appendFileSync(tempFilePath, jsonLine);
|
|
@@ -0,0 +1,28 @@
|
|
|
1
|
+
/**
|
|
2
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
3
|
+
* @param {Object} params - Parameters for formatting logs
|
|
4
|
+
* @param {string} params.error - Error message
|
|
5
|
+
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
6
|
+
* @param {string} params.logs - Test logs
|
|
7
|
+
* @returns {string}
|
|
8
|
+
*/
|
|
9
|
+
export function formatLogs({ error, steps, logs }: {
|
|
10
|
+
error: string;
|
|
11
|
+
steps: any[] | any;
|
|
12
|
+
logs: string;
|
|
13
|
+
}): string;
|
|
14
|
+
/**
|
|
15
|
+
* Formats an error with stack trace and diff information
|
|
16
|
+
* @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
|
|
17
|
+
* The error object to format
|
|
18
|
+
* @param {string} [message] - Optional error message override
|
|
19
|
+
* @returns {string}
|
|
20
|
+
*/
|
|
21
|
+
export function formatError(error: Error & {
|
|
22
|
+
inspect?: () => string;
|
|
23
|
+
operator?: string;
|
|
24
|
+
diff?: string;
|
|
25
|
+
actual?: any;
|
|
26
|
+
expected?: any;
|
|
27
|
+
}, message?: string): string;
|
|
28
|
+
export function stripColors(str: string): string;
|
|
@@ -0,0 +1,127 @@
|
|
|
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
|
+
exports.stripColors = void 0;
|
|
7
|
+
exports.formatLogs = formatLogs;
|
|
8
|
+
exports.formatError = formatError;
|
|
9
|
+
const callsite_record_1 = __importDefault(require("callsite-record"));
|
|
10
|
+
const minimatch_1 = require("minimatch");
|
|
11
|
+
const picocolors_1 = __importDefault(require("picocolors"));
|
|
12
|
+
const util_1 = require("util");
|
|
13
|
+
const path_1 = require("path");
|
|
14
|
+
const utils_js_1 = require("./utils.js");
|
|
15
|
+
const stripColors = util_1.stripVTControlCharacters || (str => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
|
|
16
|
+
exports.stripColors = stripColors;
|
|
17
|
+
/**
|
|
18
|
+
* Returns the formatted stack including the stack trace, steps, and logs.
|
|
19
|
+
* @param {Object} params - Parameters for formatting logs
|
|
20
|
+
* @param {string} params.error - Error message
|
|
21
|
+
* @param {Array|any} params.steps - Test steps (array or other types)
|
|
22
|
+
* @param {string} params.logs - Test logs
|
|
23
|
+
* @returns {string}
|
|
24
|
+
*/
|
|
25
|
+
function formatLogs({ error, steps, logs }) {
|
|
26
|
+
error = error?.trim();
|
|
27
|
+
logs = logs
|
|
28
|
+
?.trim()
|
|
29
|
+
.split('\n')
|
|
30
|
+
.map(l => (0, utils_js_1.truncate)(l))
|
|
31
|
+
.join('\n');
|
|
32
|
+
if (Array.isArray(steps)) {
|
|
33
|
+
steps = steps
|
|
34
|
+
.map(step => (0, utils_js_1.formatStep)(step))
|
|
35
|
+
.flat()
|
|
36
|
+
.join('\n');
|
|
37
|
+
}
|
|
38
|
+
else {
|
|
39
|
+
steps = null;
|
|
40
|
+
}
|
|
41
|
+
let testLogs = '';
|
|
42
|
+
if (steps)
|
|
43
|
+
testLogs += `${picocolors_1.default.bold(picocolors_1.default.blue('################[ Steps ]################'))}\n${steps}\n\n`;
|
|
44
|
+
if (logs)
|
|
45
|
+
testLogs += `${picocolors_1.default.bold(picocolors_1.default.gray('################[ Logs ]################'))}\n${logs}\n\n`;
|
|
46
|
+
if (error)
|
|
47
|
+
testLogs += `${picocolors_1.default.bold(picocolors_1.default.red('################[ Failure ]################'))}\n${error}`;
|
|
48
|
+
return testLogs;
|
|
49
|
+
}
|
|
50
|
+
/**
|
|
51
|
+
* Formats an error with stack trace and diff information
|
|
52
|
+
* @param {Error & {inspect?: () => string, operator?: string, diff?: string, actual?: any, expected?: any}} error
|
|
53
|
+
* The error object to format
|
|
54
|
+
* @param {string} [message] - Optional error message override
|
|
55
|
+
* @returns {string}
|
|
56
|
+
*/
|
|
57
|
+
function formatError(error, message) {
|
|
58
|
+
if (!message)
|
|
59
|
+
message = error.message;
|
|
60
|
+
// @ts-ignore - inspect is a custom property added by some testing frameworks
|
|
61
|
+
if (error.inspect)
|
|
62
|
+
message = error.inspect() || '';
|
|
63
|
+
let stack = '';
|
|
64
|
+
if (error.name)
|
|
65
|
+
stack += `${picocolors_1.default.red(error.name)}`;
|
|
66
|
+
// @ts-ignore - operator is a custom property added by assertion libraries
|
|
67
|
+
if (error.operator)
|
|
68
|
+
stack += ` (${picocolors_1.default.red(error.operator)})`;
|
|
69
|
+
// add new line if something was added to stack
|
|
70
|
+
if (stack)
|
|
71
|
+
stack += ': ';
|
|
72
|
+
stack += `${message}\n`;
|
|
73
|
+
// @ts-ignore - diff is a custom property added by vitest
|
|
74
|
+
if (error.diff) {
|
|
75
|
+
// diff for vitest
|
|
76
|
+
stack += error.diff;
|
|
77
|
+
stack += '\n\n';
|
|
78
|
+
}
|
|
79
|
+
else if (error.actual && error.expected && error.actual !== error.expected) {
|
|
80
|
+
// diffs for mocha, cypress, codeceptjs style
|
|
81
|
+
stack += `\n\n${picocolors_1.default.bold(picocolors_1.default.green('+ expected'))} ${picocolors_1.default.bold(picocolors_1.default.red('- actual'))}`;
|
|
82
|
+
stack += `\n${picocolors_1.default.green(`+ ${error.expected.toString().split('\n').join('\n+ ')}`)}`;
|
|
83
|
+
stack += `\n${picocolors_1.default.red(`- ${error.actual.toString().split('\n').join('\n- ')}`)}`;
|
|
84
|
+
stack += '\n\n';
|
|
85
|
+
}
|
|
86
|
+
const customFilter = process.env.TESTOMATIO_STACK_IGNORE;
|
|
87
|
+
try {
|
|
88
|
+
let hasFrame = false;
|
|
89
|
+
const record = (0, callsite_record_1.default)({
|
|
90
|
+
forError: error,
|
|
91
|
+
isCallsiteFrame: frame => {
|
|
92
|
+
if (customFilter && (0, minimatch_1.minimatch)(frame.fileName, customFilter))
|
|
93
|
+
return false;
|
|
94
|
+
if (hasFrame)
|
|
95
|
+
return false;
|
|
96
|
+
if (isNotInternalFrame(frame))
|
|
97
|
+
hasFrame = true;
|
|
98
|
+
return hasFrame;
|
|
99
|
+
},
|
|
100
|
+
});
|
|
101
|
+
// @ts-ignore
|
|
102
|
+
if (record && !record.filename.startsWith('http')) {
|
|
103
|
+
stack += record.renderSync({ stackFilter: isNotInternalFrame });
|
|
104
|
+
}
|
|
105
|
+
return stack;
|
|
106
|
+
}
|
|
107
|
+
catch (e) {
|
|
108
|
+
console.log(e);
|
|
109
|
+
}
|
|
110
|
+
}
|
|
111
|
+
/**
|
|
112
|
+
* Checks if a stack frame is not an internal frame (node_modules or internal)
|
|
113
|
+
* @param {Object} frame - Stack frame object
|
|
114
|
+
* @returns {boolean}
|
|
115
|
+
*/
|
|
116
|
+
function isNotInternalFrame(frame) {
|
|
117
|
+
return (frame.getFileName() &&
|
|
118
|
+
frame.getFileName().includes(path_1.sep) &&
|
|
119
|
+
!frame.getFileName().includes('node_modules') &&
|
|
120
|
+
!frame.getFileName().includes('internal'));
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
module.exports.formatLogs = formatLogs;
|
|
124
|
+
|
|
125
|
+
module.exports.formatError = formatError;
|
|
126
|
+
|
|
127
|
+
module.exports.stripColors = stripColors;
|
package/lib/utils/utils.js
CHANGED
|
@@ -116,19 +116,26 @@ const isValidUrl = s => {
|
|
|
116
116
|
}
|
|
117
117
|
};
|
|
118
118
|
exports.isValidUrl = isValidUrl;
|
|
119
|
-
const fileMatchRegex = /file:(
|
|
119
|
+
const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
|
|
120
120
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
121
|
-
|
|
122
|
-
.map(
|
|
121
|
+
let files = Array.from(stack.matchAll(fileMatchRegex))
|
|
122
|
+
.map(match => {
|
|
123
|
+
// match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
|
|
124
|
+
const slashes = match[1] || '';
|
|
125
|
+
const path = match[2];
|
|
126
|
+
const extension = match[3];
|
|
127
|
+
return `${slashes}${path}.${extension}`;
|
|
128
|
+
})
|
|
129
|
+
.map(f => f.trim())
|
|
123
130
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
124
131
|
.map(f => {
|
|
125
|
-
//
|
|
126
|
-
|
|
127
|
-
// Convert Windows path to Linux equivalent for test scenarios
|
|
128
|
-
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
129
|
-
}
|
|
130
|
-
return f;
|
|
132
|
+
// Normalize path separators for cross-platform compatibility
|
|
133
|
+
return f.replace(/\\/g, '/');
|
|
131
134
|
});
|
|
135
|
+
// If we're not checking file existence, remove Windows drive letters for consistency
|
|
136
|
+
if (!checkExists) {
|
|
137
|
+
files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
|
|
138
|
+
}
|
|
132
139
|
debug('Found files in stack trace: ', files);
|
|
133
140
|
return files.filter(f => {
|
|
134
141
|
if (!checkExists)
|
|
@@ -144,19 +151,88 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
144
151
|
const stackLines = stack
|
|
145
152
|
.split('\n')
|
|
146
153
|
.filter(l => l.includes(':'))
|
|
147
|
-
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
148
|
-
// .map(l => l.split(':')[0])
|
|
149
154
|
.map(l => l.trim())
|
|
150
|
-
.map(l =>
|
|
151
|
-
|
|
155
|
+
.map(l => {
|
|
156
|
+
// Remove 'at ' prefix if present
|
|
157
|
+
if (l.startsWith('at ')) {
|
|
158
|
+
return l.substring(3).trim();
|
|
159
|
+
}
|
|
160
|
+
// Find the part that looks like a file path with line number
|
|
161
|
+
const parts = l.split(' ');
|
|
162
|
+
for (const part of parts) {
|
|
163
|
+
// Check if this part has a colon
|
|
164
|
+
if (part.includes(':')) {
|
|
165
|
+
// For Windows paths, we need to handle drive letters (C:, D:, etc.)
|
|
166
|
+
// Split by colon but keep drive letter with the path
|
|
167
|
+
const colonParts = part.split(':');
|
|
168
|
+
let filePath;
|
|
169
|
+
// Check if first part is a Windows drive letter (single letter)
|
|
170
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
171
|
+
// Windows path like D:\path\file.php:24
|
|
172
|
+
// Reconstruct as D:\path\file.php
|
|
173
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
174
|
+
}
|
|
175
|
+
else {
|
|
176
|
+
// Unix path like /path/file.php:24
|
|
177
|
+
filePath = colonParts[0];
|
|
178
|
+
}
|
|
179
|
+
// Only consider it valid if the file exists
|
|
180
|
+
if (fs_1.default.existsSync(filePath)) {
|
|
181
|
+
return part;
|
|
182
|
+
}
|
|
183
|
+
}
|
|
184
|
+
}
|
|
185
|
+
// If no valid file path found in parts, return the whole line
|
|
186
|
+
// It will be filtered out later if it's not a valid file path
|
|
187
|
+
return parts.find(p => p.includes(':')) || l;
|
|
188
|
+
})
|
|
189
|
+
.filter(l => {
|
|
190
|
+
// Extract file path from line (accounting for Windows drive letters)
|
|
191
|
+
if (!l)
|
|
192
|
+
return false;
|
|
193
|
+
const colonParts = l.split(':');
|
|
194
|
+
let filePath;
|
|
195
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
196
|
+
// Windows path
|
|
197
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
198
|
+
}
|
|
199
|
+
else {
|
|
200
|
+
// Unix path
|
|
201
|
+
filePath = colonParts[0];
|
|
202
|
+
}
|
|
203
|
+
return filePath && fs_1.default.existsSync(filePath);
|
|
204
|
+
})
|
|
152
205
|
// // filter out 3rd party libs
|
|
153
206
|
.filter(l => !l?.includes(`vendor${path_1.sep}`))
|
|
154
207
|
.filter(l => !l?.includes(`node_modules${path_1.sep}`))
|
|
155
|
-
.filter(l =>
|
|
156
|
-
|
|
208
|
+
.filter(l => {
|
|
209
|
+
// Extract file path for final check (accounting for Windows drive letters)
|
|
210
|
+
const colonParts = l.split(':');
|
|
211
|
+
let filePath;
|
|
212
|
+
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
213
|
+
filePath = colonParts[0] + ':' + colonParts[1];
|
|
214
|
+
}
|
|
215
|
+
else {
|
|
216
|
+
filePath = colonParts[0];
|
|
217
|
+
}
|
|
218
|
+
return fs_1.default.lstatSync(filePath).isFile();
|
|
219
|
+
});
|
|
157
220
|
if (!stackLines.length)
|
|
158
221
|
return '';
|
|
159
|
-
|
|
222
|
+
// Extract file and line number (accounting for Windows drive letters)
|
|
223
|
+
const firstLine = stackLines[0];
|
|
224
|
+
const colonParts = firstLine.split(':');
|
|
225
|
+
let file, line;
|
|
226
|
+
if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
227
|
+
// Windows path like D:\path\file.php:24
|
|
228
|
+
file = colonParts[0] + ':' + colonParts[1];
|
|
229
|
+
line = colonParts[2];
|
|
230
|
+
}
|
|
231
|
+
else {
|
|
232
|
+
// Unix path like /path/file.php:24
|
|
233
|
+
file = colonParts[0];
|
|
234
|
+
line = colonParts[1];
|
|
235
|
+
}
|
|
160
236
|
const prepend = 3;
|
|
161
237
|
const source = fetchSourceCode(fs_1.default.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
162
238
|
if (!source)
|
|
@@ -174,6 +250,8 @@ exports.fetchSourceCodeFromStackTrace = fetchSourceCodeFromStackTrace;
|
|
|
174
250
|
exports.TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
175
251
|
exports.SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
176
252
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
253
|
+
if (!code)
|
|
254
|
+
return null;
|
|
177
255
|
const comments = code
|
|
178
256
|
.split('\n')
|
|
179
257
|
.map(l => l.trim())
|
|
@@ -216,10 +294,58 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
216
294
|
lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
217
295
|
}
|
|
218
296
|
else if (opts.lang === 'csharp') {
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
if (
|
|
222
|
-
|
|
297
|
+
// Find the method declaration line
|
|
298
|
+
let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
|
|
299
|
+
if (methodLineIndex === -1) {
|
|
300
|
+
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
301
|
+
}
|
|
302
|
+
if (methodLineIndex === -1) {
|
|
303
|
+
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
304
|
+
}
|
|
305
|
+
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
306
|
+
if (methodLineIndex !== -1) {
|
|
307
|
+
lineIndex = methodLineIndex;
|
|
308
|
+
// Scan upwards to find the start of attributes and comments
|
|
309
|
+
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
310
|
+
const trimmedLine = lines[i].trim();
|
|
311
|
+
// Include [TestCase], [Test], and other attributes
|
|
312
|
+
if (trimmedLine.startsWith('[')) {
|
|
313
|
+
lineIndex = i;
|
|
314
|
+
continue;
|
|
315
|
+
}
|
|
316
|
+
// Include XML documentation comments
|
|
317
|
+
if (trimmedLine.startsWith('///')) {
|
|
318
|
+
lineIndex = i;
|
|
319
|
+
continue;
|
|
320
|
+
}
|
|
321
|
+
// Stop at empty lines (with some tolerance)
|
|
322
|
+
if (trimmedLine === '') {
|
|
323
|
+
// Check if next non-empty line is an attribute or comment
|
|
324
|
+
let hasMoreAttributes = false;
|
|
325
|
+
for (let j = i - 1; j >= 0; j--) {
|
|
326
|
+
const nextTrimmed = lines[j].trim();
|
|
327
|
+
if (nextTrimmed === '')
|
|
328
|
+
continue;
|
|
329
|
+
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
330
|
+
hasMoreAttributes = true;
|
|
331
|
+
lineIndex = j;
|
|
332
|
+
}
|
|
333
|
+
break;
|
|
334
|
+
}
|
|
335
|
+
if (!hasMoreAttributes)
|
|
336
|
+
break;
|
|
337
|
+
continue;
|
|
338
|
+
}
|
|
339
|
+
// Stop at other method declarations or class-level elements
|
|
340
|
+
if (trimmedLine.includes('public ') ||
|
|
341
|
+
trimmedLine.includes('private ') ||
|
|
342
|
+
trimmedLine.includes('protected ') ||
|
|
343
|
+
trimmedLine.includes('internal ')) {
|
|
344
|
+
if (!trimmedLine.startsWith('['))
|
|
345
|
+
break;
|
|
346
|
+
}
|
|
347
|
+
}
|
|
348
|
+
}
|
|
223
349
|
}
|
|
224
350
|
else {
|
|
225
351
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
@@ -228,11 +354,28 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
228
354
|
if (opts.prepend) {
|
|
229
355
|
lineIndex -= opts.prepend;
|
|
230
356
|
}
|
|
231
|
-
if (lineIndex) {
|
|
357
|
+
if (lineIndex !== -1 && lineIndex !== undefined) {
|
|
232
358
|
const result = [];
|
|
359
|
+
let braceDepth = 0; // Track brace depth for C# methods
|
|
360
|
+
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
233
361
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
234
362
|
if (lines[i] === undefined)
|
|
235
363
|
continue;
|
|
364
|
+
// Track brace depth for C# to stop after method closes
|
|
365
|
+
if (opts.lang === 'csharp') {
|
|
366
|
+
const line = lines[i];
|
|
367
|
+
// Count opening and closing braces
|
|
368
|
+
const openBraces = (line.match(/\{/g) || []).length;
|
|
369
|
+
const closeBraces = (line.match(/\}/g) || []).length;
|
|
370
|
+
if (openBraces > 0)
|
|
371
|
+
methodStartFound = true;
|
|
372
|
+
braceDepth += openBraces - closeBraces;
|
|
373
|
+
// If we've started the method and depth returns to 0, method is complete
|
|
374
|
+
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
375
|
+
// Don't include the closing brace - just break
|
|
376
|
+
break;
|
|
377
|
+
}
|
|
378
|
+
}
|
|
236
379
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
237
380
|
// annotation
|
|
238
381
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#['))
|
|
@@ -271,6 +414,24 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
271
414
|
break;
|
|
272
415
|
if (opts.lang === 'java' && lines[i].includes(' class '))
|
|
273
416
|
break;
|
|
417
|
+
// For C#, additional checks if brace tracking didn't stop us
|
|
418
|
+
if (opts.lang === 'csharp') {
|
|
419
|
+
const trimmed = lines[i].trim();
|
|
420
|
+
// Stop at attribute that marks beginning of next test (but not if we're still in the current method)
|
|
421
|
+
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0)
|
|
422
|
+
break;
|
|
423
|
+
// Stop at XML documentation comments that belong to next method
|
|
424
|
+
if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0)
|
|
425
|
+
break;
|
|
426
|
+
// Stop at another method declaration (but not if we're still in the current method)
|
|
427
|
+
if (trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
|
|
428
|
+
methodStartFound &&
|
|
429
|
+
braceDepth === 0)
|
|
430
|
+
break;
|
|
431
|
+
// Stop at class declaration
|
|
432
|
+
if (trimmed.includes(' class ') && trimmed.includes('public'))
|
|
433
|
+
break;
|
|
434
|
+
}
|
|
274
435
|
}
|
|
275
436
|
result.push(lines[i]);
|
|
276
437
|
}
|
|
@@ -471,10 +632,14 @@ function transformEnvVarToBoolean(value) {
|
|
|
471
632
|
return Boolean(value);
|
|
472
633
|
}
|
|
473
634
|
function truncate(s, size = 255) {
|
|
474
|
-
if (s
|
|
475
|
-
return
|
|
635
|
+
if (s === undefined || s === null) {
|
|
636
|
+
return '';
|
|
637
|
+
}
|
|
638
|
+
const str = s.toString();
|
|
639
|
+
if (str.trim().length < size) {
|
|
640
|
+
return str;
|
|
476
641
|
}
|
|
477
|
-
return `${
|
|
642
|
+
return `${str.substring(0, size)}...`;
|
|
478
643
|
}
|
|
479
644
|
|
|
480
645
|
module.exports.getPackageVersion = getPackageVersion;
|
package/lib/xmlReader.d.ts
CHANGED
|
@@ -19,25 +19,11 @@ declare class XmlReader {
|
|
|
19
19
|
tests: any[];
|
|
20
20
|
stats: {};
|
|
21
21
|
uploader: S3Uploader;
|
|
22
|
+
enhancedNunit: boolean;
|
|
23
|
+
groupParameterized: boolean;
|
|
22
24
|
version: any;
|
|
23
25
|
connectAdapter(): import("./junit-adapter/adapter.js").default;
|
|
24
|
-
parse(fileName: any):
|
|
25
|
-
status: string;
|
|
26
|
-
create_tests: boolean;
|
|
27
|
-
tests_count: number;
|
|
28
|
-
passed_count: number;
|
|
29
|
-
skipped_count: number;
|
|
30
|
-
failed_count: number;
|
|
31
|
-
tests: any;
|
|
32
|
-
} | {
|
|
33
|
-
status: any;
|
|
34
|
-
create_tests: boolean;
|
|
35
|
-
tests_count: number;
|
|
36
|
-
passed_count: number;
|
|
37
|
-
failed_count: number;
|
|
38
|
-
skipped_count: number;
|
|
39
|
-
tests: any[];
|
|
40
|
-
};
|
|
26
|
+
parse(fileName: any): any;
|
|
41
27
|
processJUnit(jsonSuite: any): {
|
|
42
28
|
create_tests: boolean;
|
|
43
29
|
duration: number;
|
|
@@ -49,15 +35,14 @@ declare class XmlReader {
|
|
|
49
35
|
tests: any[];
|
|
50
36
|
tests_count: number;
|
|
51
37
|
};
|
|
52
|
-
processNUnit(jsonSuite: any):
|
|
53
|
-
|
|
54
|
-
|
|
55
|
-
|
|
56
|
-
|
|
57
|
-
|
|
58
|
-
|
|
59
|
-
|
|
60
|
-
};
|
|
38
|
+
processNUnit(jsonSuite: any): any;
|
|
39
|
+
/**
|
|
40
|
+
* Check if the XML is actually NUnit format (has test-suite hierarchy)
|
|
41
|
+
* @param {Object} jsonSuite - Parsed XML suite object
|
|
42
|
+
* @returns {boolean} - True if this is NUnit XML format
|
|
43
|
+
*/
|
|
44
|
+
isNUnitXml(jsonSuite: any): boolean;
|
|
45
|
+
processNUnitEnhanced(jsonSuite: any): any;
|
|
61
46
|
processTRX(jsonSuite: any): {
|
|
62
47
|
status: string;
|
|
63
48
|
create_tests: boolean;
|
|
@@ -67,6 +52,27 @@ declare class XmlReader {
|
|
|
67
52
|
failed_count: number;
|
|
68
53
|
tests: any;
|
|
69
54
|
};
|
|
55
|
+
_parseTRXTestDefinition(td: any): {
|
|
56
|
+
title: any;
|
|
57
|
+
example: any;
|
|
58
|
+
file: string;
|
|
59
|
+
description: any;
|
|
60
|
+
suite_title: any;
|
|
61
|
+
id: any;
|
|
62
|
+
};
|
|
63
|
+
_parseTRXTestResult(td: any, tests: any): {
|
|
64
|
+
suite_title: any;
|
|
65
|
+
title: any;
|
|
66
|
+
file: any;
|
|
67
|
+
description: any;
|
|
68
|
+
code: any;
|
|
69
|
+
run_time: number;
|
|
70
|
+
stack: any;
|
|
71
|
+
files: any;
|
|
72
|
+
create: boolean;
|
|
73
|
+
overwrite: boolean;
|
|
74
|
+
};
|
|
75
|
+
_mapTRXStatus(outcome: any): string;
|
|
70
76
|
processXUnit(assemblies: any): {
|
|
71
77
|
status: string;
|
|
72
78
|
create_tests: boolean;
|