@testomatio/reporter 2.3.2-beta.3-xml-import → 2.3.3

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/lib/xmlReader.js CHANGED
@@ -131,18 +131,7 @@ class XmlReader {
131
131
  const { result, total, passed, failed, inconclusive, skipped } = jsonSuite;
132
132
  reduceOptions.preferClassname = this.stats.language === 'python';
133
133
  const resultTests = processTestSuite(jsonSuite['test-suite']);
134
- debug('Raw tests extracted from NUnit XML:', resultTests.length);
135
- debug('Raw tests:', resultTests.map(t => ({ title: t.title, example: t.example, file: t.file })));
136
- // Deduplicate tests based on FQN (Assembly + Namespace + Class + Method)
137
- const deduplicatedTests = this.deduplicateTestsByFQN(resultTests);
138
- debug('Tests after deduplication:', deduplicatedTests.length);
139
- debug('Deduplicated tests:', deduplicatedTests.map(t => ({
140
- title: t.title,
141
- examples: t.examples,
142
- example: t.example,
143
- file: t.file,
144
- })));
145
- this.tests = this.tests.concat(deduplicatedTests);
134
+ this.tests = this.tests.concat(resultTests);
146
135
  return {
147
136
  status: result?.toLowerCase(),
148
137
  create_tests: true,
@@ -150,7 +139,7 @@ class XmlReader {
150
139
  passed_count: parseInt(passed, 10),
151
140
  failed_count: parseInt(failed, 10),
152
141
  skipped_count: parseInt(inconclusive + skipped, 10),
153
- tests: deduplicatedTests,
142
+ tests: resultTests,
154
143
  };
155
144
  }
156
145
  processTRX(jsonSuite) {
@@ -286,171 +275,6 @@ class XmlReader {
286
275
  tests,
287
276
  };
288
277
  }
289
- deduplicateTestsByFQN(tests) {
290
- const fqnMap = new Map();
291
- tests.forEach(test => {
292
- const fqn = this.generateNormalizedFQN(test);
293
- if (fqnMap.has(fqn)) {
294
- const existingTest = fqnMap.get(fqn);
295
- // For parameterized tests, merge as Examples
296
- if (test.example && Array.isArray(test.example) && test.example.length > 0) {
297
- // Initialize examples array if it doesn't exist
298
- if (!existingTest.examples) {
299
- existingTest.examples = [];
300
- // Add the existing test's example as the first item if it has parameters
301
- if (existingTest.example && Array.isArray(existingTest.example) && existingTest.example.length > 0) {
302
- existingTest.examples.push({
303
- parameters: existingTest.example,
304
- status: existingTest.status,
305
- run_time: existingTest.run_time,
306
- message: existingTest.message,
307
- stack: existingTest.stack,
308
- });
309
- // Clear the main test's example since it's now in examples array
310
- delete existingTest.example;
311
- }
312
- }
313
- // Add this test's execution as an example
314
- existingTest.examples.push({
315
- parameters: test.example,
316
- status: test.status,
317
- run_time: test.run_time,
318
- message: test.message,
319
- stack: test.stack,
320
- });
321
- // Update the main test status to reflect the worst status
322
- if (test.status === 'failed' || existingTest.status === 'failed') {
323
- existingTest.status = 'failed';
324
- }
325
- else if (test.status === 'skipped' && existingTest.status !== 'failed') {
326
- existingTest.status = 'skipped';
327
- }
328
- // Update total run time
329
- existingTest.run_time = (existingTest.run_time || 0) + (test.run_time || 0);
330
- }
331
- else {
332
- // Merge test properties for non-parameterized tests, prioritizing Test Explorer structure
333
- if (test.test_id && !existingTest.test_id) {
334
- existingTest.test_id = test.test_id;
335
- }
336
- // Keep the most complete test data
337
- if (test.stack && !existingTest.stack) {
338
- existingTest.stack = test.stack;
339
- }
340
- if (test.message && !existingTest.message) {
341
- existingTest.message = test.message;
342
- }
343
- }
344
- // Prefer Test Explorer structure (longer, more complete suite_title)
345
- if (test.suite_title && test.suite_title.length > existingTest.suite_title.length) {
346
- existingTest.suite_title = test.suite_title;
347
- }
348
- // Always use the source file path if available
349
- if (test.file && test.file.endsWith('.cs')) {
350
- existingTest.file = test.file;
351
- }
352
- else if (!existingTest.file || !existingTest.file.endsWith('.cs')) {
353
- existingTest.file = this.extractCsFileFromPath(test);
354
- }
355
- }
356
- else {
357
- // Fix file path to use proper .cs file names from source paths
358
- if (!test.file || !test.file.endsWith('.cs')) {
359
- test.file = this.extractCsFileFromPath(test);
360
- }
361
- fqnMap.set(fqn, test);
362
- }
363
- });
364
- return Array.from(fqnMap.values());
365
- }
366
- generateFQN(test) {
367
- // Generate Fully Qualified Name: Namespace + Class + Method (standard .NET FQN)
368
- // Don't include assembly as it can vary between different test structures
369
- const namespace = this.extractNamespace(test);
370
- const className = this.extractClassName(test);
371
- const methodName = test.title;
372
- // Use the most complete namespace.class structure available
373
- if (test.suite_title && test.suite_title.includes('.')) {
374
- return `${test.suite_title}.${methodName}`;
375
- }
376
- return `${namespace}.${className}.${methodName}`;
377
- }
378
- generateNormalizedFQN(test) {
379
- // Generate normalized FQN for deduplication by extracting the core namespace.class.method
380
- // For parameterized tests, we want the SAME FQN so they merge into one test with multiple Examples
381
- const fullClassName = test.suite_title || '';
382
- const methodName = test.title;
383
- // Extract the most specific namespace.class pattern
384
- if (fullClassName.includes('.')) {
385
- const parts = fullClassName.split('.');
386
- if (parts.length >= 2) {
387
- const className = parts[parts.length - 1];
388
- // Look for common .NET namespace patterns and normalize them:
389
- // TestProject.Tests.MyClass -> Tests.MyClass
390
- // Tests.MyClass -> Tests.MyClass
391
- // MyProject.SubNamespace.Tests.MyClass -> Tests.MyClass
392
- let normalizedNamespace = '';
393
- for (let i = parts.length - 2; i >= 0; i--) {
394
- const part = parts[i];
395
- // Build namespace from right to left, excluding project names
396
- if (part === 'Tests' || part.endsWith('Tests') || part.includes('Test')) {
397
- // Found a test namespace, use it as the normalized namespace
398
- normalizedNamespace = part;
399
- break;
400
- }
401
- else if (i === parts.length - 2) {
402
- // If no test namespace found, use the immediate parent as namespace
403
- normalizedNamespace = part;
404
- }
405
- }
406
- return `${normalizedNamespace}.${className}.${methodName}`;
407
- }
408
- }
409
- // Fallback for simple class names
410
- return `${fullClassName}.${methodName}`;
411
- }
412
- extractAssemblyName(test) {
413
- // Extract assembly name from file path or use default
414
- if (test.file) {
415
- const parts = test.file.split(/[/\\]/);
416
- return parts[0] || 'DefaultAssembly';
417
- }
418
- return 'DefaultAssembly';
419
- }
420
- extractNamespace(test) {
421
- // Extract namespace from suite_title or classname
422
- if (test.suite_title && test.suite_title.includes('.')) {
423
- const parts = test.suite_title.split('.');
424
- return parts.slice(0, -1).join('.');
425
- }
426
- return test.suite_title || 'DefaultNamespace';
427
- }
428
- extractClassName(test) {
429
- // Extract class name from suite_title
430
- if (test.suite_title && test.suite_title.includes('.')) {
431
- const parts = test.suite_title.split('.');
432
- return parts[parts.length - 1];
433
- }
434
- return test.suite_title || 'DefaultClass';
435
- }
436
- extractCsFileFromPath(test) {
437
- // Extract .cs file name from source file path, not namespace
438
- if (test.file) {
439
- // Look for actual .cs file path patterns
440
- const csFileMatch = test.file.match(/([^/\\]+\.cs)$/);
441
- if (csFileMatch) {
442
- return test.file;
443
- }
444
- // If no .cs extension, assume it's a namespace path and convert to likely file name
445
- const className = this.extractClassName(test);
446
- const pathParts = test.file.split(/[/\\]/);
447
- pathParts[pathParts.length - 1] = `${className}.cs`;
448
- return pathParts.join('/');
449
- }
450
- // Fallback to class name
451
- const className = this.extractClassName(test);
452
- return `${className}.cs`;
453
- }
454
278
  calculateStats() {
455
279
  this.stats = {
456
280
  ...this.stats,
@@ -496,13 +320,11 @@ class XmlReader {
496
320
  this.stats.language = 'csharp';
497
321
  }
498
322
  if (!fs_1.default.existsSync(file)) {
499
- debug('Failed to open file with the source code: %s', file);
323
+ debug('Failed to open file with the source code', file);
500
324
  return;
501
325
  }
502
326
  const contents = fs_1.default.readFileSync(file).toString();
503
- // Use original test name for source code lookup, not humanized title
504
- const originalTitle = t.originalTestName ? t.originalTestName.replace(/\(.*?\)/, '').trim() : t.title;
505
- t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, title: originalTitle, lang: this.stats.language });
327
+ t.code = (0, utils_js_1.fetchSourceCode)(contents, { ...t, lang: this.stats.language });
506
328
  if (t.code)
507
329
  debug('Fetched code for test %s', t.title);
508
330
  t.test_id = (0, utils_js_1.fetchIdFromCode)(t.code, { lang: this.stats.language });
@@ -608,8 +430,7 @@ function reduceTestCases(prev, item) {
608
430
  testCases
609
431
  .filter(t => !!t)
610
432
  .forEach(testCaseItem => {
611
- // Use consistent Test Explorer structure: prioritize fullname for file path
612
- const file = extractSourceFilePath(testCaseItem, item);
433
+ const file = testCaseItem.file || item.filepath || item.fullname || item.package || '';
613
434
  let stack = '';
614
435
  let message = '';
615
436
  if (testCaseItem.error)
@@ -629,33 +450,17 @@ function reduceTestCases(prev, item) {
629
450
  if (!message)
630
451
  message = stack.trim().split('\n')[0];
631
452
  const isParametrized = item.type === 'ParameterizedMethod';
453
+ const preferClassname = reduceOptions.preferClassname || isParametrized;
632
454
  // SpecFlow config
633
455
  let { title, tags, testId } = fetchProperties(isParametrized ? item : testCaseItem);
634
456
  let example = null;
635
- // Use consistent Test Explorer structure for suite title
636
- const suiteTitle = extractTestExplorerSuiteTitle(testCaseItem, item);
457
+ const suiteTitle = preferClassname ? testCaseItem.classname : item.name || testCaseItem.classname;
637
458
  title ||= testCaseItem.name || testCaseItem.methodname || testCaseItem.classname;
638
459
  tags ||= [];
639
- // Store original test name for parameter extraction
640
- const originalTestName = testCaseItem.name || testCaseItem.methodname;
641
- // Handle NUnit-style arguments from <arguments> element
642
- if (testCaseItem.arguments && testCaseItem.arguments.arg) {
643
- const args = Array.isArray(testCaseItem.arguments.arg)
644
- ? testCaseItem.arguments.arg
645
- : [testCaseItem.arguments.arg];
646
- example = args; // Store as array instead of object
647
- // Remove parameters from title for NUnit tests
648
- title = (testCaseItem.methodname || title).replace(/\(.*?\)/, '').trim();
649
- }
650
- else {
651
- // Fallback to parsing parameters from test name (SpecFlow, etc.)
652
- const exampleMatches = originalTestName?.match(/\((.*?)\)$/);
653
- if (exampleMatches) {
654
- // Extract and store parameters as Examples
655
- const parameterValues = exampleMatches[1].split(',').map(v => v.trim().replace(/['"]/g, ''));
656
- example = parameterValues;
657
- title = title.replace(/\(.*?\)/, '').trim();
658
- }
460
+ const exampleMatches = testCaseItem.name?.match(/\S\((.*?)\)/);
461
+ if (exampleMatches) {
462
+ example = { ...exampleMatches[1].split(',').map(v => v.trim().replace(/[^\w\s-]/g, '')) };
463
+ title = title.replace(/\(.*?\)/, '').trim();
659
464
  }
660
465
  stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim();
661
466
  if (!testId)
@@ -703,7 +508,6 @@ function reduceTestCases(prev, item) {
703
508
  run_time: parseFloat(testCaseItem.time || testCaseItem.duration) * 1000,
704
509
  status,
705
510
  title,
706
- originalTestName, // Store original name for parameter-aware FQN generation
707
511
  root_suite_id: TESTOMATIO_SUITE,
708
512
  suite_title: suiteTitle,
709
513
  files,
@@ -712,102 +516,6 @@ function reduceTestCases(prev, item) {
712
516
  });
713
517
  return prev;
714
518
  }
715
- function extractSourceFilePath(testCaseItem, item) {
716
- // Priority order for file path extraction to match Test Explorer structure:
717
- // 1. filepath attribute (direct .cs file path from NUnit)
718
- // 2. fullname (contains full project path)
719
- // 3. file attribute from test case
720
- // 4. package (fallback)
721
- // NUnit provides filepath attribute with actual .cs file path - use this first
722
- if (item.filepath) {
723
- // Clean up Windows/Unix path separators and ensure proper format
724
- let filePath = item.filepath.replace(/\\/g, '/');
725
- // Make relative to current working directory if absolute
726
- if (path_1.default.isAbsolute(item.filepath)) {
727
- const cwd = process.cwd().replace(/\\/g, '/');
728
- if (filePath.startsWith(cwd)) {
729
- filePath = path_1.default.relative(cwd, item.filepath).replace(/\\/g, '/');
730
- }
731
- else {
732
- // Try to extract relative path from common patterns
733
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
734
- for (const pattern of commonPatterns) {
735
- const index = filePath.lastIndexOf(pattern);
736
- if (index !== -1) {
737
- filePath = filePath.substring(index + 1);
738
- break;
739
- }
740
- }
741
- }
742
- }
743
- return filePath;
744
- }
745
- if (testCaseItem.file) {
746
- let filePath = testCaseItem.file.replace(/\\/g, '/');
747
- // Make relative to current working directory if absolute
748
- if (path_1.default.isAbsolute(testCaseItem.file)) {
749
- const cwd = process.cwd().replace(/\\/g, '/');
750
- if (filePath.startsWith(cwd)) {
751
- filePath = path_1.default.relative(cwd, testCaseItem.file).replace(/\\/g, '/');
752
- }
753
- else {
754
- // Try to extract relative path from common patterns
755
- const commonPatterns = ['/Tests/', '/test/', '/src/', '/Test/'];
756
- for (const pattern of commonPatterns) {
757
- const index = filePath.lastIndexOf(pattern);
758
- if (index !== -1) {
759
- filePath = filePath.substring(index + 1);
760
- break;
761
- }
762
- }
763
- }
764
- }
765
- return filePath;
766
- }
767
- if (item.fullname) {
768
- // Extract actual file path from fullname if it contains path separators
769
- const fullnameParts = item.fullname.split('.');
770
- if (fullnameParts.length > 2) {
771
- // For ParameterizedMethod, get the class name (not method name)
772
- // Example: "NUnit_sample_test.Tests.SampleTests.TestBooleanValue" -> "Tests/SampleTests.cs"
773
- let namespaceParts, className;
774
- if (item.type === 'ParameterizedMethod') {
775
- // For parameterized methods, the last part is the method name, second-to-last is class
776
- namespaceParts = fullnameParts.slice(1, -2); // Skip project name and method name
777
- className = fullnameParts[fullnameParts.length - 2]; // Get class name
778
- }
779
- else {
780
- // For regular classes/fixtures
781
- namespaceParts = fullnameParts.slice(1, -1); // Skip project name
782
- className = fullnameParts[fullnameParts.length - 1];
783
- }
784
- return `${namespaceParts.join('/')}/${className}.cs`;
785
- }
786
- }
787
- if (item.package)
788
- return item.package.replace(/\\/g, '/');
789
- // Fallback: construct from classname
790
- if (testCaseItem.classname) {
791
- const parts = testCaseItem.classname.split('.');
792
- const className = parts[parts.length - 1];
793
- const namespacePath = parts.slice(0, -1).join('/');
794
- return `${namespacePath}/${className}.cs`;
795
- }
796
- return '';
797
- }
798
- function extractTestExplorerSuiteTitle(testCaseItem, item) {
799
- // Extract suite title to match Test Explorer structure (Project/Namespace hierarchy)
800
- // Priority: fullname > classname > name
801
- if (item.fullname) {
802
- // Use fullname to maintain Test Explorer structure
803
- return item.fullname;
804
- }
805
- if (testCaseItem.classname) {
806
- return testCaseItem.classname;
807
- }
808
- // Fallback to item name but prefer classname structure
809
- return item.name || testCaseItem.classname || 'UnknownClass';
810
- }
811
519
  function processTestSuite(testsuite) {
812
520
  if (!testsuite)
813
521
  return [];
@@ -819,20 +527,8 @@ function processTestSuite(testsuite) {
819
527
  if (!Array.isArray(testsuite)) {
820
528
  suites = [testsuite];
821
529
  }
822
- let allResults = [];
823
- for (const suite of suites) {
824
- // Process child test suites recursively (TestFixture, ParameterizedMethod, etc.)
825
- if (suite['test-suite']) {
826
- const childResults = processTestSuite(suite['test-suite']);
827
- allResults = allResults.concat(childResults);
828
- }
829
- // Process direct test cases in this suite
830
- if (suite['test-case'] || suite.testcase) {
831
- const leafResults = reduceTestCases([], suite);
832
- allResults = allResults.concat(leafResults);
833
- }
834
- }
835
- return allResults;
530
+ const subSuites = suites.filter(s => s['test-suite'] && !testsuite['test-case']);
531
+ return [...subSuites.map(s => processTestSuite(s['test-suite'])), ...suites.reduce(reduceTestCases, [])].flat();
836
532
  }
837
533
  function fetchProperties(item) {
838
534
  const tags = [];
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "2.3.2-beta.3-xml-import",
3
+ "version": "2.3.3",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "engines": {
6
6
  "node": ">=18"
@@ -54,7 +54,7 @@
54
54
  "lint": "eslint src",
55
55
  "lint:fix": "eslint src --fix",
56
56
  "format": "npm run lint:fix && npm run pretty:fix",
57
- "test": "mocha tests/unit/**/*_test.js",
57
+ "test": "mocha 'tests/unit/**/*_test.js'",
58
58
  "test:playwright": "mocha tests/adapter/playwright.test.js",
59
59
  "test:codecept": "mocha tests/adapter/codecept.test.js tests/adapter/codecept_comprehensive.test.js tests/adapter/codecept_steps_sections.test.js",
60
60
  "test:frameworks": "npm run test:playwright && npm run test:codecept",
@@ -283,3 +283,4 @@ function getTestContextName(test) {
283
283
  }
284
284
 
285
285
  export default PlaywrightReporter;
286
+ export { extractTags };
package/src/bin/cli.js CHANGED
@@ -65,7 +65,6 @@ program
65
65
 
66
66
  // @ts-ignore
67
67
  client.updateRunStatus(STATUS.FINISHED).then(() => {
68
- console.log(pc.yellow(`Run ${process.env.TESTOMATIO_RUN} was finished`));
69
68
  process.exit(0);
70
69
  });
71
70
  });
@@ -3,45 +3,18 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- // Don't override example if it already exists from NUnit XML processing
7
- // The xmlReader.js already extracts parameters correctly from <arguments>
8
- if (!t.example) {
9
- const title = t.title.replace(/\(.*?\)/, '').trim();
10
- const exampleMatch = t.title.match(/\((.*?)\)/);
11
- if (exampleMatch) {
12
- // Keep as array for consistency with NUnit XML processing
13
- t.example = exampleMatch[1].split(',').map(param => param.trim());
14
- }
15
- t.title = title.trim();
16
- }
17
-
6
+ const title = t.title.replace(/\(.*?\)/, '').trim();
7
+ const example = t.title.match(/\((.*?)\)/);
8
+ if (example) t.example = { ...example[1].split(',') };
18
9
  const suite = t.suite_title.split('.');
19
10
  t.suite_title = suite.pop();
20
11
  t.file = namespaceToFileName(t.file);
12
+ t.title = title.trim();
21
13
  return t;
22
14
  }
23
15
 
24
16
  getFilePath(t) {
25
- if (!t.file) return null;
26
-
27
- // Normalize path separators for cross-platform compatibility
28
- let filePath = t.file.replace(/\\/g, '/');
29
-
30
- // If file already has .cs extension, use it directly
31
- if (filePath.endsWith('.cs')) {
32
- // Make relative path if it's absolute
33
- if (path.isAbsolute(filePath)) {
34
- // Try to find project-relative path
35
- const cwd = process.cwd().replace(/\\/g, '/');
36
- if (filePath.startsWith(cwd)) {
37
- filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
38
- }
39
- }
40
- return filePath;
41
- }
42
-
43
- // Convert namespace path to file path
44
- const fileName = namespaceToFileName(filePath);
17
+ const fileName = namespaceToFileName(t.file);
45
18
  return fileName;
46
19
  }
47
20
  }
@@ -49,14 +22,7 @@ class CSharpAdapter extends Adapter {
49
22
  export default CSharpAdapter;
50
23
 
51
24
  function namespaceToFileName(fileName) {
52
- if (!fileName) return '';
53
-
54
- // If already a .cs file path, clean it up
55
- if (fileName.endsWith('.cs')) {
56
- return fileName.replace(/\\/g, '/');
57
- }
58
-
59
25
  const fileParts = fileName.split('.');
60
26
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
61
- return `${fileParts.join('/')}.cs`;
27
+ return `${fileParts.join(path.sep)}.cs`;
62
28
  }
@@ -3,7 +3,7 @@ import pc from 'picocolors';
3
3
  import { Gaxios } from 'gaxios';
4
4
  import JsonCycle from 'json-cycle';
5
5
  import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
6
- import { isValidUrl, foundedTestLog, readLatestRunId } from '../utils/utils.js';
6
+ import { isValidUrl, foundedTestLog, readLatestRunId, transformEnvVarToBoolean } from '../utils/utils.js';
7
7
  import { parseFilterParams, generateFilterRequestParams, setS3Credentials } from '../utils/pipe_utils.js';
8
8
  import { config } from '../config.js';
9
9
 
@@ -79,7 +79,7 @@ class TestomatioPipe {
79
79
 
80
80
  this.isEnabled = true;
81
81
  // do not finish this run (for parallel testing)
82
- this.proceed = process.env.TESTOMATIO_PROCEED;
82
+ this.proceed = transformEnvVarToBoolean(process.env.TESTOMATIO_PROCEED);
83
83
  this.jiraId = process.env.TESTOMATIO_JIRA_ID;
84
84
  this.runId = params.runId || process.env.TESTOMATIO_RUN;
85
85
  this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
@@ -438,6 +438,9 @@ class TestomatioPipe {
438
438
  tests: params.tests,
439
439
  }
440
440
  });
441
+
442
+ console.log(APP_PREFIX, '✅ Testrun finished');
443
+
441
444
  if (this.runUrl) {
442
445
  console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
443
446
  }
@@ -139,8 +139,6 @@ export const TEST_ID_REGEX = /@T([\w\d]{8})/;
139
139
  export const SUITE_ID_REGEX = /@S([\w\d]{8})/;
140
140
 
141
141
  const fetchIdFromCode = (code, opts = {}) => {
142
- if (!code) return null;
143
-
144
142
  const comments = code
145
143
  .split('\n')
146
144
  .map(l => l.trim())
@@ -182,32 +180,8 @@ const fetchSourceCode = (contents, opts = {}) => {
182
180
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
183
181
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
184
182
  } else if (opts.lang === 'csharp') {
185
- // Enhanced C# method detection for NUnit tests
186
- lineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));
187
-
188
- if (lineIndex === -1) {
189
- lineIndex = lines.findIndex(l => l.includes(`public async Task ${title}(`));
190
- }
191
-
192
- if (lineIndex === -1) {
193
- lineIndex = lines.findIndex(l => l.includes(`${title}(`));
194
- }
195
-
196
- // Look for TestCase or Test attributes above the method
197
- if (lineIndex === -1) {
198
- const testAttributeIndex = lines.findIndex((l, index) => {
199
- if (l.includes('[TestCase') || l.includes('[Test')) {
200
- // Check next few lines for the method
201
- const nextLines = lines.slice(index, Math.min(lines.length, index + 5));
202
- const hasMethod = nextLines.some(nextLine => nextLine.includes(`${title}(`));
203
- return hasMethod;
204
- }
205
- return false;
206
- });
207
- if (testAttributeIndex !== -1) {
208
- lineIndex = testAttributeIndex;
209
- }
210
- }
183
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
184
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
211
185
  } else {
212
186
  lineIndex = lines.findIndex(l => l.includes(title));
213
187
  }
@@ -217,7 +191,7 @@ const fetchSourceCode = (contents, opts = {}) => {
217
191
  lineIndex -= opts.prepend;
218
192
  }
219
193
 
220
- if (lineIndex !== -1 && lineIndex !== undefined) {
194
+ if (lineIndex) {
221
195
  const result = [];
222
196
  for (let i = lineIndex; i < lineIndex + limit; i++) {
223
197
  if (lines[i] === undefined) continue;
@@ -242,10 +216,6 @@ const fetchSourceCode = (contents, opts = {}) => {
242
216
  if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
243
217
  if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
244
218
  if (opts.lang === 'java' && lines[i].includes(' class ')) break;
245
- if (opts.lang === 'csharp' && lines[i].trim().match(/^\[Test/)) break;
246
- if (opts.lang === 'csharp' && lines[i].includes(' public void ')) break;
247
- if (opts.lang === 'csharp' && lines[i].includes(' public async Task ')) break;
248
- if (opts.lang === 'csharp' && lines[i].includes(' class ') && lines[i].includes('public')) break;
249
219
  }
250
220
  result.push(lines[i]);
251
221
  }
@@ -380,7 +350,12 @@ const testRunnerHelper = {
380
350
  function storeRunId(runId) {
381
351
  if (!runId || runId === 'undefined') return;
382
352
  const filePath = path.join(os.tmpdir(), `testomatio.latest.run`);
383
- fs.writeFileSync(filePath, runId);
353
+ try {
354
+ fs.writeFileSync(filePath, runId);
355
+ } catch (e) {
356
+ if (e.code === 'ENOENT') return null;
357
+ debug('Could not store latest run ID file: ', e.message);
358
+ }
384
359
  }
385
360
 
386
361
  /**
@@ -399,7 +374,6 @@ function readLatestRunId() {
399
374
 
400
375
  return fs.readFileSync(filePath)?.toString()?.trim() ?? null;
401
376
  } catch (e) {
402
- console.warn('Could not read latest run ID from file: ', e);
403
377
  return null;
404
378
  }
405
379
  }
@@ -413,6 +387,7 @@ function cleanLatestRunId() {
413
387
  }
414
388
  debug(`Cleaned latest run ID (${runId}) file`, filePath);
415
389
  } catch (e) {
390
+ if (e.code === 'ENOENT') return null;
416
391
  console.warn('Could not clean latest run ID file: ', e);
417
392
  }
418
393
  }
@@ -441,6 +416,18 @@ export function getPackageVersion() {
441
416
  return packageJson.version;
442
417
  }
443
418
 
419
+ function transformEnvVarToBoolean(value) {
420
+ if (value === undefined || value === null || value === 'undefined') return false;
421
+ if (typeof value === 'boolean') return value;
422
+ if (typeof value !== 'string') value = String(value);
423
+ value = value.trim();
424
+
425
+ if (['1', 'true', 'yes', 'on'].includes(value.toLowerCase())) return true;
426
+ if (['0', 'false', 'no', 'off'].includes(value.toLowerCase())) return false;
427
+ // if not recognized, return truthy if any value is set
428
+ return Boolean(value);
429
+ }
430
+
444
431
  export {
445
432
  ansiRegExp,
446
433
  cleanLatestRunId,
@@ -463,5 +450,6 @@ export {
463
450
  specificTestInfo,
464
451
  storeRunId,
465
452
  testRunnerHelper,
453
+ transformEnvVarToBoolean,
466
454
  validateSuiteId,
467
455
  };