@testomatio/reporter 2.3.8 → 2.3.9-beta-bin-fix
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 +1 -2
- package/lib/bin/cli.js +4 -14
- package/lib/bin/reportXml.js +2 -5
- package/lib/client.d.ts +1 -1
- package/lib/client.js +22 -31
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +7 -40
- package/lib/pipe/testomatio.d.ts +1 -2
- package/lib/pipe/testomatio.js +1 -2
- package/lib/reporter.d.ts +9 -19
- package/lib/reporter.js +5 -40
- package/lib/uploader.js +0 -4
- package/lib/utils/utils.js +24 -189
- package/lib/xmlReader.d.ts +26 -32
- package/lib/xmlReader.js +52 -111
- package/package.json +5 -9
- package/src/bin/cli.js +4 -16
- package/src/bin/reportXml.js +2 -5
- package/src/client.js +28 -56
- package/src/junit-adapter/csharp.js +6 -45
- package/src/pipe/testomatio.js +1 -2
- package/src/reporter.js +4 -7
- package/src/uploader.js +0 -5
- package/src/utils/utils.js +22 -202
- package/src/xmlReader.js +46 -134
- package/lib/junit-adapter/nunit-parser.d.ts +0 -82
- package/lib/junit-adapter/nunit-parser.js +0 -431
- package/src/junit-adapter/nunit-parser.js +0 -472
- package/types/types.d.ts +0 -364
- package/types/vitest.types.d.ts +0 -93
package/src/bin/cli.js
CHANGED
|
@@ -35,20 +35,14 @@ program
|
|
|
35
35
|
program
|
|
36
36
|
.command('start')
|
|
37
37
|
.description('Start a new run and return its ID')
|
|
38
|
-
.
|
|
39
|
-
.action(async (opts) => {
|
|
38
|
+
.action(async () => {
|
|
40
39
|
cleanLatestRunId();
|
|
41
40
|
|
|
42
41
|
console.log('Starting a new Run on Testomat.io...');
|
|
43
42
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
44
43
|
const client = new TestomatClient({ apiKey });
|
|
45
44
|
|
|
46
|
-
|
|
47
|
-
if (opts.kind) {
|
|
48
|
-
createRunParams.kind = opts.kind;
|
|
49
|
-
}
|
|
50
|
-
|
|
51
|
-
client.createRun(createRunParams).then(() => {
|
|
45
|
+
client.createRun().then(() => {
|
|
52
46
|
console.log(process.env.runId);
|
|
53
47
|
process.exit(0);
|
|
54
48
|
});
|
|
@@ -81,7 +75,6 @@ program
|
|
|
81
75
|
.description('Run tests with the specified command')
|
|
82
76
|
.argument('<command>', 'Test runner command')
|
|
83
77
|
.option('--filter <filter>', 'Additional execution filter')
|
|
84
|
-
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
85
78
|
.action(async (command, opts) => {
|
|
86
79
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
87
80
|
const title = process.env.TESTOMATIO_TITLE;
|
|
@@ -127,13 +120,8 @@ program
|
|
|
127
120
|
});
|
|
128
121
|
};
|
|
129
122
|
|
|
130
|
-
const createRunParams = {};
|
|
131
|
-
if (opts.kind) {
|
|
132
|
-
createRunParams.kind = opts.kind;
|
|
133
|
-
}
|
|
134
|
-
|
|
135
123
|
if (apiKey) {
|
|
136
|
-
await client.createRun(
|
|
124
|
+
await client.createRun().then(runTests);
|
|
137
125
|
} else {
|
|
138
126
|
await runTests();
|
|
139
127
|
}
|
|
@@ -170,7 +158,7 @@ program
|
|
|
170
158
|
.option('--lang <lang>', 'Language used (python, ruby, java)')
|
|
171
159
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
172
160
|
.action(async (pattern, opts) => {
|
|
173
|
-
if (!pattern.endsWith('.xml')
|
|
161
|
+
if (!pattern.endsWith('.xml')) {
|
|
174
162
|
pattern += '.xml';
|
|
175
163
|
}
|
|
176
164
|
let { javaTests, lang } = opts;
|
package/src/bin/reportXml.js
CHANGED
|
@@ -23,7 +23,7 @@ program
|
|
|
23
23
|
.option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
|
|
24
24
|
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
25
25
|
.action(async (pattern, opts) => {
|
|
26
|
-
if (!pattern.endsWith('.xml')
|
|
26
|
+
if (!pattern.endsWith('.xml')) {
|
|
27
27
|
pattern += '.xml';
|
|
28
28
|
}
|
|
29
29
|
let { javaTests, lang } = opts;
|
|
@@ -34,10 +34,7 @@ program
|
|
|
34
34
|
}
|
|
35
35
|
lang = lang?.toLowerCase();
|
|
36
36
|
if (javaTests === true || (lang === 'java' && !javaTests)) javaTests = 'src/test/java';
|
|
37
|
-
const runReader = new XmlReader({
|
|
38
|
-
javaTests,
|
|
39
|
-
lang,
|
|
40
|
-
});
|
|
37
|
+
const runReader = new XmlReader({ javaTests, lang });
|
|
41
38
|
const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
|
|
42
39
|
if (!files.length) {
|
|
43
40
|
console.log(APP_PREFIX, `Report can't be created. No XML files found 😥`);
|
package/src/client.js
CHANGED
|
@@ -10,21 +10,11 @@ import { glob } from 'glob';
|
|
|
10
10
|
import path, { sep } from 'path';
|
|
11
11
|
import { fileURLToPath } from 'node:url';
|
|
12
12
|
import { S3Uploader } from './uploader.js';
|
|
13
|
-
import {
|
|
14
|
-
formatStep,
|
|
15
|
-
truncate,
|
|
16
|
-
readLatestRunId,
|
|
17
|
-
storeRunId,
|
|
18
|
-
validateSuiteId,
|
|
19
|
-
transformEnvVarToBoolean
|
|
20
|
-
} from './utils/utils.js';
|
|
13
|
+
import { formatStep, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
|
|
21
14
|
import { filesize as prettyBytes } from 'filesize';
|
|
22
|
-
import { stripVTControlCharacters } from 'util';
|
|
23
15
|
|
|
24
16
|
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
25
17
|
|
|
26
|
-
const stripColors = stripVTControlCharacters || ((str) => str?.replace(/\x1b\[[0-9;]*m/g, '') || '');
|
|
27
|
-
|
|
28
18
|
// removed __dirname usage, because:
|
|
29
19
|
// 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
|
|
30
20
|
// 2. got error "__dirname already defined" in compiles js code (cjs dir)
|
|
@@ -120,7 +110,7 @@ class Client {
|
|
|
120
110
|
*
|
|
121
111
|
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
122
112
|
*/
|
|
123
|
-
async createRun(params
|
|
113
|
+
async createRun(params) {
|
|
124
114
|
if (!this.pipes || !this.pipes.length)
|
|
125
115
|
this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
126
116
|
debug('Creating run...');
|
|
@@ -128,7 +118,7 @@ class Client {
|
|
|
128
118
|
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
129
119
|
|
|
130
120
|
this.queue = this.queue
|
|
131
|
-
.then(() => Promise.all(this.pipes.map(p => p.createRun(
|
|
121
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun())))
|
|
132
122
|
.catch(err => console.log(APP_PREFIX, err))
|
|
133
123
|
.then(() => {
|
|
134
124
|
const runId = this.pipeStore?.runId;
|
|
@@ -149,6 +139,19 @@ class Client {
|
|
|
149
139
|
* @returns {Promise<PipeResult[]>}
|
|
150
140
|
*/
|
|
151
141
|
async addTestRun(status, testData) {
|
|
142
|
+
if (!this.pipes || !this.pipes.length)
|
|
143
|
+
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
144
|
+
|
|
145
|
+
// all pipes disabled, skipping
|
|
146
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return [];
|
|
147
|
+
|
|
148
|
+
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
149
|
+
|
|
150
|
+
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
151
|
+
debug('Skipping test from report', testData?.title);
|
|
152
|
+
return []; // do not log skipped tests
|
|
153
|
+
}
|
|
154
|
+
|
|
152
155
|
if (!testData)
|
|
153
156
|
testData = {
|
|
154
157
|
title: 'Unknown test',
|
|
@@ -166,23 +169,15 @@ class Client {
|
|
|
166
169
|
const {
|
|
167
170
|
rid,
|
|
168
171
|
error = null,
|
|
169
|
-
steps: originalSteps,
|
|
170
|
-
title,
|
|
171
|
-
suite_title,
|
|
172
|
-
} = testData;
|
|
173
|
-
let steps = originalSteps;
|
|
174
|
-
|
|
175
|
-
const uploadedFiles = [];
|
|
176
|
-
const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
const {
|
|
180
172
|
time = 0,
|
|
181
173
|
example = null,
|
|
182
174
|
files = [],
|
|
183
175
|
filesBuffers = [],
|
|
176
|
+
steps,
|
|
184
177
|
code = null,
|
|
178
|
+
title,
|
|
185
179
|
file,
|
|
180
|
+
suite_title,
|
|
186
181
|
suite_id,
|
|
187
182
|
test_id,
|
|
188
183
|
timestamp,
|
|
@@ -193,6 +188,7 @@ class Client {
|
|
|
193
188
|
} = testData;
|
|
194
189
|
let { message = '', meta = {} } = testData;
|
|
195
190
|
|
|
191
|
+
// stringify meta values and limit keys and values length to 255
|
|
196
192
|
meta = Object.entries(meta)
|
|
197
193
|
.filter(([, value]) => value !== null && value !== undefined)
|
|
198
194
|
.reduce((acc, [key, value]) => {
|
|
@@ -200,6 +196,7 @@ class Client {
|
|
|
200
196
|
return acc;
|
|
201
197
|
}, {});
|
|
202
198
|
|
|
199
|
+
// Get links from storage using the test context
|
|
203
200
|
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
204
201
|
|
|
205
202
|
let errorFormatted = '';
|
|
@@ -208,39 +205,14 @@ class Client {
|
|
|
208
205
|
message = error?.message;
|
|
209
206
|
}
|
|
210
207
|
|
|
211
|
-
|
|
212
|
-
|
|
213
|
-
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
214
|
-
uploadedFiles.push(
|
|
215
|
-
this.uploader.uploadFileAsBuffer(
|
|
216
|
-
Buffer.from(stripColors(fullLogs), 'utf8'),
|
|
217
|
-
[this.runId, rid, `logs_${+new Date}.log`]
|
|
218
|
-
)
|
|
219
|
-
);
|
|
220
|
-
fullLogs = '';
|
|
221
|
-
steps = null;
|
|
222
|
-
}
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
if (!this.pipes || !this.pipes.length)
|
|
226
|
-
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
227
|
-
|
|
228
|
-
if (!this.pipes?.filter(p => p.isEnabled).length) {
|
|
229
|
-
if (uploadedFiles.length > 0) {
|
|
230
|
-
await Promise.all(uploadedFiles);
|
|
231
|
-
}
|
|
232
|
-
return [];
|
|
233
|
-
}
|
|
234
|
-
|
|
235
|
-
if (isTestShouldBeExculedFromReport(testData)) return [];
|
|
236
|
-
|
|
237
|
-
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
238
|
-
debug('Skipping test from report', testData?.title);
|
|
239
|
-
return [];
|
|
240
|
-
}
|
|
208
|
+
// Attach logs
|
|
209
|
+
const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
|
|
241
210
|
|
|
211
|
+
// add artifacts
|
|
242
212
|
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
243
213
|
|
|
214
|
+
const uploadedFiles = [];
|
|
215
|
+
|
|
244
216
|
for (let f of files) {
|
|
245
217
|
if (!f) continue; // f === null
|
|
246
218
|
if (typeof f === 'object') {
|
|
@@ -336,7 +308,7 @@ class Client {
|
|
|
336
308
|
const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
|
|
337
309
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
338
310
|
link: file.link,
|
|
339
|
-
sizePretty:
|
|
311
|
+
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
340
312
|
}));
|
|
341
313
|
|
|
342
314
|
uploadedArtifacts.forEach(upload => {
|
|
@@ -358,7 +330,7 @@ class Client {
|
|
|
358
330
|
);
|
|
359
331
|
const failedUploads = this.uploader.failedUploads.map(file => ({
|
|
360
332
|
relativePath: file.path.replace(process.cwd(), ''),
|
|
361
|
-
sizePretty:
|
|
333
|
+
sizePretty: prettyBytes(file.size, { round: 0 }).toString(),
|
|
362
334
|
}));
|
|
363
335
|
|
|
364
336
|
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
@@ -3,50 +3,18 @@ import Adapter from './adapter.js';
|
|
|
3
3
|
|
|
4
4
|
class CSharpAdapter extends Adapter {
|
|
5
5
|
formatTest(t) {
|
|
6
|
-
|
|
7
|
-
|
|
8
|
-
|
|
9
|
-
if (exampleMatch) {
|
|
10
|
-
// Extract parameters as object with numeric keys for API
|
|
11
|
-
const params = exampleMatch[1].split(',').map(param => param.trim()).filter(param => param !== '');
|
|
12
|
-
t.example = {};
|
|
13
|
-
params.forEach((param, index) => {
|
|
14
|
-
t.example[index] = param;
|
|
15
|
-
});
|
|
16
|
-
}
|
|
17
|
-
}
|
|
18
|
-
|
|
19
|
-
// Remove parameters from title to avoid duplicates in Test Suite
|
|
20
|
-
// The example field will be used for grouping on import
|
|
21
|
-
t.title = t.title.replace(/\(.*?\)/, '').trim();
|
|
22
|
-
|
|
6
|
+
const title = t.title.replace(/\(.*?\)/, '').trim();
|
|
7
|
+
const example = t.title.match(/\((.*?)\)/);
|
|
8
|
+
if (example) t.example = { ...example[1].split(',') };
|
|
23
9
|
const suite = t.suite_title.split('.');
|
|
24
10
|
t.suite_title = suite.pop();
|
|
25
11
|
t.file = namespaceToFileName(t.file);
|
|
12
|
+
t.title = title.trim();
|
|
26
13
|
return t;
|
|
27
14
|
}
|
|
28
15
|
|
|
29
16
|
getFilePath(t) {
|
|
30
|
-
|
|
31
|
-
|
|
32
|
-
// Normalize path separators for cross-platform compatibility
|
|
33
|
-
let filePath = t.file.replace(/\\/g, '/');
|
|
34
|
-
|
|
35
|
-
// If file already has .cs extension, use it directly
|
|
36
|
-
if (filePath.endsWith('.cs')) {
|
|
37
|
-
// Make relative path if it's absolute
|
|
38
|
-
if (path.isAbsolute(filePath)) {
|
|
39
|
-
// Try to find project-relative path
|
|
40
|
-
const cwd = process.cwd().replace(/\\/g, '/');
|
|
41
|
-
if (filePath.startsWith(cwd)) {
|
|
42
|
-
filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
|
|
43
|
-
}
|
|
44
|
-
}
|
|
45
|
-
return filePath;
|
|
46
|
-
}
|
|
47
|
-
|
|
48
|
-
// Convert namespace path to file path
|
|
49
|
-
const fileName = namespaceToFileName(filePath);
|
|
17
|
+
const fileName = namespaceToFileName(t.file);
|
|
50
18
|
return fileName;
|
|
51
19
|
}
|
|
52
20
|
}
|
|
@@ -54,14 +22,7 @@ class CSharpAdapter extends Adapter {
|
|
|
54
22
|
export default CSharpAdapter;
|
|
55
23
|
|
|
56
24
|
function namespaceToFileName(fileName) {
|
|
57
|
-
if (!fileName) return '';
|
|
58
|
-
|
|
59
|
-
// If already a .cs file path, clean it up
|
|
60
|
-
if (fileName.endsWith('.cs')) {
|
|
61
|
-
return fileName.replace(/\\/g, '/');
|
|
62
|
-
}
|
|
63
|
-
|
|
64
25
|
const fileParts = fileName.split('.');
|
|
65
26
|
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
66
|
-
return `${fileParts.join(
|
|
27
|
+
return `${fileParts.join(path.sep)}.cs`;
|
|
67
28
|
}
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -163,7 +163,7 @@ class TestomatioPipe {
|
|
|
163
163
|
|
|
164
164
|
/**
|
|
165
165
|
* Creates a new run on Testomat.io
|
|
166
|
-
* @param {{isBatchEnabled?: boolean
|
|
166
|
+
* @param {{isBatchEnabled?: boolean}} params
|
|
167
167
|
* @returns Promise<void>
|
|
168
168
|
*/
|
|
169
169
|
async createRun(params = {}) {
|
|
@@ -204,7 +204,6 @@ class TestomatioPipe {
|
|
|
204
204
|
label: this.label,
|
|
205
205
|
shared_run: this.sharedRun,
|
|
206
206
|
shared_run_timeout: this.sharedRunTimeout,
|
|
207
|
-
kind: params.kind,
|
|
208
207
|
}).filter(([, value]) => !!value),
|
|
209
208
|
);
|
|
210
209
|
debug(' >>>>>> Run params', JSON.stringify(runParams, null, 2));
|
package/src/reporter.js
CHANGED
|
@@ -1,10 +1,8 @@
|
|
|
1
|
-
import
|
|
2
|
-
import * as
|
|
1
|
+
// import TestomatClient from './client.js';
|
|
2
|
+
// import * as TRConstants from './constants.js';
|
|
3
3
|
import { services } from './services/index.js';
|
|
4
4
|
import reporterFunctions from './reporter-functions.js';
|
|
5
5
|
|
|
6
|
-
export { Client };
|
|
7
|
-
export const STATUS = TestomatioConstants.STATUS;
|
|
8
6
|
export const artifact = reporterFunctions.artifact;
|
|
9
7
|
export const log = reporterFunctions.log;
|
|
10
8
|
export const logger = services.logger;
|
|
@@ -37,7 +35,6 @@ export default {
|
|
|
37
35
|
linkTest: reporterFunctions.linkTest,
|
|
38
36
|
linkJira: reporterFunctions.linkJira,
|
|
39
37
|
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
|
|
38
|
+
// TestomatClient,
|
|
39
|
+
// TRConstants,
|
|
43
40
|
};
|
package/src/uploader.js
CHANGED
|
@@ -194,11 +194,6 @@ export class S3Uploader {
|
|
|
194
194
|
filePath = path.join(process.cwd(), filePath);
|
|
195
195
|
}
|
|
196
196
|
|
|
197
|
-
// Normalize path separators for cross-platform compatibility
|
|
198
|
-
if (typeof filePath === 'string') {
|
|
199
|
-
filePath = filePath.replace(/\\/g, '/');
|
|
200
|
-
}
|
|
201
|
-
|
|
202
197
|
const data = { rid, file: filePath, uploaded };
|
|
203
198
|
const jsonLine = `${JSON.stringify(data)}\n`;
|
|
204
199
|
fs.appendFileSync(tempFilePath, jsonLine);
|
package/src/utils/utils.js
CHANGED
|
@@ -76,29 +76,21 @@ const isValidUrl = s => {
|
|
|
76
76
|
}
|
|
77
77
|
};
|
|
78
78
|
|
|
79
|
-
const fileMatchRegex = /file:(
|
|
79
|
+
const fileMatchRegex = /file:(\/+(?:[A-Za-z]:[\\/]|\/)?[^\s]*?\.(png|avi|webm|jpg|html|txt))/gi;
|
|
80
80
|
|
|
81
81
|
const fetchFilesFromStackTrace = (stack = '', checkExists = true) => {
|
|
82
|
-
|
|
83
|
-
.map(
|
|
84
|
-
// match[0] is full match, match[1] is slashes, match[2] is path, match[3] is extension
|
|
85
|
-
const slashes = match[1] || '';
|
|
86
|
-
const path = match[2];
|
|
87
|
-
const extension = match[3];
|
|
88
|
-
return `${slashes}${path}.${extension}`;
|
|
89
|
-
})
|
|
90
|
-
.map(f => f.trim())
|
|
82
|
+
const files = Array.from(stack.matchAll(fileMatchRegex))
|
|
83
|
+
.map(f => f[1].trim())
|
|
91
84
|
.map(f => f.replace(/^\/+/, '/').replace(/^\/([A-Za-z]:)/, '$1')) // Remove extra slashes, handle Windows paths
|
|
92
85
|
.map(f => {
|
|
93
|
-
//
|
|
94
|
-
|
|
86
|
+
// Convert Windows paths to Linux paths for testing purposes
|
|
87
|
+
if (f.match(/^[A-Za-z]:[\\\/]/)) {
|
|
88
|
+
// Convert Windows path to Linux equivalent for test scenarios
|
|
89
|
+
return f.replace(/^[A-Za-z]:[\\\/]/, '/').replace(/\\/g, '/');
|
|
90
|
+
}
|
|
91
|
+
return f;
|
|
95
92
|
});
|
|
96
93
|
|
|
97
|
-
// If we're not checking file existence, remove Windows drive letters for consistency
|
|
98
|
-
if (!checkExists) {
|
|
99
|
-
files = files.map(f => f.replace(/^([A-Za-z]):/, ''));
|
|
100
|
-
}
|
|
101
|
-
|
|
102
94
|
debug('Found files in stack trace: ', files);
|
|
103
95
|
|
|
104
96
|
return files.filter(f => {
|
|
@@ -113,92 +105,21 @@ const fetchSourceCodeFromStackTrace = (stack = '') => {
|
|
|
113
105
|
const stackLines = stack
|
|
114
106
|
.split('\n')
|
|
115
107
|
.filter(l => l.includes(':'))
|
|
108
|
+
// .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
|
|
109
|
+
// .map(l => l.split(':')[0])
|
|
116
110
|
.map(l => l.trim())
|
|
117
|
-
.map(l =>
|
|
118
|
-
|
|
119
|
-
if (l.startsWith('at ')) {
|
|
120
|
-
return l.substring(3).trim();
|
|
121
|
-
}
|
|
122
|
-
// Find the part that looks like a file path with line number
|
|
123
|
-
const parts = l.split(' ');
|
|
124
|
-
for (const part of parts) {
|
|
125
|
-
// Check if this part has a colon
|
|
126
|
-
if (part.includes(':')) {
|
|
127
|
-
// For Windows paths, we need to handle drive letters (C:, D:, etc.)
|
|
128
|
-
// Split by colon but keep drive letter with the path
|
|
129
|
-
const colonParts = part.split(':');
|
|
130
|
-
let filePath;
|
|
131
|
-
|
|
132
|
-
// Check if first part is a Windows drive letter (single letter)
|
|
133
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
134
|
-
// Windows path like D:\path\file.php:24
|
|
135
|
-
// Reconstruct as D:\path\file.php
|
|
136
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
137
|
-
} else {
|
|
138
|
-
// Unix path like /path/file.php:24
|
|
139
|
-
filePath = colonParts[0];
|
|
140
|
-
}
|
|
141
|
-
|
|
142
|
-
// Only consider it valid if the file exists
|
|
143
|
-
if (fs.existsSync(filePath)) {
|
|
144
|
-
return part;
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
}
|
|
148
|
-
// If no valid file path found in parts, return the whole line
|
|
149
|
-
// It will be filtered out later if it's not a valid file path
|
|
150
|
-
return parts.find(p => p.includes(':')) || l;
|
|
151
|
-
})
|
|
152
|
-
.filter(l => {
|
|
153
|
-
// Extract file path from line (accounting for Windows drive letters)
|
|
154
|
-
if (!l) return false;
|
|
155
|
-
const colonParts = l.split(':');
|
|
156
|
-
let filePath;
|
|
157
|
-
|
|
158
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
159
|
-
// Windows path
|
|
160
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
161
|
-
} else {
|
|
162
|
-
// Unix path
|
|
163
|
-
filePath = colonParts[0];
|
|
164
|
-
}
|
|
165
|
-
|
|
166
|
-
return filePath && fs.existsSync(filePath);
|
|
167
|
-
})
|
|
111
|
+
.map(l => l.split(' ').find(p => p.includes(':')) || '')
|
|
112
|
+
.filter(l => isValid(l?.split(':')[0]))
|
|
168
113
|
|
|
169
114
|
// // filter out 3rd party libs
|
|
170
115
|
.filter(l => !l?.includes(`vendor${sep}`))
|
|
171
116
|
.filter(l => !l?.includes(`node_modules${sep}`))
|
|
172
|
-
.filter(l =>
|
|
173
|
-
|
|
174
|
-
const colonParts = l.split(':');
|
|
175
|
-
let filePath;
|
|
176
|
-
|
|
177
|
-
if (colonParts.length >= 2 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
178
|
-
filePath = colonParts[0] + ':' + colonParts[1];
|
|
179
|
-
} else {
|
|
180
|
-
filePath = colonParts[0];
|
|
181
|
-
}
|
|
182
|
-
|
|
183
|
-
return fs.lstatSync(filePath).isFile();
|
|
184
|
-
});
|
|
117
|
+
.filter(l => fs.existsSync(l.split(':')[0]))
|
|
118
|
+
.filter(l => fs.lstatSync(l.split(':')[0]).isFile());
|
|
185
119
|
|
|
186
120
|
if (!stackLines.length) return '';
|
|
187
121
|
|
|
188
|
-
|
|
189
|
-
const firstLine = stackLines[0];
|
|
190
|
-
const colonParts = firstLine.split(':');
|
|
191
|
-
let file, line;
|
|
192
|
-
|
|
193
|
-
if (colonParts.length >= 3 && colonParts[0].length === 1 && /[A-Za-z]/.test(colonParts[0])) {
|
|
194
|
-
// Windows path like D:\path\file.php:24
|
|
195
|
-
file = colonParts[0] + ':' + colonParts[1];
|
|
196
|
-
line = colonParts[2];
|
|
197
|
-
} else {
|
|
198
|
-
// Unix path like /path/file.php:24
|
|
199
|
-
file = colonParts[0];
|
|
200
|
-
line = colonParts[1];
|
|
201
|
-
}
|
|
122
|
+
const [file, line] = stackLines[0].split(':');
|
|
202
123
|
|
|
203
124
|
const prepend = 3;
|
|
204
125
|
const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 });
|
|
@@ -218,8 +139,6 @@ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
|
|
|
218
139
|
export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
|
|
219
140
|
|
|
220
141
|
const fetchIdFromCode = (code, opts = {}) => {
|
|
221
|
-
if (!code) return null;
|
|
222
|
-
|
|
223
142
|
const comments = code
|
|
224
143
|
.split('\n')
|
|
225
144
|
.map(l => l.trim())
|
|
@@ -261,65 +180,8 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
261
180
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
262
181
|
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
263
182
|
} else if (opts.lang === 'csharp') {
|
|
264
|
-
|
|
265
|
-
|
|
266
|
-
|
|
267
|
-
if (methodLineIndex === -1) {
|
|
268
|
-
methodLineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
|
|
269
|
-
}
|
|
270
|
-
|
|
271
|
-
if (methodLineIndex === -1) {
|
|
272
|
-
methodLineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
273
|
-
}
|
|
274
|
-
|
|
275
|
-
// If found, scan upwards to find [TestCase], [Test] attributes and XML comments
|
|
276
|
-
if (methodLineIndex !== -1) {
|
|
277
|
-
lineIndex = methodLineIndex;
|
|
278
|
-
|
|
279
|
-
// Scan upwards to find the start of attributes and comments
|
|
280
|
-
for (let i = methodLineIndex - 1; i >= 0; i--) {
|
|
281
|
-
const trimmedLine = lines[i].trim();
|
|
282
|
-
|
|
283
|
-
// Include [TestCase], [Test], and other attributes
|
|
284
|
-
if (trimmedLine.startsWith('[')) {
|
|
285
|
-
lineIndex = i;
|
|
286
|
-
continue;
|
|
287
|
-
}
|
|
288
|
-
|
|
289
|
-
// Include XML documentation comments
|
|
290
|
-
if (trimmedLine.startsWith('///')) {
|
|
291
|
-
lineIndex = i;
|
|
292
|
-
continue;
|
|
293
|
-
}
|
|
294
|
-
|
|
295
|
-
// Stop at empty lines (with some tolerance)
|
|
296
|
-
if (trimmedLine === '') {
|
|
297
|
-
// Check if next non-empty line is an attribute or comment
|
|
298
|
-
let hasMoreAttributes = false;
|
|
299
|
-
for (let j = i - 1; j >= 0; j--) {
|
|
300
|
-
const nextTrimmed = lines[j].trim();
|
|
301
|
-
if (nextTrimmed === '') continue;
|
|
302
|
-
if (nextTrimmed.startsWith('[') || nextTrimmed.startsWith('///')) {
|
|
303
|
-
hasMoreAttributes = true;
|
|
304
|
-
lineIndex = j;
|
|
305
|
-
}
|
|
306
|
-
break;
|
|
307
|
-
}
|
|
308
|
-
if (!hasMoreAttributes) break;
|
|
309
|
-
continue;
|
|
310
|
-
}
|
|
311
|
-
|
|
312
|
-
// Stop at other method declarations or class-level elements
|
|
313
|
-
if (
|
|
314
|
-
trimmedLine.includes('public ') ||
|
|
315
|
-
trimmedLine.includes('private ') ||
|
|
316
|
-
trimmedLine.includes('protected ') ||
|
|
317
|
-
trimmedLine.includes('internal ')
|
|
318
|
-
) {
|
|
319
|
-
if (!trimmedLine.startsWith('[')) break;
|
|
320
|
-
}
|
|
321
|
-
}
|
|
322
|
-
}
|
|
183
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
|
|
184
|
+
if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
|
|
323
185
|
} else {
|
|
324
186
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
325
187
|
}
|
|
@@ -329,31 +191,11 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
329
191
|
lineIndex -= opts.prepend;
|
|
330
192
|
}
|
|
331
193
|
|
|
332
|
-
if (lineIndex
|
|
194
|
+
if (lineIndex) {
|
|
333
195
|
const result = [];
|
|
334
|
-
let braceDepth = 0; // Track brace depth for C# methods
|
|
335
|
-
let methodStartFound = false; // Flag to indicate we've found the method opening brace
|
|
336
|
-
|
|
337
196
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
338
197
|
if (lines[i] === undefined) continue;
|
|
339
198
|
|
|
340
|
-
// Track brace depth for C# to stop after method closes
|
|
341
|
-
if (opts.lang === 'csharp') {
|
|
342
|
-
const line = lines[i];
|
|
343
|
-
// Count opening and closing braces
|
|
344
|
-
const openBraces = (line.match(/\{/g) || []).length;
|
|
345
|
-
const closeBraces = (line.match(/\}/g) || []).length;
|
|
346
|
-
|
|
347
|
-
if (openBraces > 0) methodStartFound = true;
|
|
348
|
-
braceDepth += openBraces - closeBraces;
|
|
349
|
-
|
|
350
|
-
// If we've started the method and depth returns to 0, method is complete
|
|
351
|
-
if (methodStartFound && braceDepth === 0 && closeBraces > 0) {
|
|
352
|
-
// Don't include the closing brace - just break
|
|
353
|
-
break;
|
|
354
|
-
}
|
|
355
|
-
}
|
|
356
|
-
|
|
357
199
|
if (i > lineIndex + 2 && !opts.prepend) {
|
|
358
200
|
// annotation
|
|
359
201
|
if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
|
|
@@ -374,25 +216,7 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
374
216
|
if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
|
|
375
217
|
if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
|
|
376
218
|
if (opts.lang === 'java' && lines[i].includes(' class ')) break;
|
|
377
|
-
// For C#, additional checks if brace tracking didn't stop us
|
|
378
|
-
if (opts.lang === 'csharp') {
|
|
379
|
-
const trimmed = lines[i].trim();
|
|
380
|
-
// Stop at attribute that marks beginning of next test (but not if we're still in the current method)
|
|
381
|
-
if (trimmed.match(/^\[(Test|TestCase|Theory|Fact)/) && methodStartFound && braceDepth === 0) break;
|
|
382
|
-
// Stop at XML documentation comments that belong to next method
|
|
383
|
-
if (trimmed.startsWith('///') && methodStartFound && braceDepth === 0) break;
|
|
384
|
-
// Stop at another method declaration (but not if we're still in the current method)
|
|
385
|
-
if (
|
|
386
|
-
trimmed.match(/^\s*(public|private|protected|internal)\s+(\w+|async\s+\w+)\s+\w+\s*\(/) &&
|
|
387
|
-
methodStartFound &&
|
|
388
|
-
braceDepth === 0
|
|
389
|
-
)
|
|
390
|
-
break;
|
|
391
|
-
// Stop at class declaration
|
|
392
|
-
if (trimmed.includes(' class ') && trimmed.includes('public')) break;
|
|
393
|
-
}
|
|
394
219
|
}
|
|
395
|
-
|
|
396
220
|
result.push(lines[i]);
|
|
397
221
|
}
|
|
398
222
|
return result.join('\n');
|
|
@@ -605,14 +429,10 @@ function transformEnvVarToBoolean(value) {
|
|
|
605
429
|
}
|
|
606
430
|
|
|
607
431
|
function truncate(s, size = 255) {
|
|
608
|
-
if (s
|
|
609
|
-
return
|
|
610
|
-
}
|
|
611
|
-
const str = s.toString();
|
|
612
|
-
if (str.trim().length < size) {
|
|
613
|
-
return str;
|
|
432
|
+
if (s.toString().trim().length < size) {
|
|
433
|
+
return s.toString();
|
|
614
434
|
}
|
|
615
|
-
return `${
|
|
435
|
+
return `${s.toString().substring(0, size)}...`;
|
|
616
436
|
}
|
|
617
437
|
|
|
618
438
|
export {
|