@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.
package/.eslintrc.js CHANGED
@@ -11,7 +11,7 @@ module.exports = {
11
11
  SharedArrayBuffer: 'readonly',
12
12
  },
13
13
  parserOptions: {
14
- ecmaVersion: 2018,
14
+ ecmaVersion: 2020,
15
15
  },
16
16
  rules: {
17
17
  'max-len': ['error', { code: 120 }],
@@ -25,6 +25,7 @@ module.exports = {
25
25
  'no-use-before-define': 0,
26
26
  'object-curly-newline': 0,
27
27
  'consistent-return': 0,
28
+ 'no-await-in-loop': 0,
28
29
  'no-param-reassign': 0,
29
30
  'operator-linebreak': 0,
30
31
  'prefer-destructuring': 0,
@@ -15,7 +15,7 @@ jobs:
15
15
 
16
16
  strategy:
17
17
  matrix:
18
- node-version: [12.x, 14.x, 16.x]
18
+ node-version: [14.x, 16.x]
19
19
  # See supported Node.js release schedule at https://nodejs.org/en/about/releases/
20
20
 
21
21
  steps:
package/Changelog.md CHANGED
@@ -1,3 +1,8 @@
1
+ # 0.5.10
2
+
3
+ * Fixed reporting Scenario Outline in Cypress-Cucumber
4
+ * Fixed error reports for Cypress when running in Chrome
5
+
1
6
  # 0.5.9
2
7
 
3
8
  * Added environment on Cypress report
package/README.md CHANGED
@@ -205,6 +205,109 @@ Run the following command from you project folder:
205
205
  TESTOMATIO={API_KEY} npx @testomatio/reporter -c 'npx wdio wdio.conf.js'
206
206
  ```
207
207
 
208
+ ## JUnit Reports
209
+
210
+ > **Notice** JUnit reports are supported since 0.6.0
211
+
212
+ Other frameworks and languages are supported via JUnit reports.
213
+
214
+ JUnit XML format is standard among test runners on various platforms. Testomat.io can load XML reports from test runners and create tests in a project from it. If your framework is not supported yet, generate JUnit report and upload it into Testomat.io
215
+
216
+ Testomat.io will not only create a run report but also collect all information about tests, including source code, and create tests in a system as regular importer do. The only difference that normal process of Testomat.io import does not require executing tests on import, while importing via repoter requires to have tests executed and XML report provided.
217
+
218
+ Tested Frameworks:
219
+
220
+ * JUnit (JUnit)
221
+ * Python (Pytest)
222
+ * Minitest (Ruby)
223
+ * PHPUnit (PHP)
224
+
225
+
226
+ To import JUnit reports into Testomat.io **NodeJS >=14 is required**.
227
+
228
+ Package `@testomatio/reporter` should be installed:
229
+
230
+ ```
231
+ npm i @testomatio/reporter@latest
232
+ ```
233
+
234
+ For local development it is recommended to install @testomatio/reporter globally:
235
+
236
+ ```
237
+ npm i -g @testomatio/reporter@latest
238
+ ```
239
+
240
+ Run your test framework and generate a JUnit report.
241
+
242
+ Then import XML report into Testomat.io
243
+
244
+ ```
245
+ TESTOMATIO={API_KEY} npx report-xml "{pattern}" --lang={lang}
246
+ ```
247
+
248
+ * `pattern` - is a glob pattern to match all XML files from report. For instance, `"test/report/**.xml"` or just `report.xml`
249
+ * `--lang` option should be specified to identify source code of the project. Example: `--lang=Ruby` or `--lang=Java` or `--lang=Python`.
250
+ * `--java-tests` option is avaiable for Java projects, and can be set if path to tests is different then `src/test/java`. When this option is enable, `lang` option is automatically set to `java`
251
+
252
+
253
+ > *Notice:* All options from [Advanced Usage](#advanced-usage) are also available for JUnit reporter
254
+
255
+ Check the list of examples:
256
+
257
+ #### Example: Pytest
258
+
259
+ Run pytest tests and generate a report to `report.xml`:
260
+
261
+ ```
262
+ pytest --junit-xml report.xml
263
+ ```
264
+
265
+ Import report with this command
266
+
267
+ ```
268
+ TESTOMATIO={API_KEY} npx report-xml report.xml --lang=python
269
+ ```
270
+
271
+ #### Example: JUnit with Maven
272
+
273
+ Run tests via Maven, make sure JUnit report was configured in `pom.xml`.
274
+
275
+ ```
276
+ maven clean test
277
+ ```
278
+
279
+ Import report with this command:
280
+
281
+ ```
282
+ TESTOMATIO={API_KEY} npx report-xml "target/surefire-reports/*.xml" --java-tests
283
+ ```
284
+
285
+ > You can specify `--java-test` option to set a path to tests if they are located in path other than `src/test/java`
286
+
287
+ #### Example: Ruby on Rails with Minitest
288
+
289
+ ```ruby
290
+ # test_helper.rb:
291
+
292
+ reporters = [Minitest::Reporters::DefaultReporter.new(color: true)]
293
+ # enable JUnit reporter
294
+ reporters << Minitest::Reporters::JUnitReporter.new
295
+
296
+ Minitest::Reporters.use! reporters
297
+ ```
298
+
299
+ Launch tests:
300
+
301
+ ```
302
+ rails test
303
+ ```
304
+
305
+ Import reports from `test/reports` directory:
306
+
307
+ ```
308
+ TESTOMATIO={API_KEY} npx report-xml "test/reports/*.xml" --lang ruby
309
+ ```
310
+
208
311
  ## Advanced Usage
209
312
 
210
313
  ### Create Unmatched Tests
@@ -3,6 +3,10 @@ const { parseTest, parseSuite } = require('../../util');
3
3
  const TestomatClient = require('../../client');
4
4
 
5
5
  const testomatioReporter = on => {
6
+ if (!process.env.TESTOMATIO) {
7
+ console.log('TESTOMATIO key is empty, ignoring reports');
8
+ return
9
+ }
6
10
  const client = new TestomatClient({ apiKey: process.env.TESTOMATIO });
7
11
 
8
12
  on('before:run', async (run) => {
@@ -24,15 +28,23 @@ const testomatioReporter = on => {
24
28
  const time = latestAttempt.duration;
25
29
  const error = latestAttempt.error;
26
30
 
27
- const title = test.title.pop();
31
+ let title = test.title.pop();
32
+ const examples = title.match(/\(example (#\d+)\)/);
33
+ let example = null;
34
+ if (examples && examples[1]) example = { example: examples[1] };
35
+ title = title.replace(/\(example #\d+\)/, '').trim();
36
+
28
37
  const suiteTitle = test.title.pop();
29
38
 
30
39
  const testId = parseTest(title);
31
40
  const suiteId = parseSuite(suiteTitle);
32
41
 
33
42
  if (error) {
34
- error.inspect = function() {
35
- return this.codeFrame.frame;
43
+ error.inspect = function() { // eslint-disable-line func-names
44
+ if (this && this.codeFrame) {
45
+ return this.codeFrame.frame;
46
+ }
47
+ return '';
36
48
  }
37
49
  }
38
50
 
@@ -46,7 +58,7 @@ const testomatioReporter = on => {
46
58
  const state = test.state === 'passed' ? TRConstants.PASSED : TRConstants.FAILED;
47
59
 
48
60
  addSpecTestsPromises.push(client.addTestRun(testId, state, {
49
- title, time, error, files, suite_title: suiteTitle, suite_id: suiteId
61
+ title, time, example, error, files, suite_title: suiteTitle, suite_id: suiteId
50
62
  }));
51
63
  }
52
64
 
@@ -0,0 +1,51 @@
1
+ #!/usr/bin/env node
2
+ const program = require("commander");
3
+ // const chalk = require("chalk");
4
+ const glob = require('glob');
5
+
6
+ const { APP_PREFIX } = require('../constants');
7
+ const XmlReader = require("../xmlReader");
8
+
9
+ // const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || process.env.TESTOMATIO;
10
+ // const title = process.env.TESTOMATIO_TITLE;
11
+
12
+ // console.log(chalk.cyan.bold(` 🤩 xmlReader by Testomat.io v${version}`));
13
+
14
+ program
15
+ .arguments("<pattern>")
16
+ .option("-d, --dir <dir>", "Project directory")
17
+ .option("--java-tests [java-path]", "Load Java tests from path, by default: src/test/java")
18
+ .option("--lang <lang>", "Language used (python, ruby, java)")
19
+ .action(async (pattern, opts) => {
20
+ if (!pattern.endsWith('.xml')) {
21
+ pattern += '.xml';
22
+ }
23
+ let { javaTests, lang } = opts;
24
+ if (javaTests === true) javaTests = 'src/test/java';
25
+ lang = lang?.toLowerCase();
26
+ const runReader = new XmlReader({ javaTests, lang });
27
+ const files = glob.sync(pattern, { cwd: opts.dir || process.cwd() });
28
+ if (!files.length) {
29
+ console.log(APP_PREFIX,`Report can't be created. No XML files found 😥`);
30
+ process.exitCode = 1;
31
+ return;
32
+ }
33
+
34
+ for (const file of files) {
35
+ console.log(APP_PREFIX,`Parsed ${file}`);
36
+ runReader.parse(file);
37
+ }
38
+ try {
39
+ await runReader.createRun();
40
+ await runReader.uploadData();
41
+ } catch (err) {
42
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
43
+ }
44
+ });
45
+
46
+
47
+ if (process.argv.length < 1) {
48
+ program.outputHelp();
49
+ }
50
+
51
+ program.parse(process.argv);
package/lib/client.js CHANGED
@@ -234,7 +234,7 @@ class TestomatClient {
234
234
 
235
235
  formatError(error, message) {
236
236
  if (!message) message = error.message;
237
- if (error.inspect) message = error.inspect();
237
+ if (error.inspect) message = error.inspect() || '';
238
238
 
239
239
  let stack = `\n${chalk.bold(message)}\n`;
240
240
 
@@ -0,0 +1,25 @@
1
+ class Adapter {
2
+
3
+ constructor(opts) {
4
+ this.opts = opts;
5
+ }
6
+
7
+ getFilePath(t) {
8
+ return t.file;
9
+ }
10
+
11
+ formatTest(t) {
12
+ return t;
13
+ }
14
+
15
+ formatStack(t) {
16
+ return t.stack || '';
17
+ }
18
+
19
+ formatMessage(t) {
20
+ return t.message;
21
+ }
22
+
23
+ }
24
+
25
+ module.exports = Adapter;
@@ -0,0 +1,40 @@
1
+ const path = require('path');
2
+ const Adapter = require('./adapter');
3
+
4
+ class JavaAdapter extends Adapter {
5
+
6
+ getFilePath(t) {
7
+ const fileName = namespaceToFileName(t.suite_title)
8
+ return this.opts.javaTests + path.sep + fileName;
9
+ }
10
+
11
+ formatTest(t) {
12
+ const fileParts = t.suite_title.split('.')
13
+ const example = t.title.match(/\[(.*)\]/)?.[1];
14
+ if (example) t.example = { "#": example }
15
+
16
+ t.file = namespaceToFileName(t.suite_title);
17
+ t.title = t.title.split('(')[0];
18
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
19
+ return t;
20
+ }
21
+
22
+ formatStack(t) {
23
+ const stack = super.formatStack(t);
24
+
25
+ const file = t.suite_title.split('.');
26
+
27
+ const fileLine = `at .*${file[file.length - 1]}\.java:(\\d+)` // eslint-disable-line no-useless-escape
28
+ const regexp = new RegExp(fileLine,"g")
29
+ return stack.replace(regexp, `${this.opts.javaTests}${path.sep}${namespaceToFileName(t.suite_title)}:$1:`);
30
+ }
31
+ }
32
+
33
+ function namespaceToFileName(fileName) {
34
+ const fileParts = fileName.split('.')
35
+ fileParts[fileParts.length - 1] = fileParts[fileParts.length - 1]?.replace(/\$.*/, '')
36
+ return `${fileParts.join(path.sep) }.java`;
37
+
38
+ }
39
+
40
+ module.exports = JavaAdapter;
@@ -0,0 +1,31 @@
1
+ const createCallsiteRecord = require('callsite-record');
2
+ const path = require('path');
3
+ const Adapter = require('./adapter');
4
+
5
+ class JavaScriptAdapter extends Adapter {
6
+
7
+ formatStack(t) {
8
+ let stack = super.formatStack(t);
9
+
10
+ try {
11
+ const error = new Error(stack.split('\n')[0])
12
+ error.stack = stack;
13
+ const record = createCallsiteRecord({
14
+ forError: error,
15
+ });
16
+ if (record && !record.filename.startsWith('http')) {
17
+ stack += record.renderSync({
18
+ stackFilter: frame =>
19
+ frame.getFileName().indexOf(path.sep) > -1 &&
20
+ frame.getFileName().indexOf('node_modules') < 0 &&
21
+ frame.getFileName().indexOf('internal') < 0,
22
+ });
23
+ }
24
+ return stack;
25
+ } catch (err) {
26
+ return stack;
27
+ }
28
+ }
29
+ }
30
+
31
+ module.exports = JavaScriptAdapter;
@@ -0,0 +1,42 @@
1
+ const path = require('path');
2
+ const fs = require('fs');
3
+ const Adapter = require('./adapter');
4
+
5
+ class PythonAdapter extends Adapter {
6
+
7
+ getFilePath(t) {
8
+ const fileName = namespaceToFileName(t.suite_title)
9
+ return fileName;
10
+ }
11
+
12
+ formatTest(t) {
13
+ const fileParts = t.suite_title.split('.')
14
+ const example = t.title.match(/\[(.*)\]/)?.[1];
15
+ if (example) t.example = { "#": example }
16
+
17
+ t.file = namespaceToFileName(t.suite_title);
18
+ t.title = t.title.split('[')[0];
19
+ t.suite_title = fileParts[fileParts.length - 1].replace(/\$/g, ' | ')
20
+ return t;
21
+ }
22
+
23
+ formatMessage(t) {
24
+ return t.message.split('&#10;')[0];
25
+ }
26
+
27
+ }
28
+
29
+ function namespaceToFileName(fileName) {
30
+ const fileParts = fileName.split('.')
31
+
32
+ while (fileParts.length > 0) {
33
+ const file = `${fileParts.join(path.sep) }.py`;
34
+ if (fs.existsSync(`${fileParts.join(path.sep) }.py`)) {
35
+ return file;
36
+ }
37
+ fileParts.pop();
38
+ }
39
+ return null;
40
+ }
41
+
42
+ module.exports = PythonAdapter;
@@ -0,0 +1,11 @@
1
+ const Adapter = require('./adapter');
2
+
3
+ class RubyAdapter extends Adapter {
4
+
5
+ formatStack(t) {
6
+ const stack = super.formatStack(t);
7
+ return stack.replace(/\[(.*?:.\d*)\]/g, '\n$1')
8
+ }
9
+ }
10
+
11
+ module.exports = RubyAdapter;
package/lib/util.js CHANGED
@@ -1,4 +1,8 @@
1
1
  const { URL } = require('url');
2
+ const { sep } = require('path');
3
+ const chalk = require('chalk');
4
+ const fs = require('fs');
5
+ const isValid = require('is-valid-path');
2
6
 
3
7
  /**
4
8
  * @param {String} testTitle - Test title
@@ -48,9 +52,98 @@ const isValidUrl = s => {
48
52
  }
49
53
  };
50
54
 
55
+ const fetchFilesFromStackTrace = (stack = '') => {
56
+ const files = stack.matchAll(/file:?\/(\/.*?\.(png|avi|webm|jpg|html|txt))/g);
57
+ return Array.from(files).map(f => f[1]).filter(f => fs.existsSync(f));
58
+ }
59
+
60
+ const fetchSourceCodeFromStackTrace = (stack = '') => {
61
+ const stackLines = stack.split('\n')
62
+ .filter(l => l.includes(':'))
63
+ // .map(l => l.match(/\[(.*?)\]/)?.[1] || l) // minitest format
64
+ // .map(l => l.split(':')[0])
65
+ .map(l => l.trim())
66
+ .map(l => l.split(' ').find(p => p.includes(':')))
67
+ .filter(l => isValid(l.split(':')[0]))
68
+
69
+ // // filter out 3rd party libs
70
+ .filter(l => !l.includes(`vendor${ sep}`))
71
+ .filter(l => !l.includes(`node_modules${ sep}`))
72
+ .filter(l => fs.existsSync(l.split(':')[0]))
73
+ .filter(l => fs.lstatSync(l.split(':')[0]).isFile())
74
+
75
+ if (!stackLines.length) return '';
76
+
77
+ const [file, line] = stackLines[0].split(':');
78
+
79
+ const prepend = 3;
80
+ const source = fetchSourceCode(fs.readFileSync(file).toString(), { line, prepend, limit: 7 })
81
+
82
+ return source.split('\n')
83
+ .map((l, i) => {
84
+ if (i === prepend) return `${line} > ${chalk.bold(l)}`;
85
+ return `${line - prepend + i} | ${l}`
86
+ }).join('\n')
87
+ }
88
+
89
+ const fetchSourceCode = (contents, opts = {}) => {
90
+ if (!opts.title && !opts.line) return '';
91
+
92
+ // code fragment is 20 lines
93
+ const limit = opts.limit || 50;
94
+ let lineIndex;
95
+ if (opts.line) lineIndex = opts.line - 1;
96
+ const lines = contents.split('\n')
97
+
98
+ // remove special chars from title
99
+ if (!lineIndex && opts.title) {
100
+ const title = opts.title.replace(/[([@].*/g, '')
101
+ lineIndex = lines.findIndex(l => l.includes(title))
102
+ }
103
+
104
+ if (opts.prepend) {
105
+ lineIndex -= opts.prepend;
106
+ }
107
+
108
+ if (lineIndex) {
109
+ const result = [];
110
+ for (let i = lineIndex; i < (lineIndex + limit); i++) {
111
+ if (lines[i] === undefined) continue;
112
+
113
+ if (i > lineIndex + 2 && !opts.prepend) {
114
+ // annotation
115
+ if (opts.lang === 'php' && lines[i].trim().startsWith('#[')) break;
116
+ if (opts.lang === 'php' && lines[i].includes(' private function ')) break;
117
+ if (opts.lang === 'php' && lines[i].includes(' protected function ')) break;
118
+ if (opts.lang === 'php' && lines[i].includes(' public function ')) break;
119
+ if (opts.lang === 'python' && lines[i].trim().match(/^@\w+/)) break;
120
+ if (opts.lang === 'python' && lines[i].includes(' def ')) break;
121
+ if (opts.lang === 'ruby' && lines[i].includes(' def ')) break;
122
+ if (opts.lang === 'ruby' && lines[i].includes(' test ')) break;
123
+ if (opts.lang === 'ruby' && lines[i].includes(' it ')) break;
124
+ if (opts.lang === 'ruby' && lines[i].includes(' specify ')) break;
125
+ if (opts.lang === 'ruby' && lines[i].includes(' context ')) break;
126
+ if (opts.lang === 'ts' && lines[i].includes(' it(')) break;
127
+ if (opts.lang === 'ts' && lines[i].includes(' test(')) break;
128
+ if (opts.lang === 'js' && lines[i].includes(' it(')) break;
129
+ if (opts.lang === 'js' && lines[i].includes(' test(')) break;
130
+ if (opts.lang === 'java' && lines[i].trim().match(/^@\w+/)) break;
131
+ if (opts.lang === 'java' && lines[i].includes(' public void ')) break;
132
+ if (opts.lang === 'java' && lines[i].includes(' class ')) break;
133
+ }
134
+ result.push(lines[i])
135
+ }
136
+ return result.join('\n');
137
+ }
138
+
139
+ }
140
+
51
141
  module.exports = {
52
142
  parseTest,
53
143
  parseSuite,
54
144
  ansiRegExp,
55
145
  isValidUrl,
146
+ fetchSourceCode,
147
+ fetchSourceCodeFromStackTrace,
148
+ fetchFilesFromStackTrace,
56
149
  };