@testomatio/reporter 2.8.5 → 2.9.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/adapter/codecept.js +19 -0
- package/lib/bin/cli.js +1 -1
- package/lib/bin/uploadArtifacts.js +2 -1
- package/lib/constants.d.ts +6 -0
- package/lib/constants.js +11 -1
- package/lib/pipe/coverage.js +18 -6
- package/lib/pipe/debug.d.ts +12 -9
- package/lib/pipe/debug.js +31 -40
- package/lib/pipe/testomatio.d.ts +13 -7
- package/lib/pipe/testomatio.js +21 -12
- package/lib/replay.js +1 -1
- package/lib/xmlReader.d.ts +1 -1
- package/lib/xmlReader.js +3 -4
- package/package.json +1 -1
- package/src/adapter/codecept.js +24 -2
- package/src/bin/cli.js +2 -2
- package/src/bin/uploadArtifacts.js +2 -1
- package/src/constants.js +9 -0
- package/src/pipe/coverage.js +25 -7
- package/src/pipe/debug.js +35 -36
- package/src/pipe/testomatio.js +22 -13
- package/src/replay.js +2 -2
- package/src/xmlReader.js +4 -5
- package/types/types.d.ts +10 -3
package/lib/adapter/codecept.js
CHANGED
|
@@ -43,6 +43,7 @@ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
|
|
|
43
43
|
}
|
|
44
44
|
function CodeceptReporter(config) {
|
|
45
45
|
const failedTests = [];
|
|
46
|
+
const reportedTestUids = new Set();
|
|
46
47
|
let videos = [];
|
|
47
48
|
let traces = [];
|
|
48
49
|
const reportTestPromises = [];
|
|
@@ -141,6 +142,7 @@ function CodeceptReporter(config) {
|
|
|
141
142
|
return;
|
|
142
143
|
const error = hook?.ctx?.currentTest?.err;
|
|
143
144
|
for (const test of suite.tests) {
|
|
145
|
+
reportedTestUids.add(test.uid);
|
|
144
146
|
const reportTestPromise = client.addTestRun('failed', {
|
|
145
147
|
...stripExampleFromTitle(test.title),
|
|
146
148
|
rid: test.uid,
|
|
@@ -171,9 +173,26 @@ function CodeceptReporter(config) {
|
|
|
171
173
|
await finalizeRun('all.after');
|
|
172
174
|
});
|
|
173
175
|
});
|
|
176
|
+
event.dispatcher.on(event.test.skipped, test => {
|
|
177
|
+
const { uid, tags, title } = test.simplify();
|
|
178
|
+
if (uid && reportedTestUids.has(uid))
|
|
179
|
+
return;
|
|
180
|
+
index_js_1.services.setContext(null);
|
|
181
|
+
const reportTestPromise = client.addTestRun(constants_js_1.STATUS.SKIPPED, {
|
|
182
|
+
...stripExampleFromTitle(title),
|
|
183
|
+
rid: uid,
|
|
184
|
+
test_id: (0, utils_js_1.getTestomatIdFromTestTitle)(`${title} ${tags?.join(' ')}`),
|
|
185
|
+
suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
|
|
186
|
+
time: test.duration,
|
|
187
|
+
meta: test.meta,
|
|
188
|
+
});
|
|
189
|
+
reportTestPromises.push(reportTestPromise);
|
|
190
|
+
reportedTestUids.add(uid);
|
|
191
|
+
});
|
|
174
192
|
event.dispatcher.on(event.test.after, test => {
|
|
175
193
|
const { uid, tags, title, artifacts } = test.simplify();
|
|
176
194
|
const error = test.err || null;
|
|
195
|
+
reportedTestUids.add(uid);
|
|
177
196
|
failedTests.push(uid || title);
|
|
178
197
|
const testObj = getTestAndMessage(title);
|
|
179
198
|
const files = buildArtifactFiles(artifacts);
|
package/lib/bin/cli.js
CHANGED
|
@@ -333,7 +333,7 @@ program
|
|
|
333
333
|
const client = new client_js_1.default({
|
|
334
334
|
apiKey,
|
|
335
335
|
runId,
|
|
336
|
-
|
|
336
|
+
batchMode: constants_js_1.BATCH_MODE.DISABLED,
|
|
337
337
|
});
|
|
338
338
|
let testruns = client.uploader.readUploadedFiles(runId);
|
|
339
339
|
const numTotalArtifacts = testruns.length;
|
|
@@ -13,6 +13,7 @@ const config_js_1 = require("../config.js");
|
|
|
13
13
|
const utils_js_2 = require("../utils/utils.js");
|
|
14
14
|
const dotenv_1 = __importDefault(require("dotenv"));
|
|
15
15
|
const log_js_1 = require("../utils/log.js");
|
|
16
|
+
const constants_js_1 = require("../constants.js");
|
|
16
17
|
const debug = (0, debug_1.default)('@testomatio/reporter:upload-cli');
|
|
17
18
|
const version = (0, utils_js_1.getPackageVersion)();
|
|
18
19
|
console.log(picocolors_1.default.cyan(picocolors_1.default.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
@@ -37,7 +38,7 @@ program
|
|
|
37
38
|
const client = new client_js_1.default({
|
|
38
39
|
apiKey,
|
|
39
40
|
runId,
|
|
40
|
-
|
|
41
|
+
batchMode: constants_js_1.BATCH_MODE.DISABLED,
|
|
41
42
|
});
|
|
42
43
|
let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
|
|
43
44
|
const numTotalArtifacts = testruns.length;
|
package/lib/constants.d.ts
CHANGED
|
@@ -10,6 +10,12 @@ export namespace STATUS {
|
|
|
10
10
|
let SKIPPED: string;
|
|
11
11
|
let FINISHED: string;
|
|
12
12
|
}
|
|
13
|
+
/** @type {{ AUTO: 'auto', MANUAL: 'manual', DISABLED: 'disabled' }} */
|
|
14
|
+
export const BATCH_MODE: {
|
|
15
|
+
AUTO: "auto";
|
|
16
|
+
MANUAL: "manual";
|
|
17
|
+
DISABLED: "disabled";
|
|
18
|
+
};
|
|
13
19
|
export namespace HTML_REPORT {
|
|
14
20
|
let FOLDER: string;
|
|
15
21
|
let REPORT_DEFAULT_NAME: string;
|
package/lib/constants.js
CHANGED
|
@@ -3,7 +3,7 @@ var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
|
3
3
|
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
4
|
};
|
|
5
5
|
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
-
exports.DEBUG_FILE = exports.SCREENSHOTS_ON_STEPS = exports.REPORTER_REQUEST_RETRIES = exports.testomatLogoURL = exports.REQUEST_TIMEOUT = exports.MARKDOWN_REPORT = exports.HTML_REPORT = exports.STATUS = exports.CSV_HEADERS = exports.TESTOMAT_TMP_STORAGE_DIR = exports.APP_PREFIX = void 0;
|
|
6
|
+
exports.DEBUG_FILE = exports.SCREENSHOTS_ON_STEPS = exports.REPORTER_REQUEST_RETRIES = exports.testomatLogoURL = exports.REQUEST_TIMEOUT = exports.MARKDOWN_REPORT = exports.HTML_REPORT = exports.BATCH_MODE = exports.STATUS = exports.CSV_HEADERS = exports.TESTOMAT_TMP_STORAGE_DIR = exports.APP_PREFIX = void 0;
|
|
7
7
|
exports.getCreateRunRequestTimeout = getCreateRunRequestTimeout;
|
|
8
8
|
const picocolors_1 = __importDefault(require("picocolors"));
|
|
9
9
|
const os_1 = __importDefault(require("os"));
|
|
@@ -37,6 +37,14 @@ const STATUS = {
|
|
|
37
37
|
FINISHED: 'finished',
|
|
38
38
|
};
|
|
39
39
|
exports.STATUS = STATUS;
|
|
40
|
+
// batch upload mode
|
|
41
|
+
/** @type {{ AUTO: 'auto', MANUAL: 'manual', DISABLED: 'disabled' }} */
|
|
42
|
+
const BATCH_MODE = {
|
|
43
|
+
AUTO: 'auto',
|
|
44
|
+
MANUAL: 'manual',
|
|
45
|
+
DISABLED: 'disabled',
|
|
46
|
+
};
|
|
47
|
+
exports.BATCH_MODE = BATCH_MODE;
|
|
40
48
|
// html pipe var
|
|
41
49
|
const HTML_REPORT = {
|
|
42
50
|
FOLDER: 'html-report',
|
|
@@ -79,6 +87,8 @@ module.exports.CSV_HEADERS = CSV_HEADERS;
|
|
|
79
87
|
|
|
80
88
|
module.exports.STATUS = STATUS;
|
|
81
89
|
|
|
90
|
+
module.exports.BATCH_MODE = BATCH_MODE;
|
|
91
|
+
|
|
82
92
|
module.exports.HTML_REPORT = HTML_REPORT;
|
|
83
93
|
|
|
84
94
|
module.exports.MARKDOWN_REPORT = MARKDOWN_REPORT;
|
package/lib/pipe/coverage.js
CHANGED
|
@@ -221,9 +221,10 @@ class CoveragePipe {
|
|
|
221
221
|
*/
|
|
222
222
|
#getChangedFilesFromGit(cmd) {
|
|
223
223
|
try {
|
|
224
|
+
// Capture stderr (instead of ignoring it) so Git's actual error is available for diagnostics
|
|
224
225
|
const result = (0, child_process_1.execSync)(cmd, {
|
|
225
226
|
encoding: 'utf-8',
|
|
226
|
-
stdio: ['pipe', 'pipe', '
|
|
227
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
227
228
|
});
|
|
228
229
|
return result
|
|
229
230
|
.split('\n')
|
|
@@ -231,15 +232,26 @@ class CoveragePipe {
|
|
|
231
232
|
.filter(Boolean);
|
|
232
233
|
}
|
|
233
234
|
catch (err) {
|
|
234
|
-
|
|
235
|
-
|
|
235
|
+
// Prefer Git's own stderr output, fall back to the generic error message
|
|
236
|
+
const gitOutput = (err.stderr || '').toString().trim();
|
|
237
|
+
const errorMessage = gitOutput || err.message || '';
|
|
238
|
+
// Git edge: Not a git repository
|
|
236
239
|
if (errorMessage.includes('Not a git repository')) {
|
|
237
240
|
log_js_1.log.error('❌ Error: This folder is not a Git repository.');
|
|
241
|
+
return [];
|
|
238
242
|
}
|
|
239
|
-
|
|
240
|
-
|
|
243
|
+
// Git edge: the branch/ref to diff against is not available locally.
|
|
244
|
+
// This is common in CI, where a shallow checkout fetches only the current branch.
|
|
245
|
+
if (errorMessage.includes('unknown revision') ||
|
|
246
|
+
errorMessage.includes('ambiguous argument') ||
|
|
247
|
+
errorMessage.includes('bad revision')) {
|
|
248
|
+
log_js_1.log.error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
249
|
+
log_js_1.log.error(`🔍 Branch "${this.branch}" was not found locally. ` +
|
|
250
|
+
`In CI this usually means a shallow checkout — fetch full history first, e.g. ` +
|
|
251
|
+
`actions/checkout with "fetch-depth: 0", or run "git fetch origin ${this.branch}:${this.branch}".`);
|
|
252
|
+
return [];
|
|
241
253
|
}
|
|
242
|
-
|
|
254
|
+
throw new Error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
243
255
|
}
|
|
244
256
|
}
|
|
245
257
|
/**
|
package/lib/pipe/debug.d.ts
CHANGED
|
@@ -3,18 +3,13 @@ export class DebugPipe {
|
|
|
3
3
|
params: any;
|
|
4
4
|
store: any;
|
|
5
5
|
isEnabled: boolean;
|
|
6
|
-
|
|
7
|
-
isEnabled: any;
|
|
8
|
-
intervalFunction: any;
|
|
9
|
-
intervalTime: number;
|
|
10
|
-
tests: any[];
|
|
11
|
-
batchIndex: number;
|
|
12
|
-
};
|
|
6
|
+
tests: any[];
|
|
13
7
|
logFilePath: string;
|
|
14
8
|
rootPath: string;
|
|
15
9
|
historyDir: string;
|
|
16
10
|
testomatioEnvVars: {};
|
|
17
|
-
|
|
11
|
+
flushOnExit: () => void;
|
|
12
|
+
exitListenerAttached: boolean;
|
|
18
13
|
/**
|
|
19
14
|
* Logs data to a file if logging is enabled.
|
|
20
15
|
*
|
|
@@ -24,9 +19,17 @@ export class DebugPipe {
|
|
|
24
19
|
logToFile(logData: any): Promise<void>;
|
|
25
20
|
lastActionTimestamp: number;
|
|
26
21
|
prepareRun(opts: any): Promise<any[]>;
|
|
27
|
-
createRun(params?: {}): Promise<
|
|
22
|
+
createRun(params?: {}): Promise<void>;
|
|
28
23
|
addTest(data: any): Promise<void>;
|
|
29
24
|
finishRun(params: any): Promise<void>;
|
|
30
25
|
sync(): Promise<void>;
|
|
26
|
+
/**
|
|
27
|
+
* Writes any buffered tests to the debug file as a single batch.
|
|
28
|
+
* Runs synchronously so it can also be invoked from a process `exit` handler,
|
|
29
|
+
* which is the only chance to persist tests when a hook failure (e.g. a failing
|
|
30
|
+
* AfterSuite) prevents `finishRun` from being reached. Idempotent: the buffer is
|
|
31
|
+
* drained on flush, so a later `finishRun`/exit flush is a no-op.
|
|
32
|
+
*/
|
|
33
|
+
flushBufferedTests(): void;
|
|
31
34
|
toString(): string;
|
|
32
35
|
}
|
package/lib/pipe/debug.js
CHANGED
|
@@ -17,13 +17,7 @@ class DebugPipe {
|
|
|
17
17
|
this.store = store || {};
|
|
18
18
|
this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
|
|
19
19
|
if (this.isEnabled) {
|
|
20
|
-
this.
|
|
21
|
-
isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
|
|
22
|
-
intervalFunction: null,
|
|
23
|
-
intervalTime: 5000,
|
|
24
|
-
tests: [],
|
|
25
|
-
batchIndex: 0,
|
|
26
|
-
};
|
|
20
|
+
this.tests = [];
|
|
27
21
|
const suffix = process.env.TESTOMATIO_REPLAY ? 'replay' : '';
|
|
28
22
|
const paths = (0, debug_js_1.getDebugFilePath)(suffix);
|
|
29
23
|
this.logFilePath = paths.tmp;
|
|
@@ -63,8 +57,12 @@ class DebugPipe {
|
|
|
63
57
|
this.logToFile({ datetime: new Date().toISOString(), timestamp: Date.now() });
|
|
64
58
|
this.logToFile({ data: 'variables', testomatioEnvVars: this.testomatioEnvVars });
|
|
65
59
|
this.logToFile({ data: 'store', store: this.store || {} });
|
|
66
|
-
//
|
|
67
|
-
|
|
60
|
+
// Safety net for hook failures (e.g. a failing AfterSuite) that abort the run
|
|
61
|
+
// before finishRun: buffered tests would otherwise be lost. The handler is
|
|
62
|
+
// attached lazily when the first test is buffered and detached once flushed,
|
|
63
|
+
// so processes that create many pipes don't pile up `exit` listeners.
|
|
64
|
+
this.flushOnExit = () => this.flushBufferedTests();
|
|
65
|
+
this.exitListenerAttached = false;
|
|
68
66
|
}
|
|
69
67
|
}
|
|
70
68
|
/**
|
|
@@ -89,46 +87,21 @@ class DebugPipe {
|
|
|
89
87
|
async createRun(params = {}) {
|
|
90
88
|
if (!this.isEnabled)
|
|
91
89
|
return;
|
|
92
|
-
if (params.isBatchEnabled === true || params.isBatchEnabled === false)
|
|
93
|
-
this.batch.isEnabled = params.isBatchEnabled;
|
|
94
|
-
if (!this.isEnabled)
|
|
95
|
-
return {};
|
|
96
|
-
if (this.batch.isEnabled)
|
|
97
|
-
this.batch.intervalFunction = setInterval(this.batchUpload, this.batch.intervalTime);
|
|
98
90
|
this.logToFile({ action: 'createRun', params });
|
|
99
91
|
}
|
|
100
92
|
async addTest(data) {
|
|
101
93
|
if (!this.isEnabled)
|
|
102
94
|
return;
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
|
|
107
|
-
this.logToFile(logData);
|
|
95
|
+
this.tests.push(data);
|
|
96
|
+
if (!this.exitListenerAttached) {
|
|
97
|
+
process.once('exit', this.flushOnExit);
|
|
98
|
+
this.exitListenerAttached = true;
|
|
108
99
|
}
|
|
109
|
-
else
|
|
110
|
-
this.batch.tests.push(data);
|
|
111
|
-
if (!this.batch.intervalFunction)
|
|
112
|
-
await this.batchUpload();
|
|
113
|
-
}
|
|
114
|
-
async batchUpload() {
|
|
115
|
-
this.batch.batchIndex++;
|
|
116
|
-
if (!this.batch.isEnabled)
|
|
117
|
-
return;
|
|
118
|
-
if (!this.batch.tests.length)
|
|
119
|
-
return;
|
|
120
|
-
const testsToSend = this.batch.tests.splice(0);
|
|
121
|
-
const logData = { action: 'addTestsBatch', tests: testsToSend };
|
|
122
|
-
if (this.store.runId)
|
|
123
|
-
logData.runId = this.store.runId;
|
|
124
|
-
this.logToFile(logData);
|
|
125
100
|
}
|
|
126
101
|
async finishRun(params) {
|
|
127
102
|
if (!this.isEnabled)
|
|
128
103
|
return;
|
|
129
104
|
await this.sync();
|
|
130
|
-
if (this.batch.intervalFunction)
|
|
131
|
-
clearInterval(this.batch.intervalFunction);
|
|
132
105
|
const logData = { action: 'finishRun', params };
|
|
133
106
|
if (this.store.runId)
|
|
134
107
|
logData.runId = this.store.runId;
|
|
@@ -137,9 +110,27 @@ class DebugPipe {
|
|
|
137
110
|
log_js_1.log.info(`History: ${this.historyDir}`);
|
|
138
111
|
}
|
|
139
112
|
async sync() {
|
|
140
|
-
|
|
113
|
+
this.flushBufferedTests();
|
|
114
|
+
}
|
|
115
|
+
/**
|
|
116
|
+
* Writes any buffered tests to the debug file as a single batch.
|
|
117
|
+
* Runs synchronously so it can also be invoked from a process `exit` handler,
|
|
118
|
+
* which is the only chance to persist tests when a hook failure (e.g. a failing
|
|
119
|
+
* AfterSuite) prevents `finishRun` from being reached. Idempotent: the buffer is
|
|
120
|
+
* drained on flush, so a later `finishRun`/exit flush is a no-op.
|
|
121
|
+
*/
|
|
122
|
+
flushBufferedTests() {
|
|
123
|
+
if (!this.isEnabled || !this.tests.length)
|
|
141
124
|
return;
|
|
142
|
-
|
|
125
|
+
const tests = this.tests.splice(0);
|
|
126
|
+
const logData = { action: 'addTestsBatch', tests };
|
|
127
|
+
if (this.store.runId)
|
|
128
|
+
logData.runId = this.store.runId;
|
|
129
|
+
this.logToFile(logData);
|
|
130
|
+
if (this.exitListenerAttached) {
|
|
131
|
+
process.removeListener('exit', this.flushOnExit);
|
|
132
|
+
this.exitListenerAttached = false;
|
|
133
|
+
}
|
|
143
134
|
}
|
|
144
135
|
toString() {
|
|
145
136
|
return 'Debug Reporter';
|
package/lib/pipe/testomatio.d.ts
CHANGED
|
@@ -1,16 +1,26 @@
|
|
|
1
1
|
export default TestomatioPipe;
|
|
2
2
|
export type Pipe = import("../../types/types.js").Pipe;
|
|
3
3
|
export type TestData = import("../../types/types.js").TestData;
|
|
4
|
+
export type BatchMode = import("../../types/types.js").BatchMode;
|
|
5
|
+
export type CreateRunParams = import("../../types/types.js").CreateRunParams;
|
|
4
6
|
/**
|
|
5
7
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
6
8
|
* @typedef {import('../../types/types.js').TestData} TestData
|
|
9
|
+
* @typedef {import('../../types/types.js').BatchMode} BatchMode
|
|
10
|
+
* @typedef {import('../../types/types.js').CreateRunParams} CreateRunParams
|
|
7
11
|
* @class TestomatioPipe
|
|
8
12
|
* @implements {Pipe}
|
|
9
13
|
*/
|
|
10
14
|
declare class TestomatioPipe implements Pipe {
|
|
11
15
|
constructor(params: any, store: any);
|
|
12
16
|
batch: {
|
|
13
|
-
|
|
17
|
+
/** @type {BatchMode}
|
|
18
|
+
* Batch upload mode:
|
|
19
|
+
* - `auto`: upload tests automatically by time interval (e.g. every 5 seconds).
|
|
20
|
+
* - `manual`: buffer tests and upload only when `sync()` is invoked manually.
|
|
21
|
+
* - `disabled`: send one test per request, no batching.
|
|
22
|
+
*/
|
|
23
|
+
mode: BatchMode;
|
|
14
24
|
intervalFunction: any;
|
|
15
25
|
intervalTime: number;
|
|
16
26
|
tests: any[];
|
|
@@ -50,14 +60,10 @@ declare class TestomatioPipe implements Pipe {
|
|
|
50
60
|
prepareRun(opts: any): Promise<string[]>;
|
|
51
61
|
/**
|
|
52
62
|
* Creates a new run on Testomat.io
|
|
53
|
-
* @param {
|
|
63
|
+
* @param {CreateRunParams} params
|
|
54
64
|
* @returns Promise<void>
|
|
55
65
|
*/
|
|
56
|
-
createRun(params?:
|
|
57
|
-
isBatchEnabled?: boolean;
|
|
58
|
-
kind?: string;
|
|
59
|
-
configuration?: Record<string, any>;
|
|
60
|
-
}): Promise<void>;
|
|
66
|
+
createRun(params?: CreateRunParams): Promise<void>;
|
|
61
67
|
runUrl: string;
|
|
62
68
|
runPublicUrl: any;
|
|
63
69
|
/**
|
package/lib/pipe/testomatio.js
CHANGED
|
@@ -38,15 +38,23 @@ function parseCiParams(raw) {
|
|
|
38
38
|
/**
|
|
39
39
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
40
40
|
* @typedef {import('../../types/types.js').TestData} TestData
|
|
41
|
+
* @typedef {import('../../types/types.js').BatchMode} BatchMode
|
|
42
|
+
* @typedef {import('../../types/types.js').CreateRunParams} CreateRunParams
|
|
41
43
|
* @class TestomatioPipe
|
|
42
44
|
* @implements {Pipe}
|
|
43
45
|
*/
|
|
44
46
|
class TestomatioPipe {
|
|
45
47
|
constructor(params, store) {
|
|
46
48
|
this.batch = {
|
|
47
|
-
|
|
49
|
+
/** @type {BatchMode}
|
|
50
|
+
* Batch upload mode:
|
|
51
|
+
* - `auto`: upload tests automatically by time interval (e.g. every 5 seconds).
|
|
52
|
+
* - `manual`: buffer tests and upload only when `sync()` is invoked manually.
|
|
53
|
+
* - `disabled`: send one test per request, no batching.
|
|
54
|
+
*/
|
|
55
|
+
mode: params.batchMode || (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? constants_js_1.BATCH_MODE.DISABLED : constants_js_1.BATCH_MODE.AUTO),
|
|
48
56
|
intervalFunction: null, // will be created in createRun by setInterval function
|
|
49
|
-
intervalTime:
|
|
57
|
+
intervalTime: 6000, // how often tests are sent
|
|
50
58
|
tests: [], // array of tests in batch
|
|
51
59
|
batchIndex: 0, // represents the current batch index (starts from 1 and increments by 1 for each batch)
|
|
52
60
|
numberOfTimesCalledWithoutTests: 0, // how many times batch was called without tests
|
|
@@ -193,14 +201,15 @@ class TestomatioPipe {
|
|
|
193
201
|
}
|
|
194
202
|
/**
|
|
195
203
|
* Creates a new run on Testomat.io
|
|
196
|
-
* @param {
|
|
204
|
+
* @param {CreateRunParams} params
|
|
197
205
|
* @returns Promise<void>
|
|
198
206
|
*/
|
|
199
207
|
async createRun(params = {}) {
|
|
200
|
-
|
|
208
|
+
if (params.batchMode)
|
|
209
|
+
this.batch.mode = params.batchMode;
|
|
201
210
|
if (!this.isEnabled)
|
|
202
211
|
return;
|
|
203
|
-
if (this.batch.
|
|
212
|
+
if (this.batch.mode === constants_js_1.BATCH_MODE.AUTO && this.isEnabled)
|
|
204
213
|
this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
205
214
|
if (this.store) {
|
|
206
215
|
this.store.runKind = params.kind;
|
|
@@ -391,7 +400,7 @@ class TestomatioPipe {
|
|
|
391
400
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
392
401
|
*/
|
|
393
402
|
#batchUpload = async () => {
|
|
394
|
-
if (
|
|
403
|
+
if (this.batch.mode === constants_js_1.BATCH_MODE.DISABLED)
|
|
395
404
|
return;
|
|
396
405
|
if (!this.batch.tests.length)
|
|
397
406
|
return;
|
|
@@ -401,7 +410,7 @@ class TestomatioPipe {
|
|
|
401
410
|
if (this.batch.numberOfTimesCalledWithoutTests > 10) {
|
|
402
411
|
debug('📨 Batch upload: no tests to send for 10 times, stopping batch');
|
|
403
412
|
clearInterval(this.batch.intervalFunction);
|
|
404
|
-
this.batch.
|
|
413
|
+
this.batch.mode = constants_js_1.BATCH_MODE.DISABLED;
|
|
405
414
|
}
|
|
406
415
|
if (!this.batch.tests.length) {
|
|
407
416
|
debug('📨 Batch upload: no tests to send');
|
|
@@ -454,12 +463,12 @@ class TestomatioPipe {
|
|
|
454
463
|
}
|
|
455
464
|
this.#formatData(data);
|
|
456
465
|
let uploading = null;
|
|
457
|
-
if (
|
|
466
|
+
if (this.batch.mode === constants_js_1.BATCH_MODE.DISABLED)
|
|
458
467
|
uploading = this.#uploadSingleTest(data);
|
|
459
468
|
else
|
|
460
469
|
this.batch.tests.push(data);
|
|
461
|
-
//
|
|
462
|
-
if (!this.batch.intervalFunction)
|
|
470
|
+
// auto mode but no interval running yet (e.g. createRun hasn't started it): flush immediately
|
|
471
|
+
if (this.batch.mode === constants_js_1.BATCH_MODE.AUTO && !this.batch.intervalFunction)
|
|
463
472
|
uploading = this.#batchUpload();
|
|
464
473
|
// return promise to be able to wait for it
|
|
465
474
|
return uploading;
|
|
@@ -487,7 +496,7 @@ class TestomatioPipe {
|
|
|
487
496
|
// (e.g. if test has artifacts, add test function will be invoked only after artifacts are uploaded)
|
|
488
497
|
// batch stops working after run is finished; thus, disable it to use single test uploading
|
|
489
498
|
this.batch.intervalFunction = null;
|
|
490
|
-
this.batch.
|
|
499
|
+
this.batch.mode = constants_js_1.BATCH_MODE.DISABLED;
|
|
491
500
|
}
|
|
492
501
|
debug('Finishing run...');
|
|
493
502
|
if (this.reportingCanceledDueToReqFailures) {
|
|
@@ -549,7 +558,7 @@ class TestomatioPipe {
|
|
|
549
558
|
if (this.batch.intervalFunction) {
|
|
550
559
|
clearInterval(this.batch.intervalFunction);
|
|
551
560
|
this.batch.intervalFunction = null;
|
|
552
|
-
this.batch.
|
|
561
|
+
this.batch.mode = constants_js_1.BATCH_MODE.DISABLED;
|
|
553
562
|
}
|
|
554
563
|
this.batch.tests = [];
|
|
555
564
|
}
|
package/lib/replay.js
CHANGED
|
@@ -210,7 +210,7 @@ class Replay {
|
|
|
210
210
|
process.env.TESTOMATIO_REPLAY = '1';
|
|
211
211
|
const client = new client_js_1.default({
|
|
212
212
|
apiKey: this.apiKey,
|
|
213
|
-
|
|
213
|
+
batchMode: constants_js_1.BATCH_MODE.AUTO,
|
|
214
214
|
...runParams,
|
|
215
215
|
...(runId && { runId }),
|
|
216
216
|
});
|
package/lib/xmlReader.d.ts
CHANGED
package/lib/xmlReader.js
CHANGED
|
@@ -51,8 +51,7 @@ class XmlReader {
|
|
|
51
51
|
env: TESTOMATIO_ENV,
|
|
52
52
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
53
53
|
detach: TESTOMATIO_MARK_DETACHED,
|
|
54
|
-
|
|
55
|
-
isBatchEnabled: false,
|
|
54
|
+
batchMode: constants_js_1.BATCH_MODE.MANUAL,
|
|
56
55
|
};
|
|
57
56
|
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
58
57
|
this.adapter = (0, index_js_2.default)(opts.lang?.toLowerCase(), opts);
|
|
@@ -467,7 +466,7 @@ class XmlReader {
|
|
|
467
466
|
title: this.requestParams.title,
|
|
468
467
|
env: this.requestParams.env,
|
|
469
468
|
group_title: this.requestParams.group_title,
|
|
470
|
-
|
|
469
|
+
batchMode: this.requestParams.batchMode,
|
|
471
470
|
};
|
|
472
471
|
debug('Run', runParams);
|
|
473
472
|
this.pipes = this.pipes || (await this.pipesPromise);
|
|
@@ -521,7 +520,7 @@ class XmlReader {
|
|
|
521
520
|
this.formatTests();
|
|
522
521
|
this.pipes = this.pipes || (await this.pipesPromise);
|
|
523
522
|
// Create run before uploading tests to ensure runId is set
|
|
524
|
-
await this.createRun();
|
|
523
|
+
// await this.createRun(); // makes reporting stuck after finish, thus commenting out
|
|
525
524
|
if (!this.tests || !Array.isArray(this.tests) || this.tests.length === 0) {
|
|
526
525
|
debug('No tests to upload, finishing run');
|
|
527
526
|
const finishData = {
|
package/package.json
CHANGED
package/src/adapter/codecept.js
CHANGED
|
@@ -48,6 +48,7 @@ if (MAJOR_VERSION === 3 && MINOR_VERSION < 7) {
|
|
|
48
48
|
|
|
49
49
|
function CodeceptReporter(config) {
|
|
50
50
|
const failedTests = [];
|
|
51
|
+
const reportedTestUids = new Set();
|
|
51
52
|
let videos = [];
|
|
52
53
|
let traces = [];
|
|
53
54
|
const reportTestPromises = [];
|
|
@@ -161,6 +162,7 @@ function CodeceptReporter(config) {
|
|
|
161
162
|
const error = hook?.ctx?.currentTest?.err;
|
|
162
163
|
|
|
163
164
|
for (const test of suite.tests) {
|
|
165
|
+
reportedTestUids.add(test.uid);
|
|
164
166
|
const reportTestPromise = client.addTestRun('failed', {
|
|
165
167
|
...stripExampleFromTitle(test.title),
|
|
166
168
|
rid: test.uid,
|
|
@@ -197,9 +199,29 @@ function CodeceptReporter(config) {
|
|
|
197
199
|
});
|
|
198
200
|
});
|
|
199
201
|
|
|
202
|
+
event.dispatcher.on(event.test.skipped, test => {
|
|
203
|
+
const { uid, tags, title } = test.simplify();
|
|
204
|
+
|
|
205
|
+
if (uid && reportedTestUids.has(uid)) return;
|
|
206
|
+
|
|
207
|
+
services.setContext(null);
|
|
208
|
+
|
|
209
|
+
const reportTestPromise = client.addTestRun(STATUS.SKIPPED, {
|
|
210
|
+
...stripExampleFromTitle(title),
|
|
211
|
+
rid: uid,
|
|
212
|
+
test_id: getTestomatIdFromTestTitle(`${title} ${tags?.join(' ')}`),
|
|
213
|
+
suite_title: test.parent && stripTagsFromTitle(stripExampleFromTitle(test.parent.title).title),
|
|
214
|
+
time: test.duration,
|
|
215
|
+
meta: test.meta,
|
|
216
|
+
});
|
|
217
|
+
reportTestPromises.push(reportTestPromise);
|
|
218
|
+
reportedTestUids.add(uid);
|
|
219
|
+
});
|
|
220
|
+
|
|
200
221
|
event.dispatcher.on(event.test.after, test => {
|
|
201
222
|
const { uid, tags, title, artifacts } = test.simplify();
|
|
202
223
|
const error = test.err || null;
|
|
224
|
+
reportedTestUids.add(uid);
|
|
203
225
|
failedTests.push(uid || title);
|
|
204
226
|
const testObj = getTestAndMessage(title);
|
|
205
227
|
const files = buildArtifactFiles(artifacts);
|
|
@@ -211,8 +233,8 @@ function CodeceptReporter(config) {
|
|
|
211
233
|
|
|
212
234
|
// Build step hierarchy with screenshot from screenshotOnFail
|
|
213
235
|
const stepHierarchy = buildUnifiedStepHierarchy(
|
|
214
|
-
test.steps,
|
|
215
|
-
hookSteps,
|
|
236
|
+
test.steps,
|
|
237
|
+
hookSteps,
|
|
216
238
|
screenshotOnFailPath
|
|
217
239
|
);
|
|
218
240
|
|
package/src/bin/cli.js
CHANGED
|
@@ -6,7 +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 { APP_PREFIX, STATUS, DEBUG_FILE } from '../constants.js';
|
|
9
|
+
import { APP_PREFIX, STATUS, DEBUG_FILE, BATCH_MODE } from '../constants.js';
|
|
10
10
|
import { cleanLatestRunId, getPackageVersion, applyFilter } from '../utils/utils.js';
|
|
11
11
|
import { config } from '../config.js';
|
|
12
12
|
import { readLatestRunId } from '../utils/utils.js';
|
|
@@ -369,7 +369,7 @@ program
|
|
|
369
369
|
const client = new TestomatClient({
|
|
370
370
|
apiKey,
|
|
371
371
|
runId,
|
|
372
|
-
|
|
372
|
+
batchMode: BATCH_MODE.DISABLED,
|
|
373
373
|
});
|
|
374
374
|
|
|
375
375
|
let testruns = client.uploader.readUploadedFiles(runId);
|
|
@@ -9,6 +9,7 @@ import { config } from '../config.js';
|
|
|
9
9
|
import { readLatestRunId } from '../utils/utils.js';
|
|
10
10
|
import dotenv from 'dotenv';
|
|
11
11
|
import { log } from '../utils/log.js';
|
|
12
|
+
import { BATCH_MODE } from '../constants.js';
|
|
12
13
|
|
|
13
14
|
const debug = createDebugMessages('@testomatio/reporter:upload-cli');
|
|
14
15
|
const version = getPackageVersion();
|
|
@@ -37,7 +38,7 @@ program
|
|
|
37
38
|
const client = new TestomatClient({
|
|
38
39
|
apiKey,
|
|
39
40
|
runId,
|
|
40
|
-
|
|
41
|
+
batchMode: BATCH_MODE.DISABLED,
|
|
41
42
|
});
|
|
42
43
|
let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
|
|
43
44
|
|
package/src/constants.js
CHANGED
|
@@ -28,6 +28,14 @@ const STATUS = {
|
|
|
28
28
|
SKIPPED: 'skipped',
|
|
29
29
|
FINISHED: 'finished',
|
|
30
30
|
};
|
|
31
|
+
|
|
32
|
+
// batch upload mode
|
|
33
|
+
/** @type {{ AUTO: 'auto', MANUAL: 'manual', DISABLED: 'disabled' }} */
|
|
34
|
+
const BATCH_MODE = {
|
|
35
|
+
AUTO: 'auto',
|
|
36
|
+
MANUAL: 'manual',
|
|
37
|
+
DISABLED: 'disabled',
|
|
38
|
+
};
|
|
31
39
|
// html pipe var
|
|
32
40
|
const HTML_REPORT = {
|
|
33
41
|
FOLDER: 'html-report',
|
|
@@ -61,6 +69,7 @@ export {
|
|
|
61
69
|
TESTOMAT_TMP_STORAGE_DIR,
|
|
62
70
|
CSV_HEADERS,
|
|
63
71
|
STATUS,
|
|
72
|
+
BATCH_MODE,
|
|
64
73
|
HTML_REPORT,
|
|
65
74
|
MARKDOWN_REPORT,
|
|
66
75
|
REQUEST_TIMEOUT,
|
package/src/pipe/coverage.js
CHANGED
|
@@ -261,9 +261,10 @@ class CoveragePipe { // or Changes for the future???
|
|
|
261
261
|
*/
|
|
262
262
|
#getChangedFilesFromGit(cmd) {
|
|
263
263
|
try {
|
|
264
|
+
// Capture stderr (instead of ignoring it) so Git's actual error is available for diagnostics
|
|
264
265
|
const result = execSync(cmd, {
|
|
265
266
|
encoding: 'utf-8',
|
|
266
|
-
stdio: ['pipe', 'pipe', '
|
|
267
|
+
stdio: ['pipe', 'pipe', 'pipe']
|
|
267
268
|
});
|
|
268
269
|
|
|
269
270
|
return result
|
|
@@ -272,16 +273,33 @@ class CoveragePipe { // or Changes for the future???
|
|
|
272
273
|
.filter(Boolean);
|
|
273
274
|
}
|
|
274
275
|
catch (err) {
|
|
275
|
-
|
|
276
|
-
|
|
276
|
+
// Prefer Git's own stderr output, fall back to the generic error message
|
|
277
|
+
const gitOutput = (err.stderr || '').toString().trim();
|
|
278
|
+
const errorMessage = gitOutput || err.message || '';
|
|
279
|
+
|
|
280
|
+
// Git edge: Not a git repository
|
|
277
281
|
if (errorMessage.includes('Not a git repository')) {
|
|
278
|
-
log.error(
|
|
282
|
+
log.error('❌ Error: This folder is not a Git repository.');
|
|
283
|
+
return [];
|
|
279
284
|
}
|
|
280
|
-
|
|
281
|
-
|
|
285
|
+
|
|
286
|
+
// Git edge: the branch/ref to diff against is not available locally.
|
|
287
|
+
// This is common in CI, where a shallow checkout fetches only the current branch.
|
|
288
|
+
if (
|
|
289
|
+
errorMessage.includes('unknown revision') ||
|
|
290
|
+
errorMessage.includes('ambiguous argument') ||
|
|
291
|
+
errorMessage.includes('bad revision')
|
|
292
|
+
) {
|
|
293
|
+
log.error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
294
|
+
log.error(
|
|
295
|
+
`🔍 Branch "${this.branch}" was not found locally. ` +
|
|
296
|
+
`In CI this usually means a shallow checkout — fetch full history first, e.g. ` +
|
|
297
|
+
`actions/checkout with "fetch-depth: 0", or run "git fetch origin ${this.branch}:${this.branch}".`
|
|
298
|
+
);
|
|
299
|
+
return [];
|
|
282
300
|
}
|
|
283
301
|
|
|
284
|
-
|
|
302
|
+
throw new Error(`❌ Git command failed ("${cmd}"):\n${errorMessage}`);
|
|
285
303
|
}
|
|
286
304
|
}
|
|
287
305
|
|
package/src/pipe/debug.js
CHANGED
|
@@ -14,13 +14,7 @@ export class DebugPipe {
|
|
|
14
14
|
|
|
15
15
|
this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
|
|
16
16
|
if (this.isEnabled) {
|
|
17
|
-
this.
|
|
18
|
-
isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD,
|
|
19
|
-
intervalFunction: null,
|
|
20
|
-
intervalTime: 5000,
|
|
21
|
-
tests: [],
|
|
22
|
-
batchIndex: 0,
|
|
23
|
-
};
|
|
17
|
+
this.tests = [];
|
|
24
18
|
const suffix = process.env.TESTOMATIO_REPLAY ? 'replay' : '';
|
|
25
19
|
const paths = getDebugFilePath(suffix);
|
|
26
20
|
this.logFilePath = paths.tmp;
|
|
@@ -60,8 +54,13 @@ export class DebugPipe {
|
|
|
60
54
|
this.logToFile({ datetime: new Date().toISOString(), timestamp: Date.now() });
|
|
61
55
|
this.logToFile({ data: 'variables', testomatioEnvVars: this.testomatioEnvVars });
|
|
62
56
|
this.logToFile({ data: 'store', store: this.store || {} });
|
|
63
|
-
|
|
64
|
-
|
|
57
|
+
|
|
58
|
+
// Safety net for hook failures (e.g. a failing AfterSuite) that abort the run
|
|
59
|
+
// before finishRun: buffered tests would otherwise be lost. The handler is
|
|
60
|
+
// attached lazily when the first test is buffered and detached once flushed,
|
|
61
|
+
// so processes that create many pipes don't pile up `exit` listeners.
|
|
62
|
+
this.flushOnExit = () => this.flushBufferedTests();
|
|
63
|
+
this.exitListenerAttached = false;
|
|
65
64
|
}
|
|
66
65
|
}
|
|
67
66
|
|
|
@@ -88,42 +87,22 @@ export class DebugPipe {
|
|
|
88
87
|
|
|
89
88
|
async createRun(params = {}) {
|
|
90
89
|
if (!this.isEnabled) return;
|
|
91
|
-
if (params.isBatchEnabled === true || params.isBatchEnabled === false) this.batch.isEnabled = params.isBatchEnabled;
|
|
92
|
-
|
|
93
|
-
if (!this.isEnabled) return {};
|
|
94
|
-
if (this.batch.isEnabled) this.batch.intervalFunction = setInterval(this.batchUpload, this.batch.intervalTime);
|
|
95
90
|
|
|
96
91
|
this.logToFile({ action: 'createRun', params });
|
|
97
92
|
}
|
|
98
93
|
|
|
99
94
|
async addTest(data) {
|
|
100
95
|
if (!this.isEnabled) return;
|
|
101
|
-
|
|
102
|
-
if (!this.
|
|
103
|
-
|
|
104
|
-
|
|
105
|
-
|
|
106
|
-
} else this.batch.tests.push(data);
|
|
107
|
-
|
|
108
|
-
if (!this.batch.intervalFunction) await this.batchUpload();
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
async batchUpload() {
|
|
112
|
-
this.batch.batchIndex++;
|
|
113
|
-
if (!this.batch.isEnabled) return;
|
|
114
|
-
if (!this.batch.tests.length) return;
|
|
115
|
-
|
|
116
|
-
const testsToSend = this.batch.tests.splice(0);
|
|
117
|
-
|
|
118
|
-
const logData = { action: 'addTestsBatch', tests: testsToSend };
|
|
119
|
-
if (this.store.runId) logData.runId = this.store.runId;
|
|
120
|
-
this.logToFile(logData);
|
|
96
|
+
this.tests.push(data);
|
|
97
|
+
if (!this.exitListenerAttached) {
|
|
98
|
+
process.once('exit', this.flushOnExit);
|
|
99
|
+
this.exitListenerAttached = true;
|
|
100
|
+
}
|
|
121
101
|
}
|
|
122
102
|
|
|
123
103
|
async finishRun(params) {
|
|
124
104
|
if (!this.isEnabled) return;
|
|
125
105
|
await this.sync();
|
|
126
|
-
if (this.batch.intervalFunction) clearInterval(this.batch.intervalFunction);
|
|
127
106
|
const logData = { action: 'finishRun', params };
|
|
128
107
|
if (this.store.runId) logData.runId = this.store.runId;
|
|
129
108
|
this.logToFile(logData);
|
|
@@ -133,8 +112,28 @@ export class DebugPipe {
|
|
|
133
112
|
}
|
|
134
113
|
|
|
135
114
|
async sync() {
|
|
136
|
-
|
|
137
|
-
|
|
115
|
+
this.flushBufferedTests();
|
|
116
|
+
}
|
|
117
|
+
|
|
118
|
+
/**
|
|
119
|
+
* Writes any buffered tests to the debug file as a single batch.
|
|
120
|
+
* Runs synchronously so it can also be invoked from a process `exit` handler,
|
|
121
|
+
* which is the only chance to persist tests when a hook failure (e.g. a failing
|
|
122
|
+
* AfterSuite) prevents `finishRun` from being reached. Idempotent: the buffer is
|
|
123
|
+
* drained on flush, so a later `finishRun`/exit flush is a no-op.
|
|
124
|
+
*/
|
|
125
|
+
flushBufferedTests() {
|
|
126
|
+
if (!this.isEnabled || !this.tests.length) return;
|
|
127
|
+
|
|
128
|
+
const tests = this.tests.splice(0);
|
|
129
|
+
const logData = { action: 'addTestsBatch', tests };
|
|
130
|
+
if (this.store.runId) logData.runId = this.store.runId;
|
|
131
|
+
this.logToFile(logData);
|
|
132
|
+
|
|
133
|
+
if (this.exitListenerAttached) {
|
|
134
|
+
process.removeListener('exit', this.flushOnExit);
|
|
135
|
+
this.exitListenerAttached = false;
|
|
136
|
+
}
|
|
138
137
|
}
|
|
139
138
|
|
|
140
139
|
toString() {
|
package/src/pipe/testomatio.js
CHANGED
|
@@ -5,6 +5,7 @@ import JsonCycle from 'json-cycle';
|
|
|
5
5
|
import {
|
|
6
6
|
APP_PREFIX,
|
|
7
7
|
STATUS,
|
|
8
|
+
BATCH_MODE,
|
|
8
9
|
REQUEST_TIMEOUT,
|
|
9
10
|
getCreateRunRequestTimeout,
|
|
10
11
|
REPORTER_REQUEST_RETRIES,
|
|
@@ -46,17 +47,25 @@ function parseCiParams(raw) {
|
|
|
46
47
|
/**
|
|
47
48
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
48
49
|
* @typedef {import('../../types/types.js').TestData} TestData
|
|
50
|
+
* @typedef {import('../../types/types.js').BatchMode} BatchMode
|
|
51
|
+
* @typedef {import('../../types/types.js').CreateRunParams} CreateRunParams
|
|
49
52
|
* @class TestomatioPipe
|
|
50
53
|
* @implements {Pipe}
|
|
51
54
|
*/
|
|
52
55
|
class TestomatioPipe {
|
|
53
56
|
constructor(params, store) {
|
|
54
57
|
this.batch = {
|
|
55
|
-
|
|
58
|
+
/** @type {BatchMode}
|
|
59
|
+
* Batch upload mode:
|
|
60
|
+
* - `auto`: upload tests automatically by time interval (e.g. every 5 seconds).
|
|
61
|
+
* - `manual`: buffer tests and upload only when `sync()` is invoked manually.
|
|
62
|
+
* - `disabled`: send one test per request, no batching.
|
|
63
|
+
*/
|
|
64
|
+
mode: params.batchMode || (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? BATCH_MODE.DISABLED : BATCH_MODE.AUTO),
|
|
56
65
|
intervalFunction: null, // will be created in createRun by setInterval function
|
|
57
|
-
intervalTime:
|
|
66
|
+
intervalTime: 6000, // how often tests are sent
|
|
58
67
|
tests: [], // array of tests in batch
|
|
59
|
-
batchIndex: 0,
|
|
68
|
+
batchIndex: 0, // represents the current batch index (starts from 1 and increments by 1 for each batch)
|
|
60
69
|
numberOfTimesCalledWithoutTests: 0, // how many times batch was called without tests
|
|
61
70
|
};
|
|
62
71
|
this.retriesTimestamps = [];
|
|
@@ -222,13 +231,13 @@ class TestomatioPipe {
|
|
|
222
231
|
|
|
223
232
|
/**
|
|
224
233
|
* Creates a new run on Testomat.io
|
|
225
|
-
* @param {
|
|
234
|
+
* @param {CreateRunParams} params
|
|
226
235
|
* @returns Promise<void>
|
|
227
236
|
*/
|
|
228
237
|
async createRun(params = {}) {
|
|
229
|
-
this.batch.
|
|
238
|
+
if (params.batchMode) this.batch.mode = params.batchMode;
|
|
230
239
|
if (!this.isEnabled) return;
|
|
231
|
-
if (this.batch.
|
|
240
|
+
if (this.batch.mode === BATCH_MODE.AUTO && this.isEnabled)
|
|
232
241
|
this.batch.intervalFunction = setInterval(this.#batchUpload, this.batch.intervalTime);
|
|
233
242
|
if (this.store) {
|
|
234
243
|
this.store.runKind = params.kind;
|
|
@@ -433,14 +442,14 @@ class TestomatioPipe {
|
|
|
433
442
|
* Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
|
|
434
443
|
*/
|
|
435
444
|
#batchUpload = async () => {
|
|
436
|
-
if (
|
|
445
|
+
if (this.batch.mode === BATCH_MODE.DISABLED) return;
|
|
437
446
|
if (!this.batch.tests.length) return;
|
|
438
447
|
if (this.#cancelTestReportingInCaseOfTooManyReqFailures()) return;
|
|
439
448
|
// prevent infinite loop
|
|
440
449
|
if (this.batch.numberOfTimesCalledWithoutTests > 10) {
|
|
441
450
|
debug('📨 Batch upload: no tests to send for 10 times, stopping batch');
|
|
442
451
|
clearInterval(this.batch.intervalFunction);
|
|
443
|
-
this.batch.
|
|
452
|
+
this.batch.mode = BATCH_MODE.DISABLED;
|
|
444
453
|
}
|
|
445
454
|
if (!this.batch.tests.length) {
|
|
446
455
|
debug('📨 Batch upload: no tests to send');
|
|
@@ -496,11 +505,11 @@ class TestomatioPipe {
|
|
|
496
505
|
this.#formatData(data);
|
|
497
506
|
|
|
498
507
|
let uploading = null;
|
|
499
|
-
if (
|
|
508
|
+
if (this.batch.mode === BATCH_MODE.DISABLED) uploading = this.#uploadSingleTest(data);
|
|
500
509
|
else this.batch.tests.push(data);
|
|
501
510
|
|
|
502
|
-
//
|
|
503
|
-
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
511
|
+
// auto mode but no interval running yet (e.g. createRun hasn't started it): flush immediately
|
|
512
|
+
if (this.batch.mode === BATCH_MODE.AUTO && !this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
504
513
|
|
|
505
514
|
// return promise to be able to wait for it
|
|
506
515
|
return uploading;
|
|
@@ -529,7 +538,7 @@ class TestomatioPipe {
|
|
|
529
538
|
// (e.g. if test has artifacts, add test function will be invoked only after artifacts are uploaded)
|
|
530
539
|
// batch stops working after run is finished; thus, disable it to use single test uploading
|
|
531
540
|
this.batch.intervalFunction = null;
|
|
532
|
-
this.batch.
|
|
541
|
+
this.batch.mode = BATCH_MODE.DISABLED;
|
|
533
542
|
}
|
|
534
543
|
|
|
535
544
|
debug('Finishing run...');
|
|
@@ -613,7 +622,7 @@ class TestomatioPipe {
|
|
|
613
622
|
if (this.batch.intervalFunction) {
|
|
614
623
|
clearInterval(this.batch.intervalFunction);
|
|
615
624
|
this.batch.intervalFunction = null;
|
|
616
|
-
this.batch.
|
|
625
|
+
this.batch.mode = BATCH_MODE.DISABLED;
|
|
617
626
|
}
|
|
618
627
|
this.batch.tests = [];
|
|
619
628
|
}
|
package/src/replay.js
CHANGED
|
@@ -1,7 +1,7 @@
|
|
|
1
1
|
import fs from 'fs';
|
|
2
2
|
import path from 'path';
|
|
3
3
|
import TestomatClient from './client.js';
|
|
4
|
-
import { STATUS, DEBUG_FILE } from './constants.js';
|
|
4
|
+
import { STATUS, DEBUG_FILE, BATCH_MODE } from './constants.js';
|
|
5
5
|
import { config } from './config.js';
|
|
6
6
|
|
|
7
7
|
export class Replay {
|
|
@@ -216,7 +216,7 @@ export class Replay {
|
|
|
216
216
|
|
|
217
217
|
const client = new TestomatClient({
|
|
218
218
|
apiKey: this.apiKey,
|
|
219
|
-
|
|
219
|
+
batchMode: BATCH_MODE.AUTO,
|
|
220
220
|
...runParams,
|
|
221
221
|
...(runId && { runId }),
|
|
222
222
|
});
|
package/src/xmlReader.js
CHANGED
|
@@ -3,7 +3,7 @@ import path from 'path';
|
|
|
3
3
|
import pc from 'picocolors';
|
|
4
4
|
import fs from 'fs';
|
|
5
5
|
import { XMLParser } from 'fast-xml-parser';
|
|
6
|
-
import { APP_PREFIX, STATUS } from './constants.js';
|
|
6
|
+
import { APP_PREFIX, STATUS, BATCH_MODE } from './constants.js';
|
|
7
7
|
import { randomUUID } from 'crypto';
|
|
8
8
|
import { fileURLToPath } from 'url';
|
|
9
9
|
import { NUnitXmlParser } from './junit-adapter/nunit-parser.js';
|
|
@@ -73,8 +73,7 @@ class XmlReader {
|
|
|
73
73
|
env: TESTOMATIO_ENV,
|
|
74
74
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
75
75
|
detach: TESTOMATIO_MARK_DETACHED,
|
|
76
|
-
|
|
77
|
-
isBatchEnabled: false,
|
|
76
|
+
batchMode: BATCH_MODE.MANUAL,
|
|
78
77
|
};
|
|
79
78
|
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
80
79
|
this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts);
|
|
@@ -543,7 +542,7 @@ class XmlReader {
|
|
|
543
542
|
title: this.requestParams.title,
|
|
544
543
|
env: this.requestParams.env,
|
|
545
544
|
group_title: this.requestParams.group_title,
|
|
546
|
-
|
|
545
|
+
batchMode: this.requestParams.batchMode,
|
|
547
546
|
};
|
|
548
547
|
|
|
549
548
|
debug('Run', runParams);
|
|
@@ -611,7 +610,7 @@ class XmlReader {
|
|
|
611
610
|
this.pipes = this.pipes || (await this.pipesPromise);
|
|
612
611
|
|
|
613
612
|
// Create run before uploading tests to ensure runId is set
|
|
614
|
-
await this.createRun();
|
|
613
|
+
// await this.createRun(); // makes reporting stuck after finish, thus commenting out
|
|
615
614
|
|
|
616
615
|
if (!this.tests || !Array.isArray(this.tests) || this.tests.length === 0) {
|
|
617
616
|
debug('No tests to upload, finishing run');
|
package/types/types.d.ts
CHANGED
|
@@ -234,7 +234,7 @@ export interface HtmlTestData extends TestData {
|
|
|
234
234
|
/**
|
|
235
235
|
* Extended test data for Markdown reporter.
|
|
236
236
|
*/
|
|
237
|
-
export interface MarkdownTestData extends HtmlTestData {}
|
|
237
|
+
export interface MarkdownTestData extends HtmlTestData { }
|
|
238
238
|
|
|
239
239
|
/**
|
|
240
240
|
* Object representing a result of a Run.
|
|
@@ -288,6 +288,13 @@ export enum RunStatus {
|
|
|
288
288
|
Finished = 'finished',
|
|
289
289
|
}
|
|
290
290
|
|
|
291
|
+
/** Batch upload strategy:
|
|
292
|
+
* `auto` (by time interval, e.g. every 5 seconds),
|
|
293
|
+
* `manual` (send tests via manually invoking sync() ),
|
|
294
|
+
* `disabled` (one test per request, no batching).
|
|
295
|
+
*/
|
|
296
|
+
export type BatchMode = 'auto' | 'manual' | 'disabled';
|
|
297
|
+
|
|
291
298
|
export interface Pipe {
|
|
292
299
|
isEnabled: boolean;
|
|
293
300
|
store: {};
|
|
@@ -334,8 +341,8 @@ export interface CreateRunParams {
|
|
|
334
341
|
/** Run configuration merged into the server-side run configuration. */
|
|
335
342
|
configuration?: Record<string, any>;
|
|
336
343
|
|
|
337
|
-
/** Override batch upload
|
|
338
|
-
|
|
344
|
+
/** Override batch upload mode. */
|
|
345
|
+
batchMode?: BatchMode;
|
|
339
346
|
}
|
|
340
347
|
|
|
341
348
|
/**
|