@testomatio/reporter 0.5.10 → 0.6.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/.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,11 @@
1
+ # 0.6.1
2
+
3
+ * Fixed creating RunGroup with JUnit reporter
4
+
5
+ # 0.6.0
6
+
7
+ * JUnit reporter support
8
+
1
9
  # 0.5.10
2
10
 
3
11
  * Fixed reporting Scenario Outline in Cypress-Cucumber
package/README.md CHANGED
@@ -205,6 +205,136 @@ 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 can 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
+ mvn 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
+
311
+ ### Artifacts
312
+
313
+ Screenshots or videos from tests are uploaded if a test contains output with a path to file of following format:
314
+
315
+ ```
316
+ file://path/to/screenshot.png
317
+ ```
318
+
319
+ For instance, inside Java test you can use `System.out.println` to print a path to file that should be uploaded as a screenshot.
320
+
321
+ ```java
322
+ System.out.println("file://" + pathToScreenshot);
323
+ ```
324
+
325
+ This will produce XML report which contains path to a file:
326
+
327
+ ```xml
328
+ <testcase>
329
+ <system-out><![CDATA[
330
+ file://path/to/scrrenshot.png
331
+ ]]></system-out>
332
+ </testcase>
333
+ ```
334
+
335
+ When uploaded XML report, all files from `file://` will be uploaded to corresponding tests.
336
+
337
+
208
338
  ## Advanced Usage
209
339
 
210
340
  ### Create Unmatched Tests
@@ -29,7 +29,7 @@ const testomatioReporter = on => {
29
29
  const error = latestAttempt.error;
30
30
 
31
31
  let title = test.title.pop();
32
- let examples = title.match(/\(example (#\d+)\)/);
32
+ const examples = title.match(/\(example (#\d+)\)/);
33
33
  let example = null;
34
34
  if (examples && examples[1]) example = { example: examples[1] };
35
35
  title = title.replace(/\(example #\d+\)/, '').trim();
@@ -40,7 +40,7 @@ const testomatioReporter = on => {
40
40
  const suiteId = parseSuite(suiteTitle);
41
41
 
42
42
  if (error) {
43
- error.inspect = function() {
43
+ error.inspect = function() { // eslint-disable-line func-names
44
44
  if (this && this.codeFrame) {
45
45
  return this.codeFrame.frame;
46
46
  }
@@ -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);
@@ -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
  };