@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/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
- .option('--kind <type>', 'Specify run type: automated, manual, or mixed')
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
- const createRunParams = {};
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(createRunParams).then(runTests);
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') && !pattern.includes('*')) {
161
+ if (!pattern.endsWith('.xml')) {
174
162
  pattern += '.xml';
175
163
  }
176
164
  let { javaTests, lang } = opts;
@@ -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') && !pattern.includes('*')) {
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(params))))
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
- let fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
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: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
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: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
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
- // Extract example from title if not already present
7
- if (!t.example) {
8
- const exampleMatch = t.title.match(/\((.*?)\)/);
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
- if (!t.file) return null;
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('/')}.cs`;
27
+ return `${fileParts.join(path.sep)}.cs`;
67
28
  }
@@ -163,7 +163,7 @@ class TestomatioPipe {
163
163
 
164
164
  /**
165
165
  * Creates a new run on Testomat.io
166
- * @param {{isBatchEnabled?: boolean, kind?: string}} params
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 Client from './client.js';
2
- import * as TestomatioConstants from './constants.js';
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
- TestomatioClient: Client,
41
- STATUS,
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);
@@ -76,29 +76,21 @@ const isValidUrl = s => {
76
76
  }
77
77
  };
78
78
 
79
- const fileMatchRegex = /file:(\/*)([A-Za-z]:[\\/].*?|\/.*?)\.(png|avi|webm|jpg|html|txt)/gi;
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
- let files = Array.from(stack.matchAll(fileMatchRegex))
83
- .map(match => {
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
- // Normalize path separators for cross-platform compatibility
94
- return f.replace(/\\/g, '/');
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
- // Remove 'at ' prefix if present
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
- // Extract file path for final check (accounting for Windows drive letters)
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
- // Extract file and line number (accounting for Windows drive letters)
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
- // Find the method declaration line
265
- let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
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 !== -1 && lineIndex !== undefined) {
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 === undefined || s === null) {
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 `${str.substring(0, size)}...`;
435
+ return `${s.toString().substring(0, size)}...`;
616
436
  }
617
437
 
618
438
  export {