@testomatio/reporter 0.8.0-beta.8 → 0.8.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/README.md +77 -7
- package/lib/ArtifactStorage.js +131 -0
- package/lib/adapter/codecept.js +25 -24
- package/lib/adapter/cucumber/current.js +16 -12
- package/lib/adapter/cucumber/legacy.js +14 -10
- package/lib/adapter/cypress-plugin/index.js +7 -7
- package/lib/adapter/jasmine.js +5 -4
- package/lib/adapter/jest.js +7 -11
- package/lib/adapter/mocha.js +69 -18
- package/lib/adapter/playwright.js +8 -5
- package/lib/adapter/webdriver.js +2 -1
- package/lib/bin/reportXml.js +5 -2
- package/lib/bin/startTest.js +2 -2
- package/lib/client.js +73 -49
- package/lib/constants.js +17 -2
- package/lib/fileUploader.js +86 -49
- package/lib/junit-adapter/csharp.js +0 -1
- package/lib/junit-adapter/index.js +3 -2
- package/lib/pipe/csv.js +133 -0
- package/lib/pipe/github.js +137 -73
- package/lib/pipe/gitlab.js +229 -0
- package/lib/pipe/index.js +27 -12
- package/lib/pipe/testomatio.js +71 -44
- package/lib/reporter.js +2 -0
- package/lib/util.js +46 -13
- package/lib/xmlReader.js +47 -31
- package/package.json +12 -3
- package/testcafe/index.js +0 -61
- package/testcafe/package.json +0 -14
package/lib/fileUploader.js
CHANGED
|
@@ -1,4 +1,6 @@
|
|
|
1
|
-
const
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:file-uploader');
|
|
2
|
+
const { S3 } = require('@aws-sdk/client-s3');
|
|
3
|
+
const { Upload } = require('@aws-sdk/lib-storage');
|
|
2
4
|
const fs = require('fs');
|
|
3
5
|
const path = require('path');
|
|
4
6
|
const chalk = require('chalk');
|
|
@@ -6,18 +8,38 @@ const { randomUUID } = require('crypto');
|
|
|
6
8
|
const memoize = require('lodash.memoize');
|
|
7
9
|
const { APP_PREFIX } = require('./constants');
|
|
8
10
|
|
|
9
|
-
const
|
|
10
|
-
S3_ENDPOINT,
|
|
11
|
-
S3_REGION,
|
|
12
|
-
S3_BUCKET,
|
|
13
|
-
S3_ACCESS_KEY_ID,
|
|
14
|
-
S3_SECRET_ACCESS_KEY,
|
|
15
|
-
TESTOMATIO_DISABLE_ARTIFACTS,
|
|
16
|
-
TESTOMATIO_PRIVATE_ARTIFACTS,
|
|
17
|
-
S3_FORCE_PATH_STYLE,
|
|
18
|
-
|
|
19
|
-
|
|
20
|
-
|
|
11
|
+
const keys = [
|
|
12
|
+
'S3_ENDPOINT',
|
|
13
|
+
'S3_REGION',
|
|
14
|
+
'S3_BUCKET',
|
|
15
|
+
'S3_ACCESS_KEY_ID',
|
|
16
|
+
'S3_SECRET_ACCESS_KEY',
|
|
17
|
+
'TESTOMATIO_DISABLE_ARTIFACTS',
|
|
18
|
+
'TESTOMATIO_PRIVATE_ARTIFACTS',
|
|
19
|
+
'S3_FORCE_PATH_STYLE',
|
|
20
|
+
];
|
|
21
|
+
|
|
22
|
+
let config;
|
|
23
|
+
|
|
24
|
+
function getConfig() {
|
|
25
|
+
if (config) return config;
|
|
26
|
+
config = keys.reduce((acc, key) => {
|
|
27
|
+
acc[key] = process.env[key];
|
|
28
|
+
return acc;
|
|
29
|
+
}, {});
|
|
30
|
+
debug('Config', config);
|
|
31
|
+
return config;
|
|
32
|
+
}
|
|
33
|
+
|
|
34
|
+
let isEnabled;
|
|
35
|
+
|
|
36
|
+
const isArtifactsEnabled = () => {
|
|
37
|
+
if (isEnabled !== undefined) return isEnabled;
|
|
38
|
+
const { S3_BUCKET, TESTOMATIO_DISABLE_ARTIFACTS } = getConfig();
|
|
39
|
+
isEnabled = !!(S3_BUCKET && !TESTOMATIO_DISABLE_ARTIFACTS);
|
|
40
|
+
debug(`Upload is ${isEnabled ? 'enabled' : 'disabled'}`);
|
|
41
|
+
return isEnabled;
|
|
42
|
+
};
|
|
21
43
|
|
|
22
44
|
const _getFileExtBase64 = str => {
|
|
23
45
|
const type = str.charAt(0);
|
|
@@ -32,6 +54,25 @@ const _getFileExtBase64 = str => {
|
|
|
32
54
|
);
|
|
33
55
|
};
|
|
34
56
|
|
|
57
|
+
const _getS3Config = () => {
|
|
58
|
+
const { S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_FORCE_PATH_STYLE, S3_ENDPOINT } = getConfig();
|
|
59
|
+
|
|
60
|
+
const cfg = {
|
|
61
|
+
region: S3_REGION,
|
|
62
|
+
credentials: {
|
|
63
|
+
accessKeyId: S3_ACCESS_KEY_ID,
|
|
64
|
+
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
65
|
+
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
66
|
+
}
|
|
67
|
+
};
|
|
68
|
+
|
|
69
|
+
if (S3_ENDPOINT) {
|
|
70
|
+
cfg.endpoint = S3_ENDPOINT;
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return cfg;
|
|
74
|
+
}
|
|
75
|
+
|
|
35
76
|
const uploadUsingS3 = async (filePath, runId) => {
|
|
36
77
|
let ContentType;
|
|
37
78
|
let Key;
|
|
@@ -42,40 +83,39 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
42
83
|
Key = filePath.name;
|
|
43
84
|
}
|
|
44
85
|
|
|
45
|
-
const config = {
|
|
46
|
-
region: S3_REGION,
|
|
47
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
48
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
49
|
-
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
50
|
-
};
|
|
51
|
-
|
|
52
|
-
if (S3_ENDPOINT) {
|
|
53
|
-
config.endpoint = new AWS.Endpoint(S3_ENDPOINT);
|
|
54
|
-
}
|
|
55
|
-
|
|
56
|
-
const s3 = new AWS.S3(config);
|
|
57
86
|
if (!fs.existsSync(filePath)) {
|
|
58
87
|
console.error(chalk.yellow(`Artifacts file ${filePath} does not exist. Skipping...`));
|
|
59
88
|
return;
|
|
60
89
|
}
|
|
90
|
+
|
|
91
|
+
const {
|
|
92
|
+
S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
93
|
+
} = getConfig();
|
|
61
94
|
|
|
62
95
|
const file = fs.readFileSync(filePath);
|
|
63
96
|
|
|
64
97
|
Key = `${runId}/${randomUUID()}-${Key || path.basename(filePath)}`;
|
|
65
98
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
66
99
|
|
|
100
|
+
const s3 = new S3(_getS3Config());
|
|
101
|
+
|
|
67
102
|
try {
|
|
68
|
-
const out =
|
|
69
|
-
|
|
103
|
+
const out = new Upload({
|
|
104
|
+
client: s3,
|
|
105
|
+
|
|
106
|
+
params: {
|
|
70
107
|
Bucket: S3_BUCKET,
|
|
71
108
|
Key,
|
|
72
109
|
Body: file,
|
|
73
110
|
ContentType,
|
|
74
111
|
ACL,
|
|
75
|
-
}
|
|
76
|
-
|
|
112
|
+
}
|
|
113
|
+
});
|
|
77
114
|
|
|
78
|
-
|
|
115
|
+
await out.done();
|
|
116
|
+
debug('Uploaded', out.singleUploadResult.Location)
|
|
117
|
+
|
|
118
|
+
return out.singleUploadResult.Location;
|
|
79
119
|
} catch (e) {
|
|
80
120
|
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
81
121
|
accessKeyId: S3_ACCESS_KEY_ID,
|
|
@@ -100,35 +140,32 @@ const uploadUsingS3 = async (filePath, runId) => {
|
|
|
100
140
|
};
|
|
101
141
|
|
|
102
142
|
const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
103
|
-
const config = {
|
|
104
|
-
region: S3_REGION,
|
|
105
|
-
accessKeyId: S3_ACCESS_KEY_ID,
|
|
106
|
-
secretAccessKey: S3_SECRET_ACCESS_KEY,
|
|
107
|
-
s3ForcePathStyle: S3_FORCE_PATH_STYLE,
|
|
108
|
-
};
|
|
109
|
-
|
|
110
|
-
if (S3_ENDPOINT) {
|
|
111
|
-
config.endpoint = new AWS.Endpoint(S3_ENDPOINT);
|
|
112
|
-
}
|
|
113
143
|
|
|
114
|
-
const
|
|
144
|
+
const {
|
|
145
|
+
S3_REGION, S3_ACCESS_KEY_ID, S3_SECRET_ACCESS_KEY, S3_ENDPOINT, TESTOMATIO_PRIVATE_ARTIFACTS, S3_BUCKET
|
|
146
|
+
} = getConfig();
|
|
115
147
|
|
|
116
148
|
const ACL = TESTOMATIO_PRIVATE_ARTIFACTS ? 'private' : 'public-read';
|
|
117
149
|
|
|
118
150
|
const fileExtension = _getFileExtBase64(buffer.toString('base64'));
|
|
119
151
|
const Key = `${runId}/${fileName}${fileExtension}`;
|
|
120
152
|
|
|
153
|
+
const s3 = new S3(_getS3Config());
|
|
154
|
+
|
|
121
155
|
try {
|
|
122
|
-
const out =
|
|
123
|
-
|
|
156
|
+
const out = new Upload({
|
|
157
|
+
client: s3,
|
|
158
|
+
|
|
159
|
+
params: {
|
|
124
160
|
Bucket: S3_BUCKET,
|
|
125
161
|
Key,
|
|
126
162
|
Body: buffer,
|
|
127
163
|
ACL,
|
|
128
|
-
}
|
|
129
|
-
|
|
164
|
+
}
|
|
165
|
+
});
|
|
166
|
+
await out.done();
|
|
130
167
|
|
|
131
|
-
return out.Location;
|
|
168
|
+
return out.singleUploadResult.Location;
|
|
132
169
|
} catch (e) {
|
|
133
170
|
console.log(APP_PREFIX, chalk.bold.red(`Failed uploading '${Key}'. Please check S3 credentials`), {
|
|
134
171
|
accessKeyId: S3_ACCESS_KEY_ID,
|
|
@@ -154,7 +191,7 @@ const uploadUsingS3AsBuffer = async (buffer, fileName, runId) => {
|
|
|
154
191
|
|
|
155
192
|
const uploadFileByPath = async (filePath, runId) => {
|
|
156
193
|
try {
|
|
157
|
-
if (isArtifactsEnabled) {
|
|
194
|
+
if (isArtifactsEnabled()) {
|
|
158
195
|
return uploadUsingS3(filePath, runId);
|
|
159
196
|
}
|
|
160
197
|
} catch (e) {
|
|
@@ -164,7 +201,7 @@ const uploadFileByPath = async (filePath, runId) => {
|
|
|
164
201
|
|
|
165
202
|
const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
166
203
|
try {
|
|
167
|
-
if (isArtifactsEnabled) {
|
|
204
|
+
if (isArtifactsEnabled()) {
|
|
168
205
|
return uploadUsingS3AsBuffer(buffer, fileName, runId);
|
|
169
206
|
}
|
|
170
207
|
} catch (e) {
|
|
@@ -175,5 +212,5 @@ const uploadFileAsBuffer = async (buffer, fileName, runId) => {
|
|
|
175
212
|
module.exports = {
|
|
176
213
|
uploadFileByPath: memoize(uploadFileByPath),
|
|
177
214
|
uploadFileAsBuffer: memoize(uploadFileAsBuffer),
|
|
178
|
-
isArtifactsEnabled,
|
|
215
|
+
isArtifactsEnabled: memoize(isArtifactsEnabled),
|
|
179
216
|
};
|
|
@@ -5,8 +5,7 @@ const PythonAdapter = require('./python');
|
|
|
5
5
|
const RubyAdapter = require('./ruby');
|
|
6
6
|
const CSharpAdapter = require('./csharp');
|
|
7
7
|
|
|
8
|
-
|
|
9
|
-
module.exports = function (lang, opts) {
|
|
8
|
+
function AdapterFactory(lang, opts) {
|
|
10
9
|
if (!lang) return new Adapter(opts);
|
|
11
10
|
|
|
12
11
|
if (lang === 'java') {
|
|
@@ -25,3 +24,5 @@ module.exports = function (lang, opts) {
|
|
|
25
24
|
return new CSharpAdapter(opts);
|
|
26
25
|
}
|
|
27
26
|
}
|
|
27
|
+
|
|
28
|
+
module.exports = AdapterFactory;
|
package/lib/pipe/csv.js
ADDED
|
@@ -0,0 +1,133 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:csv');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const fs = require('fs');
|
|
4
|
+
const csvWriter = require('csv-writer');
|
|
5
|
+
const chalk = require('chalk');
|
|
6
|
+
const merge = require('lodash.merge');
|
|
7
|
+
const { isSameTest, getCurrentDateTime } = require('../util');
|
|
8
|
+
const { CSV_HEADERS } = require('../constants');
|
|
9
|
+
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
12
|
+
* @typedef {import('../../types').TestData} TestData
|
|
13
|
+
* @class CsvPipe
|
|
14
|
+
* @implements {Pipe}
|
|
15
|
+
*/
|
|
16
|
+
class CsvPipe {
|
|
17
|
+
|
|
18
|
+
constructor(params, store) {
|
|
19
|
+
this.store = store || {};
|
|
20
|
+
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
21
|
+
this.isEnabled = true;
|
|
22
|
+
this.results = [];
|
|
23
|
+
|
|
24
|
+
this.outputDir = 'export';
|
|
25
|
+
this.csvFilename = process.env.TESTOMATIO_CSV_FILENAME;
|
|
26
|
+
this.isCsvSave = false;
|
|
27
|
+
|
|
28
|
+
if (this.csvFilename !== undefined && this.csvFilename.split('.').length > 0) {
|
|
29
|
+
this.isCsvSave = true;
|
|
30
|
+
|
|
31
|
+
if (this.csvFilename.split('.')[0] === 'report') {
|
|
32
|
+
this.outputFile = path.resolve(process.cwd(), this.outputDir, 'report.csv');
|
|
33
|
+
} else {
|
|
34
|
+
this.outputFile = path.resolve(
|
|
35
|
+
process.cwd(),
|
|
36
|
+
this.outputDir,
|
|
37
|
+
`${getCurrentDateTime()}_${this.csvFilename.split('.')[0]}.csv`,
|
|
38
|
+
);
|
|
39
|
+
}
|
|
40
|
+
}
|
|
41
|
+
}
|
|
42
|
+
|
|
43
|
+
async createRun() {
|
|
44
|
+
// empty
|
|
45
|
+
}
|
|
46
|
+
|
|
47
|
+
updateRun() {}
|
|
48
|
+
|
|
49
|
+
/**
|
|
50
|
+
* Create a folder that will contain the exported files
|
|
51
|
+
*/
|
|
52
|
+
checkExportDir() {
|
|
53
|
+
if (!fs.existsSync(this.outputDir)) {
|
|
54
|
+
return fs.mkdirSync(this.outputDir);
|
|
55
|
+
}
|
|
56
|
+
}
|
|
57
|
+
|
|
58
|
+
/**
|
|
59
|
+
* Save data to the csv file.
|
|
60
|
+
* @param {Object} data - data that will be added to the CSV file.
|
|
61
|
+
* Example: [{suite_title: "Suite #1", test: "Test-case-1", message: "Test msg"}]
|
|
62
|
+
* @param {Object} headers - csv file headers. Example: [{ id: 'suite_title', title: 'Suite_title' }]
|
|
63
|
+
*/
|
|
64
|
+
async saveToCsv(data, headers) {
|
|
65
|
+
debug('Data', data);
|
|
66
|
+
// First, we check whether the export directory exists: if yes - OK, no - create it.
|
|
67
|
+
this.checkExportDir();
|
|
68
|
+
|
|
69
|
+
if (!this.outputFile) {
|
|
70
|
+
console.log(this, chalk.yellow(`CSV file is not set, ignoring`));
|
|
71
|
+
return;
|
|
72
|
+
}
|
|
73
|
+
|
|
74
|
+
console.log(this, chalk.yellow(`The test results will be added to the csv. It will take some time...`));
|
|
75
|
+
// Create csv writer object
|
|
76
|
+
const writer = csvWriter.createObjectCsvWriter({
|
|
77
|
+
path: this.outputFile,
|
|
78
|
+
header: headers,
|
|
79
|
+
});
|
|
80
|
+
// Save csv file based on the current data
|
|
81
|
+
try {
|
|
82
|
+
await writer.writeRecords(data);
|
|
83
|
+
console.log(this, chalk.green(`Recording completed! You can check the result in file = ${this.outputFile}`));
|
|
84
|
+
} catch (e) {
|
|
85
|
+
console.log('Unknown error: ', e);
|
|
86
|
+
}
|
|
87
|
+
}
|
|
88
|
+
|
|
89
|
+
/**
|
|
90
|
+
* Add test data to the result array for saving. As a result of this function, we get a result object to save.
|
|
91
|
+
* @param {Object} test - object which includes each test entry.
|
|
92
|
+
*/
|
|
93
|
+
addTest(test) {
|
|
94
|
+
if (!this.isEnabled) return;
|
|
95
|
+
|
|
96
|
+
const index = this.results.findIndex(t => isSameTest(t, test));
|
|
97
|
+
// update if they were already added
|
|
98
|
+
if (index >= 0) {
|
|
99
|
+
this.results[index] = merge(this.results[index], test);
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
|
|
103
|
+
const { suite_title, title, status, message, stack } = test;
|
|
104
|
+
|
|
105
|
+
this.results.push({
|
|
106
|
+
suite_title,
|
|
107
|
+
title,
|
|
108
|
+
status,
|
|
109
|
+
message,
|
|
110
|
+
stack,
|
|
111
|
+
});
|
|
112
|
+
}
|
|
113
|
+
|
|
114
|
+
/**
|
|
115
|
+
* @param {{ tests?: TestData[] }} runParams
|
|
116
|
+
* @returns {Promise<void>}
|
|
117
|
+
*/
|
|
118
|
+
async finishRun(runParams) {
|
|
119
|
+
if (!this.isEnabled) return;
|
|
120
|
+
|
|
121
|
+
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
122
|
+
// Save results based on the default headers
|
|
123
|
+
if (this.isCsvSave) {
|
|
124
|
+
this.saveToCsv(this.results, CSV_HEADERS);
|
|
125
|
+
}
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
toString() {
|
|
129
|
+
return 'csv exporter';
|
|
130
|
+
}
|
|
131
|
+
}
|
|
132
|
+
|
|
133
|
+
module.exports = CsvPipe;
|
package/lib/pipe/github.js
CHANGED
|
@@ -1,65 +1,68 @@
|
|
|
1
|
+
const debug = require('debug')('@testomatio/reporter:pipe:github');
|
|
2
|
+
const path = require('path');
|
|
1
3
|
const chalk = require('chalk');
|
|
2
|
-
const
|
|
4
|
+
const humanizeDuration = require('humanize-duration');
|
|
5
|
+
const merge = require('lodash.merge');
|
|
6
|
+
const { Octokit } = require('@octokit/rest');
|
|
3
7
|
const { APP_PREFIX } = require('../constants');
|
|
4
8
|
const { ansiRegExp, isSameTest } = require('../util');
|
|
5
|
-
const merge = require('lodash.merge');
|
|
6
|
-
|
|
7
9
|
|
|
10
|
+
/**
|
|
11
|
+
* @typedef {import('../../types').Pipe} Pipe
|
|
12
|
+
* @typedef {import('../../types').TestData} TestData
|
|
13
|
+
* @class GitHubPipe
|
|
14
|
+
* @implements {Pipe}
|
|
15
|
+
*/
|
|
8
16
|
class GitHubPipe {
|
|
9
|
-
|
|
10
|
-
isEnabled = false;
|
|
11
|
-
|
|
12
17
|
constructor(params, store = {}) {
|
|
18
|
+
this.isEnabled = false;
|
|
13
19
|
this.store = store;
|
|
14
|
-
this.tests = []
|
|
20
|
+
this.tests = [];
|
|
15
21
|
this.token = params.GH_PAT || process.env.GH_PAT;
|
|
16
|
-
this.ref = process.env.GITHUB_REF
|
|
17
|
-
this.repo = process.env.GITHUB_REPOSITORY
|
|
22
|
+
this.ref = process.env.GITHUB_REF;
|
|
23
|
+
this.repo = process.env.GITHUB_REPOSITORY;
|
|
24
|
+
this.hiddenCommentData = `<!--- testomat.io report ${process.env.GITHUB_WORKFLOW || ''} -->`;
|
|
18
25
|
|
|
19
|
-
|
|
20
|
-
console.log(APP_PREFIX, 'GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', this.ref, this.repo);
|
|
21
|
-
}
|
|
26
|
+
debug('GitHub Pipe: ', this.token ? 'TOKEN' : '*no token*', 'Ref:', this.ref, 'Repo:', this.repo);
|
|
22
27
|
|
|
23
28
|
if (!this.token || !this.ref || !this.repo) return;
|
|
24
29
|
this.isEnabled = true;
|
|
25
30
|
const matchedIssue = this.ref.match(/refs\/pull\/(\d+)\/merge/);
|
|
26
31
|
if (!matchedIssue) return;
|
|
27
|
-
this.issue = matchedIssue[1]
|
|
32
|
+
this.issue = parseInt(matchedIssue[1], 10);
|
|
33
|
+
|
|
28
34
|
this.start = new Date();
|
|
29
35
|
|
|
30
|
-
|
|
31
|
-
console.log(APP_PREFIX, 'GitHub Pipe: Enabled');
|
|
32
|
-
}
|
|
36
|
+
debug('GitHub Pipe: Enabled');
|
|
33
37
|
}
|
|
34
|
-
|
|
35
|
-
async createRun() {}
|
|
36
38
|
|
|
37
|
-
|
|
39
|
+
async createRun() {}
|
|
38
40
|
|
|
39
41
|
addTest(test) {
|
|
42
|
+
debug('Adding test:', test);
|
|
40
43
|
if (!this.isEnabled) return;
|
|
41
44
|
|
|
42
|
-
const index = this.tests.findIndex(t => isSameTest(t, test))
|
|
45
|
+
const index = this.tests.findIndex(t => isSameTest(t, test));
|
|
43
46
|
// update if they were already added
|
|
44
47
|
if (index >= 0) {
|
|
45
48
|
this.tests[index] = merge(this.tests[index], test);
|
|
46
49
|
return;
|
|
47
50
|
}
|
|
48
51
|
|
|
49
|
-
this.tests.push(test)
|
|
52
|
+
this.tests.push(test);
|
|
50
53
|
}
|
|
51
54
|
|
|
52
55
|
async finishRun(runParams) {
|
|
53
56
|
if (!this.isEnabled) return;
|
|
57
|
+
if (!this.issue) return;
|
|
54
58
|
|
|
55
59
|
if (runParams.tests) runParams.tests.forEach(t => this.addTest(t));
|
|
56
|
-
|
|
57
60
|
|
|
58
61
|
this.octokit = new Octokit({
|
|
59
62
|
auth: this.token,
|
|
60
63
|
});
|
|
61
64
|
|
|
62
|
-
const [owner, repo] = this.repo.split('/');
|
|
65
|
+
const [owner, repo] = (this.repo || '').split('/');
|
|
63
66
|
if (!(owner || repo)) return;
|
|
64
67
|
|
|
65
68
|
// ... create a comment on GitHub
|
|
@@ -67,15 +70,17 @@ class GitHubPipe {
|
|
|
67
70
|
const failedCount = this.tests.filter(t => t.status === 'failed').length;
|
|
68
71
|
const skippedCount = this.tests.filter(t => t.status === 'skipped').length;
|
|
69
72
|
|
|
70
|
-
let summary =
|
|
71
|
-
### Run Report
|
|
73
|
+
let summary = `${this.hiddenCommentData}
|
|
72
74
|
|
|
73
|
-
|
|
|
75
|
+
| [](https://testomat.io) | ${
|
|
76
|
+
statusEmoji(runParams.status,)} ${runParams.status.toUpperCase()} ${statusEmoji(runParams.status)} |
|
|
74
77
|
| --- | --- |
|
|
75
78
|
| Tests | ✔️ **${this.tests.length}** tests run |
|
|
76
|
-
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
77
|
-
|
|
78
|
-
|
|
79
|
+
| Summary | ${statusEmoji('failed')} **${failedCount}** failed; ${statusEmoji(
|
|
80
|
+
'passed',
|
|
81
|
+
)} **${passedCount}** passed; **${statusEmoji('skipped')}** ${skippedCount} skipped |
|
|
82
|
+
| Duration | 🕐 **${humanizeDuration(parseFloat(this.tests.reduce((a, t) => a + (t.run_time || 0), 0)))}** |
|
|
83
|
+
`;
|
|
79
84
|
if (this.store.runUrl) {
|
|
80
85
|
summary += `| Testomat.io Report | 📊 [Run #${this.store.runId}](${this.store.runUrl}) | `;
|
|
81
86
|
}
|
|
@@ -92,71 +97,89 @@ class GitHubPipe {
|
|
|
92
97
|
summary += `| Run Attempt | 🌒 \`${process.env.GITHUB_RUN_ATTEMPT}\` | `;
|
|
93
98
|
}
|
|
94
99
|
if (process.env.GITHUB_RUN_ID) {
|
|
95
|
-
summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${
|
|
100
|
+
summary += `| Build Log | ✒️ ${process.env.GITHUB_SERVER_URL || 'https://github.com'}/${
|
|
101
|
+
this.repo
|
|
102
|
+
}/actions/runs/${process.env.GITHUB_RUN_ID} | `;
|
|
96
103
|
}
|
|
97
104
|
|
|
105
|
+
const failures = this.tests
|
|
106
|
+
.filter(t => t.status === 'failed')
|
|
107
|
+
.slice(0, 20)
|
|
108
|
+
.map(t => {
|
|
109
|
+
let text = `#### ${statusEmoji('failed')} ${fullName(t)} `;
|
|
110
|
+
text += '\n\n';
|
|
111
|
+
if (t.message)
|
|
112
|
+
text += `> ${t.message
|
|
113
|
+
.replace(/[^\x20-\x7E]/g, '')
|
|
114
|
+
.replace(ansiRegExp(), '')
|
|
115
|
+
.trim()}\n`;
|
|
116
|
+
if (t.stack) text += `\`\`\`diff\n${t.stack.replace(ansiRegExp(), '').trim()}\n\`\`\`\n`;
|
|
117
|
+
|
|
118
|
+
if (t.artifacts && t.artifacts.length && !process.env.TESTOMATIO_PRIVATE_ARTIFACTS) {
|
|
119
|
+
t.artifacts
|
|
120
|
+
.filter(f => !!f)
|
|
121
|
+
.filter(f => f.endsWith('.png'))
|
|
122
|
+
.forEach(f => {
|
|
123
|
+
if (f.endsWith('.png')) {
|
|
124
|
+
text += `\n`;
|
|
125
|
+
return text;
|
|
126
|
+
}
|
|
127
|
+
text += `[📄 ${path.basename(f)}](${f})\n`;
|
|
128
|
+
return text;
|
|
129
|
+
});
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
text += '\n---\n';
|
|
133
|
+
|
|
134
|
+
return text;
|
|
135
|
+
});
|
|
98
136
|
|
|
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+= `\n`
|
|
108
|
-
});
|
|
109
|
-
}
|
|
110
|
-
|
|
111
|
-
text += "\n---\n"
|
|
112
|
-
|
|
113
|
-
return text;
|
|
114
|
-
})
|
|
115
|
-
|
|
116
137
|
let body = summary;
|
|
117
138
|
|
|
118
139
|
if (failures.length) {
|
|
119
|
-
body +=
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
body +=
|
|
140
|
+
body += `\n<details>\n<summary><h3>🟥 Failures (${failures.length})</h4></summary>\n\n${failures.join('\n')}\n`;
|
|
141
|
+
if (failures.length > 20) {
|
|
142
|
+
body += '\n> Notice\n> Only first 20 failures shown*';
|
|
143
|
+
}
|
|
144
|
+
body += '\n\n</details>';
|
|
124
145
|
}
|
|
125
146
|
|
|
126
147
|
if (this.tests.length > 0) {
|
|
127
|
-
body +=
|
|
128
|
-
body += this.tests
|
|
129
|
-
|
|
130
|
-
|
|
148
|
+
body += '\n<details>\n<summary><h3>🐢 Slowest Tests</h3></summary>\n\n';
|
|
149
|
+
body += this.tests
|
|
150
|
+
// eslint-disable-next-line no-unsafe-optional-chaining
|
|
151
|
+
.sort((a, b) => b?.run_time - a?.run_time)
|
|
152
|
+
.slice(0, 5)
|
|
153
|
+
.map(t => `* ${fullName(t)} (${humanizeDuration(parseFloat(t.run_time))})`)
|
|
154
|
+
.join('\n');
|
|
155
|
+
body += '\n</details>';
|
|
131
156
|
}
|
|
157
|
+
|
|
132
158
|
|
|
159
|
+
await deletePreviousReport(this.octokit, owner, repo, this.issue, this.hiddenCommentData);
|
|
160
|
+
|
|
161
|
+
// add report as comment
|
|
133
162
|
try {
|
|
163
|
+
debug('Adding comment\n', body);
|
|
134
164
|
const resp = await this.octokit.rest.issues.createComment({
|
|
135
165
|
owner,
|
|
136
166
|
repo,
|
|
137
167
|
issue_number: this.issue,
|
|
138
168
|
body,
|
|
139
169
|
});
|
|
140
|
-
|
|
170
|
+
|
|
141
171
|
const url = resp.data?.html_url;
|
|
172
|
+
debug('Comment URL:', url);
|
|
142
173
|
this.store.githubUrl = url;
|
|
143
|
-
|
|
144
|
-
console.log(
|
|
145
|
-
APP_PREFIX,
|
|
146
|
-
chalk.yellow('GitHub'),
|
|
147
|
-
`Report created: ${chalk.magenta(url)}`,
|
|
148
|
-
);
|
|
174
|
+
|
|
175
|
+
console.log(APP_PREFIX, chalk.yellow('GitHub'), `Report created: ${chalk.magenta(url)}`);
|
|
149
176
|
} catch (err) {
|
|
150
|
-
console.log(
|
|
151
|
-
APP_PREFIX,
|
|
152
|
-
chalk.yellow('GitHub'),
|
|
153
|
-
`Couldn't create GitHub report ${err}`,
|
|
154
|
-
);
|
|
177
|
+
console.log(APP_PREFIX, chalk.yellow('GitHub'), `Couldn't create GitHub report ${err}`);
|
|
155
178
|
}
|
|
156
179
|
}
|
|
157
180
|
|
|
158
181
|
toString() {
|
|
159
|
-
return 'GitHub Reporter'
|
|
182
|
+
return 'GitHub Reporter';
|
|
160
183
|
}
|
|
161
184
|
}
|
|
162
185
|
|
|
@@ -164,15 +187,56 @@ function statusEmoji(status) {
|
|
|
164
187
|
if (status === 'passed') return '🟢';
|
|
165
188
|
if (status === 'failed') return '🔴';
|
|
166
189
|
if (status === 'skipped') return '🟡';
|
|
167
|
-
return ''
|
|
190
|
+
return '';
|
|
168
191
|
}
|
|
169
192
|
|
|
170
193
|
function fullName(t) {
|
|
171
194
|
let line = '';
|
|
172
|
-
if (t.suite_title) line = `${t.suite_title}: `;
|
|
173
|
-
line += `**${t.title}
|
|
195
|
+
if (t.suite_title) line = `${t.suite_title}: `;
|
|
196
|
+
line += `**${t.title}**`;
|
|
174
197
|
if (t.example) line += ` \`[${Object.values(t.example)}]\``;
|
|
175
198
|
return line;
|
|
176
199
|
}
|
|
177
200
|
|
|
178
|
-
|
|
201
|
+
async function deletePreviousReport(octokit, owner, repo, issue, hiddenCommentData) {
|
|
202
|
+
if (process.env.GITHUB_KEEP_OUTDATED_REPORTS) return;
|
|
203
|
+
|
|
204
|
+
// get comments
|
|
205
|
+
let comments = [];
|
|
206
|
+
try {
|
|
207
|
+
const response = await octokit.rest.issues.listComments({
|
|
208
|
+
owner,
|
|
209
|
+
repo,
|
|
210
|
+
issue_number: issue,
|
|
211
|
+
});
|
|
212
|
+
comments = response.data;
|
|
213
|
+
} catch (e) {
|
|
214
|
+
console.error('Error while attempt to retrieve comments on GitHub Pull Request:\n', e);
|
|
215
|
+
}
|
|
216
|
+
|
|
217
|
+
if (!comments.length) return;
|
|
218
|
+
|
|
219
|
+
for (const comment of comments) {
|
|
220
|
+
// if comment was left by the same workflow
|
|
221
|
+
if (comment.body.includes(hiddenCommentData)) {
|
|
222
|
+
try {
|
|
223
|
+
// delete previous comment
|
|
224
|
+
await octokit.rest.issues.deleteComment({
|
|
225
|
+
owner,
|
|
226
|
+
repo,
|
|
227
|
+
issue_number: issue,
|
|
228
|
+
comment_id: comment.id,
|
|
229
|
+
});
|
|
230
|
+
} catch (e) {
|
|
231
|
+
console.warn(`Can't delete previously added comment with testomat.io report. Ignore.`);
|
|
232
|
+
}
|
|
233
|
+
|
|
234
|
+
// pass next env var if need to clear all previous reports;
|
|
235
|
+
// only the last one is removed by default
|
|
236
|
+
if (!process.env.GITHUB_REMOVE_ALL_OUTDATED_REPORTS) break;
|
|
237
|
+
// TODO: in case of many reports should implement pagination
|
|
238
|
+
}
|
|
239
|
+
}
|
|
240
|
+
}
|
|
241
|
+
|
|
242
|
+
module.exports = GitHubPipe;
|