@testomatio/reporter 0.5.9 → 0.6.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.
@@ -0,0 +1,350 @@
1
+ const path = require("path");
2
+ const axios = require('axios');
3
+ const chalk = require('chalk');
4
+ const fs = require("fs");
5
+
6
+ // const util = require("util"); // you can see a result
7
+ const { XMLParser } = require("fast-xml-parser");
8
+ const { APP_PREFIX } = require('./constants');
9
+ const { isValidUrl, fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
10
+ const upload = require('./fileUploader');
11
+
12
+ const Adapter = require('./junit-adapter/adapter');
13
+ const JavaScriptAdapter = require('./junit-adapter/javascript');
14
+ const JavaAdapter = require('./junit-adapter/java');
15
+ const PythonAdapter = require('./junit-adapter/python');
16
+ const RubyAdapter = require('./junit-adapter/ruby');
17
+
18
+ const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
19
+ const TESTOMATIO = process.env.TESTOMATIO; // key?
20
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
21
+
22
+ const options = {
23
+ ignoreDeclaration: true,
24
+ ignoreAttributes: false,
25
+ alwaysCreateTextNode: false,
26
+ attributeNamePrefix: "",
27
+ parseTagValue: true,
28
+ };
29
+
30
+ class XmlReader {
31
+
32
+ constructor(opts = {}) {
33
+ this.requestParams = {
34
+ apiKey: opts.apiKey || TESTOMATIO,
35
+ url: opts.url || TESTOMATIO_URL,
36
+ title: TESTOMATIO_TITLE,
37
+ env: TESTOMATIO_ENV,
38
+ group_title: TESTOMATIO_RUNGROUP_TITLE,
39
+ };
40
+ this.runId = opts.runId || TESTOMATIO_RUN;
41
+ this.adapter = new Adapter(opts)
42
+ this.opts = opts;
43
+ this.axios = axios.create();
44
+ this.parser = new XMLParser(options);
45
+ this.tests = []
46
+ this.stats = {}
47
+ this.stats.language = opts.lang?.toLowerCase();
48
+ this.filesToUpload = {}
49
+ }
50
+
51
+ connectAdapter() {
52
+ if (this.opts.javaTests || this.stats.language === 'java') {
53
+ this.adapter = new JavaAdapter(this.opts);
54
+ }
55
+ if (this.stats.language === 'js') {
56
+ this.adapter = new JavaScriptAdapter(this.opts);
57
+ }
58
+ if (this.stats.language === 'python') {
59
+ this.adapter = new PythonAdapter(this.opts);
60
+ }
61
+ if (this.stats.language === 'ruby') {
62
+ this.adapter = new RubyAdapter(this.opts);
63
+ }
64
+ }
65
+
66
+ parse(fileName) {
67
+ const xmlData = fs.readFileSync(path.resolve(fileName));
68
+ const jsonResult = this.parser.parse(xmlData);
69
+ let jsonSuite;
70
+
71
+ if (jsonResult.testsuites) {
72
+ jsonSuite = jsonResult.testsuites;
73
+ } else if (jsonResult.testsuite) {
74
+ jsonSuite = jsonResult;
75
+ } else {
76
+ console.log(jsonResult)
77
+ throw new Error("Format can't be parsed")
78
+ }
79
+ const { testsuite, name, tests, failures, errors } = jsonSuite;
80
+
81
+ const resultTests = processTestSuite(testsuite);
82
+
83
+ const hasFailures = resultTests.filter(t => t.status === 'failed').length > 0;
84
+ const status = ( failures > 0 || errors > 0 || hasFailures) ? 'failed' : 'passed';
85
+
86
+ this.tests = this.tests.concat(resultTests);
87
+
88
+ return {
89
+ status,
90
+ create_tests: true,
91
+ name,
92
+ tests_count: parseInt(tests, 10),
93
+ passed_count: parseInt(tests - failures, 10),
94
+ failed_count: parseInt(failures, 10),
95
+ skipped_count: 0,
96
+ tests: resultTests,
97
+ };
98
+ }
99
+
100
+ calculateStats() {
101
+ this.stats = {
102
+ ...this.stats,
103
+ status: 'passed',
104
+ create_tests: true,
105
+ tests_count: 0,
106
+ passed_count: 0,
107
+ failed_count: 0,
108
+ skipped_count: 0,
109
+ }
110
+ this.tests.forEach(t => {
111
+ this.stats.tests_count++;
112
+ if (t.status === 'passed') this.stats.passed_count++;
113
+ if (t.status === 'failed') this.stats.failed_count++;
114
+ })
115
+ if (this.stats.failed_count) this.stats.status = 'failed';
116
+
117
+ return this.stats;
118
+ }
119
+
120
+ fetchSourceCode() {
121
+ this.tests.forEach(t => {
122
+ try {
123
+ const file = this.adapter.getFilePath(t)
124
+ if (!file) return;
125
+
126
+ if (!this.stats.language) {
127
+ if (file.endsWith('.php')) this.stats.language = 'php';
128
+ if (file.endsWith('.py')) this.stats.language = 'python';
129
+ if (file.endsWith('.java')) this.stats.language = 'java';
130
+ if (file.endsWith('.rb')) this.stats.language = 'ruby';
131
+ if (file.endsWith('.js')) this.stats.language = 'js';
132
+ if (file.endsWith('.ts')) this.stats.language = 'ts';
133
+ }
134
+
135
+ const contents = fs.readFileSync(file).toString();
136
+ t.code = fetchSourceCode(contents, { ...t, lang: this.stats.language })
137
+ } catch (err) {
138
+ if (process.env.DEBUG) {
139
+ console.log(err)
140
+ }
141
+ }
142
+ });
143
+ }
144
+
145
+ formatTests() {
146
+ this.tests.forEach(t => {
147
+ if (t.file) {
148
+ t.file = t.file.replace(process.cwd() + path.sep, '')
149
+ }
150
+
151
+ this.adapter.formatTest(t)
152
+
153
+ t.title = t.title
154
+ // insert a space before all caps
155
+ .replace(/([A-Z])/g, ' $1')
156
+ // _ chars to spaces
157
+ .replace(/_/g, ' ')
158
+ // uppercase the first character
159
+ .replace(/^(.)|\s(.)/g, $1 => $1.toLowerCase())
160
+
161
+ // remove standard prefixes
162
+ .replace(/^test\s/, '')
163
+ .replace(/^should\s/, '')
164
+ });
165
+ }
166
+
167
+ formatErrors() {
168
+ this.tests.filter(t => !!t.stack).forEach(t => {
169
+ t.stack = this.formatStack(t)
170
+ t.message = this.adapter.formatMessage(t);
171
+ });
172
+
173
+ }
174
+
175
+ formatStack(t) {
176
+ const stack = this.adapter.formatStack(t);
177
+
178
+ const sourcePart = fetchSourceCodeFromStackTrace(stack);
179
+
180
+ if (!sourcePart) return stack;
181
+
182
+ const separator = chalk.bold.red('################[ Failure ]################');
183
+
184
+ return `${stack}\n\n${separator}\n${fetchSourceCodeFromStackTrace(stack)}`;
185
+ }
186
+
187
+ async uploadArtifacts() {
188
+ if (!this.runId) return;
189
+ for (const test of this.tests.filter(t => !!t.stack)) {
190
+ const files = fetchFilesFromStackTrace(test.stack);
191
+ test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
192
+ }
193
+ }
194
+
195
+ async createRun() {
196
+ if (this.runId) return;
197
+
198
+ const runParams = {
199
+ api_key: this.requestParams.apiKey,
200
+ title: this.requestParams.title,
201
+ env: this.requestParams.env,
202
+ group_title: this.requestParams.runGroup,
203
+ };
204
+
205
+ try {
206
+ const resp = await this.axios.post(this.url, runParams, {
207
+ maxContentLength: Infinity,
208
+ maxBodyLength: Infinity,
209
+ headers: {
210
+ // Overwrite Axios's automatically set Content-Type
211
+ 'Content-Type': 'application/json',
212
+ }
213
+ });
214
+ if (resp.status >= 400) {
215
+ const data = resp.data || { message: '' };
216
+ console.log(
217
+ APP_PREFIX,
218
+ `Report couldn't be processed: (${resp.status}) ${data.message}`,
219
+ );
220
+ return;
221
+ }
222
+ this.runId = resp.data.uid;
223
+ this.runUrl = `${TESTOMATIO_URL}/${resp.data.url.split('/').splice(3).join('/')}`;
224
+ } catch(err) {
225
+ if (process.env.DEBUG) console.log(err)
226
+ const data = err?.response?.data || { message: '' };
227
+ console.log(APP_PREFIX, 'Error creating run, skipping...', err?.response?.statusText, data);
228
+ }
229
+ }
230
+
231
+ async uploadData() {
232
+ await this.uploadArtifacts();
233
+ this.calculateStats();
234
+ this.connectAdapter();
235
+ this.fetchSourceCode();
236
+ this.formatErrors();
237
+ this.formatTests();
238
+
239
+ if (process.env.DEBUG) {
240
+ console.log({
241
+ ...this.stats,
242
+ tests: this.tests,
243
+ })
244
+ }
245
+
246
+ const dataString = JSON.stringify({
247
+ ...this.stats,
248
+ api_key: this.requestParams.apiKey,
249
+ tests: this.tests,
250
+ });
251
+
252
+ try {
253
+ const resp = await this.axios.put(`${this.url}/${this.runId}`, dataString, {
254
+ maxContentLength: Infinity,
255
+ maxBodyLength: Infinity,
256
+ headers: {
257
+ // Overwrite Axios's automatically set Content-Type
258
+ 'Content-Type': 'application/json',
259
+ }
260
+ });
261
+
262
+ if (resp.status >= 400) {
263
+ const data = resp.data || { message: '' };
264
+ console.log(
265
+ APP_PREFIX,
266
+ `Report couldn't be processed: (${resp.status}) ${data.message}`,
267
+ );
268
+ return;
269
+ }
270
+
271
+ if (this.runUrl) {
272
+ console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
273
+ }
274
+ process.env.runId = this.runId;
275
+ return resp;
276
+ } catch (err) {
277
+ // if (process.env.DEBUG) console.log(err.response)
278
+ const data = err.response.data || { message: '' };
279
+ console.log(APP_PREFIX, 'Error uploading, skipping...', err.response.statusText, data);
280
+ return err.response;
281
+ }
282
+
283
+ }
284
+
285
+ get url() {
286
+ if (!isValidUrl(this.requestParams.url)) {
287
+ console.log(
288
+ APP_PREFIX,
289
+ chalk.red(`Error creating report on Testomat.io, report url '${this.requestParams.url}' is invalid`),
290
+ );
291
+ return;
292
+ }
293
+
294
+ return `${this.requestParams.url}/api/reporter`;
295
+ }
296
+ }
297
+
298
+ module.exports = XmlReader;
299
+
300
+
301
+ function reduceTestCases(prev, item) {
302
+ let testCases = item.testcase;
303
+ if (!Array.isArray(testCases)) {
304
+ testCases = [testCases]
305
+ }
306
+ testCases.filter(t => !!t).forEach(testCaseItem => {
307
+ const file = testCaseItem.file || item.filepath || '';
308
+
309
+ let stack = '';
310
+ let message = '';
311
+ if (testCaseItem.error) stack = testCaseItem.error;
312
+ if (testCaseItem.failure) stack = testCaseItem.failure;
313
+ if (testCaseItem?.failure?.message) message = testCaseItem.failure.message;
314
+ if (testCaseItem?.error?.message) message = testCaseItem.error.message;
315
+
316
+ if (testCaseItem.failure && testCaseItem.failure['#text']) stack = testCaseItem.failure['#text'];
317
+ if (testCaseItem.error && testCaseItem.error['#text']) stack = testCaseItem.error['#text'];
318
+ if (!message) message = stack.trim().split('\n')[0];
319
+
320
+ // prepend system output
321
+ stack = `${testCaseItem['system-out'] || testCaseItem.log || ''}\n\n${stack}`.trim()
322
+
323
+ prev.push({
324
+ create: true,
325
+ file,
326
+ stack,
327
+ message,
328
+ line: testCaseItem.lineno,
329
+ run_time: testCaseItem.time,
330
+ status: (testCaseItem.failure || testCaseItem.error) ? 'failed' : 'passed',
331
+ title: testCaseItem.name,
332
+ suite_title: testCaseItem.classname || item.name,
333
+ })
334
+ });
335
+ return prev;
336
+ }
337
+
338
+ function processTestSuite(testsuite) {
339
+ if (testsuite.testsuite) return processTestSuite(testsuite.testsuite)
340
+
341
+ let suites = testsuite;
342
+ if (!Array.isArray(testsuite)) {
343
+ suites = [testsuite];
344
+ }
345
+
346
+ const res = suites.reduce(reduceTestCases, []);
347
+
348
+ return res;
349
+ }
350
+
package/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "0.5.9",
3
+ "version": "0.6.0",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "repository": "git@github.com:testomatio/reporter.git",
@@ -12,7 +12,10 @@
12
12
  "callsite-record": "^4.1.4",
13
13
  "chalk": "^4.1.0",
14
14
  "commander": "^4.1.1",
15
+ "fast-xml-parser": "^4.0.8",
16
+ "glob": "^8.0.3",
15
17
  "has-flag": "^5.0.1",
18
+ "is-valid-path": "^0.1.1",
16
19
  "json-cycle": "^1.3.0",
17
20
  "lodash.memoize": "^4.1.2"
18
21
  },
