@testomatio/reporter 0.7.3 → 0.8.0-beta.10

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/Changelog.md CHANGED
@@ -1,5 +1,6 @@
1
1
  # 0.7.3
2
2
 
3
+ * CodeceptJS: Upload all traces and videos from artifacts
3
4
  * Fixed reporting skipped test in XML
4
5
  * added `--timelimit` option to `report-xml` command line
5
6
 
@@ -35,11 +35,6 @@ function CodeceptReporter(config) {
35
35
  const testTimeMap = {};
36
36
  const { apiKey } = config;
37
37
 
38
- if (!apiKey) {
39
- console.log('TESTOMATIO key is empty, ignoring reports');
40
- return;
41
- }
42
-
43
38
  const getDuration = test => {
44
39
  if (testTimeMap[test.id]) {
45
40
  return Date.now() - testTimeMap[test.id];
@@ -148,7 +143,11 @@ function CodeceptReporter(config) {
148
143
  const files = [];
149
144
  if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
150
145
  // todo: video must be uploaded later....
151
- if (artifacts.video) videos.push({ testId: id, title, path: artifacts.video, type: 'video/webm' });
146
+
147
+ for (const aid in artifacts) {
148
+ if (aid.startsWith('video')) videos.push({ testId: id, title, path: artifacts[aid], type: 'video/webm' });
149
+ if (aid.startsWith('trace')) files.push({ testId: id, title, path: artifacts[aid], type: 'application/zip' });
150
+ }
152
151
 
153
152
  client.addTestRun(testId, TRConstants.FAILED, {
154
153
  ...stripExampleFromTitle(title),
@@ -18,13 +18,7 @@ class CucumberReporter extends Formatter {
18
18
  this.failures = [];
19
19
  this.cases = [];
20
20
 
21
- const apiKey = process.env.TESTOMATIO;
22
- if (!apiKey || apiKey === '') {
23
- console.log(chalk.red('TESTOMATIO key is empty, ignoring reports'));
24
- return;
25
- }
26
-
27
- this.client = new TestomatClient({ apiKey });
21
+ this.client = new TestomatClient();
28
22
  this.status = TRConstants.PASSED;
29
23
 
30
24
  }
@@ -5,14 +5,7 @@ const { PASSED, FAILED } = require('../constants');
5
5
  class JasmineReporter {
6
6
  constructor(options) {
7
7
  this.testTimeMap = {};
8
- const { apiKey } = options;
9
-
10
- if (!apiKey) {
11
- console.log('TESTOMATIO key is empty, ignoring reports');
12
- return;
13
- }
14
-
15
- this.client = new TestomatClient({ apiKey });
8
+ this.client = new TestomatClient({ apiKey: options?.apiKey });
16
9
  this.client.createRun();
17
10
  }
18
11
 
@@ -6,14 +6,8 @@ class JestReporter {
6
6
  constructor(globalConfig, options) {
7
7
  this._globalConfig = globalConfig;
8
8
  this._options = options;
9
- const { apiKey } = options;
10
9
 
11
- if (!apiKey) {
12
- console.log('TESTOMATIO key is empty, ignoring reports');
13
- return;
14
- }
15
-
16
- this.client = new TestomatClient({ apiKey });
10
+ this.client = new TestomatClient({ apiKey: options?.apiKey });
17
11
  this.client.createRun();
18
12
  }
19
13
 
@@ -1,5 +1,6 @@
1
1
  // eslint-disable-next-line global-require, import/no-extraneous-dependencies
2
2
  const Mocha = require('mocha');
3
+ const chalk = require('chalk');
3
4
  const TestomatClient = require('../client');
4
5
  const TRConstants = require('../constants');
5
6
  const { parseTest } = require('../util');
@@ -11,13 +12,7 @@ function MochaReporter(runner, opts) {
11
12
  let passes = 0;
12
13
  let failures = 0;
13
14
 
14
- const { apiKey } = opts.reporterOptions;
15
-
16
- if (!apiKey) {
17
- console.log('TESTOMATIO key is empty, ignoring reports');
18
- return;
19
- }
20
- const client = new TestomatClient({ apiKey });
15
+ const client = new TestomatClient({ apiKey: opts?.reporterOptions?.apiKey });
21
16
 
22
17
  runner.on(EVENT_RUN_BEGIN, () => {
23
18
  client.createRun();
@@ -25,7 +20,7 @@ function MochaReporter(runner, opts) {
25
20
 
26
21
  runner.on(EVENT_TEST_PASS, test => {
27
22
  passes += 1;
28
- console.log('pass: %s', test.fullTitle());
23
+ console.log(chalk.bold.green(''), test.fullTitle());
29
24
  const testId = parseTest(test.title);
30
25
  client.addTestRun(testId, TRConstants.PASSED, {
31
26
  title: test.title,
@@ -35,7 +30,7 @@ function MochaReporter(runner, opts) {
35
30
 
36
31
  runner.on(EVENT_TEST_FAIL, (test, err) => {
37
32
  failures += 1;
38
- console.log('fail: %s -- error: %s', test.fullTitle(), err.message);
33
+ console.log(chalk.bold.red(''), test.fullTitle(), chalk.gray(err.message));
39
34
  const testId = parseTest(test.title);
40
35
  client.addTestRun(testId, TRConstants.FAILED, {
41
36
  error: err,
@@ -45,8 +40,8 @@ function MochaReporter(runner, opts) {
45
40
  });
46
41
 
47
42
  runner.on(EVENT_RUN_END, () => {
48
- console.log('end: %d/%d', passes, passes + failures);
49
43
  const status = failures === 0 ? TRConstants.PASSED : TRConstants.FAILED;
44
+ console.log(chalk.bold(status), `${passes} passed, ${failures} failed`);
50
45
  client.updateRunStatus(status);
51
46
  });
52
47
  }
@@ -10,13 +10,7 @@ const { parseTest } = require('../util');
10
10
 
11
11
  class TestomatioReporter {
12
12
  constructor(config = {}) {
13
- const { apiKey } = config;
14
-
15
- if (!apiKey) {
16
- console.log('API Key is not provided. Testomat.io report is disabled');
17
- } else {
18
- this.client = new TestomatioClient({ apiKey });
19
- }
13
+ this.client = new TestomatioClient({ apiKey: config?.apiKey });
20
14
 
21
15
  this.videos = [];
22
16
  }
@@ -7,10 +7,7 @@ class WebdriverReporter extends WDIOReporter {
7
7
  constructor(options) {
8
8
  super(options);
9
9
 
10
- const { apiKey } = options;
11
- if (!apiKey) return;
12
-
13
- this.client = new TestomatClient({ apiKey });
10
+ this.client = new TestomatClient({ apiKey: options?.apiKey });
14
11
  options = Object.assign(options, { stdout: true });
15
12
 
16
13
  this._addTestPromises = [];
package/lib/client.js CHANGED
@@ -1,19 +1,14 @@
1
1
  const axios = require('axios');
2
- const JsonCycle = require('json-cycle');
3
2
  const createCallsiteRecord = require('callsite-record');
4
3
  const { sep, join } = require('path');
5
4
  const fs = require('fs');
6
5
  const chalk = require('chalk');
7
6
  const upload = require('./fileUploader');
8
- const { isValidUrl } = require('./util');
9
7
  const { PASSED, FAILED, FINISHED, APP_PREFIX } = require('./constants');
8
+ const pipesFactory = require('./pipe');
10
9
 
11
- const TESTOMAT_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
12
- const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
10
+ const { TESTOMATIO_ENV } = process.env;
13
11
 
14
- if (TESTOMATIO_RUN) {
15
- process.env.runId = TESTOMATIO_RUN;
16
- }
17
12
 
18
13
  class TestomatClient {
19
14
  /**
@@ -21,18 +16,17 @@ class TestomatClient {
21
16
  *
22
17
  * @param {*} params
23
18
  */
24
- constructor(params) {
25
- this.apiKey = params.apiKey || process.env.TESTOMATIO;
19
+ constructor(params = {}) {
26
20
  this.title = params.title || process.env.TESTOMATIO_TITLE;
27
- this.createNewTests = !!process.env.TESTOMATIO_CREATE;
28
- this.proceed = process.env.TESTOMATIO_PROCEED;
29
21
  this.env = TESTOMATIO_ENV;
30
22
  this.parallel = params.parallel;
31
- this.runId = process.env.runId;
23
+ const store = {}
24
+ this.pipes = pipesFactory(params, store);
32
25
  this.queue = Promise.resolve();
33
26
  this.axios = axios.create();
34
27
  this.totalUploaded = 0;
35
28
  this.version = JSON.parse(fs.readFileSync(join(__dirname, '..', 'package.json')).toString()).version;
29
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
36
30
  }
37
31
 
38
32
  /**
@@ -41,47 +35,18 @@ class TestomatClient {
41
35
  * @returns {Promise} - resolves to Run id which should be used to update / add test
42
36
  */
43
37
  createRun() {
44
- const { runId } = process.env;
45
- if (!this.apiKey) throw new Error("No API key is set, can't create run");
38
+ // all pipes disabled, skipping
39
+ if (!this.pipes.filter(p => p.isEnabled).length) return;
40
+
46
41
  const runParams = {
47
- api_key: this.apiKey.trim(),
48
42
  title: this.title,
49
43
  parallel: this.parallel,
50
44
  env: this.env,
51
- group_title: TESTOMATIO_RUNGROUP_TITLE,
52
45
  };
53
46
 
54
47
  global.testomatioArtifacts = [];
55
48
 
56
- if (!isValidUrl(TESTOMAT_URL.trim())) {
57
- console.log(
58
- APP_PREFIX,
59
- chalk.red(`Error creating report on Testomat.io, report url '${TESTOMAT_URL}' is invalid`),
60
- );
61
- return;
62
- }
63
-
64
- if (runId) {
65
- this.runId = runId;
66
- this.queue = this.queue.then(() => axios.put(`${TESTOMAT_URL.trim()}/api/reporter/${runId}`, runParams));
67
- return Promise.resolve(runId);
68
- }
69
-
70
- this.queue = this.queue
71
- .then(() =>
72
- this.axios.post(`${TESTOMAT_URL.trim()}/api/reporter`, runParams).then(resp => {
73
- this.runId = resp.data.uid;
74
- this.runUrl = `${TESTOMAT_URL}/${resp.data.url.split('/').splice(3).join('/')}`;
75
- console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId, chalk.gray(`v${this.version}`));
76
- process.env.runId = this.runId;
77
- }),
78
- )
79
- .catch(() => {
80
- console.log(
81
- APP_PREFIX,
82
- 'Error creating report Testomat.io, please check if your API key is valid. Skipping report',
83
- );
84
- });
49
+ this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))));
85
50
 
86
51
  return this.queue;
87
52
  }
@@ -92,6 +57,9 @@ class TestomatClient {
92
57
  * @returns {Promise}
93
58
  */
94
59
  async addTestRun(testId, status, testData = {}) {
60
+ // all pipes disabled, skipping
61
+ if (!this.pipes.filter(p => p.isEnabled).length) return;
62
+
95
63
  const {
96
64
  error = '',
97
65
  time = '',
@@ -120,73 +88,42 @@ class TestomatClient {
120
88
  stack = this.formatSteps(stack, steps);
121
89
  }
122
90
 
123
- if (this.runId) {
124
- if (Array.isArray(global.testomatioArtifacts)) {
125
- files.push(...global.testomatioArtifacts);
126
- global.testomatioArtifacts = [];
127
- }
128
-
129
- for (const file of files) {
130
- uploadedFiles.push(upload.uploadFileByPath(file, this.runId));
131
- }
91
+ if (Array.isArray(global.testomatioArtifacts)) {
92
+ files.push(...global.testomatioArtifacts);
93
+ global.testomatioArtifacts = [];
94
+ }
132
95
 
133
- for (const [idx, buffer] of filesBuffers.entries()) {
134
- const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
96
+ for (const file of files) {
97
+ uploadedFiles.push(upload.uploadFileByPath(file, this.runId));
98
+ }
135
99
 
136
- uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.runId));
137
- }
100
+ for (const [idx, buffer] of filesBuffers.entries()) {
101
+ const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
102
+ uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.runId));
138
103
  }
139
104
 
140
- this.queue = this.queue
141
- .then(async () => {
142
- if (!this.runId) return;
143
-
144
- global.testomatioArtifacts = [];
145
-
146
- this.totalUploaded += uploadedFiles.filter(n => n).length;
147
-
148
- const json = JsonCycle.stringify({
149
- api_key: this.apiKey,
150
- create: this.createNewTests,
151
- files,
152
- steps,
153
- status,
154
- stack,
155
- example,
156
- title,
157
- suite_title,
158
- suite_id,
159
- test_id,
160
- message,
161
- run_time: time,
162
- artifacts: await Promise.all(uploadedFiles),
163
- });
164
- return this.axios.post(`${TESTOMAT_URL}/api/reporter/${this.runId}/testrun`, json, {
165
- maxContentLength: Infinity,
166
- maxBodyLength: Infinity,
167
- headers: {
168
- // Overwrite Axios's automatically set Content-Type
169
- 'Content-Type': 'application/json',
170
- },
171
- });
172
- })
173
- .catch(err => {
174
- if (err.response) {
175
- if (err.response.status >= 400) {
176
- const data = err.response.data || { message: '' };
177
- console.log(
178
- APP_PREFIX,
179
- chalk.blue(title),
180
- `Report couldn't be processed: (${err.response.status}) ${data.message}`,
181
- );
182
- return;
183
- }
184
- console.log(APP_PREFIX, chalk.blue(title), `Report couldn't be processed: ${err.response.data.message}`);
185
- } else {
186
- console.log(APP_PREFIX, chalk.blue(title), "Report couldn't be processed", err);
187
- }
188
- });
105
+ const artifacts = await Promise.all(uploadedFiles)
106
+
107
+ global.testomatioArtifacts = [];
108
+
109
+ this.totalUploaded += uploadedFiles.filter(n => n).length;
110
+
111
+ const data = {
112
+ files,
113
+ steps,
114
+ status,
115
+ stack,
116
+ example,
117
+ title,
118
+ suite_title,
119
+ suite_id,
120
+ test_id,
121
+ message,
122
+ run_time: parseFloat(time),
123
+ artifacts,
124
+ };
189
125
 
126
+ this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.addTest(data))));
190
127
  return this.queue;
191
128
  }
192
129
 
@@ -196,41 +133,26 @@ class TestomatClient {
196
133
  * @returns {Promise}
197
134
  */
198
135
  updateRunStatus(status, isParallel) {
199
- this.queue = this.queue
200
- .then(async () => {
201
- if (this.runId && !this.proceed) {
202
- let statusEvent;
203
- if (status === FINISHED) statusEvent = 'finish';
204
- if (status === PASSED) statusEvent = 'pass';
205
- if (status === FAILED) statusEvent = 'fail';
206
- if (isParallel) statusEvent += '_parallel';
207
- await this.axios.put(`${TESTOMAT_URL}/api/reporter/${this.runId}`, {
208
- api_key: this.apiKey,
209
- status_event: statusEvent,
210
- status,
211
- });
212
- if (this.runUrl) {
213
- console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
214
- }
215
-
216
- if (upload.isArtifactsEnabled && this.totalUploaded > 0) {
217
- console.log(
218
- APP_PREFIX,
219
- `🗄️ Total ${this.totalUploaded} artifacts ${
220
- process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
221
- } uploaded to S3 bucket `,
222
- );
223
- }
224
- }
225
- if (this.runUrl && this.proceed) {
226
- const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
227
- console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
228
- console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
229
- }
230
- })
231
- .catch(err => {
232
- console.log(APP_PREFIX, 'Error updating status, skipping...', err);
233
- });
136
+ // all pipes disabled, skipping
137
+ if (!this.pipes.filter(p => p.isEnabled).length) return;
138
+
139
+ let statusEvent;
140
+ if (status === FINISHED) statusEvent = 'finish';
141
+ if (status === PASSED) statusEvent = 'pass';
142
+ if (status === FAILED) statusEvent = 'fail';
143
+ if (isParallel) statusEvent += '_parallel';
144
+ const runParams = { status, statusEvent }
145
+
146
+ this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))));
147
+
148
+ if (upload.isArtifactsEnabled && this.totalUploaded > 0) {
149
+ console.log(
150
+ APP_PREFIX,
151
+ `🗄️ Total ${this.totalUploaded} artifacts ${
152
+ process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
153
+ } uploaded to S3 bucket `,
154
+ );
155
+ }
234
156
  return this.queue;
235
157
  }
236
158
 
@@ -0,0 +1,27 @@
1
+ const Adapter = require('./adapter');
2
+ const JavaScriptAdapter = require('./javascript');
3
+ const JavaAdapter = require('./java');
4
+ const PythonAdapter = require('./python');
5
+ const RubyAdapter = require('./ruby');
6
+ const CSharpAdapter = require('./csharp');
7
+
8
+
9
+ module.exports = function (lang, opts) {
10
+ if (!lang) return new Adapter(opts);
11
+
12
+ if (lang === 'java') {
13
+ return new JavaAdapter(opts);
14
+ }
15
+ if (lang === 'js') {
16
+ return new JavaScriptAdapter(opts);
17
+ }
18
+ if (lang === 'python') {
19
+ return new PythonAdapter(opts);
20
+ }
21
+ if (lang === 'ruby') {
22
+ return new RubyAdapter(opts);
23
+ }
24
+ if (lang === 'c#' || lang === 'csharp') {
25
+ return new CSharpAdapter(opts);
26
+ }
27
+ }
@@ -0,0 +1,179 @@
1
+ const chalk = require('chalk');
2
+ const { Octokit } = require("@octokit/rest");
3
+ const { APP_PREFIX } = require('../constants');
4
+ const { ansiRegExp, isSameTest } = require('../util');
5
+ const merge = require('lodash.merge');
6
+
7
+
8
+ class GitHubPipe {
9
+
10
+ isEnabled = false;
11
+
12
+ constructor(params, store = {}) {
13
+ this.store = store;
14
+ this.tests = []
15
+ this.token = params.GH_PAT || process.env.GH_PAT;
16
+ this.ref = process.env.GITHUB_REF
17
+ this.repo = process.env.GITHUB_REPOSITORY
18
+
19
+ if (process.env.DEBUG) {
20
+ console.log(APP_PREFIX, 'GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', this.ref, this.repo);
21
+ }
22
+
23
+ if (!this.token || !this.ref || !this.repo) return;
24
+ this.isEnabled = true;
25
+ const matchedIssue = this.ref.match(/refs\/pull\/(\d+)\/merge/);
26
+ if (!matchedIssue) return;
27
+ this.issue = matchedIssue[1]
28
+ this.start = new Date();
29
+
30
+ if (process.env.DEBUG) {
31
+ console.log(APP_PREFIX, 'GitHub Pipe: Enabled');
32
+ }
33
+ }
34
+
35
+ async createRun() {}
36
+
37
+ updateRun() {}
38
+
39
+ addTest(test) {
40
+ if (!this.isEnabled) return;
41
+
42
+ const index = this.tests.findIndex(t => isSameTest(t, test))
43
+ // update if they were already added
44
+ if (index >= 0) {
45
+ this.tests[index] = merge(this.tests[index], test);
46
+ return;
47
+ }
48
+
49
+ this.tests.push(test)
50
+ }
51
+
52
+ async finishRun(runParams) {
53
+ if (!this.isEnabled) return;
54
+
55
+ if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
56
+
57
+
58
+ this.octokit = new Octokit({
59
+ auth: this.token,
60
+ });
61
+
62
+ const [owner, repo] = this.repo.split('/');
63
+ if (!(owner || repo)) return;
64
+
65
+ // ... create a comment on GitHub
66
+ const passedCount = this.tests.filter(t => t.status === 'passed').length;
67
+ const failedCount = this.tests.filter(t => t.status === 'failed').length;
68
+ const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
69
+
70
+ let summary = `
71
+ ### Run Report
72
+
73
+ | | ${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
74
+ | --- | --- |
75
+ | Tests | ✔️ **${this.tests.length}** tests run |
76
+ | Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji('passed')} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
77
+ | Duration | 🕐 **${parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)).toFixed(2)}**ms |
78
+ `
79
+ if (this.store.runUrl) {
80
+ summary += `| Testomat.io Report | 📊 [Run #${this.store.runId}](${this.store.runUrl}) | `;
81
+ }
82
+ if (process.env.GITHUB_WORKFLOW) {
83
+ summary += `| Workflow | 🗂️ ${process.env.GITHUB_WORKFLOW} | `;
84
+ }
85
+ if (process.env.RUNNER_OS) {
86
+ summary += `| Operating System | 🖥️ \`${process.env.RUNNER_OS}\` ${process.env.RUNNER_ARCH || ''} | `;
87
+ }
88
+ if (process.env.GITHUB_HEAD_REF) {
89
+ summary += `| Branch | 🌳 \`${process.env.GITHUB_HEAD_REF}\` | `;
90
+ }
91
+ if (process.env.GITHUB_RUN_ATTEMPT) {
92
+ summary += `| Run Attempt | 🌒 \`${process.env.GITHUB_RUN_ATTEMPT}\` | `;
93
+ }
94
+ if (process.env.GITHUB_RUN_ID) {
95
+ summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID} | `;
96
+ }
97
+
98
+
99
+ const failures = this.tests.filter(t => t.status === 'failed').slice(0, 20).map(t => {
100
+ let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
101
+ text += "\n\n"
102
+ if (t.message) text += "> " + t.message.replace(/[^\x20-\x7E]/g, '').replace(ansiRegExp(), '').trim() + "\n"
103
+ if (t.stack) text += "```diff\n" + t.stack.replace(ansiRegExp(), '').trim() + "\n```\n";
104
+
105
+ if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
106
+ t.artifacts.filter(f => f.endsWith('.png')).forEach(f => {
107
+ if (f.endsWith('.png')) return text+= `![](${f})\n`
108
+ });
109
+ }
110
+
111
+ text += "\n---\n"
112
+
113
+ return text;
114
+ })
115
+
116
+ let body = summary;
117
+
118
+ if (failures.length) {
119
+ body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
120
+ if (failures.length > 20) {
121
+ body += "\n> Notice\n> Only first 20 failures shown*"
122
+ }
123
+ body += "\n\n</details>\n";
124
+ }
125
+
126
+ if (this.tests.length > 0) {
127
+ body += "\n<details>\n<summary><h3>🐢 Slowest Tests</h3>\n\n"
128
+ body += this.tests.sort((a, b) => b?.run_time - a?.run_time).slice(0, 5).map(t => {
129
+ return `* ${fullName(t)} (${parseFloat(t.run_time).toFixed(2)}ms)`
130
+ }).join('\n')
131
+ body += "\n</details>"
132
+ }
133
+
134
+ try {
135
+ const resp = await this.octokit.rest.issues.createComment({
136
+ owner,
137
+ repo,
138
+ issue_number: this.issue,
139
+ body,
140
+ });
141
+
142
+ const url = resp.data?.html_url;
143
+ this.store.githubUrl = url;
144
+
145
+ console.log(
146
+ APP_PREFIX,
147
+ chalk.yellow('GitHub'),
148
+ `Report created: ${chalk.magenta(url)}`,
149
+ );
150
+ } catch (err) {
151
+ console.log(
152
+ APP_PREFIX,
153
+ chalk.yellow('GitHub'),
154
+ `Couldn't create GitHub report ${err}`,
155
+ );
156
+ }
157
+ }
158
+
159
+ toString() {
160
+ return 'GitHub Reporter'
161
+ }
162
+ }
163
+
164
+ function statusEmoji(status) {
165
+ if (status === 'passed') return '🟢';
166
+ if (status === 'failed') return '🔴';
167
+ if (status === 'skipped') return '🟡';
168
+ return ''
169
+ }
170
+
171
+ function fullName(t) {
172
+ let line = '';
173
+ if (t.suite_title) line = `${t.suite_title}: `;
174
+ line += `**${t.title}**`
175
+ if (t.example) line += ` \`[${Object.values(t.example)}]\``;
176
+ return line;
177
+ }
178
+
179
+ module.exports = GitHubPipe;
@@ -0,0 +1,49 @@
1
+ const fs = require('fs');
2
+ const path = require('path');
3
+ const chalk = require('chalk');
4
+ const { APP_PREFIX } = require('../constants');
5
+ const TestomatioPipe = require('./testomatio');
6
+ const GitHubPipe = require('./github');
7
+
8
+ module.exports = function(params, opts) {
9
+ const extraPipes = [];
10
+
11
+ // Add extra pipes into package.json file:
12
+ // "testomatio": {
13
+ // "pipes": ["my-module-pipe", "./local/js/file/pipe"]
14
+ // }
15
+
16
+ const packageJsonFile = path.join(process.cwd(), 'package.json');
17
+ if (fs.existsSync(packageJsonFile)) {
18
+ const package = fs.readFileSync(packageJsonFile);
19
+ const pipeDefs = package?.testomatio?.pipes || [];
20
+
21
+ for (const pipeDef of pipeDefs) {
22
+ let pipeClass;
23
+ try {
24
+ pipeClass = require(pipeDef);
25
+ } catch (err) {
26
+ console.log(APP_PREFIX, `Can't load module Testomatio pipe module from ${pipeDef}`);
27
+ continue;
28
+ }
29
+
30
+ try {
31
+ extraPipes.push(new pipeClass(params, opts))
32
+ } catch (err) {
33
+ console.log(APP_PREFIX, `Can't instantiate Testomatio for ${pipeDef}`, err);
34
+ continue;
35
+ }
36
+ }
37
+ }
38
+
39
+
40
+ const pipes = [
41
+ new TestomatioPipe(params, opts),
42
+ new GitHubPipe(params, opts),
43
+ ...extraPipes
44
+ ];
45
+
46
+ console.log(APP_PREFIX, chalk.cyan('Pipes:'), chalk.cyan(pipes.filter(p => p.isEnabled).map(p => p.toString()).join(', ') || "No pipes enabled"));
47
+
48
+ return pipes;
49
+ }
@@ -0,0 +1,137 @@
1
+ const chalk = require('chalk');
2
+ const axios = require('axios');
3
+ const JsonCycle = require('json-cycle');
4
+ const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_RUN } = process.env;
5
+ const { APP_PREFIX } = require('../constants');
6
+ const { isValidUrl } = require('../util');
7
+
8
+ if (TESTOMATIO_RUN) {
9
+ process.env.runId = TESTOMATIO_RUN;
10
+ }
11
+
12
+ class TestomatioPipe {
13
+ isEnabled = false;
14
+
15
+ constructor(params, store = {}) {
16
+ this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
17
+ this.apiKey = params.apiKey || process.env.TESTOMATIO;
18
+ if (process.env.DEBUG) {
19
+ console.log(APP_PREFIX, 'Testomatio Pipe: ', this.apiKey ? 'API KEY' : '*no api key*');
20
+ }
21
+ if (!this.apiKey) {
22
+ return;
23
+ }
24
+ if (process.env.DEBUG) {
25
+ console.log(APP_PREFIX, 'Testomatio Pipe: Enabled');
26
+ }
27
+ this.store = store;
28
+ this.title = params.title || process.env.TESTOMATIO_TITLE;
29
+ this.axios = axios.create();
30
+ this.isEnabled = true;
31
+ this.proceed = process.env.TESTOMATIO_PROCEED;
32
+ this.runId = params.runId || process.env.runId;
33
+ this.createNewTests = !!process.env.TESTOMATIO_CREATE;
34
+
35
+ if (!isValidUrl(this.url.trim())) {
36
+ this.isEnabled = false;
37
+ console.log(
38
+ APP_PREFIX,
39
+ chalk.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`),
40
+ );
41
+ return;
42
+ }
43
+ }
44
+
45
+ async createRun(runParams) {
46
+ if (!this.isEnabled) return;
47
+
48
+ runParams.api_key = this.apiKey.trim();
49
+ runParams.group_title = TESTOMATIO_RUNGROUP_TITLE;
50
+
51
+ if (this.runId) {
52
+ return this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams)
53
+ }
54
+
55
+ try {
56
+ const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
57
+ maxContentLength: Infinity,
58
+ maxBodyLength: Infinity,
59
+ })
60
+ this.runId = resp.data.uid;
61
+ this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
62
+ this.store.runUrl = this.runUrl;
63
+ this.store.runId = this.runId;
64
+ console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId);
65
+ process.env.runId = this.runId;
66
+ } catch (err) {
67
+ console.log(
68
+ APP_PREFIX,
69
+ 'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
70
+ );
71
+ }
72
+ }
73
+
74
+ async addTest(data) {
75
+ if (!this.isEnabled) return;
76
+ if (!this.runId) return;
77
+ data.api_key = this.apiKey;
78
+ data.create = this.createNewTests;
79
+ const json = JsonCycle.stringify(data);
80
+
81
+ try {
82
+ return await this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
83
+ maxContentLength: Infinity,
84
+ maxBodyLength: Infinity,
85
+ headers: {
86
+ // Overwrite Axios's automatically set Content-Type
87
+ 'Content-Type': 'application/json',
88
+ },
89
+ });
90
+ } catch (err) {
91
+ if (err.response) {
92
+ if (err.response.status >= 400) {
93
+ const data = err.response.data || { message: '' };
94
+ console.log(
95
+ APP_PREFIX,
96
+ chalk.blue(this.title),
97
+ `Report couldn't be processed: (${err.response.status}) ${data.message}`,
98
+ );
99
+ return;
100
+ }
101
+ console.log(APP_PREFIX, chalk.blue(this.title), `Report couldn't be processed: ${err.response.data.message}`);
102
+ } else {
103
+ console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
104
+ }
105
+ }
106
+
107
+ }
108
+
109
+ async finishRun(params) {
110
+ if (!this.isEnabled) return;
111
+ try {
112
+ if (this.runId && !this.proceed) {
113
+ await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
114
+ api_key: this.apiKey,
115
+ status_event: params.statusEvent,
116
+ status: params.status,
117
+ });
118
+ if (this.runUrl) {
119
+ console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
120
+ }
121
+ }
122
+ if (this.runUrl && this.proceed) {
123
+ const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
124
+ console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
125
+ console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
126
+ }
127
+ } catch (err) {
128
+ console.log(APP_PREFIX, 'Error updating status, skipping...', err);
129
+ }
130
+ }
131
+
132
+ toString() {
133
+ return 'Testomatio Reporter';
134
+ }
135
+ }
136
+
137
+ module.exports = TestomatioPipe;
package/lib/util.js CHANGED
@@ -135,13 +135,15 @@ const fetchSourceCode = (contents, opts = {}) => {
135
135
  }
136
136
  return result.join('\n');
137
137
  }
138
-
139
138
  }
140
139
 
140
+ const isSameTest = (test, t) => t.title == test.title && t.suite_title == test.suite_title && Object.values(t.example) == Object.values(test.example) && t.test_id == test.test_id
141
+
141
142
  module.exports = {
142
143
  parseTest,
143
144
  parseSuite,
144
145
  ansiRegExp,
146
+ isSameTest,
145
147
  isValidUrl,
146
148
  fetchSourceCode,
147
149
  fetchSourceCodeFromStackTrace,
package/lib/xmlReader.js CHANGED
@@ -1,19 +1,14 @@
1
1
  const path = require("path");
2
- const axios = require('axios');
3
2
  const chalk = require('chalk');
4
3
  const fs = require("fs");
5
4
 
6
5
  // const util = require("util"); // you can see a result
7
6
  const { XMLParser } = require("fast-xml-parser");
8
7
  const { APP_PREFIX, PASSED, FAILED, SKIPPED } = require('./constants');
9
- const { isValidUrl, fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
8
+ const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
10
9
  const upload = require('./fileUploader');
11
- const Adapter = require('./junit-adapter/adapter');
12
- const JavaScriptAdapter = require('./junit-adapter/javascript');
13
- const JavaAdapter = require('./junit-adapter/java');
14
- const PythonAdapter = require('./junit-adapter/python');
15
- const RubyAdapter = require('./junit-adapter/ruby');
16
- const CSharpAdapter = require('./junit-adapter/csharp');
10
+ const pipesFactory = require('./pipe');
11
+ const adapterFactory = require('./junit-adapter');
17
12
 
18
13
  const TESTOMATIO_URL = process.env.TESTOMATIO_URL || "https://app.testomat.io";
19
14
  const TESTOMATIO = process.env.TESTOMATIO; // key?
@@ -40,32 +35,24 @@ class XmlReader {
40
35
  group_title: TESTOMATIO_RUNGROUP_TITLE,
41
36
  };
42
37
  this.runId = opts.runId || TESTOMATIO_RUN;
43
- this.adapter = new Adapter(opts)
38
+ this.adapter = adapterFactory(null, opts)
44
39
  this.opts = opts;
45
- this.axios = axios.create();
40
+ const store = {}
41
+ this.pipes = pipesFactory(opts, store);
42
+
46
43
  this.parser = new XMLParser(options);
47
44
  this.tests = []
48
45
  this.stats = {}
49
46
  this.stats.language = opts.lang?.toLowerCase();
50
47
  this.filesToUpload = {}
48
+
49
+ this.version = JSON.parse(fs.readFileSync(path.join(__dirname, '..', 'package.json')).toString()).version;
50
+ console.log(APP_PREFIX, `Testomatio Reporter v${this.version}`);
51
51
  }
52
52
 
53
53
  connectAdapter() {
54
- if (this.opts.javaTests || this.stats.language === 'java') {
55
- this.adapter = new JavaAdapter(this.opts);
56
- }
57
- if (this.stats.language === 'js') {
58
- this.adapter = new JavaScriptAdapter(this.opts);
59
- }
60
- if (this.stats.language === 'python') {
61
- this.adapter = new PythonAdapter(this.opts);
62
- }
63
- if (this.stats.language === 'ruby') {
64
- this.adapter = new RubyAdapter(this.opts);
65
- }
66
- if (this.stats.language === 'c#' || this.stats.language === 'csharp') {
67
- this.adapter = new CSharpAdapter(this.opts);
68
- }
54
+ if (this.opts.javaTests) return adapterFactory('java', this.opts);
55
+ return adapterFactory(this.stats.language, this.opts);
69
56
  }
70
57
 
71
58
  parse(fileName) {
@@ -149,7 +136,7 @@ class XmlReader {
149
136
 
150
137
  const results = jsonSuite?.TestRun?.Results?.UnitTestResult?.map(td => ({
151
138
  id: td.executionId,
152
- run_time: td.duration,
139
+ run_time: parseFloat(td.duration),
153
140
  status: td.outcome,
154
141
  stack: td.Output.StdOut
155
142
  }));
@@ -276,7 +263,6 @@ class XmlReader {
276
263
  }
277
264
 
278
265
  async uploadArtifacts() {
279
- if (!this.runId) return;
280
266
  for (const test of this.tests.filter(t => !!t.stack)) {
281
267
  const files = fetchFilesFromStackTrace(test.stack);
282
268
  test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
@@ -284,8 +270,6 @@ class XmlReader {
284
270
  }
285
271
 
286
272
  async createRun() {
287
- if (this.runId) return;
288
-
289
273
  const runParams = {
290
274
  api_key: this.requestParams.apiKey,
291
275
  title: this.requestParams.title,
@@ -295,30 +279,7 @@ class XmlReader {
295
279
 
296
280
  if (process.env.DEBUG) console.log("Run", runParams);
297
281
 
298
- try {
299
- const resp = await this.axios.post(this.url, runParams, {
300
- maxContentLength: Infinity,
301
- maxBodyLength: Infinity,
302
- headers: {
303
- // Overwrite Axios's automatically set Content-Type
304
- 'Content-Type': 'application/json',
305
- }
306
- });
307
- if (resp.status >= 400) {
308
- const data = resp.data || { message: '' };
309
- console.log(
310
- APP_PREFIX,
311
- `Report couldn't be processed: (${resp.status}) ${data.message}`,
312
- );
313
- return;
314
- }
315
- this.runId = resp.data.uid;
316
- this.runUrl = `${TESTOMATIO_URL}/${resp.data.url.split('/').splice(3).join('/')}`;
317
- } catch(err) {
318
- if (process.env.DEBUG) console.log(err)
319
- const data = err?.response?.data || { message: '' };
320
- console.log(APP_PREFIX, 'Error creating run, skipping...', err?.response?.statusText, data);
321
- }
282
+ return Promise.all(this.pipes.map(p => p.createRun(runParams)));
322
283
  }
323
284
 
324
285
  async uploadData() {
@@ -336,56 +297,14 @@ class XmlReader {
336
297
  })
337
298
  }
338
299
 
339
- const dataString = JSON.stringify({
300
+ const dataString = {
340
301
  ...this.stats,
341
302
  api_key: this.requestParams.apiKey,
342
303
  status_event: 'finish',
343
304
  tests: this.tests,
344
- });
345
-
346
- try {
347
- const resp = await this.axios.put(`${this.url}/${this.runId}`, dataString, {
348
- maxContentLength: Infinity,
349
- maxBodyLength: Infinity,
350
- headers: {
351
- // Overwrite Axios's automatically set Content-Type
352
- 'Content-Type': 'application/json',
353
- }
354
- });
355
-
356
- if (resp.status >= 400) {
357
- const data = resp.data || { message: '' };
358
- console.log(
359
- APP_PREFIX,
360
- `Report couldn't be processed: (${resp.status}) ${data.message}`,
361
- );
362
- return;
363
- }
364
-
365
- if (this.runUrl) {
366
- console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
367
- }
368
- process.env.runId = this.runId;
369
- return resp;
370
- } catch (err) {
371
- // if (process.env.DEBUG) console.log(err.response)
372
- const data = err.response.data || { message: '' };
373
- console.log(APP_PREFIX, 'Error uploading, skipping...', err.response.statusText, data);
374
- return err.response;
375
- }
376
-
377
- }
378
-
379
- get url() {
380
- if (!isValidUrl(this.requestParams.url)) {
381
- console.log(
382
- APP_PREFIX,
383
- chalk.red(`Error creating report on Testomat.io, report url '${this.requestParams.url}' is invalid`),
384
- );
385
- return;
386
- }
305
+ };
387
306
 
388
- return `${this.requestParams.url}/api/reporter`;
307
+ return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
389
308
  }
390
309
  }
391
310
 
@@ -426,7 +345,7 @@ function reduceTestCases(prev, item) {
426
345
  stack,
427
346
  message,
428
347
  line: testCaseItem.lineno,
429
- run_time: testCaseItem.time || testCaseItem.duration,
348
+ run_time: parseFloat(testCaseItem.time || testCaseItem.duration),
430
349
  status,
431
350
  title: testCaseItem.name,
432
351
  suite_title: reduceOptions.preferClassname ? testCaseItem.classname : (item.name || testCaseItem.classname),
@@ -449,4 +368,3 @@ function processTestSuite(testsuite) {
449
368
 
450
369
  return res;
451
370
  }
452
-
package/package.json CHANGED
@@ -1,12 +1,13 @@
1
1
  {
2
2
  "name": "@testomatio/reporter",
3
- "version": "0.7.3",
3
+ "version": "0.8.0-beta.10",
4
4
  "description": "Testomatio Reporter Client",
5
5
  "main": "./lib/reporter.js",
6
6
  "repository": "git@github.com:testomatio/reporter.git",
7
7
  "author": "Michael Bodnarchuk <davert@testomat.io>,Koushik Mohan <koushikmohan1996@gmail.com>",
8
8
  "license": "MIT",
9
9
  "dependencies": {
10
+ "@octokit/rest": "^19.0.5",
10
11
  "aws-sdk": "^2.1072.0",
11
12
  "axios": "^0.25.0",
12
13
  "callsite-record": "^4.1.4",
@@ -18,7 +19,8 @@
18
19
  "has-flag": "^5.0.1",
19
20
  "is-valid-path": "^0.1.1",
20
21
  "json-cycle": "^1.3.0",
21
- "lodash.memoize": "^4.1.2"
22
+ "lodash.memoize": "^4.1.2",
23
+ "lodash.merge": "^4.6.2"
22
24
  },
23
25
  "files": [
24
26
  "bin",