@testomatio/reporter 2.3.7-beta.3-xml-import → 2.3.7-beta.4-stack-artifacts

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/src/client.js CHANGED
@@ -10,7 +10,14 @@ import { glob } from 'glob';
10
10
  import path, { sep } from 'path';
11
11
  import { fileURLToPath } from 'node:url';
12
12
  import { S3Uploader } from './uploader.js';
13
- import { formatStep, truncate, readLatestRunId, storeRunId, validateSuiteId } from './utils/utils.js';
13
+ import {
14
+ formatStep,
15
+ truncate,
16
+ readLatestRunId,
17
+ storeRunId,
18
+ validateSuiteId,
19
+ transformEnvVarToBoolean
20
+ } from './utils/utils.js';
14
21
  import { filesize as prettyBytes } from 'filesize';
15
22
 
16
23
  const debug = createDebugMessages('@testomatio/reporter:client');
@@ -37,8 +44,9 @@ class Client {
37
44
  this.runId = '';
38
45
  this.queue = Promise.resolve();
39
46
 
40
- // Get package.json path - use a simple approach that works in both environments
41
- const pathToPackageJSON = path.join(process.cwd(), 'package.json');
47
+ // @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
48
+ const __dirname = path.dirname(fileURLToPath(import.meta.url));
49
+ const pathToPackageJSON = path.join(__dirname, '../package.json');
42
50
  try {
43
51
  this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
44
52
  console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
@@ -138,19 +146,6 @@ class Client {
138
146
  * @returns {Promise<PipeResult[]>}
139
147
  */
140
148
  async addTestRun(status, testData) {
141
- if (!this.pipes || !this.pipes.length)
142
- this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
143
-
144
- // all pipes disabled, skipping
145
- if (!this.pipes?.filter(p => p.isEnabled).length) return [];
146
-
147
- if (isTestShouldBeExculedFromReport(testData)) return [];
148
-
149
- if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
150
- debug('Skipping test from report', testData?.title);
151
- return []; // do not log skipped tests
152
- }
153
-
154
149
  if (!testData)
155
150
  testData = {
156
151
  title: 'Unknown test',
@@ -168,15 +163,61 @@ class Client {
168
163
  const {
169
164
  rid,
170
165
  error = null,
166
+ steps: originalSteps,
167
+ title,
168
+ suite_title,
169
+ } = testData;
170
+ let steps = originalSteps;
171
+
172
+ const uploadedFiles = [];
173
+ const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
174
+
175
+ let formattedSteps;
176
+ if (stackArtifactsEnabled) {
177
+ const timestamp = +new Date;
178
+ formattedSteps = Array.isArray(steps) ? steps.map(step => formatStep(step)).flat().join('\n') : '';
179
+
180
+ if (error?.stack?.length > 5000) {
181
+ uploadedFiles.push(
182
+ this.uploader.uploadFileAsBuffer(
183
+ Buffer.from(error.stack, 'utf8'),
184
+ [this.runId, rid, `stack_${timestamp}.log`]
185
+ )
186
+ );
187
+ }
188
+ if (formattedSteps?.length > 10000) {
189
+ uploadedFiles.push(
190
+ this.uploader.uploadFileAsBuffer(
191
+ Buffer.from(JSON.stringify(steps, null, 2), 'utf8'),
192
+ [this.runId, rid, `steps_${timestamp}.json`]
193
+ )
194
+ );
195
+ }
196
+ }
197
+ if (!this.pipes || !this.pipes.length)
198
+ this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
199
+
200
+ if (!this.pipes?.filter(p => p.isEnabled).length) {
201
+ if (uploadedFiles.length > 0) {
202
+ await Promise.all(uploadedFiles);
203
+ }
204
+ return [];
205
+ }
206
+
207
+ if (isTestShouldBeExculedFromReport(testData)) return [];
208
+
209
+ if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
210
+ debug('Skipping test from report', testData?.title);
211
+ return [];
212
+ }
213
+
214
+ const {
171
215
  time = 0,
172
216
  example = null,
173
217
  files = [],
174
218
  filesBuffers = [],
175
- steps,
176
219
  code = null,
177
- title,
178
220
  file,
179
- suite_title,
180
221
  suite_id,
181
222
  test_id,
182
223
  timestamp,
@@ -187,7 +228,6 @@ class Client {
187
228
  } = testData;
188
229
  let { message = '', meta = {} } = testData;
189
230
 
190
- // stringify meta values and limit keys and values length to 255
191
231
  meta = Object.entries(meta)
192
232
  .filter(([, value]) => value !== null && value !== undefined)
193
233
  .reduce((acc, [key, value]) => {
@@ -195,7 +235,6 @@ class Client {
195
235
  return acc;
196
236
  }, {});
197
237
 
198
- // Get links from storage using the test context
199
238
  const testContext = suite_title ? `${suite_title} ${title}` : title;
200
239
 
201
240
  let errorFormatted = '';
@@ -204,13 +243,27 @@ class Client {
204
243
  message = error?.message;
205
244
  }
206
245
 
207
- // Attach logs
208
- const fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
246
+ if (stackArtifactsEnabled) {
247
+ if (error?.stack?.length > 5000) errorFormatted = `[Large stack saved as artifact]`;
248
+ if (formattedSteps?.length > 10000) steps = null;
249
+ } else {
250
+ formattedSteps = Array.isArray(steps) ? steps.map(step => formatStep(step)).flat().join('\n') : '';
251
+ }
209
252
 
210
- // add artifacts
211
- if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
253
+ let fullLogs = this.formatLogs({ error: errorFormatted, steps, logs: testData.logs });
212
254
 
213
- const uploadedFiles = [];
255
+ if (stackArtifactsEnabled && fullLogs.length > 5000) {
256
+ const timestamp = +new Date;
257
+ uploadedFiles.push(
258
+ this.uploader.uploadFileAsBuffer(
259
+ Buffer.from(fullLogs, 'utf8'),
260
+ [this.runId, rid, `logs_${timestamp}.log`]
261
+ )
262
+ );
263
+ fullLogs = fullLogs.slice(0, 5000) + '\n\n[Full logs saved as artifact]';
264
+ }
265
+
266
+ if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
214
267
 
215
268
  for (let f of files) {
216
269
  if (!f) continue; // f === null
@@ -386,11 +439,7 @@ class Client {
386
439
  */
387
440
  formatLogs({ error, steps, logs }) {
388
441
  error = error?.trim();
389
- logs = logs
390
- ?.trim()
391
- .split('\n')
392
- .map(l => truncate(l))
393
- .join('\n');
442
+ logs = logs?.trim().split('\n').map(l => truncate(l)).join('\n');
394
443
 
395
444
  if (Array.isArray(steps)) {
396
445
  steps = steps
@@ -3,50 +3,18 @@ import Adapter from './adapter.js';
3
3
 
4
4
  class CSharpAdapter extends Adapter {
5
5
  formatTest(t) {
6
- // Extract example from title if not already present
7
- if (!t.example) {
8
- const exampleMatch = t.title.match(/\((.*?)\)/);
9
- if (exampleMatch) {
10
- // Extract parameters as object with numeric keys for API
11
- const params = exampleMatch[1].split(',').map(param => param.trim());
12
- t.example = {};
13
- params.forEach((param, index) => {
14
- t.example[index] = param;
15
- });
16
- }
17
- }
18
-
19
- // Remove parameters from title to avoid duplicates in Test Suite
20
- // The example field will be used for grouping on import
21
- t.title = t.title.replace(/\(.*?\)/, '').trim();
22
-
6
+ const title = t.title.replace(/\(.*?\)/, '').trim();
7
+ const example = t.title.match(/\((.*?)\)/);
8
+ if (example) t.example = { ...example[1].split(',') };
23
9
  const suite = t.suite_title.split('.');
24
10
  t.suite_title = suite.pop();
25
11
  t.file = namespaceToFileName(t.file);
12
+ t.title = title.trim();
26
13
  return t;
27
14
  }
28
15
 
29
16
  getFilePath(t) {
30
- if (!t.file) return null;
31
-
32
- // Normalize path separators for cross-platform compatibility
33
- let filePath = t.file.replace(/\\/g, '/');
34
-
35
- // If file already has .cs extension, use it directly
36
- if (filePath.endsWith('.cs')) {
37
- // Make relative path if it's absolute
38
- if (path.isAbsolute(filePath)) {
39
- // Try to find project-relative path
40
- const cwd = process.cwd().replace(/\\/g, '/');
41
- if (filePath.startsWith(cwd)) {
42
- filePath = path.relative(cwd, filePath).replace(/\\/g, '/');
43
- }
44
- }
45
- return filePath;
46
- }
47
-
48
- // Convert namespace path to file path
49
- const fileName = namespaceToFileName(filePath);
17
+ const fileName = namespaceToFileName(t.file);
50
18
  return fileName;
51
19
  }
52
20
  }
@@ -54,14 +22,7 @@ class CSharpAdapter extends Adapter {
54
22
  export default CSharpAdapter;
55
23
 
56
24
  function namespaceToFileName(fileName) {
57
- if (!fileName) return '';
58
-
59
- // If already a .cs file path, clean it up
60
- if (fileName.endsWith('.cs')) {
61
- return fileName.replace(/\\/g, '/');
62
- }
63
-
64
25
  const fileParts = fileName.split('.');
65
26
  fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '');
66
- return `${fileParts.join('/')}.cs`;
27
+ return `${fileParts.join(path.sep)}.cs`;
67
28
  }
package/src/pipe/debug.js CHANGED
@@ -15,7 +15,7 @@ export class DebugPipe {
15
15
  this.isEnabled = !!process.env.TESTOMATIO_DEBUG || !!process.env.DEBUG;
16
16
  if (this.isEnabled) {
17
17
  this.batch = {
18
- isEnabled: this.params.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
18
+ isEnabled: this.params.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
19
19
  intervalFunction: null,
20
20
  intervalTime: 5000,
21
21
  tests: [],
@@ -93,7 +93,8 @@ export class DebugPipe {
93
93
  const logData = { action: 'addTest', testId: data };
94
94
  if (this.store.runId) logData.runId = this.store.runId;
95
95
  this.logToFile(logData);
96
- } else this.batch.tests.push(data);
96
+ }
97
+ else this.batch.tests.push(data);
97
98
 
98
99
  if (!this.batch.intervalFunction) await this.batchUpload();
99
100
  }
@@ -20,7 +20,7 @@ if (process.env.TESTOMATIO_RUN) process.env.runId = process.env.TESTOMATIO_RUN;
20
20
  class TestomatioPipe {
21
21
  constructor(params, store) {
22
22
  this.batch = {
23
- isEnabled: params?.isBatchEnabled ?? (process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ? false : true),
23
+ isEnabled: params?.isBatchEnabled ?? !process.env.TESTOMATIO_DISABLE_BATCH_UPLOAD ?? true,
24
24
  intervalFunction: null, // will be created in createRun by setInterval function
25
25
  intervalTime: 5000, // how often tests are sent
26
26
  tests: [], // array of tests in batch
@@ -60,8 +60,8 @@ class TestomatioPipe {
60
60
  retryConfig: {
61
61
  retry: REPORTER_REQUEST_RETRIES.retriesPerRequest,
62
62
  retryDelay: REPORTER_REQUEST_RETRIES.retryTimeout,
63
- httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
64
- shouldRetry: error => {
63
+ httpMethodsToRetry: ['GET','PUT','HEAD','OPTIONS','DELETE','POST'],
64
+ shouldRetry: (error) => {
65
65
  if (!error.response) return false;
66
66
  switch (error.response?.status) {
67
67
  case 400: // Bad request (probably wrong API key)
@@ -73,8 +73,8 @@ class TestomatioPipe {
73
73
  break;
74
74
  }
75
75
  return error.response?.status >= 401; // Retry on 401+ and 5xx
76
- },
77
- },
76
+ }
77
+ }
78
78
  });
79
79
 
80
80
  this.isEnabled = true;
@@ -104,6 +104,7 @@ class TestomatioPipe {
104
104
  // add test ID + run ID
105
105
  if (data.rid) data.rid = `${this.runId}-${data.rid}`;
106
106
 
107
+
107
108
  if (!process.env.TESTOMATIO_STACK_PASSED && data.status === STATUS.PASSED) {
108
109
  data.stack = null;
109
110
  }
@@ -119,6 +120,7 @@ class TestomatioPipe {
119
120
  return data;
120
121
  }
121
122
 
123
+
122
124
  /**
123
125
  * Asynchronously prepares and retrieves the Testomat.io test grepList based on the provided options.
124
126
  * @param {Object} opts - The options for preparing the test grepList.
@@ -213,7 +215,7 @@ class TestomatioPipe {
213
215
  method: 'PUT',
214
216
  url: `/api/reporter/${this.runId}`,
215
217
  data: runParams,
216
- responseType: 'json',
218
+ responseType: 'json'
217
219
  });
218
220
  if (resp.data.artifacts) setS3Credentials(resp.data.artifacts);
219
221
  return;
@@ -226,7 +228,7 @@ class TestomatioPipe {
226
228
  url: '/api/reporter',
227
229
  data: runParams,
228
230
  maxContentLength: Infinity,
229
- responseType: 'json',
231
+ responseType: 'json'
230
232
  });
231
233
 
232
234
  this.runId = resp.data.uid;
@@ -285,44 +287,44 @@ class TestomatioPipe {
285
287
 
286
288
  debug('Adding test', json);
287
289
 
288
- return this.client
289
- .request({
290
- method: 'POST',
291
- url: `/api/reporter/${this.runId}/testrun`,
292
- data: json,
293
- headers: {
294
- 'Content-Type': 'application/json',
295
- },
296
- maxContentLength: Infinity,
297
- })
298
- .catch(err => {
299
- this.requestFailures++;
300
- this.notReportedTestsCount++;
301
- if (err.response) {
302
- if (err.response.status >= 400) {
303
- const responseData = err.response.data || { message: '' };
304
- console.log(
305
- APP_PREFIX,
306
- pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
- pc.gray(data?.title || ''),
308
- );
309
- if (err.response?.data?.message?.includes('could not be matched')) {
310
- this.hasUnmatchedTests = true;
311
- }
312
- return;
313
- }
290
+ return this.client.request({
291
+ method: 'POST',
292
+ url: `/api/reporter/${this.runId}/testrun`,
293
+ data: json,
294
+ headers: {
295
+ 'Content-Type': 'application/json',
296
+ },
297
+ maxContentLength: Infinity
298
+ }).catch(err => {
299
+ this.requestFailures++;
300
+ this.notReportedTestsCount++;
301
+ if (err.response) {
302
+ if (err.response.status >= 400) {
303
+ const responseData = err.response.data || { message: '' };
314
304
  console.log(
315
305
  APP_PREFIX,
316
- pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
- `Report couldn't be processed: ${err?.response?.data?.message}`,
306
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
307
+ pc.gray(data?.title || ''),
318
308
  );
319
- printCreateIssue(err);
320
- } else {
321
- console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
309
+ if (err.response?.data?.message?.includes('could not be matched')) {
310
+ this.hasUnmatchedTests = true;
311
+ }
312
+ return;
322
313
  }
323
- });
314
+ console.log(
315
+ APP_PREFIX,
316
+ pc.yellow(`Warning: ${data?.title || ''} (${err.response?.status})`),
317
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
318
+ );
319
+ printCreateIssue(err);
320
+ } else {
321
+ console.log(APP_PREFIX, pc.blue(data?.title || ''), "Report couldn't be processed", err);
322
+ }
323
+ });
324
324
  };
325
325
 
326
+
327
+
326
328
  /**
327
329
  * Uploads tests as a batch (multiple tests at once). Intended to be used with a setInterval
328
330
  */
@@ -347,42 +349,43 @@ class TestomatioPipe {
347
349
  const testsToSend = this.batch.tests.splice(0);
348
350
  debug('📨 Batch upload', testsToSend.length, 'tests');
349
351
 
350
- return this.client
351
- .request({
352
- method: 'POST',
353
- url: `/api/reporter/${this.runId}/testrun`,
354
- data: {
355
- api_key: this.apiKey,
356
- tests: testsToSend,
357
- batch_index: this.batch.batchIndex,
358
- },
359
- headers: {
360
- 'Content-Type': 'application/json',
361
- },
362
- maxContentLength: Infinity,
363
- })
364
- .catch(err => {
365
- this.requestFailures++;
366
- this.notReportedTestsCount += testsToSend.length;
367
- if (err.response) {
368
- if (err.response.status >= 400) {
369
- const responseData = err.response.data || { message: '' };
370
- console.log(APP_PREFIX, pc.yellow(`Warning: ${responseData.message} (${err.response.status})`));
371
- if (err.response?.data?.message?.includes('could not be matched')) {
372
- this.hasUnmatchedTests = true;
373
- }
374
- return;
375
- }
352
+ return this.client.request({
353
+ method: 'POST',
354
+ url: `/api/reporter/${this.runId}/testrun`,
355
+ data: {
356
+ api_key: this.apiKey,
357
+ tests: testsToSend,
358
+ batch_index: this.batch.batchIndex
359
+ },
360
+ headers: {
361
+ 'Content-Type': 'application/json',
362
+ },
363
+ maxContentLength: Infinity
364
+ }).catch(err => {
365
+ this.requestFailures++;
366
+ this.notReportedTestsCount += testsToSend.length;
367
+ if (err.response) {
368
+ if (err.response.status >= 400) {
369
+ const responseData = err.response.data || { message: '' };
376
370
  console.log(
377
371
  APP_PREFIX,
378
- pc.yellow(`Warning: (${err.response?.status})`),
379
- `Report couldn't be processed: ${err?.response?.data?.message}`,
372
+ pc.yellow(`Warning: ${responseData.message} (${err.response.status})`),
380
373
  );
381
- printCreateIssue(err);
382
- } else {
383
- console.log(APP_PREFIX, "Report couldn't be processed", err);
374
+ if (err.response?.data?.message?.includes('could not be matched')) {
375
+ this.hasUnmatchedTests = true;
376
+ }
377
+ return;
384
378
  }
385
- });
379
+ console.log(
380
+ APP_PREFIX,
381
+ pc.yellow(`Warning: (${err.response?.status})`),
382
+ `Report couldn't be processed: ${err?.response?.data?.message}`,
383
+ );
384
+ printCreateIssue(err);
385
+ } else {
386
+ console.log(APP_PREFIX, "Report couldn't be processed", err);
387
+ }
388
+ });
386
389
  };
387
390
 
388
391
  /**
@@ -405,9 +408,9 @@ class TestomatioPipe {
405
408
  else this.batch.tests.push(data);
406
409
 
407
410
  // if test is added after run which is already finished
408
- if (!this.batch.intervalFunction) uploading = this.#batchUpload();
411
+ if (!this.batch.intervalFunction) uploading = this.#batchUpload();
409
412
 
410
- // return promise to be able to wait for it
413
+ // return promise to be able to wait for it
411
414
  return uploading;
412
415
  }
413
416
 
@@ -456,7 +459,7 @@ class TestomatioPipe {
456
459
  status_event,
457
460
  detach: params.detach,
458
461
  tests: params.tests,
459
- },
462
+ }
460
463
  });
461
464
 
462
465
  if (this.runUrl) {
@@ -469,7 +472,7 @@ class TestomatioPipe {
469
472
  if (this.runUrl && this.proceed) {
470
473
  const notFinishedMessage = pc.yellow(pc.bold('Run was not finished because of $TESTOMATIO_PROCEED'));
471
474
  console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${pc.magenta(this.runUrl)}`);
472
- console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx @testomatio/reporter finish`);
475
+ console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
473
476
  }
474
477
 
475
478
  if (this.hasUnmatchedTests) {
@@ -522,6 +525,9 @@ function printCreateIssue(err) {
522
525
  console.log({ body: body?.replace(/"(tstmt_[^"]+)"/g, 'tstmt_*'), url, baseURL, method, time });
523
526
  console.log('```');
524
527
  });
528
+
525
529
  }
526
530
 
531
+
532
+
527
533
  export default TestomatioPipe;
package/src/reporter.js CHANGED
@@ -1,8 +1,10 @@
1
- // import TestomatClient from './client.js';
2
- // import * as TRConstants from './constants.js';
1
+ import Client from './client.js';
2
+ import * as TestomatioConstants from './constants.js';
3
3
  import { services } from './services/index.js';
4
4
  import reporterFunctions from './reporter-functions.js';
5
5
 
6
+ export { Client };
7
+ export const STATUS = TestomatioConstants.STATUS;
6
8
  export const artifact = reporterFunctions.artifact;
7
9
  export const log = reporterFunctions.log;
8
10
  export const logger = services.logger;
@@ -35,6 +37,7 @@ export default {
35
37
  linkTest: reporterFunctions.linkTest,
36
38
  linkJira: reporterFunctions.linkJira,
37
39
 
38
- // TestomatClient,
39
- // TRConstants,
40
+ TestomatioClient: Client,
41
+ STATUS,
42
+
40
43
  };