@@ -20,7 +23,8 @@
20
23
  "pretty": "prettier --write .",
21
24
  "lint": "eslint lib",
22
25
  "lint:fix": "eslint lib --fix",
23
- "test": "npm run test:adapter",
26
+ "test": "mocha tests/**",
27
+ "test:unit": "mocha tests",
24
28
  "test:adapter": "mocha './tests/adapter/index.test.js'",
25
29
  "test:adapter:jest:example": "jest './tests/adapter/examples/jest/index.test.js' --config='./tests/adapter/examples/jest/jest.config.js'",
26
30
  "test:adapter:mocha:example": "mocha './tests/adapter/examples/mocha/index.test.js' --config='./tests/adapter/examples/mocha/mocha.config.js'",
@@ -49,6 +53,7 @@
49
53
  "puppeteer": "^13.1.2"
50
54
  },
51
55
  "bin": {
52
- "start-test-run": "./lib/bin/startTest.js"
56
+ "start-test-run": "./lib/bin/startTest.js",
57
+ "report-xml": "./lib/bin/reportXml.js"
53
58
  }
54
59
  }
@@ -1,5 +1,5 @@
1
1
  const host = 'localhost';
2
- const port = 9000;
2
+ const port = 19000;
3
3
 
4
4
  module.exports = {
5
5
  host,