@testomatio/reporter 2.5.1 → 2.6.0-beta.1.allure

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,523 @@
1
+ import createDebugMessages from 'debug';
2
+ import path from 'path';
3
+ import pc from 'picocolors';
4
+ import fs from 'fs';
5
+ import { glob } from 'glob';
6
+ import { APP_PREFIX, STATUS } from './constants.js';
7
+ import { randomUUID } from 'crypto';
8
+ import { fileURLToPath } from 'url';
9
+ import { config } from './config.js';
10
+ import { S3Uploader } from './uploader.js';
11
+ import { pipesFactory } from './pipe/index.js';
12
+ import {
13
+ fetchSourceCode,
14
+ fetchIdFromCode,
15
+ } from './utils/utils.js';
16
+ import adapterFactory from './junit-adapter/index.js';
17
+
18
+ // @ts-ignore
19
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
20
+
21
+ const debug = createDebugMessages('@testomatio/reporter:allure');
22
+
23
+ const TESTOMATIO_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
24
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_SUITE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
25
+
26
+ class AllureReader {
27
+ constructor(opts = {}) {
28
+ this.requestParams = {
29
+ apiKey: opts.apiKey || config.TESTOMATIO,
30
+ url: opts.url || TESTOMATIO_URL,
31
+ title: TESTOMATIO_TITLE,
32
+ env: TESTOMATIO_ENV,
33
+ group_title: TESTOMATIO_RUNGROUP_TITLE,
34
+ isBatchEnabled: true,
35
+ };
36
+ this.runId = opts.runId || TESTOMATIO_RUN;
37
+ this.opts = opts || {};
38
+ this.withPackage = opts.withPackage || false;
39
+ this.store = {};
40
+ this.pipesPromise = pipesFactory(opts, this.store);
41
+ this._tests = [];
42
+ this.stats = {};
43
+ this.suites = {};
44
+ this.uploader = new S3Uploader();
45
+
46
+ const packageJsonPath = path.resolve(__dirname, '..', 'package.json');
47
+ this.version = JSON.parse(fs.readFileSync(packageJsonPath).toString()).version;
48
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
49
+ }
50
+
51
+ get tests() {
52
+ return this._tests;
53
+ }
54
+
55
+ set tests(value) {
56
+ this._tests = value;
57
+ }
58
+
59
+ async createRun() {
60
+ const runParams = {
61
+ api_key: this.requestParams.apiKey,
62
+ title: this.requestParams.title,
63
+ env: this.requestParams.env,
64
+ group_title: this.requestParams.group_title,
65
+ isBatchEnabled: this.requestParams.isBatchEnabled,
66
+ };
67
+
68
+ debug('Run', runParams);
69
+ this.pipes = this.pipes || (await this.pipesPromise);
70
+
71
+ const run = await Promise.all(this.pipes.map(p => p.createRun(runParams)));
72
+ this.uploader.checkEnabled();
73
+ return run;
74
+ }
75
+
76
+ parse(resultsPattern) {
77
+ this._tests = [];
78
+ let pattern = resultsPattern;
79
+
80
+ // Auto-append wildcard if pattern refers to a directory (like XML command does)
81
+ if (!pattern.endsWith('.json') && !pattern.includes('*')) {
82
+ if (pattern.endsWith('/') || (fs.existsSync(pattern) && fs.statSync(pattern).isDirectory())) {
83
+ pattern = pattern.replace(/\/+$/, '') + '/*-result.json';
84
+ } else {
85
+ pattern += '*-result.json';
86
+ }
87
+ }
88
+
89
+ const resultsDir = path.dirname(pattern);
90
+
91
+ console.log(APP_PREFIX, `Scanning for Allure results in: ${resultsDir}`);
92
+ console.log(APP_PREFIX, `Using pattern: ${pattern}`);
93
+
94
+ const resultFiles = glob.sync(pattern);
95
+ const containerFiles = glob.sync(pattern.replace('*-result.json', '*-container.json'));
96
+
97
+ if (resultFiles.length === 0 && containerFiles.length === 0) {
98
+ throw new Error(`No Allure result files found matching pattern: ${pattern}`);
99
+ }
100
+
101
+ console.log(APP_PREFIX, `Found ${resultFiles.length} result files and ${containerFiles.length} container files`);
102
+
103
+ this.parseContainerFiles(containerFiles);
104
+
105
+ // Store all tests temporarily for deduplication by historyId
106
+ const allTests = [];
107
+ for (const file of resultFiles) {
108
+ const fullPath = file;
109
+ const fileDir = path.dirname(file);
110
+ try {
111
+ const resultData = JSON.parse(fs.readFileSync(fullPath, 'utf8'));
112
+ const test = this.processAllureResult(resultData, fileDir);
113
+ if (test) {
114
+ test._historyId = resultData.historyId;
115
+ test._stop = resultData.stop || 0;
116
+ allTests.push(test);
117
+ }
118
+ } catch (err) {
119
+ console.warn(APP_PREFIX, `Failed to parse ${file}:`, err.message);
120
+ debug('Parse error:', err);
121
+ }
122
+ }
123
+
124
+ const attemptsMap = new Map();
125
+ for (const test of allTests) {
126
+ const historyId = test._historyId || test.rid;
127
+ if (!attemptsMap.has(historyId)) {
128
+ attemptsMap.set(historyId, []);
129
+ }
130
+ attemptsMap.get(historyId).push(test);
131
+ }
132
+
133
+ const uniqueTestsMap = new Map();
134
+ for (const [historyId, attempts] of attemptsMap) {
135
+ uniqueTestsMap.set(historyId, this.combineRetryAttempts(attempts));
136
+ }
137
+
138
+ // Convert map to array and clean up internal fields
139
+ this._tests = Array.from(uniqueTestsMap.values()).map(t => {
140
+ delete t._historyId;
141
+ delete t._stop;
142
+ return t;
143
+ });
144
+
145
+ console.log(APP_PREFIX, `Processed ${this._tests.length} unique tests (from ${allTests.length} result files)`);
146
+
147
+ return this.calculateStats();
148
+ }
149
+
150
+ parseContainerFiles(containerFiles) {
151
+ for (const file of containerFiles) {
152
+ try {
153
+ const data = JSON.parse(fs.readFileSync(file, 'utf8'));
154
+ if (data.name && data.children) {
155
+ data.children.forEach(uuid => {
156
+ this.suites[uuid] = data.name;
157
+ });
158
+ }
159
+ } catch (err) {
160
+ debug('Failed to parse container file:', file, err.message);
161
+ }
162
+ }
163
+ debug('Parsed suites:', this.suites);
164
+ }
165
+
166
+ processAllureResult(result, resultsDir) {
167
+ const test = {
168
+ rid: result.uuid || randomUUID(),
169
+ title: result.name || 'Unknown test',
170
+ status: this.mapStatus(result.status),
171
+ suite_title: this.extractSuiteTitle(result),
172
+ file: this.extractFile(result),
173
+ run_time: this.calculateRunTime(result),
174
+ steps: this.convertSteps(result.steps || []),
175
+ message: result.statusDetails?.message || '',
176
+ stack: result.statusDetails?.trace || '',
177
+ meta: this.extractMeta(result),
178
+ links: this.extractLinks(result),
179
+ artifacts: [],
180
+ create: true,
181
+ overwrite: true,
182
+ };
183
+
184
+ // Add description if present
185
+ if (result.description) {
186
+ test.description = result.description;
187
+ }
188
+
189
+ if (result.parameters && result.parameters.length > 0) {
190
+ test.example = this.convertParameters(result.parameters);
191
+ }
192
+
193
+ if (result.attachments && result.attachments.length > 0) {
194
+ const attachments = result.attachments
195
+ .map(att => {
196
+ const fullPath = path.join(resultsDir, att.source);
197
+ if (fs.existsSync(fullPath)) {
198
+ return fullPath;
199
+ }
200
+ debug('Attachment file not found:', fullPath);
201
+ return null;
202
+ })
203
+ .filter(Boolean);
204
+
205
+ if (attachments.length > 0) {
206
+ test.files = attachments;
207
+ }
208
+ }
209
+
210
+ return test;
211
+ }
212
+
213
+ mapStatus(status) {
214
+ const statusMap = {
215
+ passed: 'passed',
216
+ failed: 'failed',
217
+ broken: 'failed',
218
+ skipped: 'skipped',
219
+ pending: 'skipped',
220
+ };
221
+ return statusMap[status] || 'failed';
222
+ }
223
+
224
+ extractSuiteTitle(result) {
225
+ const labels = result.labels || [];
226
+
227
+ // Only use suite label for suite_title
228
+ // Epic and Feature are sent as separate labels in meta
229
+ const suiteLabel = labels.find(l => l.name === 'suite')?.value;
230
+
231
+ if (suiteLabel) {
232
+ return this.stripNamespace(suiteLabel);
233
+ }
234
+
235
+ // Fallback to parentSuite or subSuite if no suite label
236
+ const parentSuite = labels.find(l => l.name === 'parentSuite')?.value;
237
+ const subSuite = labels.find(l => l.name === 'subSuite')?.value;
238
+
239
+ if (parentSuite && subSuite) {
240
+ return `${parentSuite} / ${subSuite}`;
241
+ }
242
+ if (parentSuite) return parentSuite;
243
+ if (subSuite) return subSuite;
244
+
245
+ return 'Default Suite';
246
+ }
247
+
248
+ stripNamespace(suiteName) {
249
+ if (suiteName && suiteName.includes('.')) {
250
+ return suiteName.split('.').pop();
251
+ }
252
+ return suiteName;
253
+ }
254
+
255
+ extractFile(result) {
256
+ const labels = result.labels || [];
257
+ const packageLabel = labels.find(l => l.name === 'package')?.value;
258
+ const testClassLabel = labels.find(l => l.name === 'testClass')?.value;
259
+
260
+ if (!packageLabel) {
261
+ return null;
262
+ }
263
+
264
+ const ext = this.getFileExtension(result);
265
+ let className;
266
+
267
+ if (testClassLabel) {
268
+ className = testClassLabel.split('.').pop();
269
+ } else if (result.fullName) {
270
+ const fullNameParts = result.fullName.split('.');
271
+ className = fullNameParts[fullNameParts.length - 2] || fullNameParts[fullNameParts.length - 1];
272
+ } else {
273
+ return null;
274
+ }
275
+
276
+ if (this.withPackage) {
277
+ const parts = packageLabel.split('.');
278
+ return `${parts.join('/')}/${className}.${ext}`;
279
+ }
280
+
281
+ return `${className}.${ext}`;
282
+ }
283
+
284
+ getFileExtension(result) {
285
+ const labels = result.labels || [];
286
+ const languageLabel = labels.find(l => l.name === 'language')?.value;
287
+
288
+ const extMap = {
289
+ java: 'java',
290
+ kotlin: 'kt',
291
+ javascript: 'js',
292
+ typescript: 'ts',
293
+ python: 'py',
294
+ ruby: 'rb',
295
+ 'c#': 'cs',
296
+ php: 'php',
297
+ };
298
+
299
+ return extMap[languageLabel?.toLowerCase()] || 'java';
300
+ }
301
+
302
+ extractMeta(result) {
303
+ const labels = result.labels || [];
304
+ const excludedLabels = [
305
+ 'suite', 'package', 'parentSuite', 'subSuite',
306
+ 'testClass', 'testMethod', 'epic', 'feature',
307
+ ];
308
+
309
+ const meta = {};
310
+ labels.forEach(label => {
311
+ if (!excludedLabels.includes(label.name)) {
312
+ meta[label.name] = label.value;
313
+ }
314
+ });
315
+
316
+ return meta;
317
+ }
318
+
319
+ extractLinks(result) {
320
+ const labels = result.labels || [];
321
+ const links = [];
322
+
323
+ const epicLabel = labels.find(l => l.name === 'epic');
324
+ const featureLabel = labels.find(l => l.name === 'feature');
325
+
326
+ if (epicLabel?.value) {
327
+ links.push({ label: `epic:${epicLabel.value}` });
328
+ }
329
+
330
+ if (featureLabel?.value) {
331
+ links.push({ label: `feature:${featureLabel.value}` });
332
+ }
333
+
334
+ return links.length > 0 ? links : undefined;
335
+ }
336
+
337
+ convertSteps(steps, depth = 0) {
338
+ if (depth >= 10) return null;
339
+
340
+ return steps
341
+ .map(step => {
342
+ const convertedStep = {
343
+ category: 'user',
344
+ title: step.name || step.title || 'Unknown step',
345
+ duration: this.calculateRunTime(step),
346
+ steps: this.convertSteps(step.steps || [], depth + 1),
347
+ };
348
+
349
+ if (convertedStep.steps && convertedStep.steps.length === 0) {
350
+ delete convertedStep.steps;
351
+ }
352
+
353
+ if (convertedStep.duration === 0) {
354
+ delete convertedStep.duration;
355
+ }
356
+
357
+ return convertedStep;
358
+ })
359
+ .filter(Boolean);
360
+ }
361
+
362
+ calculateRunTime(item) {
363
+ if (item.start && item.stop) {
364
+ const durationMs = item.stop - item.start;
365
+ return durationMs / 1000;
366
+ }
367
+ return null;
368
+ }
369
+
370
+ convertParameters(parameters) {
371
+ const example = {};
372
+ parameters.forEach(param => {
373
+ if (param.name) {
374
+ example[param.name] = param.value;
375
+ }
376
+ });
377
+ return example;
378
+ }
379
+
380
+ combineRetryAttempts(attempts) {
381
+ attempts.sort((a, b) => (a._stop || 0) - (b._stop || 0));
382
+
383
+ const finalTest = attempts[attempts.length - 1];
384
+ const retryCount = attempts.length - 1;
385
+
386
+ if (retryCount > 0) {
387
+ finalTest.retries = retryCount;
388
+ }
389
+
390
+ const failedAttempts = attempts.filter(t => t.status === 'failed');
391
+
392
+ if (failedAttempts.length === 0) {
393
+ return finalTest;
394
+ }
395
+
396
+ const failureMessages = [];
397
+ const failureStacks = [];
398
+
399
+ for (const failed of failedAttempts) {
400
+ const attemptNum = attempts.indexOf(failed) + 1;
401
+ if (failed.message) {
402
+ failureMessages.push(`[Attempt ${attemptNum}] ${failed.message}`);
403
+ }
404
+ if (failed.stack) {
405
+ failureStacks.push(`\n--- Attempt ${attemptNum} ---\n${failed.stack}`);
406
+ }
407
+ }
408
+
409
+ if (failureMessages.length > 0) {
410
+ finalTest.message = failureMessages.join('\n');
411
+ }
412
+ if (failureStacks.length > 0) {
413
+ finalTest.stack = failureStacks.join('\n');
414
+ }
415
+
416
+ if (finalTest.status === 'passed') {
417
+ finalTest.status = 'failed';
418
+ const retryMsg = `Test passed after ${failedAttempts.length} retries. Previous failures:\n`;
419
+ finalTest.message = retryMsg + finalTest.message;
420
+ }
421
+
422
+ return finalTest;
423
+ }
424
+
425
+ calculateStats() {
426
+ this.stats = {
427
+ create_tests: true,
428
+ tests_count: this._tests.length,
429
+ passed_count: 0,
430
+ failed_count: 0,
431
+ skipped_count: 0,
432
+ duration: 0,
433
+ status: 'passed',
434
+ tests: this._tests,
435
+ };
436
+ this._tests.forEach(t => {
437
+ if (t.status === 'passed') this.stats.passed_count++;
438
+ if (t.status === 'failed') this.stats.failed_count++;
439
+ if (t.status === 'skipped') this.stats.skipped_count++;
440
+ });
441
+ this.stats.duration = this._tests.reduce((acc, t) => acc + (t.run_time || 0), 0);
442
+ if (this.stats.failed_count) this.stats.status = 'failed';
443
+
444
+ debug('Stats:', this.stats);
445
+ return this.stats;
446
+ }
447
+
448
+ fetchSourceCode() {
449
+ const adapter = this.adapter || adapterFactory(this.getLanguage(), this.opts);
450
+
451
+ this._tests.forEach(t => {
452
+ try {
453
+ let filePath = t.file;
454
+
455
+ if (adapter && adapter.getFilePath) {
456
+ filePath = adapter.getFilePath(t);
457
+ }
458
+
459
+ if (!filePath) return;
460
+
461
+ if (!fs.existsSync(filePath)) {
462
+ debug('Source file not found:', filePath);
463
+ return;
464
+ }
465
+
466
+ const contents = fs.readFileSync(filePath).toString();
467
+ const code = fetchSourceCode(contents, { ...t, lang: this.getLanguage() });
468
+ if (code) {
469
+ t.code = code;
470
+ debug('Fetched code for test %s', t.title);
471
+ }
472
+
473
+ const testId = fetchIdFromCode(contents, { lang: this.getLanguage() });
474
+ if (testId) {
475
+ t.test_id = testId;
476
+ debug('Fetched test id %s for test %s', testId, t.title);
477
+ }
478
+ } catch (err) {
479
+ debug('Failed to fetch source code:', err.message);
480
+ }
481
+ });
482
+ }
483
+
484
+ getLanguage() {
485
+ if (this._tests.length === 0) return null;
486
+ return this._tests[0].meta?.language || this.opts.lang;
487
+ }
488
+
489
+ async uploadArtifacts() {
490
+ for (const test of this._tests.filter(t => t.files && t.files.length > 0)) {
491
+ const runId = this.runId || this.store.runId || Date.now().toString();
492
+ const artifacts = await Promise.all(
493
+ test.files.map(f => this.uploader.uploadFileByPath(f, [runId, test.rid, path.basename(f)])),
494
+ );
495
+ test.artifacts = artifacts.filter(a => a && a.link).map(a => a.link);
496
+ delete test.files;
497
+ if (test.artifacts.length > 0) {
498
+ console.log(APP_PREFIX, `🗄️ Uploaded ${pc.bold(`${test.artifacts.length} artifacts`)} for test ${test.title}`);
499
+ }
500
+ }
501
+ }
502
+
503
+ async uploadData() {
504
+ await this.uploadArtifacts();
505
+ this.calculateStats();
506
+ this.fetchSourceCode();
507
+
508
+ const dataString = {
509
+ ...this.stats,
510
+ api_key: this.requestParams.apiKey,
511
+ status: 'finished',
512
+ duration: this.stats.duration,
513
+ tests: this._tests,
514
+ };
515
+
516
+ debug('Uploading data', dataString);
517
+
518
+ this.pipes = this.pipes || (await this.pipesPromise);
519
+ return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
520
+ }
521
+ }
522
+
523
+ export default AllureReader;
package/src/bin/cli.js CHANGED
@@ -6,6 +6,7 @@ import { glob } from 'glob';
6
6
  import createDebugMessages from 'debug';
