@testomatio/reporter 1.3.6-beta → 1.4.1-beta-1

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/xmlReader.js CHANGED
@@ -4,12 +4,17 @@ const chalk = require('chalk');
4
4
  const fs = require("fs");
5
5
  const { XMLParser } = require("fast-xml-parser");
6
6
  const { APP_PREFIX, STATUS } = require('./constants');
7
- const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
7
+ const { fetchFilesFromStackTrace,
8
+ fetchIdFromOutput,
9
+ fetchSourceCode,
10
+ fetchSourceCodeFromStackTrace,
11
+ fetchIdFromCode,
12
+ humanize
13
+ } = require('./utils/utils');
8
14
  const upload = require('./fileUploader');
9
15
  const pipesFactory = require('./pipe');
10
16
  const adapterFactory = require('./junit-adapter');
11
17
 
12
-
13
18
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
14
19
  const TESTOMATIO = process.env.TESTOMATIO; // key?
15
20
  const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
@@ -25,7 +30,7 @@ const options = {
25
30
  const reduceOptions = {};
26
31
 
27
32
  class XmlReader {
28
-
33
+
29
34
  constructor(opts = {}) {
30
35
  this.requestParams = {
31
36
  apiKey: opts.apiKey || TESTOMATIO,
@@ -35,12 +40,12 @@ class XmlReader {
35
40
  group_title: TESTOMATIO_RUNGROUP_TITLE,
36
41
  };
37
42
  this.runId = opts.runId || TESTOMATIO_RUN;
38
- this.adapter = adapterFactory(null, opts)
43
+ this.adapter = adapterFactory(opts.lang?.toLowerCase(), opts)
39
44
  if (!this.adapter) throw new Error('XML adapter for this format not found');
40
45
 
41
46
  this.opts = opts || {};
42
- const store = {}
43
- this.pipes = pipesFactory(opts, store);
47
+ this.store = {}
48
+ this.pipes = pipesFactory(opts, this.store);
44
49
 
45
50
  this.parser = new XMLParser(options);
46
51
  this.tests = []
@@ -49,27 +54,33 @@ class XmlReader {
49
54
  this.filesToUpload = {}
50
55
 
51
56
  this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
52
- console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
57
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
53
58
  }
54
59
 
55
60
  connectAdapter() {
56
- if (this.opts.javaTests) return adapterFactory('java', this.opts);
57
- return adapterFactory(this.stats.language, this.opts);
61
+ if (this.opts.javaTests) {
62
+ this.adapter = adapterFactory('java', this.opts);
63
+ return this.adapter;
64
+ }
65
+ this.adapter = adapterFactory(this.stats.language, this.opts);
66
+ return this.adapter;
58
67
  }
59
68
 
60
- parse(fileName) {
69
+ parse(fileName) {
61
70
  const xmlData = fs.readFileSync(path.resolve(fileName));
62
71
  const jsonResult = this.parser.parse(xmlData);
63
72
  let jsonSuite;
64
-
73
+
65
74
  if (jsonResult.testsuites) {
66
75
  jsonSuite = jsonResult.testsuites;
67
76
  } else if (jsonResult.testsuite) {
68
77
  jsonSuite = jsonResult;
69
78
  } else if (jsonResult.TestRun) {
70
- return this.processTRX(jsonResult);
79
+ return this.processTRX(jsonResult);
71
80
  } else if (jsonResult['test-run']) {
72
81
  return this.processNUnit(jsonResult['test-run']);
82
+ } else if (jsonResult.assemblies) {
83
+ return this.processXUnit(jsonResult.assemblies);
73
84
  } else {
74
85
  console.log(jsonResult)
75
86
  throw new Error("Format can't be parsed")
@@ -83,9 +94,9 @@ class XmlReader {
83
94
 
84
95
  reduceOptions.preferClassname = this.stats.language === 'python';
85
96
  const resultTests = processTestSuite(testsuite);
86
-
97
+
87
98
  const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
88
- const status = ( failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
99
+ const status = (failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
89
100
 
90
101
  this.tests = this.tests.concat(resultTests);
91
102
 
@@ -118,7 +129,7 @@ class XmlReader {
118
129
  skipped_count: parseInt(inconclusive + skipped, 10),
119
130
  tests: resultTests,
120
131
  };
121
- }
132
+ }
122
133
 
123
134
  processTRX(jsonSuite) {
124
135
  let defs = jsonSuite?.TestRun?.TestDefinitions?.UnitTest;
@@ -127,10 +138,10 @@ class XmlReader {
127
138
  const tests = defs.map(td => {
128
139
  const title = td.name.replace(/\(.*?\)/, '').trim();
129
140
  let example = td.name.match(/\((.*?)\)/);
130
- if (example) example = { ...example[1].split(',')};
141
+ if (example) example = { ...example[1].split(',') };
131
142
  const suite = td.TestMethod.className.split(', ')[0].split('.');
132
143
  const suite_title = suite.pop();
133
- return {
144
+ return {
134
145
  title,
135
146
  example,
136
147
  file: suite.join('/'),
@@ -143,18 +154,18 @@ class XmlReader {
143
154
  let result = jsonSuite?.TestRun?.Results?.UnitTestResult;
144
155
  if (!Array.isArray(result)) result = [result].filter(d => !!d);
145
156
 
146
- const results = result.map(td => ({
157
+ const results = result.map(td => ({
147
158
  id: td.executionId,
148
159
  // seconds are used in junit reports, but ms are used by testomatio
149
160
  run_time: parseFloat(td.duration) * 1000,
150
- status: td.outcome,
161
+ status: td.outcome,
151
162
  stack: td.Output.StdOut,
152
163
  files: td?.ResultFiles?.ResultFile?.map(rf => rf.path)
153
164
  }));
154
165
 
155
-
166
+
156
167
  results.forEach(r => {
157
- const test = tests.find(t => t.id === r.id) || { };
168
+ const test = tests.find(t => t.id === r.id) || {};
158
169
  r.suite_title = test.suite_title;
159
170
  r.title = test.title?.trim();
160
171
  if (test.code) r.code = test.code;
@@ -169,7 +180,7 @@ class XmlReader {
169
180
  });
170
181
 
171
182
  debug(results);
172
-
183
+
173
184
  const counters = jsonSuite?.TestRun?.ResultSummary?.Counters || {};
174
185
 
175
186
  const failed_count = parseInt(counters.failed, 10) + parseInt(counters.error, 10);
@@ -187,7 +198,80 @@ class XmlReader {
187
198
  skipped_count: parseInt(counters.notExecuted, 10),
188
199
  failed_count,
189
200
  tests: results,
190
- };
201
+ };
202
+ }
203
+
204
+ processXUnit(assemblies) {
205
+ const tests = [];
206
+
207
+ assemblies = Array.isArray(assemblies.assembly) ? assemblies.assembly : [assemblies.assembly];
208
+
209
+ assemblies.forEach(assembly => {
210
+ const { collection } = assembly;
211
+
212
+ const suites = Array.isArray(collection) ? collection : [collection];
213
+
214
+ suites.forEach(suite => {
215
+ const { test } = suite;
216
+ if (!test) return;
217
+ const cases = Array.isArray(test) ? test : [test];
218
+ cases.forEach(testCase => {
219
+ const { type, time, result } = testCase;
220
+
221
+ let message = '';
222
+ let stack = '';
223
+
224
+ if (testCase.failure) {
225
+ message = testCase.failure.message;
226
+ stack = testCase.failure['stack-trace']
227
+ }
228
+ if (testCase.reason) {
229
+ message = testCase.reason.message;
230
+ }
231
+
232
+ let status = STATUS.PASSED;
233
+ if (result === 'Pass') status = STATUS.PASSED;
234
+ if (result === 'Fail') status = STATUS.FAILED;
235
+ if (result === 'Skip') status = STATUS.SKIPPED;
236
+
237
+ const pathParts = type.split('.');
238
+ const suite_title = pathParts[pathParts.length - 1];
239
+ const file = pathParts.slice(0, -1).join('/');
240
+ const title = testCase.method || testCase.name.split('.').pop();
241
+ const run_time = parseFloat(time) * 1000;
242
+
243
+ tests.push({
244
+ create: true,
245
+ stack,
246
+ message,
247
+ file,
248
+ status,
249
+ title,
250
+ suite_title,
251
+ run_time,
252
+ });
253
+
254
+ });
255
+ });
256
+ });
257
+
258
+ const hasFailures = tests.filter(t => t.status === STATUS.FAILED).length > 0;
259
+ const status = hasFailures ? STATUS.FAILED : STATUS.PASSED;
260
+
261
+ this.tests = tests;
262
+
263
+ debug(tests);
264
+
265
+ return {
266
+ status,
267
+ create_tests: true,
268
+ name: 'xUnit',
269
+ tests_count: tests.length,
270
+ passed_count: tests.filter(t => t.status === STATUS.PASSED).length,
271
+ failed_count: tests.filter(t => t.status === STATUS.FAILED).length,
272
+ skipped_count: tests.filter(t => t.status === STATUS.SKIPPED).length,
273
+ tests,
274
+ };
191
275
  }
192
276
 
193
277
  calculateStats() {
@@ -221,12 +305,19 @@ class XmlReader {
221
305
  if (file.endsWith('.py')) this.stats.language = 'python';
222
306
  if (file.endsWith('.java')) this.stats.language = 'java';
223
307
  if (file.endsWith('.rb')) this.stats.language = 'ruby';
224
- if (file.endsWith('.js')) this.stats.language = 'js';
308
+ if (file.endsWith('.js')) this.stats.language = 'js';
225
309
  if (file.endsWith('.ts')) this.stats.language = 'ts';
226
310
  }
227
311
 
312
+ if (!fs.existsSync(file)) {
313
+ debug('Failed to open file with the source code', file)
314
+ return;
315
+ }
228
316
  const contents = fs.readFileSync(file).toString();
229
317
  t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
318
+ if (t.code) debug('Fetched code for test %s', t.title);
319
+ t.test_id = fetchIdFromCode(t.code, { lang: this.stats.language })
320
+ if (t.test_id) debug('Fetched test id %s for test %s', t.test_id, t.title);
230
321
  } catch (err) {
231
322
  debug(err)
232
323
  }
@@ -241,18 +332,7 @@ class XmlReader {
241
332
 
242
333
  this.adapter.formatTest(t)
243
334
 
244
- t.title = t.title
245
- // insert a space before all caps
246
- .replace(/([A-Z])/g, ' $1')
247
- // _ chars to spaces
248
- .replace(/_/g, ' ')
249
- // uppercase the first character
250
- .replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
251
-
252
- // remove standard prefixes
253
- .replace(/^test\s/, '')
254
- .replace(/^should\s/, '')
255
- .trim();
335
+ t.title = humanize(t.title);
256
336
  });
257
337
  }
258
338
 
@@ -281,8 +361,12 @@ class XmlReader {
281
361
  let files = [];
282
362
  if (test.files?.length) files = test.files.map(f => path.join(process.cwd(), f))
283
363
  files = [...files, ...fetchFilesFromStackTrace(test.stack)];
284
- debug('Uploading files', files)
285
- test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
364
+
365
+ if (!files.length) continue;
366
+
367
+ const runId = this.runId || this.store.runId || Date.now().toString();
368
+ test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, runId)));
369
+ console.log(APP_PREFIX, `🗄️ Uploaded ${chalk.bold(`${files.length} artifacts`)} for test ${test.title}`);
286
370
  }
287
371
  }
288
372
 
@@ -292,7 +376,7 @@ class XmlReader {
292
376
  title: this.requestParams.title,
293
377
  env: this.requestParams.env,
294
378
  group_title: this.requestParams.group_title,
295
- };
379
+ };
296
380
 
297
381
  debug("Run", runParams);
298
382
 
@@ -310,9 +394,9 @@ class XmlReader {
310
394
  debug(
311
395
  'Uploading data',
312
396
  {
313
- ...this.stats,
314
- tests: this.tests,
315
- })
397
+ ...this.stats,
398
+ tests: this.tests,
399
+ })
316
400
 
317
401
  const dataString = {
318
402
  ...this.stats,
@@ -334,6 +418,8 @@ function reduceTestCases(prev, item) {
334
418
  if (!Array.isArray(testCases)) {
335
419
  testCases = [testCases]
336
420
  }
421
+ const suiteOutput = item['system-out'] || item.output || item.log || '';
422
+ const suiteErr = item['system-err'] || item.output || item.log || '';
337
423
  testCases.filter(t => !!t).forEach(testCaseItem => {
338
424
  const file = testCaseItem.file || item.filepath || '';
339
425
 
@@ -349,8 +435,9 @@ function reduceTestCases(prev, item) {
349
435
  if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
350
436
  if (!message) message = stack.trim().split('\n')[0];
351
437
 
352
- // prepend system output
353
- stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}`.trim()
438
+ // eslint-disable-next-line
439
+ stack = `${testCaseItem['system-out'] || testCaseItem.output || testCaseItem.log || ''}\n\n${stack}\n\n${suiteOutput}\n\n${suiteErr}`.trim()
440
+ const testId = fetchIdFromOutput(stack);
354
441
 
355
442
  let status = STATUS.PASSED.toString();
356
443
  if ('failure' in testCaseItem || 'error' in testCaseItem) status = STATUS.FAILED;
@@ -360,6 +447,7 @@ function reduceTestCases(prev, item) {
360
447
  create: true,
361
448
  file,
362
449
  stack,
450
+ test_id: testId,
363
451
  message,
364
452
  line: testCaseItem.lineno,
365
453
  // seconds are used in junit reports, but ms are used by testomatio
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "1.3.6-beta",
3
+ "version": "1.4.1-beta-1",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "typings": "typings/index.d.ts",
@@ -20,15 +20,16 @@
20
20
  "debug": "^4.3.4",
21
21
  "dotenv": "^16.0.1",
22
22
  "fast-xml-parser": "^4.0.8",
23
- "glob": "^8.0.3",
23
+ "glob": "^10.3",
24
24
  "has-flag": "^5.0.1",
25
25
  "humanize-duration": "^3.27.3",
26
26
  "is-valid-path": "^0.1.1",
27
27
  "json-cycle": "^1.3.0",
28
- "lodash": "^4.17.21",
29
28
  "lodash.memoize": "^4.1.2",
30
29
  "lodash.merge": "^4.6.2",
31
- "uuid": "^9.0.0"
30
+ "minimatch": "^9.0.3",
31
+ "uuid": "^9.0.0",
32
+ "promise-retry": "^2.0.1"
32
33
  },
33
34
  "files": [
34
35
  "bin",
@@ -42,21 +43,21 @@
42
43
  "lint:fix": "eslint lib --fix",
43
44
  "test": "mocha tests/**",
44
45
  "init": "cd ./tests/adapter/examples/cucumber && npm i",
45
- "test:unit": "mocha tests",
46
46
  "test:adapter": "mocha './tests/adapter/index.test.js'",
47
47
  "test:pipes": "mocha './tests/pipes/*_test.js'",
48
48
  "test:adapter:jest:example": "jest './tests/adapter/examples/jest/index.test.js' --config='./tests/adapter/examples/jest/jest.config.js'",
49
49
  "test:adapter:mocha:example": "mocha './tests/adapter/examples/mocha/index.test.js' --config='./tests/adapter/examples/mocha/mocha.config.js'",
50
50
  "test:adapter:jasmine:example": "./tests/adapter/examples/jasmine/passReporterOpts.sh && jasmine './tests/adapter/examples/jasmine/index.test.js' --reporter=./../../../lib/adapter/jasmine.js",
51
51
  "test:adapter:codecept:example": "codeceptjs run --config='./tests/adapter/examples/codecept/codecept.conf.js'",
52
- "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js"
52
+ "test:adapter:cucumber:example": "cd ./tests/adapter/examples/cucumber && npx cucumber-js",
53
+ "test:storage": "npx mocha ./tests-storage/**"
53
54
  },
54
55
  "devDependencies": {
55
56
  "@cucumber/cucumber": "^9.3.0",
56
57
  "@redocly/cli": "^1.0.0-beta.125",
57
58
  "@wdio/reporter": "^7.16.13",
58
59
  "chai": "^4.3.6",
59
- "codeceptjs": "^3.2.3",
60
+ "codeceptjs": "latest",
60
61
  "cucumber": "^6.0.7",
61
62
  "eslint": "^8.7.0",
62
63
  "eslint-config-airbnb-base": "^15.0.0",
@@ -66,6 +67,7 @@
66
67
  "jest": "^27.4.7",
67
68
  "mocha": "^9.2.0",
68
69
  "mock-http-server": "^1.4.5",
70
+ "pino": "^8.15.0",
69
71
  "prettier": "2.5.1",
70
72
  "puppeteer": "^13.1.2"
71
73
  },
@@ -1,142 +0,0 @@
1
- const debug = require('debug')('@testomatio/reporter:storage');
2
- const { join, resolve } = require('path');
3
- const fs = require('fs');
4
- const os = require('os');
5
- const uuid = require('uuid');
6
- const { TESTOMAT_ARTIFACT_SUFFIX } = require('./constants');
7
- const { specificTestInfo } = require('./util');
8
-
9
- class ArtifactStorage {
10
-
11
- constructor(params) {
12
- this.isFile = false;
13
- this.isMemory = true;
14
- this.storage = params?.toFile;
15
-
16
- if (this.storage) {
17
- // this is fine to use when you start saving artifacts;
18
- // but when you need to retrieve them, you will not know if there is "isFile" param,
19
- // because you need to create a new instance of ArtifactStorage inside client
20
- // so the solution is make it opposite to isMemory (which will be retrieved from global)
21
- this.isFile = true;
22
- this.isMemory = false;
23
- this.tmpDirFullpath = this.createTestomatTmpDir(ArtifactStorage._tmpPrefix);
24
- debug('SAVE to tmp folder mode enabled!');
25
- }
26
- }
27
-
28
- static async storeToFile(tmpDirName, artifact, testSuffix = "test") {
29
- // this assumes to use multiple files for each test. do we really want this?
30
- const suffix = TESTOMAT_ARTIFACT_SUFFIX + uuid.v4() + testSuffix;
31
- const dirpath = join(os.tmpdir(), tmpDirName);
32
- // why json?
33
- const filepath = resolve(dirpath, `${suffix}.json`);
34
-
35
- return fs.promises.appendFile(filepath, JSON.stringify(artifact));
36
- }
37
-
38
- static async artifact(artifact, context) {
39
- // TODO: mode for different pre-hooks. As variant - "all-tests" || "only_fail"
40
- // const mode = process.env.TESTOMAT_ARTIFACTS || "only_fail";
41
-
42
- // you save the artifact without specifying its source - test id
43
- if (Array.isArray(global.testomatioArtifacts)) {
44
- debug("Saving artifacts to global storage");
45
-
46
- global.testomatioArtifacts.push(artifact);
47
- }
48
-
49
- if (global?.testomatioArtifacts === undefined) {
50
- const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
51
-
52
- const testSuffix = specificTestInfo(context.test);
53
-
54
- if (Array.isArray(tmpDirNames) && tmpDirNames.length) {
55
- debug("Saving artifacts to memory tmp folder");
56
-
57
- return ArtifactStorage.storeToFile(tmpDirNames[tmpDirNames.length - 1], artifact, testSuffix);
58
- }
59
- }
60
- }
61
-
62
- async artifactByTestName(test) {
63
- const list = [];
64
-
65
- if (this.isFile && this.tmpDirFullpath) {
66
- const files = fs.readdirSync(this.tmpDirFullpath);
67
-
68
- for (const file of files) {
69
- if (file.includes(test)) {
70
- const buff = await fs.promises.readFile(`${this.tmpDirFullpath }/${ file}`);
71
-
72
- list.push(JSON.parse(buff.toString()));
73
- }
74
- }
75
- }
76
-
77
- return list;
78
- }
79
-
80
- // returns all the content from all files; do we really need it?
81
- async tmpContents() {
82
- const contents = [];
83
-
84
- if (this.isFile && this.tmpDirFullpath) {
85
- const files = fs.readdirSync(this.tmpDirFullpath);
86
-
87
- for (const file of files) {
88
- const buff = await fs.promises.readFile(`${this.tmpDirFullpath }/${ file}`);
89
- const content = buff.toString();
90
-
91
- contents.push(content);
92
- }
93
- }
94
-
95
- return contents;
96
- }
97
-
98
- createTestomatTmpDir(clientPrefix) {
99
- return fs.mkdtempSync(join(os.tmpdir(), clientPrefix));
100
- }
101
-
102
- clearTmpDirByName(name) {
103
- const tmpDirPath = join(os.tmpdir(), name);
104
-
105
- if (fs.existsSync(tmpDirPath)) {
106
- fs.rmSync(tmpDirPath, { recursive: true });
107
- debug(` Testomat tmpDir = ${tmpDirPath} was deleted successfully!`);
108
-
109
-
110
- }
111
- }
112
-
113
- // no need to use multiple dirs; we can implement it later if required
114
- static tmpTestomatDirNames() {
115
- const subname = ArtifactStorage._tmpPrefix || "tsmt_reporter";
116
-
117
- return fs.readdirSync(os.tmpdir(), { withFileTypes: true })
118
- .filter((item) => item.isDirectory())
119
- .map((item) => item.name)
120
- .filter((name) => name.includes(subname));
121
- }
122
-
123
- cleanup() {
124
- // when you do a cleanup, I know nothing about isFile param
125
- if (this.isFile && this.tmpDirFullpath) {
126
- const tmpDirNames = ArtifactStorage.tmpTestomatDirNames();
127
-
128
- if (Array.isArray(tmpDirNames) && tmpDirNames.length) {
129
- for (const name of tmpDirNames) {
130
- this.clearTmpDirByName(name);
131
- }
132
- }
133
- }
134
- else {
135
- debug("The tmp folder has not been created! Nothing to delete");
136
- }
137
- }
138
- }
139
-
140
- ArtifactStorage._tmpPrefix = "tsmt_reporter";
141
-
142
- module.exports = ArtifactStorage;
@@ -1,25 +0,0 @@
1
- // const debug = require('debug')('@testomatio/reporter:logger');
2
- const { DataStorage } = require('./dataStorage');
3
-
4
- class ArtifactStorage {
5
- constructor(params = {}) {
6
- this.dataStorage = new DataStorage('artifact', params);
7
-
8
- // singleton
9
- if (!ArtifactStorage.instance) {
10
- ArtifactStorage.instance = this;
11
- }
12
- }
13
-
14
- save(data, context = null) {
15
- this.dataStorage.putData(data, context);
16
- }
17
-
18
- get(context) {
19
- this.dataStorage.getData(context);
20
- }
21
- }
22
-
23
- ArtifactStorage.instance = null;
24
-
25
- module.exports = new ArtifactStorage();