@testomatio/reporter 0.7.3 → 0.8.0-beta.2
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 +1 -0
- package/lib/adapter/codecept.js +5 -1
- package/lib/client.js +49 -136
- package/lib/junit-adapter/index.js +27 -0
- package/lib/pipe/github.js +163 -0
- package/lib/pipe/index.js +45 -0
- package/lib/pipe/testomatio.js +128 -0
- package/lib/util.js +4 -0
- package/lib/xmlReader.js +16 -101
- package/package.json +2 -1
package/Changelog.md
CHANGED
package/lib/adapter/codecept.js
CHANGED
|
@@ -148,7 +148,11 @@ function CodeceptReporter(config) {
|
|
|
148
148
|
const files = [];
|
|
149
149
|
if (artifacts.screenshot) files.push({ path: artifacts.screenshot, type: 'image/png' });
|
|
150
150
|
// todo: video must be uploaded later....
|
|
151
|
-
|
|
151
|
+
|
|
152
|
+
for (const aid in artifacts) {
|
|
153
|
+
if (aid.startsWith('video')) videos.push({ testId: id, title, path: artifacts[aid], type: 'video/webm' });
|
|
154
|
+
if (aid.startsWith('trace')) files.push({ testId: id, title, path: artifacts[aid], type: 'application/zip' });
|
|
155
|
+
}
|
|
152
156
|
|
|
153
157
|
client.addTestRun(testId, TRConstants.FAILED, {
|
|
154
158
|
...stripExampleFromTitle(title),
|
package/lib/client.js
CHANGED
|
@@ -1,19 +1,13 @@
|
|
|
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');
|
|
9
|
+
const { TESTOMATIO_ENV } = process.env;
|
|
10
10
|
|
|
11
|
-
const TESTOMAT_URL = process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
12
|
-
const { TESTOMATIO_RUNGROUP_TITLE, TESTOMATIO_ENV, TESTOMATIO_RUN } = process.env;
|
|
13
|
-
|
|
14
|
-
if (TESTOMATIO_RUN) {
|
|
15
|
-
process.env.runId = TESTOMATIO_RUN;
|
|
16
|
-
}
|
|
17
11
|
|
|
18
12
|
class TestomatClient {
|
|
19
13
|
/**
|
|
@@ -24,11 +18,10 @@ class TestomatClient {
|
|
|
24
18
|
constructor(params) {
|
|
25
19
|
this.apiKey = params.apiKey || process.env.TESTOMATIO;
|
|
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
|
-
|
|
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;
|
|
@@ -42,46 +35,15 @@ class TestomatClient {
|
|
|
42
35
|
*/
|
|
43
36
|
createRun() {
|
|
44
37
|
const { runId } = process.env;
|
|
45
|
-
if (!this.apiKey) throw new Error("No API key is set, can't create run");
|
|
46
38
|
const runParams = {
|
|
47
|
-
api_key: this.apiKey.trim(),
|
|
48
39
|
title: this.title,
|
|
49
40
|
parallel: this.parallel,
|
|
50
41
|
env: this.env,
|
|
51
|
-
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
52
42
|
};
|
|
53
43
|
|
|
54
44
|
global.testomatioArtifacts = [];
|
|
55
45
|
|
|
56
|
-
|
|
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
|
-
});
|
|
46
|
+
this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.createRun(runParams))));
|
|
85
47
|
|
|
86
48
|
return this.queue;
|
|
87
49
|
}
|
|
@@ -120,73 +82,42 @@ class TestomatClient {
|
|
|
120
82
|
stack = this.formatSteps(stack, steps);
|
|
121
83
|
}
|
|
122
84
|
|
|
123
|
-
if (
|
|
124
|
-
|
|
125
|
-
|
|
126
|
-
|
|
127
|
-
}
|
|
128
|
-
|
|
129
|
-
for (const file of files) {
|
|
130
|
-
uploadedFiles.push(upload.uploadFileByPath(file, this.runId));
|
|
131
|
-
}
|
|
85
|
+
if (Array.isArray(global.testomatioArtifacts)) {
|
|
86
|
+
files.push(...global.testomatioArtifacts);
|
|
87
|
+
global.testomatioArtifacts = [];
|
|
88
|
+
}
|
|
132
89
|
|
|
133
|
-
|
|
134
|
-
|
|
90
|
+
for (const file of files) {
|
|
91
|
+
uploadedFiles.push(upload.uploadFileByPath(file, this.runId));
|
|
92
|
+
}
|
|
135
93
|
|
|
136
|
-
|
|
137
|
-
}
|
|
94
|
+
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
95
|
+
const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
|
|
96
|
+
uploadedFiles.push(upload.uploadFileAsBuffer(buffer, fileName, this.runId));
|
|
138
97
|
}
|
|
139
98
|
|
|
140
|
-
|
|
141
|
-
.then(async () => {
|
|
142
|
-
if (!this.runId) return;
|
|
99
|
+
const artifacts = await Promise.all(uploadedFiles)
|
|
143
100
|
|
|
144
|
-
|
|
101
|
+
global.testomatioArtifacts = [];
|
|
145
102
|
|
|
146
|
-
|
|
103
|
+
this.totalUploaded += uploadedFiles.filter(n => n).length;
|
|
147
104
|
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
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 data = {
|
|
106
|
+
files,
|
|
107
|
+
steps,
|
|
108
|
+
status,
|
|
109
|
+
stack,
|
|
110
|
+
example,
|
|
111
|
+
title,
|
|
112
|
+
suite_title,
|
|
113
|
+
suite_id,
|
|
114
|
+
test_id,
|
|
115
|
+
message,
|
|
116
|
+
run_time: parseFloat(time),
|
|
117
|
+
artifacts,
|
|
118
|
+
};
|
|
189
119
|
|
|
120
|
+
this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.addTest(data))));
|
|
190
121
|
return this.queue;
|
|
191
122
|
}
|
|
192
123
|
|
|
@@ -196,41 +127,23 @@ class TestomatClient {
|
|
|
196
127
|
* @returns {Promise}
|
|
197
128
|
*/
|
|
198
129
|
updateRunStatus(status, isParallel) {
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
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
|
-
}
|
|
130
|
+
let statusEvent;
|
|
131
|
+
if (status === FINISHED) statusEvent = 'finish';
|
|
132
|
+
if (status === PASSED) statusEvent = 'pass';
|
|
133
|
+
if (status === FAILED) statusEvent = 'fail';
|
|
134
|
+
if (isParallel) statusEvent += '_parallel';
|
|
135
|
+
const runParams = { status, statusEvent }
|
|
215
136
|
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
219
|
-
|
|
220
|
-
|
|
221
|
-
|
|
222
|
-
|
|
223
|
-
|
|
224
|
-
|
|
225
|
-
|
|
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
|
-
});
|
|
137
|
+
this.queue = this.queue.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))));
|
|
138
|
+
|
|
139
|
+
if (upload.isArtifactsEnabled && this.totalUploaded > 0) {
|
|
140
|
+
console.log(
|
|
141
|
+
APP_PREFIX,
|
|
142
|
+
`🗄️ Total ${this.totalUploaded} artifacts ${
|
|
143
|
+
process.env.TESTOMATIO_PRIVATE_ARTIFACTS ? 'privately' : chalk.bold('publicly')
|
|
144
|
+
} uploaded to S3 bucket `,
|
|
145
|
+
);
|
|
146
|
+
}
|
|
234
147
|
return this.queue;
|
|
235
148
|
}
|
|
236
149
|
|
|
@@ -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,163 @@
|
|
|
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_ACTION_REPOSITORY
|
|
18
|
+
if (!this.token || !this.ref || !this.repo) return;
|
|
19
|
+
this.isEnabled = true;
|
|
20
|
+
this.issue = this.ref.match(/refs\/pull\/(\d+)\/merge/)[1]
|
|
21
|
+
this.start = new Date();
|
|
22
|
+
}
|
|
23
|
+
|
|
24
|
+
async createRun() {}
|
|
25
|
+
|
|
26
|
+
updateRun() {}
|
|
27
|
+
|
|
28
|
+
addTest(test) {
|
|
29
|
+
if (!this.isEnabled) return;
|
|
30
|
+
|
|
31
|
+
const index = this.tests.findIndex(t => isSameTest(t, test))
|
|
32
|
+
// update if they were already added
|
|
33
|
+
if (index >= 0) {
|
|
34
|
+
this.tests[index] = merge(this.tests[index], test);
|
|
35
|
+
return;
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
this.tests.push(test)
|
|
39
|
+
}
|
|
40
|
+
|
|
41
|
+
async finishRun(runParams) {
|
|
42
|
+
if (!this.isEnabled) return;
|
|
43
|
+
|
|
44
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
45
|
+
|
|
46
|
+
|
|
47
|
+
this.octokit = new Octokit({
|
|
48
|
+
auth: this.token,
|
|
49
|
+
});
|
|
50
|
+
|
|
51
|
+
const [owner, repo] = this.repo.split('/');
|
|
52
|
+
if (!(owner || repo)) return;
|
|
53
|
+
|
|
54
|
+
// ... create a comment on GitHub
|
|
55
|
+
const passedCount = this.tests.filter(t => t.status === 'passed').length;
|
|
56
|
+
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
57
|
+
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
58
|
+
|
|
59
|
+
let summary = `
|
|
60
|
+
### Run Report
|
|
61
|
+
|
|
62
|
+
| | ${statusEmoji(runParams.status)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
63
|
+
| --- | --- |
|
|
64
|
+
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
65
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji('passed')} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
66
|
+
| Duration | 🕐 **${parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)).toFixed(2)}**ms |
|
|
67
|
+
`
|
|
68
|
+
if (this.store.runUrl) {
|
|
69
|
+
summary += `| Testomat.io Report | 📊 [Run #${this.store.runId}](${this.store.runUrl}) | `;
|
|
70
|
+
}
|
|
71
|
+
if (process.env.GITHUB_WORKFLOW) {
|
|
72
|
+
summary += `| Workflow | 🗂️ ${process.env.GITHUB_WORKFLOW} | `;
|
|
73
|
+
}
|
|
74
|
+
if (process.env.RUNNER_OS) {
|
|
75
|
+
summary += `| Operating System | 🖥️ \`${process.env.RUNNER_OS}\` ${process.env.RUNNER_ARCH || ''} | `;
|
|
76
|
+
}
|
|
77
|
+
if (process.env.GITHUB_HEAD_REF) {
|
|
78
|
+
summary += `| Branch | 🌳 \`${process.env.GITHUB_HEAD_REF}\` | `;
|
|
79
|
+
}
|
|
80
|
+
if (process.env.GITHUB_RUN_ATTEMPT) {
|
|
81
|
+
summary += `| Run Attempt | 🌒 \`${process.env.GITHUB_RUN_ATTEMPT}\` | `;
|
|
82
|
+
}
|
|
83
|
+
if (process.env.GITHUB_RUN_ID) {
|
|
84
|
+
summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${this.repo}/actions/runs/${process.env.GITHUB_RUN_ID} | `;
|
|
85
|
+
}
|
|
86
|
+
|
|
87
|
+
|
|
88
|
+
const failures = this.tests.filter(t => t.status === 'failed').slice(0, 20).map(t => {
|
|
89
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
90
|
+
text += "\n\n"
|
|
91
|
+
if (t.message) text += "> " + t.message.replace(/[^\x20-\x7E]/g, '').replace(ansiRegExp(), '').trim() + "\n"
|
|
92
|
+
if (t.stack) text += "```diff\n" + t.stack.replace(ansiRegExp(), '').trim() + "\n```\n";
|
|
93
|
+
|
|
94
|
+
if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
95
|
+
t.artifacts.filter(f => f.endsWith('.png')).forEach(f => {
|
|
96
|
+
if (f.endsWith('.png')) return text+= `\n`
|
|
97
|
+
});
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
text += "\n---\n"
|
|
101
|
+
|
|
102
|
+
return text;
|
|
103
|
+
})
|
|
104
|
+
|
|
105
|
+
let body = summary;
|
|
106
|
+
|
|
107
|
+
if (failures.length) {
|
|
108
|
+
body += `<details>\n<summary>### 🟥 Failures (${failures.length})</summary>\n${failures.join('\n')} </details>`;
|
|
109
|
+
}
|
|
110
|
+
|
|
111
|
+
if (failures.length > 20) {
|
|
112
|
+
body += "\n> Notice\n> Only first 20 failures shown*"
|
|
113
|
+
}
|
|
114
|
+
|
|
115
|
+
if (this.tests.length > 0) {
|
|
116
|
+
body += "\n### 🐢 Slowest Tests\n\n"
|
|
117
|
+
body += this.tests.sort((a, b) => b?.run_time - a?.run_time).slice(0, 5).map(t => {
|
|
118
|
+
return `* ${fullName(t)} (${parseFloat(t.run_time).toFixed(2)}ms)`
|
|
119
|
+
}).join('\n')
|
|
120
|
+
}
|
|
121
|
+
|
|
122
|
+
try {
|
|
123
|
+
const resp = await this.octokit.rest.issues.createComment({
|
|
124
|
+
owner,
|
|
125
|
+
repo,
|
|
126
|
+
issue_number: this.issue,
|
|
127
|
+
body,
|
|
128
|
+
});
|
|
129
|
+
|
|
130
|
+
const url = resp.data?.html_url;
|
|
131
|
+
this.store.githubUrl = url;
|
|
132
|
+
|
|
133
|
+
console.log(
|
|
134
|
+
APP_PREFIX,
|
|
135
|
+
chalk.yellow('GitHub'),
|
|
136
|
+
`Report created: ${chalk.magenta(url)}`,
|
|
137
|
+
);
|
|
138
|
+
} catch (err) {
|
|
139
|
+
console.log(
|
|
140
|
+
APP_PREFIX,
|
|
141
|
+
chalk.yellow('GitHub'),
|
|
142
|
+
`Couldn't create GitHub report ${err}`,
|
|
143
|
+
);
|
|
144
|
+
}
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
function statusEmoji(status) {
|
|
149
|
+
if (status === 'passed') return '🟢';
|
|
150
|
+
if (status === 'failed') return '🔴';
|
|
151
|
+
if (status === 'skipped') return '🟡';
|
|
152
|
+
return ''
|
|
153
|
+
}
|
|
154
|
+
|
|
155
|
+
function fullName(t) {
|
|
156
|
+
let line = '';
|
|
157
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
158
|
+
line += `**${t.title}**`
|
|
159
|
+
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
160
|
+
return line;
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
module.exports = GitHubPipe;
|
|
@@ -0,0 +1,45 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const { APP_PREFIX } = require('../constants');
|
|
4
|
+
const TestomatioPipe = require('./testomatio');
|
|
5
|
+
const GitHubPipe = require('./github');
|
|
6
|
+
|
|
7
|
+
module.exports = function(params, opts) {
|
|
8
|
+
const extraPipes = [];
|
|
9
|
+
|
|
10
|
+
// Add extra pipes into package.json file:
|
|
11
|
+
// "testomatio": {
|
|
12
|
+
// "pipes": ["my-module-pipe", "./local/js/file/pipe"]
|
|
13
|
+
// }
|
|
14
|
+
|
|
15
|
+
const packageJsonFile = path.join(process.cwd(), 'package.json');
|
|
16
|
+
if (fs.existsSync(packageJsonFile)) {
|
|
17
|
+
const package = fs.readFileSync(packageJsonFile);
|
|
18
|
+
const pipeDefs = package?.testomatio?.pipes || [];
|
|
19
|
+
|
|
20
|
+
for (const pipeDef of pipeDefs) {
|
|
21
|
+
let pipeClass;
|
|
22
|
+
try {
|
|
23
|
+
pipeClass = require(pipeDef);
|
|
24
|
+
} catch (err) {
|
|
25
|
+
console.log(APP_PREFIX, `Can't load module Testomatio pipe module from ${pipeDef}`);
|
|
26
|
+
continue;
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
try {
|
|
30
|
+
extraPipes.push(new pipeClass(params, opts))
|
|
31
|
+
} catch (err) {
|
|
32
|
+
console.log(APP_PREFIX, `Can't instantiate Testomatio for ${pipeDef}`, err);
|
|
33
|
+
continue;
|
|
34
|
+
}
|
|
35
|
+
}
|
|
36
|
+
}
|
|
37
|
+
|
|
38
|
+
|
|
39
|
+
const defaultPipes = [
|
|
40
|
+
new TestomatioPipe(params, opts),
|
|
41
|
+
new GitHubPipe(params, opts),
|
|
42
|
+
];
|
|
43
|
+
|
|
44
|
+
return [...defaultPipes, ...extraPipes];
|
|
45
|
+
}
|
|
@@ -0,0 +1,128 @@
|
|
|
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 (!this.apiKey) {
|
|
19
|
+
return;
|
|
20
|
+
}
|
|
21
|
+
this.store = store;
|
|
22
|
+
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
23
|
+
this.axios = axios.create();
|
|
24
|
+
this.isEnabled = true;
|
|
25
|
+
this.proceed = process.env.TESTOMATIO_PROCEED;
|
|
26
|
+
this.runId = params.runId || process.env.runId;
|
|
27
|
+
this.createNewTests = !!process.env.TESTOMATIO_CREATE;
|
|
28
|
+
|
|
29
|
+
|
|
30
|
+
if (!isValidUrl(this.url.trim())) {
|
|
31
|
+
this.isEnabled = false;
|
|
32
|
+
console.log(
|
|
33
|
+
APP_PREFIX,
|
|
34
|
+
chalk.red(`Error creating report on Testomat.io, report url '${this.url}' is invalid`),
|
|
35
|
+
);
|
|
36
|
+
return;
|
|
37
|
+
}
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
async createRun(runParams) {
|
|
41
|
+
if (!this.isEnabled) return;
|
|
42
|
+
|
|
43
|
+
runParams.api_key = this.apiKey.trim();
|
|
44
|
+
runParams.group_title = TESTOMATIO_RUNGROUP_TITLE;
|
|
45
|
+
|
|
46
|
+
if (this.runId) {
|
|
47
|
+
return this.axios.put(`${this.url.trim()}/api/reporter/${this.runId}`, runParams)
|
|
48
|
+
}
|
|
49
|
+
|
|
50
|
+
try {
|
|
51
|
+
const resp = await this.axios.post(`${this.url.trim()}/api/reporter`, runParams, {
|
|
52
|
+
maxContentLength: Infinity,
|
|
53
|
+
maxBodyLength: Infinity,
|
|
54
|
+
})
|
|
55
|
+
this.runId = resp.data.uid;
|
|
56
|
+
this.runUrl = `${this.url}/${resp.data.url.split('/').splice(3).join('/')}`;
|
|
57
|
+
this.store.runUrl = this.runUrl;
|
|
58
|
+
this.store.runId = this.runId;
|
|
59
|
+
console.log(APP_PREFIX, '📊 Report created. Report ID:', this.runId, chalk.gray(`v${this.version}`));
|
|
60
|
+
process.env.runId = this.runId;
|
|
61
|
+
} catch (err) {
|
|
62
|
+
console.log(
|
|
63
|
+
APP_PREFIX,
|
|
64
|
+
'Error creating Testomat.io report, please check if your API key is valid. Skipping report',
|
|
65
|
+
);
|
|
66
|
+
}
|
|
67
|
+
}
|
|
68
|
+
|
|
69
|
+
async addTest(data) {
|
|
70
|
+
if (!this.isEnabled) return;
|
|
71
|
+
if (!this.runId) return;
|
|
72
|
+
data.api_key = this.apiKey;
|
|
73
|
+
data.create = this.createNewTests;
|
|
74
|
+
const json = JsonCycle.stringify(data);
|
|
75
|
+
|
|
76
|
+
try {
|
|
77
|
+
return await this.axios.post(`${this.url}/api/reporter/${this.runId}/testrun`, json, {
|
|
78
|
+
maxContentLength: Infinity,
|
|
79
|
+
maxBodyLength: Infinity,
|
|
80
|
+
headers: {
|
|
81
|
+
// Overwrite Axios's automatically set Content-Type
|
|
82
|
+
'Content-Type': 'application/json',
|
|
83
|
+
},
|
|
84
|
+
});
|
|
85
|
+
} catch (err) {
|
|
86
|
+
if (err.response) {
|
|
87
|
+
if (err.response.status >= 400) {
|
|
88
|
+
const data = err.response.data || { message: '' };
|
|
89
|
+
console.log(
|
|
90
|
+
APP_PREFIX,
|
|
91
|
+
chalk.blue(this.title),
|
|
92
|
+
`Report couldn't be processed: (${err.response.status}) ${data.message}`,
|
|
93
|
+
);
|
|
94
|
+
return;
|
|
95
|
+
}
|
|
96
|
+
console.log(APP_PREFIX, chalk.blue(this.title), `Report couldn't be processed: ${err.response.data.message}`);
|
|
97
|
+
} else {
|
|
98
|
+
console.log(APP_PREFIX, chalk.blue(this.title), "Report couldn't be processed", err);
|
|
99
|
+
}
|
|
100
|
+
}
|
|
101
|
+
|
|
102
|
+
}
|
|
103
|
+
|
|
104
|
+
async finishRun(params) {
|
|
105
|
+
if (!this.isEnabled) return;
|
|
106
|
+
try {
|
|
107
|
+
if (this.runId && !this.proceed) {
|
|
108
|
+
await this.axios.put(`${this.url}/api/reporter/${this.runId}`, {
|
|
109
|
+
api_key: this.apiKey,
|
|
110
|
+
status_event: params.statusEvent,
|
|
111
|
+
status: params.status,
|
|
112
|
+
});
|
|
113
|
+
if (this.runUrl) {
|
|
114
|
+
console.log(APP_PREFIX, '📊 Report Saved. Report URL:', chalk.magenta(this.runUrl));
|
|
115
|
+
}
|
|
116
|
+
}
|
|
117
|
+
if (this.runUrl && this.proceed) {
|
|
118
|
+
const notFinishedMessage = chalk.yellow.bold('Run was not finished because of $TESTOMATIO_PROCEED');
|
|
119
|
+
console.log(APP_PREFIX, `📊 ${notFinishedMessage}. Report URL: ${chalk.magenta(this.runUrl)}`);
|
|
120
|
+
console.log(APP_PREFIX, `🛬 Run to finish it: TESTOMATIO_RUN=${this.runId} npx start-test-run --finish`);
|
|
121
|
+
}
|
|
122
|
+
} catch (err) {
|
|
123
|
+
console.log(APP_PREFIX, 'Error updating status, skipping...', err);
|
|
124
|
+
}
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
module.exports = TestomatioPipe;
|
package/lib/util.js
CHANGED
|
@@ -135,13 +135,17 @@ const fetchSourceCode = (contents, opts = {}) => {
|
|
|
135
135
|
}
|
|
136
136
|
return result.join('\n');
|
|
137
137
|
}
|
|
138
|
+
}
|
|
138
139
|
|
|
140
|
+
const isSameTest = (test, t) => {
|
|
141
|
+
return t.title == test.title && t.suite_title == test.suite_title && Object.values(t.example) == Object.values(test.example) && t.test_id == test.test_id;
|
|
139
142
|
}
|
|
140
143
|
|
|
141
144
|
module.exports = {
|
|
142
145
|
parseTest,
|
|
143
146
|
parseSuite,
|
|
144
147
|
ansiRegExp,
|
|
148
|
+
isSameTest,
|
|
145
149
|
isValidUrl,
|
|
146
150
|
fetchSourceCode,
|
|
147
151
|
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
|
-
const {
|
|
9
|
-
const {
|
|
7
|
+
const { PASSED, FAILED, SKIPPED } = require('./constants');
|
|
8
|
+
const { fetchFilesFromStackTrace, fetchSourceCode, fetchSourceCodeFromStackTrace } = require('./util');
|
|
10
9
|
const upload = require('./fileUploader');
|
|
11
|
-
const
|
|
12
|
-
const
|
|
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,9 +35,11 @@ class XmlReader {
|
|
|
40
35
|
group_title: TESTOMATIO_RUNGROUP_TITLE,
|
|
41
36
|
};
|
|
42
37
|
this.runId = opts.runId || TESTOMATIO_RUN;
|
|
43
|
-
this.adapter =
|
|
38
|
+
this.adapter = adapterFactory(null, opts)
|
|
44
39
|
this.opts = opts;
|
|
45
|
-
|
|
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 = {}
|
|
@@ -51,21 +48,8 @@ class XmlReader {
|
|
|
51
48
|
}
|
|
52
49
|
|
|
53
50
|
connectAdapter() {
|
|
54
|
-
if (this.opts.javaTests
|
|
55
|
-
|
|
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
|
-
}
|
|
51
|
+
if (this.opts.javaTests) return adapterFactory('java', this.opts);
|
|
52
|
+
return adapterFactory(this.stats.language, this.opts);
|
|
69
53
|
}
|
|
70
54
|
|
|
71
55
|
parse(fileName) {
|
|
@@ -149,7 +133,7 @@ class XmlReader {
|
|
|
149
133
|
|
|
150
134
|
const results = jsonSuite?.TestRun?.Results?.UnitTestResult?.map(td => ({
|
|
151
135
|
id: td.executionId,
|
|
152
|
-
run_time: td.duration,
|
|
136
|
+
run_time: parseFloat(td.duration),
|
|
153
137
|
status: td.outcome,
|
|
154
138
|
stack: td.Output.StdOut
|
|
155
139
|
}));
|
|
@@ -276,7 +260,6 @@ class XmlReader {
|
|
|
276
260
|
}
|
|
277
261
|
|
|
278
262
|
async uploadArtifacts() {
|
|
279
|
-
if (!this.runId) return;
|
|
280
263
|
for (const test of this.tests.filter(t => !!t.stack)) {
|
|
281
264
|
const files = fetchFilesFromStackTrace(test.stack);
|
|
282
265
|
test.artifacts = await Promise.all(files.map(f => upload.uploadFileByPath(f, this.runId)));
|
|
@@ -284,8 +267,6 @@ class XmlReader {
|
|
|
284
267
|
}
|
|
285
268
|
|
|
286
269
|
async createRun() {
|
|
287
|
-
if (this.runId) return;
|
|
288
|
-
|
|
289
270
|
const runParams = {
|
|
290
271
|
api_key: this.requestParams.apiKey,
|
|
291
272
|
title: this.requestParams.title,
|
|
@@ -295,30 +276,7 @@ class XmlReader {
|
|
|
295
276
|
|
|
296
277
|
if (process.env.DEBUG) console.log("Run", runParams);
|
|
297
278
|
|
|
298
|
-
|
|
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
|
-
}
|
|
279
|
+
return Promise.all(this.pipes.map(p => p.createRun(runParams)));
|
|
322
280
|
}
|
|
323
281
|
|
|
324
282
|
async uploadData() {
|
|
@@ -336,56 +294,14 @@ class XmlReader {
|
|
|
336
294
|
})
|
|
337
295
|
}
|
|
338
296
|
|
|
339
|
-
const dataString =
|
|
297
|
+
const dataString = {
|
|
340
298
|
...this.stats,
|
|
341
299
|
api_key: this.requestParams.apiKey,
|
|
342
300
|
status_event: 'finish',
|
|
343
301
|
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
|
-
}
|
|
302
|
+
};
|
|
387
303
|
|
|
388
|
-
return
|
|
304
|
+
return Promise.all(this.pipes.map(p => p.finishRun(dataString)));
|
|
389
305
|
}
|
|
390
306
|
}
|
|
391
307
|
|
|
@@ -426,7 +342,7 @@ function reduceTestCases(prev, item) {
|
|
|
426
342
|
stack,
|
|
427
343
|
message,
|
|
428
344
|
line: testCaseItem.lineno,
|
|
429
|
-
run_time: testCaseItem.time || testCaseItem.duration,
|
|
345
|
+
run_time: parseFloat(testCaseItem.time || testCaseItem.duration),
|
|
430
346
|
status,
|
|
431
347
|
title: testCaseItem.name,
|
|
432
348
|
suite_title: reduceOptions.preferClassname ? testCaseItem.classname : (item.name || testCaseItem.classname),
|
|
@@ -449,4 +365,3 @@ function processTestSuite(testsuite) {
|
|
|
449
365
|
|
|
450
366
|
return res;
|
|
451
367
|
}
|
|
452
|
-
|
package/package.json
CHANGED
|
@@ -1,12 +1,13 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "@testomatio/reporter",
|
|
3
|
-
"version": "0.
|
|
3
|
+
"version": "0.8.0-beta.2",
|
|
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",
|