7
7
  import TestomatClient from '../client.js';
8
8
  import XmlReader from '../xmlReader.js';
9
+ import AllureReader from '../allureReader.js';
9
10
  import { APP_PREFIX, STATUS } from '../constants.js';
10
11
  import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
11
12
  import { config } from '../config.js';
@@ -116,15 +117,14 @@ program
116
117
 
117
118
  debug(`Execution pattern: "${pattern}"`);
118
119
 
119
- if(opts.filterList) {
120
+ if (opts.filterList) {
120
121
  console.log(APP_PREFIX, pc.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
121
122
  console.log(APP_PREFIX, pc.green(`Full Running Command: ${filteredCommand}`));
122
123
  return;
123
124
  }
124
-
125
+
125
126
  command = filteredCommand;
126
- }
127
- catch (err) {
127
+ } catch (err) {
128
128
  console.log(APP_PREFIX, err.message || err);
129
129
  return;
130
130
  }
@@ -237,6 +237,40 @@ program
237
237
  if (timeoutTimer) clearTimeout(timeoutTimer);
238
238
  });
239
239
 
240
+ program
241
+ .command('allure')
242
+ .description('Parse Allure result files and upload to Testomat.io')
243
+ .argument('<pattern>', 'Allure result directory pattern')
244
+ .option('-d, --dir <dir>', 'Project directory')
245
+ .option('--timelimit <time>', 'default time limit in seconds to kill a stuck process')
246
+ .option('--with-package', 'Keep full package path in file names (default: strip package prefix)')
247
+ .action(async (pattern, opts) => {
248
+ const runReader = new AllureReader({ withPackage: opts.withPackage });
249
+
250
+ let timeoutTimer;
251
+ if (opts.timelimit) {
252
+ timeoutTimer = setTimeout(
253
+ () => {
254
+ console.log(
255
+ `⚠️ Reached timeout of ${opts.timelimit}s. Exiting... (Exit code is 0 to not fail the pipeline)`,
256
+ );
257
+ process.exit(0);
258
+ },
259
+ parseInt(opts.timelimit, 10) * 1000,
260
+ );
261
+ }
262
+
263
+ try {
264
+ await runReader.parse(pattern);
265
+ await runReader.createRun();
266
+ await runReader.uploadData();
267
+ } catch (err) {
268
+ console.log(APP_PREFIX, 'Error uploading Allure results:', err);
269
+ }
270
+
271
+ if (timeoutTimer) clearTimeout(timeoutTimer);
272
+ });
273
+
240
274
  program
