@testomatio/reporter 2.10.1 → 2.12.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/lib/bin/cli.js +8 -1
- package/lib/client.d.ts +2 -2
- package/lib/client.js +3 -3
- package/lib/junit-adapter/dart.d.ts +5 -0
- package/lib/junit-adapter/dart.js +101 -0
- package/lib/junit-adapter/index.js +4 -0
- package/lib/pipe/html.js +1 -1
- package/lib/pipe/markdown.js +1 -1
- package/lib/pipe/testomatio.js +14 -2
- package/lib/utils/pipe_utils.js +2 -0
- package/lib/utils/utils.js +45 -2
- package/lib/xmlReader.js +2 -0
- package/package.json +1 -1
- package/src/bin/cli.js +10 -2
- package/src/client.js +3 -3
- package/src/junit-adapter/dart.js +103 -0
- package/src/junit-adapter/index.js +4 -0
- package/src/pipe/html.js +1 -1
- package/src/pipe/markdown.js +1 -1
- package/src/pipe/testomatio.js +13 -2
- package/src/utils/pipe_utils.js +1 -0
- package/src/utils/utils.js +48 -2
- package/src/xmlReader.js +1 -0
- package/types/types.d.ts +5 -0
package/lib/bin/cli.js
CHANGED
|
@@ -84,6 +84,10 @@ program
|
|
|
84
84
|
log_js_1.log.error(picocolors_1.default.red('Failed to create run on Testomat.io.'));
|
|
85
85
|
process.exit(1);
|
|
86
86
|
}
|
|
87
|
+
// tests are executed later, so report the run as pending with the tests it was scoped to:
|
|
88
|
+
// pipes add their report now and replace it when the run is finished
|
|
89
|
+
const plannedTests = (client.pipeStore.preparedTestIds || []).map(id => ({ test_id: id, title: id }));
|
|
90
|
+
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
87
91
|
// stdout carries ONLY the run id so it can be captured: RUN_ID=$(reporter start)
|
|
88
92
|
console.log(runId);
|
|
89
93
|
process.exit(0);
|
|
@@ -100,8 +104,11 @@ program
|
|
|
100
104
|
console.log('Finishing Run on Testomat.io...');
|
|
101
105
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
102
106
|
const client = new client_js_1.default({ apiKey });
|
|
107
|
+
const finishParams = {};
|
|
108
|
+
if ((0, utils_js_1.transformEnvVarToBoolean)(process.env.TESTOMATIO_FINISH_SHARED_RUN))
|
|
109
|
+
finishParams.force_finish_shared_run = true;
|
|
103
110
|
// @ts-ignore
|
|
104
|
-
client.updateRunStatus(constants_js_1.STATUS.FINISHED).then(() => {
|
|
111
|
+
client.updateRunStatus(constants_js_1.STATUS.FINISHED, finishParams).then(() => {
|
|
105
112
|
process.exit(0);
|
|
106
113
|
});
|
|
107
114
|
});
|
package/lib/client.d.ts
CHANGED
|
@@ -64,11 +64,11 @@ export class Client {
|
|
|
64
64
|
/**
|
|
65
65
|
*
|
|
66
66
|
* Updates the status of the current test run and finishes the run.
|
|
67
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
67
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
68
68
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
69
69
|
* Must be one of "passed", "failed", or "finished"
|
|
70
70
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
71
71
|
*/
|
|
72
|
-
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
72
|
+
updateRunStatus(status: "passed" | "failed" | "skipped" | "finished" | "pending", params?: Partial<import("../types/types.js").RunData>): Promise<any>;
|
|
73
73
|
}
|
|
74
74
|
import { S3Uploader } from './uploader.js';
|
package/lib/client.js
CHANGED
|
@@ -192,9 +192,9 @@ class Client {
|
|
|
192
192
|
title: 'Unknown test',
|
|
193
193
|
suite_title: 'Unknown suite',
|
|
194
194
|
};
|
|
195
|
-
// Add timestamp if not already present (
|
|
195
|
+
// Add timestamp if not already present (Unix seconds)
|
|
196
196
|
if (!testData.timestamp && !process.env.TESTOMATIO_NO_TIMESTAMP) {
|
|
197
|
-
testData.timestamp = Math.floor((performance.timeOrigin + performance.now())
|
|
197
|
+
testData.timestamp = Math.floor((performance.timeOrigin + performance.now()) / 1000);
|
|
198
198
|
}
|
|
199
199
|
/**
|
|
200
200
|
* @type {TestData}
|
|
@@ -319,7 +319,7 @@ class Client {
|
|
|
319
319
|
/**
|
|
320
320
|
*
|
|
321
321
|
* Updates the status of the current test run and finishes the run.
|
|
322
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
322
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
323
323
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
324
324
|
* Must be one of "passed", "failed", or "finished"
|
|
325
325
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
@@ -0,0 +1,101 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const path_1 = __importDefault(require("path"));
|
|
7
|
+
const adapter_js_1 = __importDefault(require("./adapter.js"));
|
|
8
|
+
class DartAdapter extends adapter_js_1.default {
|
|
9
|
+
getFilePath(t) {
|
|
10
|
+
if (t.title.startsWith('runDartTest[')) {
|
|
11
|
+
// Android: runDartTest[tests.path.to.test Test name]
|
|
12
|
+
const fileName = namespaceToFileName(t.title.split('[')[1].split(' ')[0]);
|
|
13
|
+
return fileName;
|
|
14
|
+
}
|
|
15
|
+
// iOS: RunnerUITests tests.path.to.test Test name
|
|
16
|
+
const parts = t.title.split(' ');
|
|
17
|
+
const pathToken = parts.length > 1 ? parts[1] : parts[0];
|
|
18
|
+
return namespaceToFileName(pathToken);
|
|
19
|
+
}
|
|
20
|
+
formatTest(t) {
|
|
21
|
+
if (t.title.startsWith('runDartTest[')) {
|
|
22
|
+
// Android: runDartTest[<path> <name>]
|
|
23
|
+
// First space is between the path and the test name; strip up to and including it, remove trailing ']'
|
|
24
|
+
const spaceIndex = t.title.indexOf(' ');
|
|
25
|
+
if (spaceIndex > -1) {
|
|
26
|
+
const pathToken = t.title.slice('runDartTest['.length, spaceIndex);
|
|
27
|
+
t.file = namespaceToFileName(pathToken);
|
|
28
|
+
t.title = t.title.slice(spaceIndex + 1).replace(/\]$/, '');
|
|
29
|
+
}
|
|
30
|
+
}
|
|
31
|
+
else {
|
|
32
|
+
// iOS: <ClassName> <test.path> <test name>
|
|
33
|
+
// Skip both the classname token and the dot-separated test path token
|
|
34
|
+
const parts = t.title.split(' ');
|
|
35
|
+
if (parts.length > 2 && parts[1] && parts[1].includes('.')) {
|
|
36
|
+
t.file = namespaceToFileName(parts[1]);
|
|
37
|
+
t.title = parts.slice(2).join(' ');
|
|
38
|
+
}
|
|
39
|
+
else {
|
|
40
|
+
const spaceIndex = t.title.indexOf(' ');
|
|
41
|
+
if (spaceIndex > -1) {
|
|
42
|
+
t.title = t.title.slice(spaceIndex + 1);
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
}
|
|
46
|
+
// classname: cut everything after the last dot (inclusive)
|
|
47
|
+
if (t.suite_title && t.suite_title.includes('.')) {
|
|
48
|
+
const lastDot = t.suite_title.lastIndexOf('.');
|
|
49
|
+
t.suite_title = t.suite_title.slice(0, lastDot);
|
|
50
|
+
}
|
|
51
|
+
if (!t.file) {
|
|
52
|
+
t.file = namespaceToFileName(t.suite_title || '');
|
|
53
|
+
}
|
|
54
|
+
// detect params
|
|
55
|
+
const paramMatches = t.title.match(/\[(.*?)\]/g);
|
|
56
|
+
if (paramMatches) {
|
|
57
|
+
const params = paramMatches.map((_match, index) => `param${index + 1}`);
|
|
58
|
+
if (params.length === 1)
|
|
59
|
+
params[0] = 'param';
|
|
60
|
+
let paramIndex = 0;
|
|
61
|
+
t.title = t.title.replace(/\[(.*?)\]/g, () => {
|
|
62
|
+
if (params.length < 2)
|
|
63
|
+
return `\${param}`;
|
|
64
|
+
const paramName = params[paramIndex] || `param${paramIndex + 1}`;
|
|
65
|
+
paramIndex++;
|
|
66
|
+
return `\${${paramName}}`;
|
|
67
|
+
});
|
|
68
|
+
const example = {};
|
|
69
|
+
paramMatches.forEach((match, index) => {
|
|
70
|
+
example[params[index]] = match.replace(/[[\]]/g, '');
|
|
71
|
+
});
|
|
72
|
+
t.example = example;
|
|
73
|
+
}
|
|
74
|
+
return t;
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
function namespaceToFileName(fileName) {
|
|
78
|
+
let testName = fileName;
|
|
79
|
+
// If it's already an extracted test name (contains dots but no runDartTest prefix)
|
|
80
|
+
if (fileName.includes('.') && !fileName.startsWith('runDartTest')) {
|
|
81
|
+
// Take only the first part before any spaces (the actual test path)
|
|
82
|
+
testName = fileName.split(' ')[0];
|
|
83
|
+
}
|
|
84
|
+
else {
|
|
85
|
+
// Legacy handling for full runDartTest[...] format
|
|
86
|
+
const dartMatch = fileName.match(/runDartTest\[(.+?) /);
|
|
87
|
+
if (dartMatch) {
|
|
88
|
+
testName = dartMatch[1];
|
|
89
|
+
}
|
|
90
|
+
else {
|
|
91
|
+
const parts = fileName.split(' ');
|
|
92
|
+
if (parts.length > 1) {
|
|
93
|
+
testName = parts[1];
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
}
|
|
97
|
+
const fileParts = testName.split('.');
|
|
98
|
+
fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
|
|
99
|
+
return `${fileParts.join(path_1.default.sep)}.dart`;
|
|
100
|
+
}
|
|
101
|
+
module.exports = DartAdapter;
|
|
@@ -10,6 +10,7 @@ const python_js_1 = __importDefault(require("./python.js"));
|
|
|
10
10
|
const ruby_js_1 = __importDefault(require("./ruby.js"));
|
|
11
11
|
const csharp_js_1 = __importDefault(require("./csharp.js"));
|
|
12
12
|
const kotlin_js_1 = __importDefault(require("./kotlin.js"));
|
|
13
|
+
const dart_js_1 = __importDefault(require("./dart.js"));
|
|
13
14
|
function AdapterFactory(lang, opts) {
|
|
14
15
|
if (lang === 'java') {
|
|
15
16
|
return new java_js_1.default(opts);
|
|
@@ -29,6 +30,9 @@ function AdapterFactory(lang, opts) {
|
|
|
29
30
|
if (lang === 'kotlin') {
|
|
30
31
|
return new kotlin_js_1.default(opts);
|
|
31
32
|
}
|
|
33
|
+
if (lang === 'dart') {
|
|
34
|
+
return new dart_js_1.default(opts);
|
|
35
|
+
}
|
|
32
36
|
return new adapter_js_1.default(opts);
|
|
33
37
|
}
|
|
34
38
|
module.exports = AdapterFactory;
|
package/lib/pipe/html.js
CHANGED
|
@@ -216,7 +216,7 @@ class HtmlPipe {
|
|
|
216
216
|
runUrl: this.store.runUrl || '',
|
|
217
217
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
218
218
|
executionDate: getCurrentDateTimeFormatted(),
|
|
219
|
-
description: [runParams.description || this.store.coverageDescription || this.store.description
|
|
219
|
+
description: [this.description, runParams.description || this.store.coverageDescription || this.store.description]
|
|
220
220
|
.filter(Boolean)
|
|
221
221
|
.join('\n\n') || '',
|
|
222
222
|
configuration: buildDisplayConfiguration(this.configuration || this.store.configuration || runParams.configuration || null),
|
package/lib/pipe/markdown.js
CHANGED
|
@@ -117,7 +117,7 @@ class MarkdownPipe {
|
|
|
117
117
|
isParallel: runParams?.isParallel,
|
|
118
118
|
executionTime: testExecutionSumTime(aggregated),
|
|
119
119
|
executionDate: getCurrentDateTimeFormatted(),
|
|
120
|
-
description: [runParams?.description || this.store.coverageDescription || this.store.description
|
|
120
|
+
description: [this.description, runParams?.description || this.store.coverageDescription || this.store.description]
|
|
121
121
|
.filter(Boolean)
|
|
122
122
|
.join('\n\n') || '',
|
|
123
123
|
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -147,6 +147,13 @@ class TestomatioPipe {
|
|
|
147
147
|
// add test ID + run ID
|
|
148
148
|
if (data.rid)
|
|
149
149
|
data.rid = `${this.runId}-${data.rid}`;
|
|
150
|
+
// convert timestamp to seconds
|
|
151
|
+
if (data.timestamp > 1e11) {
|
|
152
|
+
if (data.timestamp > 1e14)
|
|
153
|
+
data.timestamp = Math.floor(data.timestamp / 1e6); // convert microseconds
|
|
154
|
+
else if (data.timestamp > 1e11)
|
|
155
|
+
data.timestamp = Math.floor(data.timestamp / 1e3); // convert milliseconds
|
|
156
|
+
}
|
|
150
157
|
if (!process.env.TESTOMATIO_STACK_PASSED && data.status === constants_js_1.STATUS.PASSED) {
|
|
151
158
|
data.stack = null;
|
|
152
159
|
}
|
|
@@ -240,8 +247,9 @@ class TestomatioPipe {
|
|
|
240
247
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
241
248
|
};
|
|
242
249
|
}
|
|
243
|
-
// Run description:
|
|
244
|
-
|
|
250
|
+
// Run description: the user-provided TESTOMATIO_DESCRIPTION with the coverage-derived block
|
|
251
|
+
// (if any) added after it. Neither overrides the other.
|
|
252
|
+
const description = [this.description, coverageDescription].filter(Boolean).join('\n\n') || null;
|
|
245
253
|
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
246
254
|
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
247
255
|
if (params.configuration && typeof params.configuration === 'object') {
|
|
@@ -504,6 +512,9 @@ class TestomatioPipe {
|
|
|
504
512
|
console.warn(`${constants_js_1.APP_PREFIX} ${errorMessage}`);
|
|
505
513
|
}
|
|
506
514
|
const { status } = params;
|
|
515
|
+
// a pending run was just created: nothing to update here, only other pipes report it
|
|
516
|
+
if (status === 'pending')
|
|
517
|
+
return;
|
|
507
518
|
let status_event;
|
|
508
519
|
if (status === constants_js_1.STATUS.FINISHED)
|
|
509
520
|
status_event = 'finish';
|
|
@@ -522,6 +533,7 @@ class TestomatioPipe {
|
|
|
522
533
|
status_event,
|
|
523
534
|
detach: params.detach,
|
|
524
535
|
tests: params.tests,
|
|
536
|
+
...(params.force_finish_shared_run && { force_finish_shared_run: true }),
|
|
525
537
|
},
|
|
526
538
|
});
|
|
527
539
|
if (this.runUrl) {
|
package/lib/utils/pipe_utils.js
CHANGED
package/lib/utils/utils.js
CHANGED
|
@@ -303,6 +303,24 @@ const fetchIdFromOutput = output => {
|
|
|
303
303
|
return output.match(TID_FULL_PATTERN)?.[2];
|
|
304
304
|
};
|
|
305
305
|
exports.fetchIdFromOutput = fetchIdFromOutput;
|
|
306
|
+
const findDartTestLine = (lines, expectedTitle) => {
|
|
307
|
+
const testFunctions = ['patrolTest', 'testWidgets'];
|
|
308
|
+
const quotedTitles = [`'${expectedTitle}',`, `"${expectedTitle}",`, `r'${expectedTitle}',`, `r"${expectedTitle}",`];
|
|
309
|
+
return lines.findIndex((_line, index) => {
|
|
310
|
+
const declaration = lines
|
|
311
|
+
.slice(index, index + 5)
|
|
312
|
+
.map(line => line.trim())
|
|
313
|
+
.join(' ');
|
|
314
|
+
const functionName = testFunctions.find(name => declaration.startsWith(name));
|
|
315
|
+
if (!functionName)
|
|
316
|
+
return false;
|
|
317
|
+
const argumentsList = declaration.slice(functionName.length).trimStart();
|
|
318
|
+
if (!argumentsList.startsWith('('))
|
|
319
|
+
return false;
|
|
320
|
+
const firstArgument = argumentsList.slice(1).trimStart();
|
|
321
|
+
return quotedTitles.some(title => firstArgument.startsWith(title));
|
|
322
|
+
});
|
|
323
|
+
};
|
|
306
324
|
const fetchSourceCode = (contents, opts = {}) => {
|
|
307
325
|
if (!opts.title && !opts.line)
|
|
308
326
|
return '';
|
|
@@ -387,6 +405,31 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
387
405
|
}
|
|
388
406
|
}
|
|
389
407
|
}
|
|
408
|
+
else if (opts.lang === 'dart') {
|
|
409
|
+
// For Dart, locate the specific patrolTest/testWidgets block by title first —
|
|
410
|
+
// otherwise every test sharing the same main() gets the same @T comment.
|
|
411
|
+
const rawTitle = opts.title || '';
|
|
412
|
+
let dartTestTitle = '';
|
|
413
|
+
if (rawTitle.startsWith('runDartTest[')) {
|
|
414
|
+
// Android: runDartTest[<path> <test name>]
|
|
415
|
+
const spaceIndex = rawTitle.indexOf(' ');
|
|
416
|
+
if (spaceIndex > -1) {
|
|
417
|
+
dartTestTitle = rawTitle.slice(spaceIndex + 1).replace(/\]$/, '');
|
|
418
|
+
}
|
|
419
|
+
}
|
|
420
|
+
else {
|
|
421
|
+
// iOS: <ClassName> <test.path> <test name>
|
|
422
|
+
const parts = rawTitle.split(' ');
|
|
423
|
+
if (parts.length > 2 && parts[1] && parts[1].includes('.')) {
|
|
424
|
+
dartTestTitle = parts.slice(2).join(' ');
|
|
425
|
+
}
|
|
426
|
+
}
|
|
427
|
+
let testLineIndex = -1;
|
|
428
|
+
if (dartTestTitle) {
|
|
429
|
+
testLineIndex = findDartTestLine(lines, dartTestTitle);
|
|
430
|
+
}
|
|
431
|
+
lineIndex = testLineIndex;
|
|
432
|
+
}
|
|
390
433
|
else {
|
|
391
434
|
lineIndex = lines.findIndex(l => l.includes(title));
|
|
392
435
|
}
|
|
@@ -401,8 +444,8 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
401
444
|
for (let i = lineIndex; i < lineIndex + limit; i++) {
|
|
402
445
|
if (lines[i] === undefined)
|
|
403
446
|
continue;
|
|
404
|
-
// Track brace depth for C# to stop after method closes
|
|
405
|
-
if (opts.lang === 'csharp') {
|
|
447
|
+
// Track brace depth for C# and Dart to stop after method/main closes
|
|
448
|
+
if (opts.lang === 'csharp' || opts.lang === 'dart') {
|
|
406
449
|
const line = lines[i];
|
|
407
450
|
// Count opening and closing braces
|
|
408
451
|
const openBraces = (line.match(/\{/g) || []).length;
|
package/lib/xmlReader.js
CHANGED
|
@@ -405,6 +405,8 @@ class XmlReader {
|
|
|
405
405
|
this.stats.language = 'ts';
|
|
406
406
|
if (file.endsWith('.cs'))
|
|
407
407
|
this.stats.language = 'csharp';
|
|
408
|
+
if (file.endsWith('.dart'))
|
|
409
|
+
this.stats.language = 'dart';
|
|
408
410
|
}
|
|
409
411
|
if (!fs_1.default.existsSync(file)) {
|
|
410
412
|
debug('Failed to open file with the source code', file);
|
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';
|
|
@@ -87,6 +87,11 @@ program
|
|
|
87
87
|
process.exit(1);
|
|
88
88
|
}
|
|
89
89
|
|
|
90
|
+
// tests are executed later, so report the run as pending with the tests it was scoped to:
|
|
91
|
+
// pipes add their report now and replace it when the run is finished
|
|
92
|
+
const plannedTests = (client.pipeStore.preparedTestIds || []).map(id => ({ test_id: id, title: id }));
|
|
93
|
+
await client.updateRunStatus('pending', { tests: plannedTests });
|
|
94
|
+
|
|
90
95
|
// stdout carries ONLY the run id so it can be captured: RUN_ID=$(reporter start)
|
|
91
96
|
console.log(runId);
|
|
92
97
|
process.exit(0);
|
|
@@ -107,8 +112,11 @@ program
|
|
|
107
112
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config.TESTOMATIO;
|
|
108
113
|
const client = new TestomatClient({ apiKey });
|
|
109
114
|
|
|
115
|
+
const finishParams = {};
|
|
116
|
+
if (transformEnvVarToBoolean(process.env.TESTOMATIO_FINISH_SHARED_RUN)) finishParams.force_finish_shared_run = true;
|
|
117
|
+
|
|
110
118
|
// @ts-ignore
|
|
111
|
-
client.updateRunStatus(STATUS.FINISHED).then(() => {
|
|
119
|
+
client.updateRunStatus(STATUS.FINISHED, finishParams).then(() => {
|
|
112
120
|
process.exit(0);
|
|
113
121
|
});
|
|
114
122
|
});
|
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
|
/**
|
|
@@ -371,7 +371,7 @@ class Client {
|
|
|
371
371
|
/**
|
|
372
372
|
*
|
|
373
373
|
* Updates the status of the current test run and finishes the run.
|
|
374
|
-
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
374
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished' | 'pending'} status - The status of the current test run.
|
|
375
375
|
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
376
376
|
* Must be one of "passed", "failed", or "finished"
|
|
377
377
|
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
@@ -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/html.js
CHANGED
|
@@ -268,7 +268,7 @@ class HtmlPipe {
|
|
|
268
268
|
executionTime: testExecutionSumTime(aggregatedTests),
|
|
269
269
|
executionDate: getCurrentDateTimeFormatted(),
|
|
270
270
|
description:
|
|
271
|
-
[runParams.description || this.store.coverageDescription || this.store.description
|
|
271
|
+
[this.description, runParams.description || this.store.coverageDescription || this.store.description]
|
|
272
272
|
.filter(Boolean)
|
|
273
273
|
.join('\n\n') || '',
|
|
274
274
|
configuration: buildDisplayConfiguration(
|
package/src/pipe/markdown.js
CHANGED
|
@@ -140,7 +140,7 @@ class MarkdownPipe {
|
|
|
140
140
|
executionTime: testExecutionSumTime(aggregated),
|
|
141
141
|
executionDate: getCurrentDateTimeFormatted(),
|
|
142
142
|
description:
|
|
143
|
-
[runParams?.description || this.store.coverageDescription || this.store.description
|
|
143
|
+
[this.description, runParams?.description || this.store.coverageDescription || this.store.description]
|
|
144
144
|
.filter(Boolean)
|
|
145
145
|
.join('\n\n') || '',
|
|
146
146
|
configuration: this.configuration || this.store.configuration || runParams?.configuration || null,
|
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
|
}
|
|
@@ -273,8 +279,9 @@ class TestomatioPipe {
|
|
|
273
279
|
suites: coverageConfiguration.suites?.map(id => id.replace(/^S/, '')) || [],
|
|
274
280
|
};
|
|
275
281
|
}
|
|
276
|
-
// Run description:
|
|
277
|
-
|
|
282
|
+
// Run description: the user-provided TESTOMATIO_DESCRIPTION with the coverage-derived block
|
|
283
|
+
// (if any) added after it. Neither overrides the other.
|
|
284
|
+
const description = [this.description, coverageDescription].filter(Boolean).join('\n\n') || null;
|
|
278
285
|
|
|
279
286
|
// Merge caller-supplied configuration (e.g. { exploratory: true }) into runParams.configuration.
|
|
280
287
|
// Caller values win on key conflict; coverage-derived tests/suites lists are preserved when not overridden.
|
|
@@ -552,6 +559,9 @@ class TestomatioPipe {
|
|
|
552
559
|
|
|
553
560
|
const { status } = params;
|
|
554
561
|
|
|
562
|
+
// a pending run was just created: nothing to update here, only other pipes report it
|
|
563
|
+
if (status === 'pending') return;
|
|
564
|
+
|
|
555
565
|
let status_event;
|
|
556
566
|
|
|
557
567
|
if (status === STATUS.FINISHED) status_event = 'finish';
|
|
@@ -569,6 +579,7 @@ class TestomatioPipe {
|
|
|
569
579
|
status_event,
|
|
570
580
|
detach: params.detach,
|
|
571
581
|
tests: params.tests,
|
|
582
|
+
...(params.force_finish_shared_run && { force_finish_shared_run: true }),
|
|
572
583
|
},
|
|
573
584
|
});
|
|
574
585
|
|
package/src/utils/pipe_utils.js
CHANGED
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 {
|
|
@@ -286,6 +289,8 @@ export enum RunStatus {
|
|
|
286
289
|
Passed = 'passed',
|
|
287
290
|
Failed = 'failed',
|
|
288
291
|
Finished = 'finished',
|
|
292
|
+
/** run created but not executed yet, reported by `reporter start` */
|
|
293
|
+
Pending = 'pending',
|
|
289
294
|
}
|
|
290
295
|
|
|
291
296
|
/** Batch upload strategy:
|