testomatio-reporter-cli 2.10.1 → 2.11.0
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/package.json +1 -1
- package/src/bin/cli.js +5 -2
- package/src/client.js +2 -2
- package/src/junit-adapter/dart.js +103 -0
- package/src/junit-adapter/index.js +4 -0
- package/src/pipe/testomatio.js +7 -0
- package/src/utils/utils.js +48 -2
- package/src/xmlReader.js +1 -0
- package/types/types.d.ts +3 -0
package/package.json
CHANGED
package/src/bin/cli.js
CHANGED
|
@@ -8,7 +8,7 @@ import TestomatClient from '../client.js';
|
|
|
8
8
|
import XmlReader from '../xmlReader.js';
|
|
9
9
|
import AllureReader from '../allureReader.js';
|
|
10
10
|
import { APP_PREFIX, STATUS, DEBUG_FILE, BATCH_MODE } from '../constants.js';
|
|
11
|
-
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
11
|
+
import { cleanLatestRunId, getPackageVersion, applyFilter, transformEnvVarToBoolean } from '../utils/utils.js';
|
|
12
12
|
import { config } from '../config.js';
|
|
13
13
|
import { readLatestRunId } from '../utils/utils.js';
|
|
14
14
|
import pc from 'picocolors';
|
|
@@ -107,8 +107,11 @@ program
|
|
|
107
107
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
108
108
|
const client = new TestomatClient({ apiKey });
|
|
109
109
|
|
|
110
|
+
const finishParams = {};
|
|
111
|
+
if (transformEnvVarToBoolean(process.env.TESTOMATIO_FINISH_SHARED_RUN)) finishParams.force_finish_shared_run = true;
|
|
112
|
+
|
|
110
113
|
// @ts-ignore
|
|
111
|
-
client.updateRunStatus(STATUS.FINISHED).then(() => {
|
|
114
|
+
client.updateRunStatus(STATUS.FINISHED, finishParams).then(() => {
|
|
112
115
|
process.exit(0);
|
|
113
116
|
});
|
|
114
117
|
});
|
package/src/client.js
CHANGED
|
@@ -206,9 +206,9 @@ class Client {
|
|
|
206
206
|
suite_title: 'Unknown suite',
|
|
207
207
|
};
|
|
208
208
|
|
|
209
|
-
// Add timestamp if not already present (
|
|
209
|
+
// Add timestamp if not already present (Unix seconds)
|
|
210
210
|
if (!testData.timestamp && !process.env.TESTOMATIO_NO_TIMESTAMP) {
|
|
211
|
-
testData.timestamp = Math.floor((performance.timeOrigin + performance.now())
|
|
211
|
+
testData.timestamp = Math.floor((performance.timeOrigin + performance.now()) / 1000);
|
|
212
212
|
}
|
|
213
213
|
|
|
214
214
|
/**
|
|
@@ -0,0 +1,103 @@
|
|
|
1
|
+
import path from 'path';
|
|
2
|
+
import Adapter from './adapter.js';
|
|
3
|
+
|
|
4
|
+
class DartAdapter extends Adapter {
|
|
5
|
+
|
|
6
|
+
getFilePath(t) {
|
|
7
|
+
if (t.title.startsWith('runDartTest[')) {
|
|
8
|
+
// Android: runDartTest[tests.path.to.test Test name]
|
|
9
|
+
const fileName = namespaceToFileName(t.title.split('[')[1].split(' ')[0]);
|
|
10
|
+
return fileName;
|
|
11
|
+
}
|
|
12
|
+
// iOS: RunnerUITests tests.path.to.test Test name
|
|
13
|
+
const parts = t.title.split(' ');
|
|
14
|
+
const pathToken = parts.length > 1 ? parts[1] : parts[0];
|
|
15
|
+
return namespaceToFileName(pathToken);
|
|
16
|
+
}
|
|
17
|
+
|
|
18
|
+
formatTest(t) {
|
|
19
|
+
if (t.title.startsWith('runDartTest[')) {
|
|
20
|
+
// Android: runDartTest[<path> <name>]
|
|
21
|
+
// First space is between the path and the test name; strip up to and including it, remove trailing ']'
|
|
22
|
+
const spaceIndex = t.title.indexOf(' ');
|
|
23
|
+
if (spaceIndex > -1) {
|
|
24
|
+
const pathToken = t.title.slice('runDartTest['.length, spaceIndex);
|
|
25
|
+
t.file = namespaceToFileName(pathToken);
|
|
26
|
+
t.title = t.title.slice(spaceIndex + 1).replace(/\]$/, '');
|
|
27
|
+
}
|
|
28
|
+
} else {
|
|
29
|
+
// iOS: <ClassName> <test.path> <test name>
|
|
30
|
+
// Skip both the classname token and the dot-separated test path token
|
|
31
|
+
const parts = t.title.split(' ');
|
|
32
|
+
if (parts.length > 2 && parts[1] && parts[1].includes('.')) {
|
|
33
|
+
t.file = namespaceToFileName(parts[1]);
|
|
34
|
+
t.title = parts.slice(2).join(' ');
|
|
35
|
+
} else {
|
|
36
|
+
const spaceIndex = t.title.indexOf(' ');
|
|
37
|
+
if (spaceIndex > -1) {
|
|
38
|
+
t.title = t.title.slice(spaceIndex + 1);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
// classname: cut everything after the last dot (inclusive)
|
|
44
|
+
if (t.suite_title && t.suite_title.includes('.')) {
|
|
45
|
+
const lastDot = t.suite_title.lastIndexOf('.');
|
|
46
|
+
t.suite_title = t.suite_title.slice(0, lastDot);
|
|
47
|
+
}
|
|
48
|
+
|
|
49
|
+
if (!t.file) {
|
|
50
|
+
t.file = namespaceToFileName(t.suite_title || '');
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
// detect params
|
|
54
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
55
|
+
|
|
56
|
+
if (paramMatches) {
|
|
57
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
58
|
+
if (params.length === 1) params[0] = 'param';
|
|
59
|
+
let paramIndex = 0;
|
|
60
|
+
|
|
61
|
+
t.title = t.title.replace(/\[(.*?)\]/g, () => {
|
|
62
|
+
if (params.length < 2) return `\${param}`;
|
|
63
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
64
|
+
paramIndex++;
|
|
65
|
+
return `\${${paramName}}`;
|
|
66
|
+
});
|
|
67
|
+
const example = {};
|
|
68
|
+
paramMatches.forEach((match, index) => {
|
|
69
|
+
example[params[index]] = match.replace(/[[\]]/g, '');
|
|
70
|
+
});
|
|
71
|
+
t.example = example;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
|
|
78
|
+
function namespaceToFileName(fileName) {
|
|
79
|
+
let testName = fileName;
|
|
80
|
+
|
|
81
|
+
// If it's already an extracted test name (contains dots but no runDartTest prefix)
|
|
82
|
+
if (fileName.includes('.') && !fileName.startsWith('runDartTest')) {
|
|
83
|
+
// Take only the first part before any spaces (the actual test path)
|
|
84
|
+
testName = fileName.split(' ')[0];
|
|
85
|
+
} else {
|
|
86
|
+
// Legacy handling for full runDartTest[...] format
|
|
87
|
+
const dartMatch = fileName.match(/runDartTest\[(.+?) /);
|
|
88
|
+
if (dartMatch) {
|
|
89
|
+
testName = dartMatch[1];
|
|
90
|
+
} else {
|
|
91
|
+
const parts = fileName.split(' ');
|
|
92
|
+
if (parts.length > 1) {
|
|
93
|
+
testName = parts[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
|
|
98
|
+
const fileParts = testName.split('.');
|
|
99
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
100
|
+
return `${fileParts.join(path.sep)}.dart`;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
export default DartAdapter;
|
|
@@ -5,6 +5,7 @@ import PythonAdapter from './python.js';
|
|
|
5
5
|
import RubyAdapter from './ruby.js';
|
|
6
6
|
import CSharpAdapter from './csharp.js';
|
|
7
7
|
import KotlinAdapter from './kotlin.js';
|
|
8
|
+
import DartAdapter from './dart.js';
|
|
8
9
|
|
|
9
10
|
function AdapterFactory(lang, opts) {
|
|
10
11
|
if (lang === 'java') {
|
|
@@ -25,6 +26,9 @@ function AdapterFactory(lang, opts) {
|
|
|
25
26
|
if (lang === 'kotlin') {
|
|
26
27
|
return new KotlinAdapter(opts);
|
|
27
28
|
}
|
|
29
|
+
if (lang === 'dart') {
|
|
30
|
+
return new DartAdapter(opts);
|
|
31
|
+
}
|
|
28
32
|
|
|
29
33
|
return new Adapter(opts);
|
|
30
34
|
}
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -167,6 +167,12 @@ class TestomatioPipe {
|
|
|
167
167
|
// add test ID + run ID
|
|
168
168
|
if (data.rid) data.rid = `${this.runId}-${data.rid}`;
|
|
169
169
|
|
|
170
|
+
// convert timestamp to seconds
|
|
171
|
+
if (data.timestamp > 1e11) {
|
|
172
|
+
if (data.timestamp > 1e14) data.timestamp = Math.floor(data.timestamp / 1e6); // convert microseconds
|
|
173
|
+
else if (data.timestamp > 1e11) data.timestamp = Math.floor(data.timestamp / 1e3); // convert milliseconds
|
|
174
|
+
}
|
|
175
|
+
|
|
170
176
|
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
|
|
171
177
|
data.stack = null;
|
|
172
178
|
}
|
|
@@ -569,6 +575,7 @@ class TestomatioPipe {
|
|
|
569
575
|
status_event,
|
|
570
576
|
detach: params.detach,
|
|
571
577
|
tests: params.tests,
|
|
578
|
+
...(params.force_finish_shared_run && { force_finish_shared_run: true }),
|
|
572
579
|
},
|
|
573
580
|
});
|
|
574
581
|
|
package/src/utils/utils.js
CHANGED
|
@@ -271,6 +271,26 @@ const fetchIdFromOutput = output => {
|
|
|
271
271
|
return output.match(TID_FULL_PATTERN)?.[2];
|
|
272
272
|
};
|
|
273
273
|
|
|
274
|
+
const findDartTestLine = (lines, expectedTitle) => {
|
|
275
|
+
const testFunctions = ['patrolTest', 'testWidgets'];
|
|
276
|
+
const quotedTitles = [`'${expectedTitle}',`, `"${expectedTitle}",`, `r'${expectedTitle}',`, `r"${expectedTitle}",`];
|
|
277
|
+
|
|
278
|
+
return lines.findIndex((_line, index) => {
|
|
279
|
+
const declaration = lines
|
|
280
|
+
.slice(index, index + 5)
|
|
281
|
+
.map(line => line.trim())
|
|
282
|
+
.join(' ');
|
|
283
|
+
const functionName = testFunctions.find(name => declaration.startsWith(name));
|
|
284
|
+
if (!functionName) return false;
|
|
285
|
+
|
|
286
|
+
const argumentsList = declaration.slice(functionName.length).trimStart();
|
|
287
|
+
if (!argumentsList.startsWith('(')) return false;
|
|
288
|
+
|
|
289
|
+
const firstArgument = argumentsList.slice(1).trimStart();
|
|
290
|
+
return quotedTitles.some(title => firstArgument.startsWith(title));
|
|
291
|
+
});
|
|
292
|
+
};
|
|
293
|
+
|
|
274
294
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
275
295
|
if (!opts.title && !opts.line) return '';
|
|
276
296
|
|
|
@@ -354,6 +374,32 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
354
374
|
}
|
|
355
375
|
}
|
|
356
376
|
}
|
|
377
|
+
} else if (opts.lang === 'dart') {
|
|
378
|
+
// For Dart, locate the specific patrolTest/testWidgets block by title first —
|
|
379
|
+
// otherwise every test sharing the same main() gets the same @T comment.
|
|
380
|
+
const rawTitle = opts.title || '';
|
|
381
|
+
let dartTestTitle = '';
|
|
382
|
+
|
|
383
|
+
if (rawTitle.startsWith('runDartTest[')) {
|
|
384
|
+
// Android: runDartTest[<path> <test name>]
|
|
385
|
+
const spaceIndex = rawTitle.indexOf(' ');
|
|
386
|
+
if (spaceIndex > -1) {
|
|
387
|
+
dartTestTitle = rawTitle.slice(spaceIndex + 1).replace(/\]$/, '');
|
|
388
|
+
}
|
|
389
|
+
} else {
|
|
390
|
+
// iOS: <ClassName> <test.path> <test name>
|
|
391
|
+
const parts = rawTitle.split(' ');
|
|
392
|
+
if (parts.length > 2 && parts[1] && parts[1].includes('.')) {
|
|
393
|
+
dartTestTitle = parts.slice(2).join(' ');
|
|
394
|
+
}
|
|
395
|
+
}
|
|
396
|
+
|
|
397
|
+
let testLineIndex = -1;
|
|
398
|
+
if (dartTestTitle) {
|
|
399
|
+
testLineIndex = findDartTestLine(lines, dartTestTitle);
|
|
400
|
+
}
|
|
401
|
+
|
|
402
|
+
lineIndex = testLineIndex;
|
|
357
403
|
} else {
|
|
358
404
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
359
405
|
}
|
|
@@ -371,8 +417,8 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
371
417
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
372
418
|
if (lines[i] === undefined) continue;
|
|
373
419
|
|
|
374
|
-
// Track brace depth for C# to stop after method closes
|
|
375
|
-
if (opts.lang === 'csharp') {
|
|
420
|
+
// Track brace depth for C# and Dart to stop after method/main closes
|
|
421
|
+
if (opts.lang === 'csharp' || opts.lang === 'dart') {
|
|
376
422
|
const line = lines[i];
|
|
377
423
|
// Count opening and closing braces
|
|
378
424
|
const openBraces = (line.match(/\{/g) || []).length;
|
package/src/xmlReader.js
CHANGED
|
@@ -472,6 +472,7 @@ class XmlReader {
|
|
|
472
472
|
if (file.endsWith('.js')) this.stats.language = 'js';
|
|
473
473
|
if (file.endsWith('.ts')) this.stats.language = 'ts';
|
|
474
474
|
if (file.endsWith('.cs')) this.stats.language = 'csharp';
|
|
475
|
+
if (file.endsWith('.dart')) this.stats.language = 'dart';
|
|
475
476
|
}
|
|
476
477
|
|
|
477
478
|
if (!fs.existsSync(file)) {
|
package/types/types.d.ts
CHANGED
|
@@ -274,6 +274,9 @@ export interface RunData {
|
|
|
274
274
|
* An array of `TestData` objects representing the individual test cases in the test run.
|
|
275
275
|
* Used for JUNit report when we don't send the tests in realtime but in a batch as a part of final result */
|
|
276
276
|
tests?: TestData[];
|
|
277
|
+
|
|
278
|
+
/** Force-finish a shared run even if other participants haven't finished yet. Set via `TESTOMATIO_FINISH_SHARED_RUN`. */
|
|
279
|
+
force_finish_shared_run?: boolean;
|
|
277
280
|
}
|
|
278
281
|
|
|
279
282
|
export enum TestStatus {
|