testomatio-reporter-cli 2.8.4 → 2.8.5-beta.2-yarn
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 +3 -3
- package/bin/cli.js +6 -26
- package/package.json +39 -4
- package/src/adapter/codecept.js +626 -0
- package/src/adapter/cucumber/current.js +230 -0
- package/src/adapter/cucumber/legacy.js +158 -0
- package/src/adapter/cucumber.js +4 -0
- package/src/adapter/cypress-plugin/index.js +110 -0
- package/src/adapter/jasmine.js +60 -0
- package/src/adapter/jest.js +108 -0
- package/src/adapter/mocha.cjs +2 -0
- package/src/adapter/mocha.js +211 -0
- package/src/adapter/nightwatch.js +88 -0
- package/src/adapter/playwright.js +343 -0
- package/src/adapter/utils/playwright.js +121 -0
- package/src/adapter/utils/step-formatter.js +232 -0
- package/src/adapter/vitest.js +455 -0
- package/src/adapter/webdriver.js +201 -0
- package/src/bin/cli.js +507 -0
- package/src/bin/reportXml.js +79 -0
- package/src/bin/startTest.js +54 -0
- package/src/bin/uploadArtifacts.js +91 -0
- package/src/client.js +524 -0
- package/src/config.js +30 -0
- package/src/constants.js +72 -0
- package/src/data-storage.js +204 -0
- package/src/helpers.js +1 -0
- package/src/junit-adapter/adapter.js +23 -0
- package/src/junit-adapter/csharp.js +70 -0
- package/src/junit-adapter/index.js +28 -0
- package/src/junit-adapter/java.js +58 -0
- package/src/junit-adapter/javascript.js +31 -0
- package/src/junit-adapter/nunit-parser.js +474 -0
- package/src/junit-adapter/python.js +42 -0
- package/src/junit-adapter/ruby.js +10 -0
- package/src/output.js +57 -0
- package/src/pipe/bitbucket.js +285 -0
- package/src/pipe/coverage.js +500 -0
- package/src/pipe/csv.js +161 -0
- package/src/pipe/debug.js +143 -0
- package/src/pipe/github.js +256 -0
- package/src/pipe/gitlab.js +258 -0
- package/src/pipe/html.js +1153 -0
- package/src/pipe/index.js +73 -0
- package/src/pipe/markdown.js +753 -0
- package/src/pipe/testomatio.js +707 -0
- package/src/replay.js +274 -0
- package/src/reporter-functions.js +155 -0
- package/src/reporter.js +42 -0
- package/src/services/artifacts.js +59 -0
- package/src/services/index.js +15 -0
- package/src/services/key-values.js +59 -0
- package/src/services/links.js +69 -0
- package/src/services/logger.js +320 -0
- package/src/template/emptyData.svg +23 -0
- package/src/template/testomatio-old.hbs +1421 -0
- package/src/template/testomatio.hbs +3726 -0
- package/src/uploader.js +382 -0
- package/src/utils/constants.js +12 -0
- package/src/utils/debug.js +20 -0
- package/src/utils/log-formatter.js +118 -0
- package/src/utils/log.js +88 -0
- package/src/utils/pipe_utils.js +193 -0
- package/src/utils/utils.js +732 -0
- package/src/xmlReader.js +834 -0
- package/types/types.d.ts +425 -0
- package/types/vitest.types.d.ts +93 -0
|
@@ -0,0 +1,91 @@
|
|
|
1
|
+
#!/usr/bin/env node
|
|
2
|
+
|
|
3
|
+
import { Command } from 'commander';
|
|
4
|
+
import pc from 'picocolors';
|
|
5
|
+
import createDebugMessages from 'debug';
|
|
6
|
+
import TestomatClient from '../client.js';
|
|
7
|
+
import { getPackageVersion } from '../utils/utils.js';
|
|
8
|
+
import { config } from '../config.js';
|
|
9
|
+
import { readLatestRunId } from '../utils/utils.js';
|
|
10
|
+
import dotenv from 'dotenv';
|
|
11
|
+
import { log } from '../utils/log.js';
|
|
12
|
+
|
|
13
|
+
const debug = createDebugMessages('@testomatio/reporter:upload-cli');
|
|
14
|
+
const version = getPackageVersion();
|
|
15
|
+
console.log(pc.cyan(pc.bold(` 🤩 Testomat.io Reporter v${version}`)));
|
|
16
|
+
const program = new Command();
|
|
17
|
+
|
|
18
|
+
program
|
|
19
|
+
.option('--env-file <envfile>', 'Load environment variables from env file')
|
|
20
|
+
.option('--force', 'Re-upload artifacts even if they were uploaded before')
|
|
21
|
+
.action(async opts => {
|
|
22
|
+
if (opts.envFile) {
|
|
23
|
+
dotenv.config({ path: opts.envFile });
|
|
24
|
+
} else {
|
|
25
|
+
dotenv.config();
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
const apiKey = config.TESTOMATIO;
|
|
29
|
+
process.env.TESTOMATIO_DISABLE_ARTIFACTS = '';
|
|
30
|
+
const runId = process.env.TESTOMATIO_RUN || process.env.runId || readLatestRunId();
|
|
31
|
+
|
|
32
|
+
if (!runId) {
|
|
33
|
+
console.log('TESTOMATIO_RUN environment variable must be set or restored from a previous run.');
|
|
34
|
+
return process.exit(1);
|
|
35
|
+
}
|
|
36
|
+
|
|
37
|
+
const client = new TestomatClient({
|
|
38
|
+
apiKey,
|
|
39
|
+
runId,
|
|
40
|
+
isBatchEnabled: false,
|
|
41
|
+
});
|
|
42
|
+
let testruns = client.uploader.readUploadedFiles(process.env.TESTOMATIO_RUN);
|
|
43
|
+
|
|
44
|
+
const numTotalArtifacts = testruns.length;
|
|
45
|
+
|
|
46
|
+
debug('Found testruns:', testruns);
|
|
47
|
+
|
|
48
|
+
if (!opts.force) testruns = testruns.filter(tr => !tr.uploaded);
|
|
49
|
+
|
|
50
|
+
if (!testruns.length) {
|
|
51
|
+
log.info('Total artifacts:', numTotalArtifacts);
|
|
52
|
+
if (numTotalArtifacts) {
|
|
53
|
+
log.info('No new artifacts to upload');
|
|
54
|
+
log.info('To re-upload artifacts run this command with --force flag');
|
|
55
|
+
}
|
|
56
|
+
process.exit(0);
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
const testrunsByRid = testruns.reduce((acc, { rid, file }) => {
|
|
60
|
+
if (!acc[rid]) {
|
|
61
|
+
acc[rid] = [];
|
|
62
|
+
}
|
|
63
|
+
if (!acc[rid].includes(file)) acc[rid].push(file);
|
|
64
|
+
return acc;
|
|
65
|
+
}, {});
|
|
66
|
+
|
|
67
|
+
// we need to obtain S3 credentials
|
|
68
|
+
await client.createRun();
|
|
69
|
+
|
|
70
|
+
client.uploader.checkEnabled();
|
|
71
|
+
client.uploader.disableLogStorage();
|
|
72
|
+
|
|
73
|
+
for (const rid in testrunsByRid) {
|
|
74
|
+
const files = testrunsByRid[rid];
|
|
75
|
+
await client.addTestRun(undefined, {
|
|
76
|
+
rid,
|
|
77
|
+
files,
|
|
78
|
+
});
|
|
79
|
+
}
|
|
80
|
+
|
|
81
|
+
log.info(client.uploader.successfulUploads.length, 'artifacts uploaded');
|
|
82
|
+
if (client.uploader.failedUploads.length) {
|
|
83
|
+
log.info(client.uploader.failedUploads.length, 'artifacts failed to upload');
|
|
84
|
+
}
|
|
85
|
+
});
|
|
86
|
+
|
|
87
|
+
if (process.argv.length <= 1) {
|
|
88
|
+
program.outputHelp();
|
|
89
|
+
}
|
|
90
|
+
|
|
91
|
+
program.parse(process.argv);
|
package/src/client.js
ADDED
|
@@ -0,0 +1,524 @@
|
|
|
1
|
+
import createDebugMessages from 'debug';
|
|
2
|
+
import fs from 'fs';
|
|
3
|
+
import pc from 'picocolors';
|
|
4
|
+
import { APP_PREFIX, STATUS, SCREENSHOTS_ON_STEPS } from './constants.js';
|
|
5
|
+
import { pipesFactory } from './pipe/index.js';
|
|
6
|
+
import { glob } from 'glob';
|
|
7
|
+
import path from 'path';
|
|
8
|
+
import { fileURLToPath } from 'node:url';
|
|
9
|
+
import { S3Uploader } from './uploader.js';
|
|
10
|
+
import { readLatestRunId, storeRunId, validateSuiteId, transformEnvVarToBoolean, isHttpUrl } from './utils/utils.js';
|
|
11
|
+
import { generateShortFilename } from './adapter/utils/step-formatter.js';
|
|
12
|
+
import { filesize as prettyBytes } from 'filesize';
|
|
13
|
+
import { formatLogs, formatError, stripColors } from './utils/log-formatter.js';
|
|
14
|
+
import { log } from './utils/log.js';
|
|
15
|
+
|
|
16
|
+
const debug = createDebugMessages('@testomatio/reporter:client');
|
|
17
|
+
|
|
18
|
+
// removed __dirname usage, because:
|
|
19
|
+
// 1. replaced with ESM syntax (import.meta.url), but it throws an error on tsc compilation;
|
|
20
|
+
// 2. got error "__dirname already defined" in compiles js code (cjs dir)
|
|
21
|
+
|
|
22
|
+
let listOfTestFilesToExcludeFromReport = null;
|
|
23
|
+
|
|
24
|
+
/**
|
|
25
|
+
* @typedef {import('../types/types.js').TestData} TestData
|
|
26
|
+
* @typedef {import('../types/types.js').PipeResult} PipeResult
|
|
27
|
+
*/
|
|
28
|
+
|
|
29
|
+
class Client {
|
|
30
|
+
/**
|
|
31
|
+
* Create a Testomat client instance
|
|
32
|
+
* @returns
|
|
33
|
+
*/
|
|
34
|
+
constructor(params = {}) {
|
|
35
|
+
this.paramsForPipesFactory = params;
|
|
36
|
+
this.pipeStore = {};
|
|
37
|
+
this.runId = '';
|
|
38
|
+
this.queue = Promise.resolve();
|
|
39
|
+
|
|
40
|
+
// @ts-ignore this line will be removed in compiled code, because __dirname is defined in commonjs
|
|
41
|
+
const __dirname = path.dirname(fileURLToPath(import.meta.url));
|
|
42
|
+
const pathToPackageJSON = path.join(__dirname, '../package.json');
|
|
43
|
+
try {
|
|
44
|
+
this.version = JSON.parse(fs.readFileSync(pathToPackageJSON).toString()).version;
|
|
45
|
+
log.info(`Testomatio Reporter v${this.version}`);
|
|
46
|
+
} catch (e) {
|
|
47
|
+
// do nothing
|
|
48
|
+
}
|
|
49
|
+
this.executionList = Promise.resolve();
|
|
50
|
+
|
|
51
|
+
this.uploader = new S3Uploader();
|
|
52
|
+
}
|
|
53
|
+
|
|
54
|
+
/**
|
|
55
|
+
* Asynchronously prepares the execution list for running tests through various pipes.
|
|
56
|
+
* Each pipe in the client is checked for enablement,
|
|
57
|
+
* and if all pipes are disabled, the function returns a resolved Promise.
|
|
58
|
+
* Otherwise, it executes the `prepareRun` method for each enabled pipe and collects the results.
|
|
59
|
+
* The results are then filtered to remove any undefined values.
|
|
60
|
+
* If no valid results are found, the function returns undefined.
|
|
61
|
+
* Otherwise, it returns the first non-empty array from the filtered results.
|
|
62
|
+
*
|
|
63
|
+
* @param {Object} params - The options for preparing the test execution list.
|
|
64
|
+
* @param {string} params.pipe - Name of the executed pipe.
|
|
65
|
+
* @param {string} params.pipeOptions - Filter option.
|
|
66
|
+
* @returns {Promise<any>} - A Promise that resolves to an
|
|
67
|
+
* array containing the prepared execution list,
|
|
68
|
+
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
69
|
+
*/
|
|
70
|
+
|
|
71
|
+
async prepareRun(params) {
|
|
72
|
+
const { pipe, pipeOptions } = params;
|
|
73
|
+
|
|
74
|
+
// ❗ Validation: pipe is required
|
|
75
|
+
if (!pipe || !pipeOptions) {
|
|
76
|
+
log.warn(`❗ No valid pipe found in filter cmd. Expected format: <pipe>:<options>
|
|
77
|
+
Examples:
|
|
78
|
+
--filter "testomatio:tag-name=frontend"
|
|
79
|
+
--filter "coverage:file=coverage.yml"
|
|
80
|
+
--filter-list "coverage:file=coverage.yml"
|
|
81
|
+
Received: "${params}"`);
|
|
82
|
+
return;
|
|
83
|
+
}
|
|
84
|
+
|
|
85
|
+
this.pipes = await pipesFactory(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
86
|
+
|
|
87
|
+
// all pipes disabled, skipping
|
|
88
|
+
if (!this.pipes.some(p => p.isEnabled)) {
|
|
89
|
+
return Promise.resolve();
|
|
90
|
+
}
|
|
91
|
+
|
|
92
|
+
try {
|
|
93
|
+
const p = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
|
|
94
|
+
// const p = this.pipes.find(p => p.id === `${pipe.toLowerCase()}`); TODO: as future updates
|
|
95
|
+
|
|
96
|
+
if (!p?.isEnabled) {
|
|
97
|
+
log.warn('🚫 No active pipes were found in the system. Execution aborted!');
|
|
98
|
+
return;
|
|
99
|
+
}
|
|
100
|
+
|
|
101
|
+
// Run only the selected pipe
|
|
102
|
+
const rawResult = await p.prepareRun(pipeOptions);
|
|
103
|
+
const result = Array.isArray(rawResult) ? rawResult : [];
|
|
104
|
+
|
|
105
|
+
debug('Execution tests list', result);
|
|
106
|
+
|
|
107
|
+
return result;
|
|
108
|
+
} catch (err) {
|
|
109
|
+
log.error(err);
|
|
110
|
+
}
|
|
111
|
+
}
|
|
112
|
+
|
|
113
|
+
/**
|
|
114
|
+
* Used to create a new Test run
|
|
115
|
+
*
|
|
116
|
+
* @returns {Promise<any>} - resolves to Run id which should be used to update / add test
|
|
117
|
+
*/
|
|
118
|
+
async createRun(params = {}) {
|
|
119
|
+
const pipeParams = { ...(this.paramsForPipesFactory || {}), ...(params || {}) };
|
|
120
|
+
if (!this.pipes || !this.pipes.length)
|
|
121
|
+
this.pipes = await pipesFactory(pipeParams, this.pipeStore);
|
|
122
|
+
debug('Creating run...');
|
|
123
|
+
// all pipes disabled, skipping
|
|
124
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
125
|
+
|
|
126
|
+
this.queue = this.queue
|
|
127
|
+
.then(() => Promise.all(this.pipes.map(p => p.createRun(params))))
|
|
128
|
+
.catch(err => log.info(err))
|
|
129
|
+
.then(() => {
|
|
130
|
+
const runId = this.pipeStore?.runId;
|
|
131
|
+
if (runId) this.runId = runId;
|
|
132
|
+
storeRunId(this.runId);
|
|
133
|
+
})
|
|
134
|
+
.then(() => this.uploader.checkEnabled())
|
|
135
|
+
.then(() => undefined); // fixes return type
|
|
136
|
+
// debug('Run', this.queue);
|
|
137
|
+
return this.queue;
|
|
138
|
+
}
|
|
139
|
+
|
|
140
|
+
/**
|
|
141
|
+
* Recursively uploads artifacts from steps
|
|
142
|
+
*
|
|
143
|
+
* @param {*} steps - Steps payload (validated inside function)
|
|
144
|
+
* @param {string} testRid - Test/result ID
|
|
145
|
+
* @returns {Promise<void>}
|
|
146
|
+
*/
|
|
147
|
+
async uploadStepArtifacts(steps, testRid) {
|
|
148
|
+
if (!steps || !Array.isArray(steps)) return;
|
|
149
|
+
if (!this.uploader.isEnabled || !SCREENSHOTS_ON_STEPS) return;
|
|
150
|
+
|
|
151
|
+
try {
|
|
152
|
+
for (const step of steps) {
|
|
153
|
+
if (!(step.artifacts && Array.isArray(step.artifacts))) {
|
|
154
|
+
if (step.steps) {
|
|
155
|
+
await this.uploadStepArtifacts(step.steps, testRid);
|
|
156
|
+
}
|
|
157
|
+
continue;
|
|
158
|
+
}
|
|
159
|
+
|
|
160
|
+
const uploadedArtifacts = [];
|
|
161
|
+
for (const artifact of step.artifacts) {
|
|
162
|
+
if (typeof artifact === 'string' && !isHttpUrl(artifact)) {
|
|
163
|
+
const filename = generateShortFilename(artifact);
|
|
164
|
+
try {
|
|
165
|
+
const uploadResult = await this.uploader.uploadFileByPath(
|
|
166
|
+
artifact,
|
|
167
|
+
[this.runId, testRid, 'steps', filename]
|
|
168
|
+
);
|
|
169
|
+
if (uploadResult) {
|
|
170
|
+
uploadedArtifacts.push(uploadResult);
|
|
171
|
+
} else {
|
|
172
|
+
uploadedArtifacts.push(artifact);
|
|
173
|
+
}
|
|
174
|
+
} catch (uploadErr) {
|
|
175
|
+
uploadedArtifacts.push(artifact);
|
|
176
|
+
}
|
|
177
|
+
} else {
|
|
178
|
+
uploadedArtifacts.push(artifact);
|
|
179
|
+
}
|
|
180
|
+
}
|
|
181
|
+
step.artifacts = uploadedArtifacts;
|
|
182
|
+
|
|
183
|
+
if (step.steps) {
|
|
184
|
+
await this.uploadStepArtifacts(step.steps, testRid);
|
|
185
|
+
}
|
|
186
|
+
}
|
|
187
|
+
|
|
188
|
+
} catch (err) {
|
|
189
|
+
console.error(APP_PREFIX, 'Error in uploadStepArtifacts for testRid', testRid, ':', err);
|
|
190
|
+
throw err;
|
|
191
|
+
}
|
|
192
|
+
}
|
|
193
|
+
|
|
194
|
+
/**
|
|
195
|
+
* Updates test status and its data
|
|
196
|
+
*
|
|
197
|
+
* @param {string|undefined} status
|
|
198
|
+
* @param {TestData} [testData]
|
|
199
|
+
* @returns {Promise<PipeResult[]>}
|
|
200
|
+
*/
|
|
201
|
+
async addTestRun(status, testData) {
|
|
202
|
+
if (!testData)
|
|
203
|
+
testData = {
|
|
204
|
+
title: 'Unknown test',
|
|
205
|
+
suite_title: 'Unknown suite',
|
|
206
|
+
};
|
|
207
|
+
|
|
208
|
+
// Add timestamp if not already present (microseconds since Unix epoch)
|
|
209
|
+
if (!testData.timestamp && !process.env.TESTOMATIO_NO_TIMESTAMP) {
|
|
210
|
+
testData.timestamp = Math.floor((performance.timeOrigin + performance.now()) * 1000);
|
|
211
|
+
}
|
|
212
|
+
|
|
213
|
+
/**
|
|
214
|
+
* @type {TestData}
|
|
215
|
+
*/
|
|
216
|
+
const { rid, error = null, steps: originalSteps, title, suite_title } = testData;
|
|
217
|
+
let steps = originalSteps;
|
|
218
|
+
|
|
219
|
+
// Capture step artifact paths BEFORE uploadStepArtifacts mutates them to URLs,
|
|
220
|
+
// so we can exclude them from the test-level artifacts list later.
|
|
221
|
+
const stepArtifactPaths = collectStepArtifactPaths(steps);
|
|
222
|
+
|
|
223
|
+
// Upload artifacts from steps
|
|
224
|
+
try {
|
|
225
|
+
await this.uploadStepArtifacts(steps, rid);
|
|
226
|
+
} catch (err) {
|
|
227
|
+
console.log(APP_PREFIX, 'Failed to upload step artifacts:', err);
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
const uploadedFiles = [];
|
|
231
|
+
const stackArtifactsEnabled = transformEnvVarToBoolean(process.env.TESTOMATIO_STACK_ARTIFACTS);
|
|
232
|
+
|
|
233
|
+
const {
|
|
234
|
+
time = 0,
|
|
235
|
+
example = null,
|
|
236
|
+
filesBuffers = [],
|
|
237
|
+
code = null,
|
|
238
|
+
file,
|
|
239
|
+
suite_id,
|
|
240
|
+
test_id,
|
|
241
|
+
timestamp,
|
|
242
|
+
links,
|
|
243
|
+
overwrite,
|
|
244
|
+
tags,
|
|
245
|
+
} = testData;
|
|
246
|
+
let { files = [], manuallyAttachedArtifacts, message = '', meta = {} } = testData;
|
|
247
|
+
|
|
248
|
+
if (stepArtifactPaths.size) {
|
|
249
|
+
const isStepArtifact = item => stepArtifactPaths.has(typeof item === 'object' ? item?.path : item);
|
|
250
|
+
files = files.filter(f => !isStepArtifact(f));
|
|
251
|
+
if (Array.isArray(manuallyAttachedArtifacts)) {
|
|
252
|
+
manuallyAttachedArtifacts = manuallyAttachedArtifacts.filter(a => !isStepArtifact(a));
|
|
253
|
+
}
|
|
254
|
+
}
|
|
255
|
+
|
|
256
|
+
meta = Object.entries(meta)
|
|
257
|
+
.filter(([, value]) => value !== null && value !== undefined)
|
|
258
|
+
.reduce((acc, [key, value]) => {
|
|
259
|
+
if (key) acc[key] = value;
|
|
260
|
+
return acc;
|
|
261
|
+
}, {});
|
|
262
|
+
|
|
263
|
+
const testContext = suite_title ? `${suite_title} ${title}` : title;
|
|
264
|
+
|
|
265
|
+
let errorFormatted = '';
|
|
266
|
+
if (error) {
|
|
267
|
+
errorFormatted += formatError(error) || '';
|
|
268
|
+
message = error?.message;
|
|
269
|
+
}
|
|
270
|
+
|
|
271
|
+
let fullLogs = formatLogs({ error: errorFormatted, logs: testData.logs });
|
|
272
|
+
|
|
273
|
+
if (stackArtifactsEnabled && fullLogs?.trim()?.length > 0) {
|
|
274
|
+
uploadedFiles.push(
|
|
275
|
+
this.uploader.uploadFileAsBuffer(Buffer.from(stripColors(fullLogs), 'utf8'), [
|
|
276
|
+
this.runId,
|
|
277
|
+
rid,
|
|
278
|
+
`logs_${+new Date()}.log`,
|
|
279
|
+
]),
|
|
280
|
+
);
|
|
281
|
+
fullLogs = '';
|
|
282
|
+
steps = null;
|
|
283
|
+
}
|
|
284
|
+
|
|
285
|
+
if (!this.pipes || !this.pipes.length)
|
|
286
|
+
this.pipes = await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
287
|
+
|
|
288
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) {
|
|
289
|
+
if (uploadedFiles.length > 0) {
|
|
290
|
+
await Promise.all(uploadedFiles);
|
|
291
|
+
}
|
|
292
|
+
return [];
|
|
293
|
+
}
|
|
294
|
+
|
|
295
|
+
if (isTestShouldBeExcludedFromReport(testData)) return [];
|
|
296
|
+
|
|
297
|
+
if (status === STATUS.SKIPPED && process.env.TESTOMATIO_EXCLUDE_SKIPPED) {
|
|
298
|
+
debug('Skipping test from report', testData?.title);
|
|
299
|
+
return [];
|
|
300
|
+
}
|
|
301
|
+
|
|
302
|
+
if (manuallyAttachedArtifacts?.length) files.push(...manuallyAttachedArtifacts);
|
|
303
|
+
|
|
304
|
+
for (let f of files) {
|
|
305
|
+
if (!f) continue; // f === null
|
|
306
|
+
if (typeof f === 'object') {
|
|
307
|
+
if (!f.path) continue;
|
|
308
|
+
|
|
309
|
+
f = f.path;
|
|
310
|
+
}
|
|
311
|
+
|
|
312
|
+
uploadedFiles.push(this.uploader.uploadFileByPath(f, [this.runId, rid, path.basename(f)]));
|
|
313
|
+
}
|
|
314
|
+
|
|
315
|
+
for (const [idx, buffer] of filesBuffers.entries()) {
|
|
316
|
+
const fileName = `${idx + 1}-${title.replace(/\s+/g, '-')}`;
|
|
317
|
+
uploadedFiles.push(this.uploader.uploadFileAsBuffer(buffer, [this.runId, rid, fileName]));
|
|
318
|
+
}
|
|
319
|
+
|
|
320
|
+
const artifacts = (await Promise.all(uploadedFiles)).filter(n => !!n);
|
|
321
|
+
|
|
322
|
+
const workspaceDir = process.env.TESTOMATIO_WORKDIR || process.cwd();
|
|
323
|
+
const relativeFile = file ? path.relative(workspaceDir, file) : file;
|
|
324
|
+
const rootSuiteId = validateSuiteId(process.env.TESTOMATIO_SUITE);
|
|
325
|
+
|
|
326
|
+
const data = {
|
|
327
|
+
rid,
|
|
328
|
+
files,
|
|
329
|
+
steps,
|
|
330
|
+
status,
|
|
331
|
+
stack: fullLogs,
|
|
332
|
+
example,
|
|
333
|
+
file: relativeFile,
|
|
334
|
+
code,
|
|
335
|
+
title,
|
|
336
|
+
suite_title,
|
|
337
|
+
suite_id,
|
|
338
|
+
test_id,
|
|
339
|
+
message,
|
|
340
|
+
run_time: typeof time === 'number' ? time : parseFloat(time),
|
|
341
|
+
timestamp,
|
|
342
|
+
artifacts,
|
|
343
|
+
meta,
|
|
344
|
+
links,
|
|
345
|
+
overwrite,
|
|
346
|
+
tags,
|
|
347
|
+
...(rootSuiteId && { root_suite_id: rootSuiteId }),
|
|
348
|
+
};
|
|
349
|
+
|
|
350
|
+
// debug('Adding test run...', data);
|
|
351
|
+
|
|
352
|
+
// @ts-ignore
|
|
353
|
+
this.queue = this.queue.then(() =>
|
|
354
|
+
Promise.all(
|
|
355
|
+
this.pipes.map(async pipe => {
|
|
356
|
+
try {
|
|
357
|
+
const result = await pipe.addTest(data);
|
|
358
|
+
return { pipe: pipe.toString(), result };
|
|
359
|
+
} catch (err) {
|
|
360
|
+
log.info(pipe.toString(), err);
|
|
361
|
+
}
|
|
362
|
+
}),
|
|
363
|
+
),
|
|
364
|
+
);
|
|
365
|
+
|
|
366
|
+
// @ts-ignore
|
|
367
|
+
return this.queue;
|
|
368
|
+
}
|
|
369
|
+
|
|
370
|
+
/**
|
|
371
|
+
*
|
|
372
|
+
* Updates the status of the current test run and finishes the run.
|
|
373
|
+
* @param {'passed' | 'failed' | 'skipped' | 'finished'} status - The status of the current test run.
|
|
374
|
+
* @param {Partial<import('../types/types.js').RunData>} [params] - Additional run params (e.g. duration).
|
|
375
|
+
* Must be one of "passed", "failed", or "finished"
|
|
376
|
+
* @returns {Promise<any>} - A Promise that resolves when finishes the run.
|
|
377
|
+
*/
|
|
378
|
+
async updateRunStatus(status, params = {}) {
|
|
379
|
+
this.pipes ||= await pipesFactory(this.paramsForPipesFactory || {}, this.pipeStore);
|
|
380
|
+
this.runId ||= readLatestRunId();
|
|
381
|
+
|
|
382
|
+
debug('Updating run status...');
|
|
383
|
+
// all pipes disabled, skipping
|
|
384
|
+
if (!this.pipes?.filter(p => p.isEnabled).length) return Promise.resolve();
|
|
385
|
+
|
|
386
|
+
const runParams = { ...params, status };
|
|
387
|
+
|
|
388
|
+
this.queue = this.queue
|
|
389
|
+
.then(() => Promise.all(this.pipes.map(p => p.finishRun(runParams))))
|
|
390
|
+
.then(() => {
|
|
391
|
+
if (!this.uploader.isEnabled) return;
|
|
392
|
+
|
|
393
|
+
const filesizeStrMaxLength = 7;
|
|
394
|
+
|
|
395
|
+
if (this.uploader.successfulUploads.length) {
|
|
396
|
+
debug('\n', APP_PREFIX, `🗄️ ${this.uploader.successfulUploads.length} artifacts uploaded to S3 bucket`);
|
|
397
|
+
const uploadedArtifacts = this.uploader.successfulUploads.map(file => ({
|
|
398
|
+
relativePath: file.path.replace(process.cwd(), ''),
|
|
399
|
+
link: file.link,
|
|
400
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
401
|
+
}));
|
|
402
|
+
|
|
403
|
+
uploadedArtifacts.forEach(upload => {
|
|
404
|
+
debug(
|
|
405
|
+
`🟢Uploaded artifact`,
|
|
406
|
+
`${upload.relativePath},`,
|
|
407
|
+
'size:',
|
|
408
|
+
`${upload.sizePretty},`,
|
|
409
|
+
'link:',
|
|
410
|
+
`${upload.link}`,
|
|
411
|
+
);
|
|
412
|
+
});
|
|
413
|
+
}
|
|
414
|
+
|
|
415
|
+
if (this.uploader.failedUploads.length) {
|
|
416
|
+
log.info(`🗄️ ${this.uploader.failedUploads.length} artifacts 🔴${pc.bold('failed')} to upload`);
|
|
417
|
+
const failedUploads = this.uploader.failedUploads.map(file => ({
|
|
418
|
+
relativePath: file.path.replace(process.cwd(), ''),
|
|
419
|
+
sizePretty: file.size == null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
420
|
+
}));
|
|
421
|
+
|
|
422
|
+
const pathPadding = Math.max(...failedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
423
|
+
|
|
424
|
+
failedUploads.forEach(upload => {
|
|
425
|
+
console.log(
|
|
426
|
+
` ${pc.gray('|')} 🔴 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
|
|
427
|
+
`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
|
|
428
|
+
)}`,
|
|
429
|
+
);
|
|
430
|
+
});
|
|
431
|
+
}
|
|
432
|
+
|
|
433
|
+
if (this.uploader.skippedUploads.length) {
|
|
434
|
+
log.info(`🗄️ ${pc.bold(this.uploader.skippedUploads.length)} artifacts uploading 🟡${pc.bold('skipped')}`);
|
|
435
|
+
const skippedUploads = this.uploader.skippedUploads.map(file => ({
|
|
436
|
+
relativePath: file.path.replace(process.cwd(), ''),
|
|
437
|
+
sizePretty: file.size === null ? 'unknown' : prettyBytes(file.size, { round: 0 }).toString(),
|
|
438
|
+
}));
|
|
439
|
+
const pathPadding = Math.max(...skippedUploads.map(upload => upload.relativePath.length)) + 1;
|
|
440
|
+
skippedUploads.forEach(upload => {
|
|
441
|
+
console.log(
|
|
442
|
+
` ${pc.gray('|')} 🟡 ${upload.relativePath.padEnd(pathPadding)} ${pc.gray(
|
|
443
|
+
`| ${upload.sizePretty.padStart(filesizeStrMaxLength)} |`,
|
|
444
|
+
)}`,
|
|
445
|
+
);
|
|
446
|
+
});
|
|
447
|
+
}
|
|
448
|
+
|
|
449
|
+
if (this.uploader.skippedUploads.length || this.uploader.failedUploads.length) {
|
|
450
|
+
const command = `TESTOMATIO=<your_api_key> TESTOMATIO_RUN=${
|
|
451
|
+
this.runId
|
|
452
|
+
} npx @testomatio/reporter upload-artifacts`;
|
|
453
|
+
const numberOfNotUploadedArtifacts = this.uploader.skippedUploads.length + this.uploader.failedUploads.length;
|
|
454
|
+
log.info(`${numberOfNotUploadedArtifacts} artifacts were not uploaded.
|
|
455
|
+
Run "${pc.magenta(command)}" with valid S3 credentials to upload skipped & failed artifacts`);
|
|
456
|
+
}
|
|
457
|
+
})
|
|
458
|
+
.catch(err => log.info(err));
|
|
459
|
+
|
|
460
|
+
return this.queue;
|
|
461
|
+
}
|
|
462
|
+
}
|
|
463
|
+
|
|
464
|
+
/**
|
|
465
|
+
* Walks the step tree and returns the set of artifact path/url values
|
|
466
|
+
* referenced by `step.artifacts` at any depth.
|
|
467
|
+
*
|
|
468
|
+
* @param {any} steps
|
|
469
|
+
* @param {Set<string>} [paths]
|
|
470
|
+
* @returns {Set<string>}
|
|
471
|
+
*/
|
|
472
|
+
function collectStepArtifactPaths(steps, paths = new Set()) {
|
|
473
|
+
if (!Array.isArray(steps)) return paths;
|
|
474
|
+
for (const step of steps) {
|
|
475
|
+
if (!step) continue;
|
|
476
|
+
if (Array.isArray(step.artifacts)) {
|
|
477
|
+
for (const a of step.artifacts) {
|
|
478
|
+
if (typeof a === 'string') paths.add(a);
|
|
479
|
+
else if (a && typeof a === 'object' && typeof a.path === 'string') paths.add(a.path);
|
|
480
|
+
}
|
|
481
|
+
}
|
|
482
|
+
collectStepArtifactPaths(step.steps, paths);
|
|
483
|
+
}
|
|
484
|
+
return paths;
|
|
485
|
+
}
|
|
486
|
+
|
|
487
|
+
/**
|
|
488
|
+
*
|
|
489
|
+
* @param {TestData} testData
|
|
490
|
+
* @returns boolean
|
|
491
|
+
*/
|
|
492
|
+
function isTestShouldBeExcludedFromReport(testData) {
|
|
493
|
+
// const fileName = path.basename(test.location?.file || '');
|
|
494
|
+
const globExcludeFilesPattern = process.env.TESTOMATIO_EXCLUDE_FILES_FROM_REPORT_GLOB_PATTERN;
|
|
495
|
+
if (!globExcludeFilesPattern) return false;
|
|
496
|
+
|
|
497
|
+
if (!testData.file) {
|
|
498
|
+
debug('No "file" property found for test ', testData.title);
|
|
499
|
+
return false;
|
|
500
|
+
}
|
|
501
|
+
|
|
502
|
+
const excludePatternsList = globExcludeFilesPattern.split(';');
|
|
503
|
+
|
|
504
|
+
// as scanning files is time consuming operation, just save the result in variable to avoid multiple scans
|
|
505
|
+
if (!listOfTestFilesToExcludeFromReport) {
|
|
506
|
+
// list of files with relative paths
|
|
507
|
+
listOfTestFilesToExcludeFromReport = glob.sync(excludePatternsList, { ignore: '**/node_modules/**' });
|
|
508
|
+
debug('Tests from next files will not be reported:', listOfTestFilesToExcludeFromReport);
|
|
509
|
+
}
|
|
510
|
+
|
|
511
|
+
const testFileRelativePath = path.relative(process.cwd(), testData.file);
|
|
512
|
+
|
|
513
|
+
// no files found matching the exclusion pattern
|
|
514
|
+
if (!listOfTestFilesToExcludeFromReport.length) return false;
|
|
515
|
+
|
|
516
|
+
if (listOfTestFilesToExcludeFromReport.includes(testFileRelativePath)) {
|
|
517
|
+
debug(`Excluding test '${testData.title}' <${testFileRelativePath}> from reporting`);
|
|
518
|
+
return true;
|
|
519
|
+
}
|
|
520
|
+
return false;
|
|
521
|
+
}
|
|
522
|
+
|
|
523
|
+
export { Client };
|
|
524
|
+
export default Client;
|
package/src/config.js
ADDED
|
@@ -0,0 +1,30 @@
|
|
|
1
|
+
// This file is used to read environment variables from .env file
|
|
2
|
+
import createDebugMessages from 'debug';
|
|
3
|
+
|
|
4
|
+
const debug = createDebugMessages('@testomatio/reporter:config');
|
|
5
|
+
|
|
6
|
+
/* for possibility to use multiple env files (reading different paths)
|
|
7
|
+
const envFileVars = dotenv.config({ path: '.env' }).parsed; */
|
|
8
|
+
|
|
9
|
+
if (process.env.TESTOMATIO_API_KEY && !process.env.TESTOMATIO) {
|
|
10
|
+
process.env.TESTOMATIO = process.env.TESTOMATIO_API_KEY;
|
|
11
|
+
}
|
|
12
|
+
if (process.env.TESTOMATIO_TOKEN && !process.env.TESTOMATIO) {
|
|
13
|
+
process.env.TESTOMATIO = process.env.TESTOMATIO_TOKEN;
|
|
14
|
+
}
|
|
15
|
+
|
|
16
|
+
if (process.env.TESTOMATIO === 'undefined')
|
|
17
|
+
console.error('TESTOMATIO is "undefined". Something went wrong. Contact dev team.');
|
|
18
|
+
|
|
19
|
+
// select only TESTOMATIO related variables (only to print them in debug)
|
|
20
|
+
const testomatioEnvVars =
|
|
21
|
+
Object.keys(process.env)
|
|
22
|
+
.filter(key => key.startsWith('TESTOMATIO') || key.startsWith('S3_'))
|
|
23
|
+
.reduce((obj, key) => {
|
|
24
|
+
obj[key] = process.env[key];
|
|
25
|
+
return obj;
|
|
26
|
+
}, {}) || {};
|
|
27
|
+
debug('TESTOMATIO variables:', testomatioEnvVars);
|
|
28
|
+
|
|
29
|
+
// includes variables from .env file and process.env
|
|
30
|
+
export const config = process.env;
|