@testomatio/reporter 2.1.0-beta-nightwatch → 2.1.0-beta.1-codeceptjs
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/README.md +1 -0
- package/lib/adapter/codecept.js +288 -202
- package/lib/adapter/cypress-plugin/index.js +0 -2
- package/lib/adapter/mocha.js +0 -1
- package/lib/adapter/nightwatch.js +5 -5
- package/lib/adapter/playwright.js +11 -3
- package/lib/adapter/webdriver.d.ts +1 -1
- package/lib/adapter/webdriver.js +18 -8
- package/lib/bin/cli.js +73 -8
- package/lib/bin/reportXml.js +4 -2
- package/lib/bin/startTest.js +3 -2
- package/lib/bin/uploadArtifacts.js +5 -4
- package/lib/client.js +30 -10
- package/lib/data-storage.d.ts +5 -5
- package/lib/data-storage.js +23 -13
- package/lib/junit-adapter/csharp.d.ts +1 -0
- package/lib/junit-adapter/csharp.js +11 -1
- package/lib/pipe/bitbucket.d.ts +2 -0
- package/lib/pipe/bitbucket.js +38 -26
- package/lib/pipe/debug.js +27 -6
- package/lib/pipe/github.d.ts +2 -2
- package/lib/pipe/github.js +35 -3
- package/lib/pipe/gitlab.d.ts +2 -0
- package/lib/pipe/gitlab.js +27 -9
- package/lib/pipe/html.js +0 -3
- package/lib/pipe/index.js +17 -7
- package/lib/pipe/testomatio.d.ts +3 -2
- package/lib/pipe/testomatio.js +85 -75
- package/lib/replay.d.ts +31 -0
- package/lib/replay.js +255 -0
- package/lib/reporter-functions.d.ts +7 -0
- package/lib/reporter-functions.js +36 -0
- package/lib/reporter.d.ts +15 -12
- package/lib/reporter.js +4 -1
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/index.d.ts +2 -0
- package/lib/services/index.js +2 -0
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/labels.d.ts +22 -0
- package/lib/services/labels.js +62 -0
- package/lib/services/logger.d.ts +1 -1
- package/lib/services/logger.js +1 -2
- package/lib/template/testomatio.hbs +443 -68
- package/lib/uploader.js +10 -6
- package/lib/utils/constants.d.ts +12 -0
- package/lib/utils/constants.js +15 -0
- package/lib/utils/utils.d.ts +10 -1
- package/lib/utils/utils.js +70 -22
- package/lib/xmlReader.js +54 -19
- package/package.json +16 -11
- package/src/adapter/codecept.js +320 -214
- package/src/adapter/cypress-plugin/index.js +0 -2
- package/src/adapter/mocha.js +0 -1
- package/src/adapter/nightwatch.js +1 -1
- package/src/adapter/playwright.js +10 -7
- package/src/adapter/webdriver.js +2 -2
- package/src/bin/cli.js +70 -2
- package/src/bin/reportXml.js +4 -1
- package/src/bin/startTest.js +2 -1
- package/src/bin/uploadArtifacts.js +2 -1
- package/src/client.js +18 -3
- package/src/data-storage.js +6 -6
- package/src/junit-adapter/csharp.js +13 -1
- package/src/pipe/bitbucket.js +22 -24
- package/src/pipe/debug.js +26 -5
- package/src/pipe/github.js +1 -2
- package/src/pipe/gitlab.js +27 -9
- package/src/pipe/html.js +1 -4
- package/src/pipe/testomatio.js +106 -105
- package/src/replay.js +262 -0
- package/src/reporter-functions.js +41 -0
- package/src/reporter.js +3 -0
- package/src/services/index.js +2 -0
- package/src/services/labels.js +59 -0
- package/src/services/logger.js +1 -2
- package/src/template/testomatio.hbs +443 -68
- package/src/uploader.js +11 -6
- package/src/utils/constants.js +12 -0
- package/src/utils/utils.js +46 -13
- package/src/xmlReader.js +70 -18
package/src/pipe/testomatio.js
CHANGED
|
@@ -1,12 +1,6 @@
|
|
|
1
1
|
import createDebugMessages from 'debug';
|
|
2
2
|
import pc from 'picocolors';
|
|
3
|
-
|
|
4
|
-
// Retry interceptor function
|
|
5
|
-
import axiosRetry from 'axios-retry';
|
|
6
|
-
|
|
7
|
-
// Default axios instance
|
|
8
|
-
import axios from 'axios';
|
|
9
|
-
|
|
3
|
+
import { Gaxios } from 'gaxios';
|
|
10
4
|
import JsonCycle from 'json-cycle';
|
|
11
5
|
import { APP_PREFIX, STATUS, AXIOS_TIMEOUT, REPORTER_REQUEST_RETRIES } from '../constants.js';
|
|
12
6
|
import { isValidUrl, foundedTestLog } from '../utils/utils.js';
|
|
@@ -15,9 +9,7 @@ import { config } from '../config.js';
|
|
|
15
9
|
|
|
16
10
|
const debug = createDebugMessages('@testomatio/reporter:pipe:testomatio');
|
|
17
11
|
|
|
18
|
-
if (process.env.TESTOMATIO_RUN)
|
|
19
|
-
// process.env.runId = process.env.TESTOMATIO_RUN;
|
|
20
|
-
}
|
|
12
|
+
if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
|
|
21
13
|
|
|
22
14
|
/**
|
|
23
15
|
* @typedef {import('../../types/types.js').Pipe} Pipe
|
|
@@ -59,50 +51,38 @@ class TestomatioPipe {
|
|
|
59
51
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
60
52
|
this.env = process.env.TESTOMATIO_ENV;
|
|
61
53
|
this.label = process.env.TESTOMATIO_LABEL;
|
|
62
|
-
|
|
63
|
-
|
|
54
|
+
|
|
55
|
+
// Create a new instance of gaxios with a custom config
|
|
56
|
+
this.client = new Gaxios({
|
|
64
57
|
baseURL: `${this.url.trim()}`,
|
|
65
58
|
timeout: AXIOS_TIMEOUT,
|
|
66
|
-
proxy: proxy
|
|
67
|
-
|
|
68
|
-
|
|
69
|
-
|
|
70
|
-
|
|
59
|
+
proxy: proxy ? proxy.toString() : undefined,
|
|
60
|
+
retry: true,
|
|
61
|
+
retryConfig: {
|
|
62
|
+
retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
63
|
+
retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
64
|
+
httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
|
|
65
|
+
shouldRetry: (error) => {
|
|
66
|
+
if (!error.response) return false;
|
|
67
|
+
switch (error.response?.status) {
|
|
68
|
+
case 400: // Bad request (probably wrong API key)
|
|
69
|
+
case 404: // Test not matched
|
|
70
|
+
case 429: // Rate limit exceeded
|
|
71
|
+
case 500: // Internal server error
|
|
72
|
+
return false;
|
|
73
|
+
default:
|
|
74
|
+
break;
|
|
71
75
|
}
|
|
72
|
-
|
|
73
|
-
});
|
|
74
|
-
|
|
75
|
-
// Pass the axios instance to the retry function
|
|
76
|
-
axiosRetry(this.axios, {
|
|
77
|
-
// do not use retries for unit tests
|
|
78
|
-
retries: REPORTER_REQUEST_RETRIES.retriesPerRequest, // Number of retries
|
|
79
|
-
shouldResetTimeout: true,
|
|
80
|
-
retryCondition: error => {
|
|
81
|
-
if (!error.response) return false;
|
|
82
|
-
switch (error.response?.status) {
|
|
83
|
-
case 400: // Bad request (probably wrong API key)
|
|
84
|
-
case 404: // Test not matched
|
|
85
|
-
case 429: // Rate limit exceeded
|
|
86
|
-
case 500: // Internal server error
|
|
87
|
-
return false;
|
|
88
|
-
default:
|
|
89
|
-
break;
|
|
76
|
+
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
90
77
|
}
|
|
91
|
-
|
|
92
|
-
},
|
|
93
|
-
retryDelay: () => REPORTER_REQUEST_RETRIES.retryTimeout, // sum = 15sec
|
|
94
|
-
onRetry: async (retryCount, error) => {
|
|
95
|
-
this.retriesTimestamps.push(Date.now());
|
|
96
|
-
|
|
97
|
-
debug(`${error.message || `Request failed ${error.status}`}. Retry #${retryCount} ...`);
|
|
98
|
-
},
|
|
78
|
+
}
|
|
99
79
|
});
|
|
100
80
|
|
|
101
81
|
this.isEnabled = true;
|
|
102
82
|
// do not finish this run (for parallel testing)
|
|
103
83
|
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
104
84
|
this.jiraId = process.env.TESTOMATIO_JIRA_ID;
|
|
105
|
-
this.runId = params.runId || process.env.
|
|
85
|
+
this.runId = params.runId || process.env.TESTOMATIO_RUN;
|
|
106
86
|
this.createNewTests = params.createNewTests ?? !!process.env.TESTOMATIO_CREATE;
|
|
107
87
|
this.hasUnmatchedTests = false;
|
|
108
88
|
this.requestFailures = 0;
|
|
@@ -136,12 +116,15 @@ class TestomatioPipe {
|
|
|
136
116
|
return;
|
|
137
117
|
}
|
|
138
118
|
|
|
139
|
-
const resp = await this.
|
|
140
|
-
|
|
119
|
+
const resp = await this.client.request({
|
|
120
|
+
method: 'GET',
|
|
121
|
+
url: '/api/test_grep',
|
|
122
|
+
params: q
|
|
123
|
+
});
|
|
141
124
|
|
|
142
|
-
if (Array.isArray(data?.tests) && data?.tests?.length > 0) {
|
|
143
|
-
foundedTestLog(APP_PREFIX, data.tests);
|
|
144
|
-
return data.tests;
|
|
125
|
+
if (Array.isArray(resp.data?.tests) && resp.data?.tests?.length > 0) {
|
|
126
|
+
foundedTestLog(APP_PREFIX, resp.data.tests);
|
|
127
|
+
return resp.data.tests;
|
|
145
128
|
}
|
|
146
129
|
|
|
147
130
|
console.log(APP_PREFIX, `⛔ No tests found for your --filter --> ${type}=${id}`);
|
|
@@ -201,16 +184,23 @@ class TestomatioPipe {
|
|
|
201
184
|
if (this.runId) {
|
|
202
185
|
this.store.runId = this.runId;
|
|
203
186
|
debug(`Run with id ${this.runId} already created, updating...`);
|
|
204
|
-
const resp = await this.
|
|
187
|
+
const resp = await this.client.request({
|
|
188
|
+
method: 'PUT',
|
|
189
|
+
url: `/api/reporter/${this.runId}`,
|
|
190
|
+
data: runParams
|
|
191
|
+
});
|
|
205
192
|
if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
|
|
206
193
|
return;
|
|
207
194
|
}
|
|
208
195
|
|
|
209
196
|
debug('Creating run...');
|
|
210
197
|
try {
|
|
211
|
-
const resp = await this.
|
|
198
|
+
const resp = await this.client.request({
|
|
199
|
+
method: 'POST',
|
|
200
|
+
url: '/api/reporter',
|
|
201
|
+
data: runParams,
|
|
212
202
|
maxContentLength: Infinity,
|
|
213
|
-
|
|
203
|
+
responseType: 'json'
|
|
214
204
|
});
|
|
215
205
|
|
|
216
206
|
this.runId = resp.data.uid;
|
|
@@ -227,6 +217,7 @@ class TestomatioPipe {
|
|
|
227
217
|
debug('Run created', this.runId);
|
|
228
218
|
} catch (err) {
|
|
229
219
|
const errorText = err.response?.data?.message || err.message;
|
|
220
|
+
debug('Error creating run', err);
|
|
230
221
|
console.log(errorText || err);
|
|
231
222
|
if (!this.apiKey) console.error('Testomat.io API key is not set');
|
|
232
223
|
if (!this.apiKey?.startsWith('tstmt')) console.error('Testomat.io API key is invalid');
|
|
@@ -273,7 +264,15 @@ class TestomatioPipe {
|
|
|
273
264
|
|
|
274
265
|
debug('Adding test', json);
|
|
275
266
|
|
|
276
|
-
return this.
|
|
267
|
+
return this.client.request({
|
|
268
|
+
method: 'POST',
|
|
269
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
270
|
+
data: json,
|
|
271
|
+
headers: {
|
|
272
|
+
'Content-Type': 'application/json',
|
|
273
|
+
},
|
|
274
|
+
maxContentLength: Infinity
|
|
275
|
+
}).catch(err => {
|
|
277
276
|
this.requestFailures++;
|
|
278
277
|
this.notReportedTestsCount++;
|
|
279
278
|
if (err.response) {
|
|
@@ -325,44 +324,51 @@ class TestomatioPipe {
|
|
|
325
324
|
const testsToSend = this.batch.tests.splice(0);
|
|
326
325
|
debug('📨 Batch upload', testsToSend.length, 'tests');
|
|
327
326
|
|
|
328
|
-
return this.
|
|
329
|
-
|
|
330
|
-
|
|
331
|
-
|
|
332
|
-
|
|
333
|
-
|
|
334
|
-
|
|
335
|
-
|
|
336
|
-
|
|
337
|
-
|
|
338
|
-
|
|
339
|
-
|
|
340
|
-
|
|
341
|
-
|
|
342
|
-
|
|
343
|
-
|
|
344
|
-
|
|
345
|
-
|
|
346
|
-
this.hasUnmatchedTests = true;
|
|
347
|
-
}
|
|
348
|
-
return;
|
|
349
|
-
}
|
|
327
|
+
return this.client.request({
|
|
328
|
+
method: 'POST',
|
|
329
|
+
url: `/api/reporter/${this.runId}/testrun`,
|
|
330
|
+
data: {
|
|
331
|
+
api_key: this.apiKey,
|
|
332
|
+
tests: testsToSend,
|
|
333
|
+
batch_index: this.batch.batchIndex
|
|
334
|
+
},
|
|
335
|
+
headers: {
|
|
336
|
+
'Content-Type': 'application/json',
|
|
337
|
+
},
|
|
338
|
+
maxContentLength: Infinity
|
|
339
|
+
}).catch(err => {
|
|
340
|
+
this.requestFailures++;
|
|
341
|
+
this.notReportedTestsCount += testsToSend.length;
|
|
342
|
+
if (err.response) {
|
|
343
|
+
if (err.response.status >= 400) {
|
|
344
|
+
const responseData = err.response.data || { message: '' };
|
|
350
345
|
console.log(
|
|
351
346
|
APP_PREFIX,
|
|
352
|
-
pc.yellow(`Warning: (${err.response
|
|
353
|
-
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
347
|
+
pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
|
|
354
348
|
);
|
|
355
|
-
|
|
356
|
-
|
|
357
|
-
|
|
349
|
+
if (err.response?.data?.message?.includes('could not be matched')) {
|
|
350
|
+
this.hasUnmatchedTests = true;
|
|
351
|
+
}
|
|
352
|
+
return;
|
|
358
353
|
}
|
|
359
|
-
|
|
354
|
+
console.log(
|
|
355
|
+
APP_PREFIX,
|
|
356
|
+
pc.yellow(`Warning: (${err.response?.status})`),
|
|
357
|
+
`Report couldn't be processed: ${err?.response?.data?.message}`,
|
|
358
|
+
);
|
|
359
|
+
printCreateIssue(err);
|
|
360
|
+
} else {
|
|
361
|
+
console.log(APP_PREFIX, "Report couldn't be processed", err);
|
|
362
|
+
}
|
|
363
|
+
});
|
|
360
364
|
};
|
|
361
365
|
|
|
362
366
|
/**
|
|
363
367
|
* Adds a test to the batch uploader (or reports a single test if batch uploading is disabled)
|
|
364
368
|
*/
|
|
365
369
|
addTest(data) {
|
|
370
|
+
this.isEnabled = this.apiKey ?? this.isEnabled;
|
|
371
|
+
|
|
366
372
|
if (!this.isEnabled) return;
|
|
367
373
|
if (!this.runId) return;
|
|
368
374
|
|
|
@@ -371,11 +377,15 @@ class TestomatioPipe {
|
|
|
371
377
|
data.api_key = this.apiKey;
|
|
372
378
|
data.create = this.createNewTests;
|
|
373
379
|
|
|
374
|
-
|
|
380
|
+
let uploading = null;
|
|
381
|
+
if (!this.batch.isEnabled) uploading = this.#uploadSingleTest(data);
|
|
375
382
|
else this.batch.tests.push(data);
|
|
376
383
|
|
|
377
384
|
// if test is added after run which is already finished
|
|
378
|
-
|
|
385
|
+
if (!this.batch.intervalFunction) uploading = this.#batchUpload();
|
|
386
|
+
|
|
387
|
+
// return promise to be able to wait for it
|
|
388
|
+
return uploading;
|
|
379
389
|
}
|
|
380
390
|
|
|
381
391
|
/**
|
|
@@ -415,12 +425,16 @@ class TestomatioPipe {
|
|
|
415
425
|
|
|
416
426
|
try {
|
|
417
427
|
if (this.runId && !this.proceed) {
|
|
418
|
-
await this.
|
|
419
|
-
|
|
420
|
-
|
|
421
|
-
|
|
422
|
-
|
|
423
|
-
|
|
428
|
+
await this.client.request({
|
|
429
|
+
method: 'PUT',
|
|
430
|
+
url: `/api/reporter/${this.runId}`,
|
|
431
|
+
data: {
|
|
432
|
+
api_key: this.apiKey,
|
|
433
|
+
duration: params.duration,
|
|
434
|
+
status_event,
|
|
435
|
+
detach: params.detach,
|
|
436
|
+
tests: params.tests,
|
|
437
|
+
}
|
|
424
438
|
});
|
|
425
439
|
if (this.runUrl) {
|
|
426
440
|
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', pc.magenta(this.runUrl));
|
|
@@ -437,14 +451,11 @@ class TestomatioPipe {
|
|
|
437
451
|
|
|
438
452
|
if (this.hasUnmatchedTests) {
|
|
439
453
|
console.log('');
|
|
440
|
-
// eslint-disable-next-line max-len
|
|
441
454
|
console.log(APP_PREFIX, pc.yellow(pc.bold('⚠️ Some reported tests were not found in Testomat.io project')));
|
|
442
|
-
// eslint-disable-next-line max-len
|
|
443
455
|
console.log(
|
|
444
456
|
APP_PREFIX,
|
|
445
457
|
`If you use Testomat.io as a reporter only, please re-run tests using ${pc.bold('TESTOMATIO_CREATE=1')}`,
|
|
446
458
|
);
|
|
447
|
-
// eslint-disable-next-line max-len
|
|
448
459
|
console.log(
|
|
449
460
|
APP_PREFIX,
|
|
450
461
|
`But to keep your tests consistent it is recommended to ${pc.bold('import tests first')}`,
|
|
@@ -453,7 +464,6 @@ class TestomatioPipe {
|
|
|
453
464
|
console.log(APP_PREFIX, 'You can do that automatically via command line tools:');
|
|
454
465
|
console.log(APP_PREFIX, pc.bold('npx check-tests ... --update-ids'), 'See: https://bit.ly/js-update-ids');
|
|
455
466
|
console.log(APP_PREFIX, 'or for Cucumber:');
|
|
456
|
-
// eslint-disable-next-line max-len
|
|
457
467
|
console.log(APP_PREFIX, pc.bold('npx check-cucumber ... --update-ids'), 'See: https://bit.ly/bdd-update-ids');
|
|
458
468
|
}
|
|
459
469
|
} catch (err) {
|
|
@@ -478,26 +488,17 @@ function printCreateIssue(err) {
|
|
|
478
488
|
console.log(
|
|
479
489
|
APP_PREFIX,
|
|
480
490
|
'If you think this is a bug please create an issue: https://github.com/testomatio/reporter/issues/new',
|
|
481
|
-
);
|
|
491
|
+
);
|
|
482
492
|
console.log(APP_PREFIX, 'Provide this information:');
|
|
483
493
|
console.log('Error:', err.message || err.code);
|
|
484
494
|
if (!err.config) return;
|
|
485
495
|
|
|
486
496
|
const time = new Date().toUTCString();
|
|
487
|
-
const {
|
|
497
|
+
const { body, url, baseURL, method } = err?.config || {};
|
|
488
498
|
console.log('```js');
|
|
489
|
-
console.log({
|
|
499
|
+
console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
|
|
490
500
|
console.log('```');
|
|
491
501
|
});
|
|
492
502
|
}
|
|
493
503
|
|
|
494
|
-
const axiosAddTestrunRequestConfig = {
|
|
495
|
-
maxContentLength: Infinity,
|
|
496
|
-
maxBodyLength: Infinity,
|
|
497
|
-
headers: {
|
|
498
|
-
// Overwrite Axios's automatically set Content-Type
|
|
499
|
-
'Content-Type': 'application/json',
|
|
500
|
-
},
|
|
501
|
-
};
|
|
502
|
-
|
|
503
504
|
export default TestomatioPipe;
|
package/src/replay.js
ADDED
|
@@ -0,0 +1,262 @@
|
|
|
1
|
+
import fs from 'fs';
|
|
2
|
+
import path from 'path';
|
|
3
|
+
import os from 'os';
|
|
4
|
+
import TestomatClient from './client.js';
|
|
5
|
+
import { STATUS } from './constants.js';
|
|
6
|
+
import { config } from './config.js';
|
|
7
|
+
|
|
8
|
+
export class Replay {
|
|
9
|
+
constructor(options = {}) {
|
|
10
|
+
this.apiKey = options.apiKey || config.TESTOMATIO || undefined;
|
|
11
|
+
this.dryRun = options.dryRun || false;
|
|
12
|
+
this.onProgress = options.onProgress || (() => {});
|
|
13
|
+
this.onLog = options.onLog || console.log;
|
|
14
|
+
this.onError = options.onError || console.error;
|
|
15
|
+
}
|
|
16
|
+
|
|
17
|
+
/**
|
|
18
|
+
* Get the default debug file path
|
|
19
|
+
* @returns {string} Path to the latest debug file
|
|
20
|
+
*/
|
|
21
|
+
getDefaultDebugFile() {
|
|
22
|
+
return path.join(os.tmpdir(), 'testomatio.debug.latest.json');
|
|
23
|
+
}
|
|
24
|
+
|
|
25
|
+
/**
|
|
26
|
+
* Parse a debug file and extract test data
|
|
27
|
+
* @param {string} debugFile - Path to the debug file
|
|
28
|
+
* @returns {Object} Parsed debug data
|
|
29
|
+
*/
|
|
30
|
+
parseDebugFile(debugFile) {
|
|
31
|
+
if (!fs.existsSync(debugFile)) {
|
|
32
|
+
throw new Error(`Debug file not found: ${debugFile}`);
|
|
33
|
+
}
|
|
34
|
+
|
|
35
|
+
const fileContent = fs.readFileSync(debugFile, 'utf-8');
|
|
36
|
+
const lines = fileContent.trim().split('\n').filter(line => line.trim() !== '');
|
|
37
|
+
|
|
38
|
+
if (lines.length === 0) {
|
|
39
|
+
throw new Error('Debug file is empty');
|
|
40
|
+
}
|
|
41
|
+
|
|
42
|
+
let runParams = {};
|
|
43
|
+
let finishParams = {};
|
|
44
|
+
let parseErrors = 0;
|
|
45
|
+
const testsMap = new Map(); // Use Map to deduplicate by rid
|
|
46
|
+
const testsWithoutRid = []; // For tests without rid (backward compatibility)
|
|
47
|
+
const envVars = {};
|
|
48
|
+
let runId = null;
|
|
49
|
+
|
|
50
|
+
// Parse debug file line by line
|
|
51
|
+
for (const [lineIndex, line] of lines.entries()) {
|
|
52
|
+
try {
|
|
53
|
+
const logEntry = JSON.parse(line);
|
|
54
|
+
|
|
55
|
+
if (logEntry.data === 'variables' && logEntry.testomatioEnvVars) {
|
|
56
|
+
Object.assign(envVars, logEntry.testomatioEnvVars);
|
|
57
|
+
} else if (logEntry.action === 'createRun') {
|
|
58
|
+
runParams = logEntry.params || {};
|
|
59
|
+
} else if (logEntry.action === 'addTestsBatch' && logEntry.tests) {
|
|
60
|
+
// Extract runId if available
|
|
61
|
+
if (logEntry.runId && !runId) {
|
|
62
|
+
runId = logEntry.runId;
|
|
63
|
+
}
|
|
64
|
+
// Process each test in the batch
|
|
65
|
+
for (const test of logEntry.tests) {
|
|
66
|
+
if (test.rid) {
|
|
67
|
+
// Handle tests with rid (deduplicate)
|
|
68
|
+
const existingTest = testsMap.get(test.rid);
|
|
69
|
+
if (existingTest) {
|
|
70
|
+
// Merge test data - prioritize non-null/non-empty values
|
|
71
|
+
const mergedTest = { ...existingTest };
|
|
72
|
+
Object.keys(test).forEach(key => {
|
|
73
|
+
if (test[key] !== null && test[key] !== undefined) {
|
|
74
|
+
if (key === 'files' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
75
|
+
// Merge files arrays
|
|
76
|
+
mergedTest.files = [...(existingTest.files || []), ...test[key]];
|
|
77
|
+
} else if (key === 'artifacts' && Array.isArray(test[key]) && test[key].length > 0) {
|
|
78
|
+
// Merge artifacts arrays
|
|
79
|
+
mergedTest.artifacts = [...(existingTest.artifacts || []), ...test[key]];
|
|
80
|
+
} else if (existingTest[key] === null || existingTest[key] === undefined ||
|
|
81
|
+
(Array.isArray(existingTest[key]) && existingTest[key].length === 0)) {
|
|
82
|
+
// Use new value if existing is null/undefined/empty array
|
|
83
|
+
mergedTest[key] = test[key];
|
|
84
|
+
}
|
|
85
|
+
}
|
|
86
|
+
});
|
|
87
|
+
testsMap.set(test.rid, mergedTest);
|
|
88
|
+
} else {
|
|
89
|
+
testsMap.set(test.rid, { ...test });
|
|
90
|
+
}
|
|
91
|
+
} else {
|
|
92
|
+
// Handle tests without rid (no deduplication)
|
|
93
|
+
testsWithoutRid.push({ ...test });
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
} else if (logEntry.action === 'addTest' && logEntry.testId) {
|
|
97
|
+
// Extract runId if available
|
|
98
|
+
if (logEntry.runId && !runId) {
|
|
99
|
+
runId = logEntry.runId;
|
|
100
|
+
}
|
|
101
|
+
const test = logEntry.testId;
|
|
102
|
+
if (test.rid) {
|
|
103
|
+
// Handle tests with rid (deduplicate)
|
|
104
|
+
const existingTest = testsMap.get(test.rid);
|
|
105
|
+
if (existingTest) {
|
|
106
|
+
// Merge with existing test
|
|
107
|
+
const mergedTest = { ...existingTest, ...test };
|
|
108
|
+
testsMap.set(test.rid, mergedTest);
|
|
109
|
+
} else {
|
|
110
|
+
testsMap.set(test.rid, { ...test });
|
|
111
|
+
}
|
|
112
|
+
} else {
|
|
113
|
+
// Handle tests without rid (no deduplication)
|
|
114
|
+
testsWithoutRid.push({ ...test });
|
|
115
|
+
}
|
|
116
|
+
} else if (logEntry.actions === 'finishRun') {
|
|
117
|
+
finishParams = logEntry.params || {};
|
|
118
|
+
}
|
|
119
|
+
} catch (err) {
|
|
120
|
+
parseErrors++;
|
|
121
|
+
if (parseErrors <= 3) {
|
|
122
|
+
// Only show first 3 parse errors
|
|
123
|
+
this.onError(`Failed to parse line ${lineIndex + 1}: ${line.substring(0, 100)}...`);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
if (parseErrors > 3) {
|
|
129
|
+
this.onError(`${parseErrors - 3} more parse errors occurred`);
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
// Combine tests with rid and tests without rid
|
|
133
|
+
const allTests = [...Array.from(testsMap.values()), ...testsWithoutRid];
|
|
134
|
+
|
|
135
|
+
return {
|
|
136
|
+
runParams,
|
|
137
|
+
finishParams,
|
|
138
|
+
tests: allTests,
|
|
139
|
+
envVars,
|
|
140
|
+
parseErrors,
|
|
141
|
+
totalLines: lines.length,
|
|
142
|
+
runId
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Restore environment variables from debug data
|
|
148
|
+
* @param {Object} envVars - Environment variables to restore
|
|
149
|
+
*/
|
|
150
|
+
restoreEnvironmentVariables(envVars) {
|
|
151
|
+
// Only restore env vars that aren't already set (don't override current values)
|
|
152
|
+
Object.keys(envVars).forEach(key => {
|
|
153
|
+
if (process.env[key] === undefined || process.env[key] === '') {
|
|
154
|
+
process.env[key] = envVars[key];
|
|
155
|
+
}
|
|
156
|
+
});
|
|
157
|
+
}
|
|
158
|
+
|
|
159
|
+
/**
|
|
160
|
+
* Replay test data to Testomat.io
|
|
161
|
+
* @param {string} debugFile - Path to debug file (optional, uses default if not provided)
|
|
162
|
+
* @returns {Promise<Object>} Replay results
|
|
163
|
+
*/
|
|
164
|
+
async replay(debugFile) {
|
|
165
|
+
if (!debugFile) {
|
|
166
|
+
debugFile = this.getDefaultDebugFile();
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
if (!this.apiKey) {
|
|
170
|
+
throw new Error('TESTOMATIO API key not found. Set TESTOMATIO environment variable.');
|
|
171
|
+
}
|
|
172
|
+
|
|
173
|
+
this.onLog(`Replaying data from debug file: ${debugFile}`);
|
|
174
|
+
|
|
175
|
+
// Parse the debug file
|
|
176
|
+
const debugData = this.parseDebugFile(debugFile);
|
|
177
|
+
const { runParams, finishParams, tests, envVars, runId } = debugData;
|
|
178
|
+
|
|
179
|
+
this.onLog(`Found ${tests.length} tests to replay`);
|
|
180
|
+
|
|
181
|
+
if (tests.length === 0) {
|
|
182
|
+
throw new Error('No test data found in debug file');
|
|
183
|
+
}
|
|
184
|
+
|
|
185
|
+
// Restore environment variables
|
|
186
|
+
this.restoreEnvironmentVariables(envVars);
|
|
187
|
+
|
|
188
|
+
if (this.dryRun) {
|
|
189
|
+
return {
|
|
190
|
+
success: true,
|
|
191
|
+
testsCount: tests.length,
|
|
192
|
+
runParams,
|
|
193
|
+
finishParams,
|
|
194
|
+
envVars,
|
|
195
|
+
runId,
|
|
196
|
+
dryRun: true
|
|
197
|
+
};
|
|
198
|
+
}
|
|
199
|
+
|
|
200
|
+
// Create client and restore the run
|
|
201
|
+
const client = new TestomatClient({
|
|
202
|
+
apiKey: this.apiKey,
|
|
203
|
+
isBatchEnabled: true,
|
|
204
|
+
...runParams,
|
|
205
|
+
});
|
|
206
|
+
|
|
207
|
+
// Use the stored runId if available, otherwise create a new run
|
|
208
|
+
if (runId) {
|
|
209
|
+
this.onLog(`Using existing run ID: ${runId}`);
|
|
210
|
+
client.runId = runId;
|
|
211
|
+
} else {
|
|
212
|
+
this.onLog('Publishing to run...');
|
|
213
|
+
await client.createRun(runParams);
|
|
214
|
+
}
|
|
215
|
+
|
|
216
|
+
// Send each test result
|
|
217
|
+
let successCount = 0;
|
|
218
|
+
let failureCount = 0;
|
|
219
|
+
|
|
220
|
+
for (const [index, test] of tests.entries()) {
|
|
221
|
+
try {
|
|
222
|
+
await client.addTestRun(test.status, test);
|
|
223
|
+
successCount++;
|
|
224
|
+
this.onProgress({
|
|
225
|
+
current: index + 1,
|
|
226
|
+
total: tests.length,
|
|
227
|
+
test,
|
|
228
|
+
success: true
|
|
229
|
+
});
|
|
230
|
+
} catch (err) {
|
|
231
|
+
failureCount++;
|
|
232
|
+
this.onError(`Failed to send test ${index + 1}: ${err.message}`);
|
|
233
|
+
this.onProgress({
|
|
234
|
+
current: index + 1,
|
|
235
|
+
total: tests.length,
|
|
236
|
+
test,
|
|
237
|
+
success: false,
|
|
238
|
+
error: err.message
|
|
239
|
+
});
|
|
240
|
+
}
|
|
241
|
+
}
|
|
242
|
+
|
|
243
|
+
await client.updateRunStatus(finishParams.status || STATUS.FINISHED, finishParams.parallel || false);
|
|
244
|
+
|
|
245
|
+
const result = {
|
|
246
|
+
success: true,
|
|
247
|
+
testsCount: tests.length,
|
|
248
|
+
successCount,
|
|
249
|
+
failureCount,
|
|
250
|
+
runParams,
|
|
251
|
+
finishParams,
|
|
252
|
+
envVars,
|
|
253
|
+
runId: runId || client.runId
|
|
254
|
+
};
|
|
255
|
+
|
|
256
|
+
this.onLog(`Successfully replayed ${successCount}/${tests.length} tests from debug file`);
|
|
257
|
+
|
|
258
|
+
return result;
|
|
259
|
+
}
|
|
260
|
+
}
|
|
261
|
+
|
|
262
|
+
export default Replay;
|
|
@@ -47,9 +47,50 @@ function setKeyValue(keyValue, value = null) {
|
|
|
47
47
|
services.keyValues.put(keyValue);
|
|
48
48
|
}
|
|
49
49
|
|
|
50
|
+
/**
|
|
51
|
+
* Add a single label to the test report
|
|
52
|
+
* @param {string} key - label key (e.g. 'severity', 'feature', or just 'smoke' for labels without values)
|
|
53
|
+
* @param {string} [value] - optional label value (e.g. 'high', 'login')
|
|
54
|
+
*/
|
|
55
|
+
function setLabel(key, value = null) {
|
|
56
|
+
if (!key || typeof key !== 'string') {
|
|
57
|
+
console.warn('Label key must be a non-empty string');
|
|
58
|
+
return;
|
|
59
|
+
}
|
|
60
|
+
|
|
61
|
+
// Limit key length to 255 characters
|
|
62
|
+
if (key.length > 255) {
|
|
63
|
+
console.warn('Label key is too long, trimmed to 255 characters:', key);
|
|
64
|
+
key = key.substring(0, 255);
|
|
65
|
+
}
|
|
66
|
+
|
|
67
|
+
let labelString = key;
|
|
68
|
+
if (value !== null && value !== undefined && value !== '') {
|
|
69
|
+
if (typeof value !== 'string') {
|
|
70
|
+
console.warn('Label value must be a string, converting:', value);
|
|
71
|
+
value = String(value);
|
|
72
|
+
}
|
|
73
|
+
// Limit value length to 255 characters
|
|
74
|
+
if (value.length > 255) {
|
|
75
|
+
console.warn('Label value is too long, trimmed to 255 characters:', value);
|
|
76
|
+
value = value.substring(0, 255);
|
|
77
|
+
}
|
|
78
|
+
labelString = `${key}:${value}`;
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
// Limit total label length to 255 characters
|
|
82
|
+
if (labelString.length > 255) {
|
|
83
|
+
console.warn('Label is too long, trimmed to 255 characters:', labelString);
|
|
84
|
+
labelString = labelString.substring(0, 255);
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
services.labels.put([labelString]);
|
|
88
|
+
}
|
|
89
|
+
|
|
50
90
|
export default {
|
|
51
91
|
artifact: saveArtifact,
|
|
52
92
|
log: logMessage,
|
|
53
93
|
step: addStep,
|
|
54
94
|
keyValue: setKeyValue,
|
|
95
|
+
label: setLabel,
|
|
55
96
|
};
|
package/src/reporter.js
CHANGED
|
@@ -8,6 +8,7 @@ export const log = reporterFunctions.log;
|
|
|
8
8
|
export const logger = services.logger;
|
|
9
9
|
export const meta = reporterFunctions.keyValue;
|
|
10
10
|
export const step = reporterFunctions.step;
|
|
11
|
+
export const label = reporterFunctions.label;
|
|
11
12
|
|
|
12
13
|
/**
|
|
13
14
|
* @typedef {import('./reporter-functions.js')} artifact
|
|
@@ -15,6 +16,7 @@ export const step = reporterFunctions.step;
|
|
|
15
16
|
* @typedef {import('./services/index.js')} logger
|
|
16
17
|
* @typedef {import('./reporter-functions.js')} meta
|
|
17
18
|
* @typedef {import('./reporter-functions.js')} step
|
|
19
|
+
* @typedef {import('./reporter-functions.js')} label
|
|
18
20
|
*/
|
|
19
21
|
export default {
|
|
20
22
|
/**
|
|
@@ -27,6 +29,7 @@ export default {
|
|
|
27
29
|
logger: services.logger,
|
|
28
30
|
meta: reporterFunctions.keyValue,
|
|
29
31
|
step: reporterFunctions.step,
|
|
32
|
+
label: reporterFunctions.label,
|
|
30
33
|
|
|
31
34
|
// TestomatClient,
|
|
32
35
|
// TRConstants,
|
package/src/services/index.js
CHANGED
|
@@ -1,12 +1,14 @@
|
|
|
1
1
|
import { logger } from './logger.js';
|
|
2
2
|
import { artifactStorage } from './artifacts.js';
|
|
3
3
|
import { keyValueStorage } from './key-values.js';
|
|
4
|
+
import { labelStorage } from './labels.js';
|
|
4
5
|
import { dataStorage } from '../data-storage.js';
|
|
5
6
|
|
|
6
7
|
export const services = {
|
|
7
8
|
logger,
|
|
8
9
|
artifacts: artifactStorage,
|
|
9
10
|
keyValues: keyValueStorage,
|
|
11
|
+
labels: labelStorage,
|
|
10
12
|
setContext: context => {
|
|
11
13
|
dataStorage.setContext(context);
|
|
12
14
|
},
|