241
275
  .command('upload-artifacts')
242
276
  .description('Upload artifacts to Testomat.io')
@@ -4,6 +4,7 @@ import JavaAdapter from './java.js';
4
4
  import PythonAdapter from './python.js';
5
5
  import RubyAdapter from './ruby.js';
6
6
  import CSharpAdapter from './csharp.js';
7
+ import KotlinAdapter from './kotlin.js';
7
8
 
8
9
  function AdapterFactory(lang, opts) {
9
10
  if (lang === 'java') {
@@ -21,6 +22,9 @@ function AdapterFactory(lang, opts) {
21
22
  if (lang === 'c#' || lang === 'csharp') {
22
23
  return new CSharpAdapter(opts);
23
24
  }
25
+ if (lang === 'kotlin') {
26
+ return new KotlinAdapter(opts);
27
+ }
24
28
 
25
29
  return new Adapter(opts);
26
30
  }
@@ -0,0 +1,48 @@
1
+ import path from 'path';
2
+ import Adapter from './adapter.js';
3
+
4
+ class KotlinAdapter extends Adapter {
5
+ getFilePath(t) {
6
+ const fileName = namespaceToFileName(t.suite_title);
7
+ return this.opts.javaTests + path.sep + fileName;
8
+ }
9
+
10
+ formatTest(t) {
11
+ const fileParts = t.suite_title.split('.');
12
+
13
+ t.file = namespaceToFileName(t.suite_title);
14
+ t.title = t.title.split('(')[0];
15
+
16
+ // detect params
17
+ const paramMatches = t.title.match(/\[(.*?)\]/g);
18
+
19
+ if (paramMatches) {
20
+ const params = paramMatches.map((_match, index) => `param${index + 1}`);
21
+ if (params.length === 1) params[0] = 'param';
22
+ let paramIndex = 0;
23
+
24
+ t.title = t.title.replace(/: \[(.*?)\]/g, () => {
25
+ if (params.length < 2) return `\${param}`;
26
+ const paramName = params[paramIndex] || `param${paramIndex + 1}`;
27
+ paramIndex++;
28
+ return `\${${paramName}}`;
29
+ });
30
+ const example = {};
31
+ paramMatches.forEach((match, index) => {
32
+ example[params[index]] = match.replace(/[[\]]/g, '');
33
+ });
34
+ t.example = example;
35
+ }
36
+
37
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ');
38
+ return t;
39
+ }
40
+ }
41
+
42
+ function namespaceToFileName(fileName) {
43
+ const fileParts = fileName.split('.');
44
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
45
+ return `${fileParts.join(path.sep)}.kt`;
46
+ }
47
+
48
+ export default KotlinAdapter;
package/src/pipe/debug.js CHANGED
@@ -4,6 +4,7 @@ import os from 'os';
4
4
  import createDebugMessages from 'debug';
5
5
  import { APP_PREFIX } from '../constants.js';
6
6
  import prettyMs from 'pretty-ms';
7
+ import { transformEnvVarToBoolean } from '../utils/utils.js';
7
8
 
8
9
  const debug = createDebugMessages('@testomatio/reporter:pipe:debug');
9
10
 
@@ -15,7 +16,7 @@ export class DebugPipe {
15
16
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
17
  if (this.isEnabled) {
17
18
  this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
19
+ isEnabled: this.params.isBatchEnabled ?? !transformEnvVarToBoolean(process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD),
19
20
  intervalFunction: null,
20
21
  intervalTime: 5000,
21
22
  tests: [],
@@ -25,7 +25,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
25
25
  class TestomatioPipe {
26
26
  constructor(params, store) {
27
27
  this.batch = {
28
- isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
28
+ isEnabled: params?.isBatchEnabled ?? !transformEnvVarToBoolean(process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD),
29
29
  intervalFunction: null, // will be created in createRun by setInterval function
30
30
  intervalTime: 5000, // how often tests are sent
31
31
  tests: [], // array of tests in batch
@@ -276,6 +276,11 @@ const fetchSourceCode = (contents, opts = {}) => {
276
276
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
277
277
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`public void ${title}`));
278
278
  if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
279
+ } else if (opts.lang === 'kotlin') {
280
+ lineIndex = lines.findIndex(l => l.includes(`fun test${title}`));
281
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`@DisplayName("${title}`));
282
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`fun ${title}`));
283
+ if (lineIndex === -1) lineIndex = lines.findIndex(l => l.includes(`${title}(`));
279
284
  } else if (opts.lang === 'csharp') {
280
285
  // Find the method declaration line
281
286
  let methodLineIndex = lines.findIndex(l => l.includes(`public void ${title}(`));