@testomatio/reporter 2.3.9 → 2.4.0
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 +1 -1
- package/lib/bin/cli.js +26 -7
- package/lib/client.js +18 -10
- package/lib/data-storage.d.ts +1 -1
- package/lib/helpers.d.ts +1 -0
- package/lib/helpers.js +4 -0
- package/lib/pipe/coverage.d.ts +82 -0
- package/lib/pipe/coverage.js +373 -0
- package/lib/pipe/index.js +2 -0
- package/lib/pipe/testomatio.d.ts +1 -1
- package/lib/pipe/testomatio.js +25 -4
- package/lib/reporter-functions.js +13 -9
- package/lib/reporter.d.ts +12 -12
- package/lib/services/artifacts.d.ts +1 -1
- package/lib/services/key-values.d.ts +1 -1
- package/lib/services/links.d.ts +1 -1
- package/lib/services/logger.d.ts +1 -1
- package/lib/utils/pipe_utils.d.ts +15 -0
- package/lib/utils/pipe_utils.js +44 -2
- package/lib/utils/utils.d.ts +6 -0
- package/lib/utils/utils.js +71 -1
- package/package.json +5 -4
- package/src/bin/cli.js +35 -9
- package/src/client.js +22 -14
- package/src/helpers.js +1 -0
- package/src/pipe/coverage.js +440 -0
- package/src/pipe/index.js +2 -0
- package/src/pipe/testomatio.js +34 -5
- package/src/reporter-functions.js +13 -9
- package/src/utils/pipe_utils.js +52 -3
- package/src/utils/utils.js +75 -0
package/README.md
CHANGED
|
@@ -20,7 +20,7 @@ Testomat.io Reporter (this npm package) supports:
|
|
|
20
20
|
- 💯 Free & open-source.
|
|
21
21
|
- 📊 Public and private Run reports on cloud via [Testomat.io App](https://testomat.io) 👇
|
|
22
22
|
|
|
23
|
-
|
|
23
|
+
<img width="1920" height="1085" alt="image" src="https://github.com/user-attachments/assets/cf823e8b-1305-4ed2-a7c5-712efec12ceb" />
|
|
24
24
|
|
|
25
25
|
## How It Works
|
|
26
26
|
|
package/lib/bin/cli.js
CHANGED
|
@@ -75,6 +75,7 @@ program
|
|
|
75
75
|
.description('Run tests with the specified command')
|
|
76
76
|
.argument('<command>', 'Test runner command')
|
|
77
77
|
.option('--filter <filter>', 'Additional execution filter')
|
|
78
|
+
.option('--filter-list <filter>', 'Get a list of all tests by filter before running')
|
|
78
79
|
.option('--kind <type>', 'Specify run type: automated, manual, or mixed')
|
|
79
80
|
.action(async (command, opts) => {
|
|
80
81
|
const apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
@@ -84,17 +85,32 @@ program
|
|
|
84
85
|
return process.exit(255);
|
|
85
86
|
}
|
|
86
87
|
const client = new client_js_1.default({ apiKey, title });
|
|
87
|
-
if (opts.filter) {
|
|
88
|
-
|
|
88
|
+
if (opts.filter || opts.filterList) {
|
|
89
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "testomatio:tag-name=frontend"
|
|
90
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter "coverage:file=coverage.yml"
|
|
91
|
+
// Example of use: npx @testomatio/reporter run "npx jest" --filter-list "coverage:file=coverage.yml"
|
|
92
|
+
const [pipe, ...optsArray] = opts?.filter ? opts?.filter.split(':') : opts?.filterList.split(':');
|
|
89
93
|
const pipeOptions = optsArray.join(':');
|
|
94
|
+
const prepareRunParams = { pipe, pipeOptions };
|
|
90
95
|
try {
|
|
91
|
-
const tests = await client.prepareRun(
|
|
92
|
-
if (tests
|
|
93
|
-
|
|
96
|
+
const tests = await client.prepareRun(prepareRunParams);
|
|
97
|
+
if (!tests || tests.length === 0) {
|
|
98
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.yellow('No tests found.'));
|
|
99
|
+
return;
|
|
94
100
|
}
|
|
101
|
+
const pattern = `(${tests.join('|')})`;
|
|
102
|
+
const filteredCommand = (0, utils_js_1.applyFilter)(command, tests);
|
|
103
|
+
debug(`Execution pattern: "${pattern}"`);
|
|
104
|
+
if (opts.filterList) {
|
|
105
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.blue(`Matched test/suite IDs: ${tests.join(', ')}`));
|
|
106
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.green(`Full Running Command: ${filteredCommand}`));
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
109
|
+
command = filteredCommand;
|
|
95
110
|
}
|
|
96
111
|
catch (err) {
|
|
97
|
-
console.log(constants_js_1.APP_PREFIX, err);
|
|
112
|
+
console.log(constants_js_1.APP_PREFIX, err.message || err);
|
|
113
|
+
return;
|
|
98
114
|
}
|
|
99
115
|
}
|
|
100
116
|
console.log(constants_js_1.APP_PREFIX, `🚀 Running`, picocolors_1.default.green(command));
|
|
@@ -102,7 +118,7 @@ program
|
|
|
102
118
|
const testCmds = command.split(' ');
|
|
103
119
|
const cmd = (0, cross_spawn_1.spawn)(testCmds[0], testCmds.slice(1), {
|
|
104
120
|
stdio: 'inherit',
|
|
105
|
-
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId },
|
|
121
|
+
env: { ...process.env, TESTOMATIO_PROCEED: 'true', runId: client.runId, TESTOMATIO_RUN: client.runId },
|
|
106
122
|
});
|
|
107
123
|
cmd.on('close', async (code) => {
|
|
108
124
|
const emoji = code === 0 ? '🟢' : '🔴';
|
|
@@ -115,6 +131,9 @@ program
|
|
|
115
131
|
});
|
|
116
132
|
};
|
|
117
133
|
const createRunParams = {};
|
|
134
|
+
if (title) {
|
|
135
|
+
createRunParams.title = title;
|
|
136
|
+
}
|
|
118
137
|
if (opts.kind) {
|
|
119
138
|
createRunParams.kind = opts.kind;
|
|
120
139
|
}
|
package/lib/client.js
CHANGED
|
@@ -64,24 +64,32 @@ class Client {
|
|
|
64
64
|
* or resolves to undefined if no valid results are found or if all pipes are disabled.
|
|
65
65
|
*/
|
|
66
66
|
async prepareRun(params) {
|
|
67
|
-
this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
68
67
|
const { pipe, pipeOptions } = params;
|
|
68
|
+
// ❗ Validation: pipe is required
|
|
69
|
+
if (!pipe || !pipeOptions) {
|
|
70
|
+
console.warn(`❗ No valid pipe found in filter cmd. Expected format: <pipe>:<options>
|
|
71
|
+
Examples:
|
|
72
|
+
--filter "testomatio:tag-name=frontend"
|
|
73
|
+
--filter "coverage:file=coverage.yml"
|
|
74
|
+
--filter-list "coverage:file=coverage.yml"
|
|
75
|
+
Received: "${params}"`);
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
this.pipes = await (0, index_js_1.pipesFactory)(params || this.paramsForPipesFactory || {}, this.pipeStore);
|
|
69
79
|
// all pipes disabled, skipping
|
|
70
80
|
if (!this.pipes.some(p => p.isEnabled)) {
|
|
71
81
|
return Promise.resolve();
|
|
72
82
|
}
|
|
73
83
|
try {
|
|
74
|
-
const
|
|
75
|
-
|
|
76
|
-
|
|
77
|
-
console.warn(constants_js_1.APP_PREFIX,
|
|
78
|
-
return;
|
|
79
|
-
}
|
|
80
|
-
const results = await Promise.all(this.pipes.map(async (p) => ({ pipe: p.toString(), result: await p.prepareRun(pipeOptions) })));
|
|
81
|
-
const result = results.filter(p => p.pipe.includes('Testomatio'))[0]?.result;
|
|
82
|
-
if (!result || result.length === 0) {
|
|
84
|
+
const p = this.pipes.find(p => p.constructor.name.toLowerCase() === `${pipe.toLowerCase()}pipe`);
|
|
85
|
+
// const p = this.pipes.find(p => p.id === `${pipe.toLowerCase()}`); TODO: as future updates
|
|
86
|
+
if (!p?.isEnabled) {
|
|
87
|
+
console.warn(constants_js_1.APP_PREFIX, "🚫 No active pipes were found in the system. Execution aborted!");
|
|
83
88
|
return;
|
|
84
89
|
}
|
|
90
|
+
// Run only the selected pipe
|
|
91
|
+
const rawResult = await p.prepareRun(pipeOptions);
|
|
92
|
+
const result = Array.isArray(rawResult) ? rawResult : [];
|
|
85
93
|
debug('Execution tests list', result);
|
|
86
94
|
return result;
|
|
87
95
|
}
|
package/lib/data-storage.d.ts
CHANGED
package/lib/helpers.d.ts
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
export const isPlaywright: boolean;
|
package/lib/helpers.js
ADDED
|
@@ -0,0 +1,82 @@
|
|
|
1
|
+
export default CoveragePipe;
|
|
2
|
+
declare class CoveragePipe {
|
|
3
|
+
constructor(params: any, store: any);
|
|
4
|
+
id: string;
|
|
5
|
+
store: any;
|
|
6
|
+
branch: any;
|
|
7
|
+
isDefaultGitChanges: boolean;
|
|
8
|
+
isEnabled: boolean;
|
|
9
|
+
coverageFilePath: any;
|
|
10
|
+
isBranchDefault: boolean;
|
|
11
|
+
formattedDate: string;
|
|
12
|
+
title: string;
|
|
13
|
+
apiKey: string;
|
|
14
|
+
url: any;
|
|
15
|
+
client: Gaxios;
|
|
16
|
+
parsedCoverage: {};
|
|
17
|
+
changedFiles: any[];
|
|
18
|
+
matchedLines: Set<any>;
|
|
19
|
+
tests: Set<any>;
|
|
20
|
+
suiteIds: Set<any>;
|
|
21
|
+
tagLabels: Set<any>;
|
|
22
|
+
results: any[];
|
|
23
|
+
prepareRun(opts: any): Promise<any[]>;
|
|
24
|
+
addTest(data: any): void;
|
|
25
|
+
createRun(): Promise<void>;
|
|
26
|
+
updateRun(): void;
|
|
27
|
+
finishRun(runParams: any): Promise<void>;
|
|
28
|
+
toString(): string;
|
|
29
|
+
/**
|
|
30
|
+
* Retrieves the list of files changed in the current Git working directory
|
|
31
|
+
* compared to a specified branch.
|
|
32
|
+
*
|
|
33
|
+
* This method builds a Git diff command and attempts to retrieve the changed
|
|
34
|
+
* files using that command. It logs helpful information and errors during the process.
|
|
35
|
+
*
|
|
36
|
+
* If no changed files are found, or an error occurs at any stage, the method logs
|
|
37
|
+
* the issue and returns `undefined`.
|
|
38
|
+
*
|
|
39
|
+
* @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
|
|
40
|
+
* otherwise, returns `undefined`.
|
|
41
|
+
*/
|
|
42
|
+
getGitChangedFiles(): this | undefined;
|
|
43
|
+
/**
|
|
44
|
+
* Validates the coverage file path (stored in `this.coverageFilePath`).
|
|
45
|
+
*
|
|
46
|
+
* This method checks:
|
|
47
|
+
* - That the file exists on disk.
|
|
48
|
+
* - That it is a regular file (not a directory or special file).
|
|
49
|
+
* - That it has a `.yml` extension to ensure it's a YAML file.
|
|
50
|
+
*
|
|
51
|
+
* Logs descriptive error messages for any failures.
|
|
52
|
+
*
|
|
53
|
+
* @returns {this | undefined} "true" in case if coverage file is valid and we can keep going;
|
|
54
|
+
* otherwise, `undefined`.
|
|
55
|
+
*/
|
|
56
|
+
validateCoverageFile(): this | undefined;
|
|
57
|
+
/**
|
|
58
|
+
* Parses the YAML coverage file (located at `this.coverageFilePath`) into a JavaScript object.
|
|
59
|
+
*
|
|
60
|
+
* - Reads the file content using UTF-8 encoding.
|
|
61
|
+
* - Parses it as YAML using the `yaml` library.
|
|
62
|
+
* - Stores the result in `this.parsedCoverage`.
|
|
63
|
+
* - If parsing fails, logs an error and returns `undefined`.
|
|
64
|
+
*
|
|
65
|
+
* @returns {this | undefined} The current parsed coverage yml file change lines, or "undefined" if parsing fails.
|
|
66
|
+
*/
|
|
67
|
+
parseCoverageFile(): this | undefined;
|
|
68
|
+
/**
|
|
69
|
+
* Extracts relevant test identifiers from changed files based on coverage mapping.
|
|
70
|
+
*
|
|
71
|
+
* Iterates over changed files and matches them against patterns in the parsed coverage data.
|
|
72
|
+
* For each match, it extracts test IDs or tags and stores them in corresponding sets:
|
|
73
|
+
* - Test IDs (starting with '@T' or '@S') are stored in `this.tests`
|
|
74
|
+
* - Tag labels (starting with 'tag:') are stored in `this.tagLabels`
|
|
75
|
+
* - Matched file paths are stored in `this.matchedLines`
|
|
76
|
+
*
|
|
77
|
+
* @returns {Promise <Set<string>>} A set of file paths that matched coverage patterns (`this.matchedLines`).
|
|
78
|
+
*/
|
|
79
|
+
extractRelevantTestsFromChanges(): Promise<Set<string>>;
|
|
80
|
+
#private;
|
|
81
|
+
}
|
|
82
|
+
import { Gaxios } from 'gaxios';
|
|
@@ -0,0 +1,373 @@
|
|
|
1
|
+
"use strict";
|
|
2
|
+
var __importDefault = (this && this.__importDefault) || function (mod) {
|
|
3
|
+
return (mod && mod.__esModule) ? mod : { "default": mod };
|
|
4
|
+
};
|
|
5
|
+
Object.defineProperty(exports, "__esModule", { value: true });
|
|
6
|
+
const fs_1 = __importDefault(require("fs"));
|
|
7
|
+
const path_1 = __importDefault(require("path"));
|
|
8
|
+
const js_yaml_1 = __importDefault(require("js-yaml"));
|
|
9
|
+
const child_process_1 = require("child_process");
|
|
10
|
+
const gaxios_1 = require("gaxios");
|
|
11
|
+
const minimatch_1 = require("minimatch");
|
|
12
|
+
const constants_js_1 = require("../constants.js");
|
|
13
|
+
const pipe_utils_js_1 = require("../utils/pipe_utils.js");
|
|
14
|
+
const pipe_utils_js_2 = require("../utils/pipe_utils.js");
|
|
15
|
+
const config_js_1 = require("../config.js");
|
|
16
|
+
const debug_1 = __importDefault(require("debug"));
|
|
17
|
+
const debug = (0, debug_1.default)('@testomatio/reporter:pipe:csv');
|
|
18
|
+
// Example of use 'coverage:file=coverage/coverage.yml,diff=master' cmd:
|
|
19
|
+
// | Option | Git command | Notes |
|
|
20
|
+
// | --- | --- | --- |
|
|
21
|
+
// | --filter "coverage:file=coverage/coverage.yml,diff=new-branch" | ✅ git diff new-branch --name-only | - |
|
|
22
|
+
// | --filter "coverage:diff=master,file=coverage.yml" | ✅ git diff master --name-only | - |
|
|
23
|
+
// | --filter "coverage:file=coverage/coverage.yml" | ✅ git diff master --name-only | default branch = "master" |
|
|
24
|
+
// | --filter "coverage:file=coverage.yml,dif=noexist-branch" | ❌ Git command failed ...| - |
|
|
25
|
+
// | --filter "coverage:file=coverage.yml,diff=noexist-branch" | ❌ Git command failed ...| because no branch found |
|
|
26
|
+
// | --filter "coverage:file=no-exist-coverage.yml" | ❌ Coverage file not found: <>filename>.yml | - |
|
|
27
|
+
// | --filter "coverage:filepath=coverage.yml" | 🚫 Missing required parameter: "file"... | - |
|
|
28
|
+
// | --filter "coverage:diff=my-branch" | 🚫 Missing required parameter: "file"...| - |
|
|
29
|
+
//maybe GitChanges or GitCoverage
|
|
30
|
+
class CoveragePipe {
|
|
31
|
+
#GIT = {
|
|
32
|
+
default_branch: 'master',
|
|
33
|
+
diff_command: 'git diff',
|
|
34
|
+
only_file_opt: '--name-only',
|
|
35
|
+
uncommitted_marker: 'uncommitted',
|
|
36
|
+
// test_defaultGitChangedFile - uses only for unit tests in "coverage_pipe_test.js" file
|
|
37
|
+
test_defaultGitChangedFile: ['todomvc-tests/edit-todos_test.js'],
|
|
38
|
+
};
|
|
39
|
+
constructor(params, store) {
|
|
40
|
+
this.id = 'coverage'; // as future updates -> find by id in client.js
|
|
41
|
+
this.store = store || {};
|
|
42
|
+
this.branch = undefined;
|
|
43
|
+
this.isDefaultGitChanges = false; // COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
|
|
44
|
+
this.isEnabled = false;
|
|
45
|
+
const { pipeOptions } = params;
|
|
46
|
+
const options = (0, pipe_utils_js_2.parsePipeOptions)(pipeOptions || "");
|
|
47
|
+
debug("Pipe options", options);
|
|
48
|
+
// this.isDefaultGitChanges - COVERAGE_BY_DEFAULT_GIT_FILE env uses only for unit tests
|
|
49
|
+
this.isDefaultGitChanges = process.env.COVERAGE_BY_DEFAULT_GIT_FILE === '1' ? true : false;
|
|
50
|
+
this.coverageFilePath = options?.file || process.env.COVERAGE_FILEPATH || undefined;
|
|
51
|
+
if (!this.coverageFilePath)
|
|
52
|
+
return;
|
|
53
|
+
this.branch = options?.diff || process.env.COVERAGE_BRANCH || this.#GIT.default_branch;
|
|
54
|
+
this.isBranchDefault = !options.diff && !process.env.COVERAGE_BRANCH;
|
|
55
|
+
if (this.isBranchDefault) {
|
|
56
|
+
console.log(constants_js_1.APP_PREFIX, `🟡 No "diff" branch provided. That's why we use default one = "${this.branch}".\n` +
|
|
57
|
+
'👉 You can set it via --filter "coverage:file=coverage.yml,diff=your-branch"');
|
|
58
|
+
}
|
|
59
|
+
// Client config section
|
|
60
|
+
this.formattedDate = new Date().toISOString().replace(/T/, '-').replace(/:/g, '-').split('.')[0];
|
|
61
|
+
this.title = process.env.TESTOMATIO_TITLE || `Testomatio Coverage Test Execution - ${this.formattedDate}`;
|
|
62
|
+
this.apiKey = process.env['INPUT_TESTOMATIO-KEY'] || config_js_1.config.TESTOMATIO;
|
|
63
|
+
this.url = params.testomatioUrl || process.env.TESTOMATIO_URL || 'https://app.testomat.io';
|
|
64
|
+
const proxyUrl = process.env.HTTP_PROXY || process.env.HTTPS_PROXY || null;
|
|
65
|
+
const proxy = proxyUrl ? new URL(proxyUrl) : null;
|
|
66
|
+
// Create a new instance of gaxios with a custom config
|
|
67
|
+
this.client = new gaxios_1.Gaxios({
|
|
68
|
+
baseURL: `${this.url.trim()}`,
|
|
69
|
+
timeout: constants_js_1.AXIOS_TIMEOUT,
|
|
70
|
+
proxy: proxy ? proxy.toString() : undefined,
|
|
71
|
+
retry: true,
|
|
72
|
+
retryConfig: {
|
|
73
|
+
retry: constants_js_1.REPORTER_REQUEST_RETRIES.retriesPerRequest,
|
|
74
|
+
retryDelay: constants_js_1.REPORTER_REQUEST_RETRIES.retryTimeout,
|
|
75
|
+
httpMethodsToRetry: ['GET', 'PUT', 'HEAD', 'OPTIONS', 'DELETE', 'POST'],
|
|
76
|
+
shouldRetry: (error) => {
|
|
77
|
+
if (!error.response)
|
|
78
|
+
return false;
|
|
79
|
+
switch (error.response?.status) {
|
|
80
|
+
case 400: // Bad request (probably wrong API key)
|
|
81
|
+
case 404: // Test not matched
|
|
82
|
+
case 429: // Rate limit exceeded
|
|
83
|
+
case 500: // Internal server error
|
|
84
|
+
return false;
|
|
85
|
+
default:
|
|
86
|
+
break;
|
|
87
|
+
}
|
|
88
|
+
return error.response?.status >= 401; // Retry on 401+ and 5xx
|
|
89
|
+
}
|
|
90
|
+
}
|
|
91
|
+
});
|
|
92
|
+
// In case if we have all needed data
|
|
93
|
+
this.isEnabled = true;
|
|
94
|
+
debug('Coverage Pipe initialized', {
|
|
95
|
+
branch: this.branch,
|
|
96
|
+
coverageFilePath: this.coverageFilePath,
|
|
97
|
+
});
|
|
98
|
+
this.parsedCoverage = {};
|
|
99
|
+
this.changedFiles = [];
|
|
100
|
+
this.matchedLines = new Set();
|
|
101
|
+
this.tests = new Set();
|
|
102
|
+
this.suiteIds = new Set();
|
|
103
|
+
this.tagLabels = new Set();
|
|
104
|
+
this.results = [];
|
|
105
|
+
debug(`Coverage Pipe: is Enabled = ${this.isEnabled}`);
|
|
106
|
+
}
|
|
107
|
+
async prepareRun(opts) {
|
|
108
|
+
// Reset internal mutable state for isolation
|
|
109
|
+
this.tests.clear();
|
|
110
|
+
this.suiteIds.clear();
|
|
111
|
+
this.tagLabels.clear();
|
|
112
|
+
this.results = [];
|
|
113
|
+
if (!this.isEnabled)
|
|
114
|
+
return [];
|
|
115
|
+
// Step 1: Validate coverage file path & Git changes & Coverage parsing
|
|
116
|
+
if (!this.getGitChangedFiles()?.validateCoverageFile()?.parseCoverageFile())
|
|
117
|
+
return [];
|
|
118
|
+
// Step 2: Extract all available tests and compare with coverage file
|
|
119
|
+
const lines = await this.extractRelevantTestsFromChanges();
|
|
120
|
+
if (lines.size === 0) {
|
|
121
|
+
console.log(constants_js_1.APP_PREFIX, 'ℹ️ No matching entries in coverage file for provided Git changes.');
|
|
122
|
+
return [];
|
|
123
|
+
}
|
|
124
|
+
// Step 3: Handle tag labels tests from the server
|
|
125
|
+
// if (this.tagLabels && this.tagLabels.size > 0) { //TODO: in case if we add labels in future!!!
|
|
126
|
+
if (this.tagLabels.size > 0) {
|
|
127
|
+
for (const tag of this.tagLabels) {
|
|
128
|
+
const tagType = 'tag';
|
|
129
|
+
const tests = await this.#getTestomatioTestsByParam(tagType, tag);
|
|
130
|
+
if (!tests)
|
|
131
|
+
return [];
|
|
132
|
+
console.log(constants_js_1.APP_PREFIX, `✅ We found ${tests.length === 1 ? 'one entry' : `${tests.length} (test/suite) entries`}` +
|
|
133
|
+
' in Testomat.io service side.');
|
|
134
|
+
tests.forEach(testId => this.tests.add(testId));
|
|
135
|
+
}
|
|
136
|
+
}
|
|
137
|
+
if (this.tests.size === 0) {
|
|
138
|
+
console.log(constants_js_1.APP_PREFIX, 'ℹ️ No tests found for execution based on Git changes.');
|
|
139
|
+
return [];
|
|
140
|
+
}
|
|
141
|
+
this.results = [...this.tests];
|
|
142
|
+
return this.results;
|
|
143
|
+
}
|
|
144
|
+
addTest(data) { }
|
|
145
|
+
async createRun() { }
|
|
146
|
+
updateRun() { }
|
|
147
|
+
async finishRun(runParams) { }
|
|
148
|
+
toString() {
|
|
149
|
+
return 'Coverage Reporter';
|
|
150
|
+
}
|
|
151
|
+
/**
|
|
152
|
+
* Fetches a list of tests from the Testomat.io server based on a given filter parameter.
|
|
153
|
+
*
|
|
154
|
+
* The method parses the provided filter (`type=id`), builds the appropriate request parameters,
|
|
155
|
+
* sends a GET request to the Testomat.io `/api/test_grep` endpoint, and returns the matching tests.
|
|
156
|
+
*
|
|
157
|
+
* If the filter is invalid, no query is generated, or the server responds with no matching tests,
|
|
158
|
+
* it logs relevant information and returns `undefined`.
|
|
159
|
+
*
|
|
160
|
+
* @async function
|
|
161
|
+
* @param {string} type - The filter string in the format like `tag-name` for tag by.
|
|
162
|
+
* @param {string} id - The filter string in the format like `smoke`.
|
|
163
|
+
* @returns {Promise<Array<Object>|undefined>} Resolves to an array of test objects if found, otherwise `undefined`.
|
|
164
|
+
*/
|
|
165
|
+
async #getTestomatioTestsByParam(type, id) {
|
|
166
|
+
// Get tests from the server
|
|
167
|
+
try {
|
|
168
|
+
const q = (0, pipe_utils_js_1.generateFilterRequestParams)({
|
|
169
|
+
type,
|
|
170
|
+
id,
|
|
171
|
+
apiKey: this?.apiKey?.trim(),
|
|
172
|
+
});
|
|
173
|
+
if (!q) {
|
|
174
|
+
return;
|
|
175
|
+
}
|
|
176
|
+
const resp = await this.client.request({
|
|
177
|
+
method: 'GET',
|
|
178
|
+
url: '/api/test_grep',
|
|
179
|
+
...q,
|
|
180
|
+
});
|
|
181
|
+
if (!Array.isArray(resp.data?.tests) && resp.data?.tests?.length === 0) {
|
|
182
|
+
console.log(constants_js_1.APP_PREFIX, `🔍 No test by ${type}=${id} were found on the Testomat.io server side!`);
|
|
183
|
+
return undefined;
|
|
184
|
+
}
|
|
185
|
+
return resp.data.tests;
|
|
186
|
+
}
|
|
187
|
+
catch (err) {
|
|
188
|
+
console.error(constants_js_1.APP_PREFIX, `🚩 Error getting available tests from the Testomat.io by "test_grep" option: ${err}`);
|
|
189
|
+
return undefined;
|
|
190
|
+
}
|
|
191
|
+
}
|
|
192
|
+
/**
|
|
193
|
+
* Executes a Git command to retrieve a list of changed files.
|
|
194
|
+
*
|
|
195
|
+
* @param {string} cmd - The Git command to execute.
|
|
196
|
+
* @returns {string[]} An array of changed file paths. Returns an empty array if an error occurs
|
|
197
|
+
* (e.g., not a Git repository or command failure).
|
|
198
|
+
*/
|
|
199
|
+
#getChangedFilesFromGit(cmd) {
|
|
200
|
+
try {
|
|
201
|
+
const result = (0, child_process_1.execSync)(cmd, {
|
|
202
|
+
encoding: 'utf-8',
|
|
203
|
+
stdio: ['pipe', 'pipe', 'ignore']
|
|
204
|
+
});
|
|
205
|
+
return result
|
|
206
|
+
.split('\n')
|
|
207
|
+
.map(f => f.trim())
|
|
208
|
+
.filter(Boolean);
|
|
209
|
+
}
|
|
210
|
+
catch (err) {
|
|
211
|
+
const errorMessage = err.message || '';
|
|
212
|
+
// Git edge: Not a git repository or other error
|
|
213
|
+
if (errorMessage.includes('Not a git repository')) {
|
|
214
|
+
console.error(constants_js_1.APP_PREFIX, '❌ Error: This folder is not a Git repository.');
|
|
215
|
+
}
|
|
216
|
+
else {
|
|
217
|
+
throw new Error(`❌ Git command failed ("${cmd}"):\n`, errorMessage);
|
|
218
|
+
}
|
|
219
|
+
return [];
|
|
220
|
+
}
|
|
221
|
+
}
|
|
222
|
+
/**
|
|
223
|
+
* Builds a Git command string to list file changes between the current state
|
|
224
|
+
* and a specified Git branch using `git diff --name-only`.
|
|
225
|
+
*
|
|
226
|
+
* Private pipe function
|
|
227
|
+
* @throws {Error} Throws an error if `this.branch` is not defined.
|
|
228
|
+
* @returns {string} A Git command string, e.g., 'git diff <branch> --name-only'.
|
|
229
|
+
*/
|
|
230
|
+
#buildGitCommand() {
|
|
231
|
+
if (!this.branch)
|
|
232
|
+
throw new Error(`❌ Invalid changes option for setted branch!`);
|
|
233
|
+
return `git diff ${this.branch} --name-only`; // Example: 'git diff <master> --name-only'
|
|
234
|
+
}
|
|
235
|
+
/**
|
|
236
|
+
* Retrieves the list of files changed in the current Git working directory
|
|
237
|
+
* compared to a specified branch.
|
|
238
|
+
*
|
|
239
|
+
* This method builds a Git diff command and attempts to retrieve the changed
|
|
240
|
+
* files using that command. It logs helpful information and errors during the process.
|
|
241
|
+
*
|
|
242
|
+
* If no changed files are found, or an error occurs at any stage, the method logs
|
|
243
|
+
* the issue and returns `undefined`.
|
|
244
|
+
*
|
|
245
|
+
* @returns {this | undefined} Returns the current instance (`this`) if changed files are found;
|
|
246
|
+
* otherwise, returns `undefined`.
|
|
247
|
+
*/
|
|
248
|
+
getGitChangedFiles() {
|
|
249
|
+
let cmd;
|
|
250
|
+
try {
|
|
251
|
+
cmd = this.#buildGitCommand();
|
|
252
|
+
}
|
|
253
|
+
catch (err) {
|
|
254
|
+
console.error(constants_js_1.APP_PREFIX, err.message);
|
|
255
|
+
return undefined;
|
|
256
|
+
}
|
|
257
|
+
console.error(constants_js_1.APP_PREFIX, `ℹ️ We will use '${cmd}' Git command.`);
|
|
258
|
+
try {
|
|
259
|
+
// For clear unit testing process -> Like test_defaultGitChangedFile = todomvc-tests/edit-todos_test.js
|
|
260
|
+
if (this.isDefaultGitChanges) {
|
|
261
|
+
this.changedFiles = this.#GIT.test_defaultGitChangedFile;
|
|
262
|
+
}
|
|
263
|
+
else {
|
|
264
|
+
this.changedFiles = this.#getChangedFilesFromGit(cmd);
|
|
265
|
+
if (this.changedFiles.length === 0) {
|
|
266
|
+
console.log(constants_js_1.APP_PREFIX, 'ℹ️ No files changed in the latest Git commit. Skipping coverage processing.');
|
|
267
|
+
return undefined;
|
|
268
|
+
}
|
|
269
|
+
}
|
|
270
|
+
}
|
|
271
|
+
catch (err) {
|
|
272
|
+
console.error(constants_js_1.APP_PREFIX, err.message);
|
|
273
|
+
console.error(constants_js_1.APP_PREFIX, "🔍 Pls, check this Git command manually to understand the original problem.");
|
|
274
|
+
return undefined;
|
|
275
|
+
}
|
|
276
|
+
console.log(constants_js_1.APP_PREFIX, `📑 GIT changed files:\n - ${this.changedFiles.join('\n - ')}`);
|
|
277
|
+
return this;
|
|
278
|
+
}
|
|
279
|
+
/**
|
|
280
|
+
* Validates the coverage file path (stored in `this.coverageFilePath`).
|
|
281
|
+
*
|
|
282
|
+
* This method checks:
|
|
283
|
+
* - That the file exists on disk.
|
|
284
|
+
* - That it is a regular file (not a directory or special file).
|
|
285
|
+
* - That it has a `.yml` extension to ensure it's a YAML file.
|
|
286
|
+
*
|
|
287
|
+
* Logs descriptive error messages for any failures.
|
|
288
|
+
*
|
|
289
|
+
* @returns {this | undefined} "true" in case if coverage file is valid and we can keep going;
|
|
290
|
+
* otherwise, `undefined`.
|
|
291
|
+
*/
|
|
292
|
+
validateCoverageFile() {
|
|
293
|
+
// Validate the presence of the coverage filepath
|
|
294
|
+
if (!fs_1.default.existsSync(this.coverageFilePath)) {
|
|
295
|
+
console.log(constants_js_1.APP_PREFIX, '❌ Coverage file not found:', this.coverageFilePath);
|
|
296
|
+
return undefined;
|
|
297
|
+
}
|
|
298
|
+
// Ensure the given path is a file (not a directory or other type)
|
|
299
|
+
const stat = fs_1.default.statSync(this.coverageFilePath);
|
|
300
|
+
if (!stat.isFile()) {
|
|
301
|
+
console.log(constants_js_1.APP_PREFIX, '❌ Provided coverage path is not a file:', this.coverageFilePath);
|
|
302
|
+
return undefined;
|
|
303
|
+
}
|
|
304
|
+
// Validate the file extension to be ".yml" to ensure it's a YAML file
|
|
305
|
+
if (path_1.default.extname(this.coverageFilePath) !== ".yml") {
|
|
306
|
+
console.log(constants_js_1.APP_PREFIX, '❌ Coverage file must have a .yml extension:', this.coverageFilePath);
|
|
307
|
+
return undefined;
|
|
308
|
+
}
|
|
309
|
+
debug('Coverage file validation is OK!');
|
|
310
|
+
return this;
|
|
311
|
+
}
|
|
312
|
+
/**
|
|
313
|
+
* Parses the YAML coverage file (located at `this.coverageFilePath`) into a JavaScript object.
|
|
314
|
+
*
|
|
315
|
+
* - Reads the file content using UTF-8 encoding.
|
|
316
|
+
* - Parses it as YAML using the `yaml` library.
|
|
317
|
+
* - Stores the result in `this.parsedCoverage`.
|
|
318
|
+
* - If parsing fails, logs an error and returns `undefined`.
|
|
319
|
+
*
|
|
320
|
+
* @returns {this | undefined} The current parsed coverage yml file change lines, or "undefined" if parsing fails.
|
|
321
|
+
*/
|
|
322
|
+
parseCoverageFile() {
|
|
323
|
+
try {
|
|
324
|
+
// Read the contents of the YAML file and attempt to parse the YAML into a JavaScript object
|
|
325
|
+
const rawYml = fs_1.default.readFileSync(this.coverageFilePath, 'utf8');
|
|
326
|
+
this.parsedCoverage = js_yaml_1.default.load(rawYml) || {};
|
|
327
|
+
debug(`Coverage filepath = ${this.coverageFilePath})`);
|
|
328
|
+
console.log(constants_js_1.APP_PREFIX, `✅ Coverage file parsed successfully: ${this.coverageFilePath}`);
|
|
329
|
+
return this;
|
|
330
|
+
}
|
|
331
|
+
catch (err) {
|
|
332
|
+
console.error(constants_js_1.APP_PREFIX, '❌ Failed to parse YAML:', err.message);
|
|
333
|
+
return undefined;
|
|
334
|
+
}
|
|
335
|
+
}
|
|
336
|
+
/**
|
|
337
|
+
* Extracts relevant test identifiers from changed files based on coverage mapping.
|
|
338
|
+
*
|
|
339
|
+
* Iterates over changed files and matches them against patterns in the parsed coverage data.
|
|
340
|
+
* For each match, it extracts test IDs or tags and stores them in corresponding sets:
|
|
341
|
+
* - Test IDs (starting with '@T' or '@S') are stored in `this.tests`
|
|
342
|
+
* - Tag labels (starting with 'tag:') are stored in `this.tagLabels`
|
|
343
|
+
* - Matched file paths are stored in `this.matchedLines`
|
|
344
|
+
*
|
|
345
|
+
* @returns {Promise <Set<string>>} A set of file paths that matched coverage patterns (`this.matchedLines`).
|
|
346
|
+
*/
|
|
347
|
+
async extractRelevantTestsFromChanges() {
|
|
348
|
+
for (const changedFile of this.changedFiles) {
|
|
349
|
+
for (const [pattern, ids] of Object.entries(this.parsedCoverage)) {
|
|
350
|
+
if ((0, minimatch_1.minimatch)(changedFile, pattern)) {
|
|
351
|
+
this.matchedLines.add(changedFile);
|
|
352
|
+
ids.forEach(id => {
|
|
353
|
+
// Example: "@Tt74099t1"
|
|
354
|
+
if (id.startsWith('@T')) {
|
|
355
|
+
this.tests.add(id.slice(1));
|
|
356
|
+
}
|
|
357
|
+
// Example: "@Sd74099c1"
|
|
358
|
+
else if (id.startsWith('@S')) {
|
|
359
|
+
this.tests.add(id.slice(1));
|
|
360
|
+
}
|
|
361
|
+
// Example: "tag:@TestSmoke"
|
|
362
|
+
else if (id.startsWith('tag')) {
|
|
363
|
+
this.tagLabels.add(id.split(':')[1].slice(1));
|
|
364
|
+
}
|
|
365
|
+
});
|
|
366
|
+
}
|
|
367
|
+
}
|
|
368
|
+
}
|
|
369
|
+
debug(`Matched lines: ${this.matchedLines}`);
|
|
370
|
+
return this.matchedLines;
|
|
371
|
+
}
|
|
372
|
+
}
|
|
373
|
+
module.exports = CoveragePipe;
|
package/lib/pipe/index.js
CHANGED
|
@@ -46,6 +46,7 @@ const github_js_1 = __importDefault(require("./github.js"));
|
|
|
46
46
|
const gitlab_js_1 = __importDefault(require("./gitlab.js"));
|
|
47
47
|
const csv_js_1 = __importDefault(require("./csv.js"));
|
|
48
48
|
const html_js_1 = __importDefault(require("./html.js"));
|
|
49
|
+
const coverage_js_1 = __importDefault(require("./coverage.js"));
|
|
49
50
|
const bitbucket_js_1 = require("./bitbucket.js");
|
|
50
51
|
const debug_js_1 = require("./debug.js");
|
|
51
52
|
async function pipesFactory(params, opts) {
|
|
@@ -83,6 +84,7 @@ async function pipesFactory(params, opts) {
|
|
|
83
84
|
new csv_js_1.default(params, opts),
|
|
84
85
|
new html_js_1.default(params, opts),
|
|
85
86
|
new bitbucket_js_1.BitbucketPipe(params, opts),
|
|
87
|
+
new coverage_js_1.default(params, opts),
|
|
86
88
|
new debug_js_1.DebugPipe(params, opts),
|
|
87
89
|
...extraPipes,
|
|
88
90
|
];
|
package/lib/pipe/testomatio.d.ts
CHANGED
package/lib/pipe/testomatio.js
CHANGED
|
@@ -46,7 +46,24 @@ class TestomatioPipe {
|
|
|
46
46
|
this.store = store || {};
|
|
47
47
|
this.title = params.title || process.env.TESTOMATIO_TITLE;
|
|
48
48
|
this.sharedRun = !!process.env.TESTOMATIO_SHARED_RUN;
|
|
49
|
-
this.sharedRunTimeout =
|
|
49
|
+
this.sharedRunTimeout = process.env.TESTOMATIO_SHARED_RUN_TIMEOUT
|
|
50
|
+
? parseInt(process.env.TESTOMATIO_SHARED_RUN_TIMEOUT, 10)
|
|
51
|
+
: undefined;
|
|
52
|
+
if (this.sharedRunTimeout && !this.sharedRun) {
|
|
53
|
+
debug('Auto-enabling sharedRun because sharedRunTimeout is set');
|
|
54
|
+
this.sharedRun = true;
|
|
55
|
+
}
|
|
56
|
+
if (!this.title && (this.sharedRun || this.sharedRunTimeout)) {
|
|
57
|
+
const sha = (0, utils_js_1.getGitCommitSha)();
|
|
58
|
+
if (sha) {
|
|
59
|
+
this.title = `Shared Run - ${sha}`;
|
|
60
|
+
console.log(constants_js_1.APP_PREFIX, `🔄 Auto-generated title for shared run: ${this.title}`);
|
|
61
|
+
}
|
|
62
|
+
else {
|
|
63
|
+
console.log(constants_js_1.APP_PREFIX, picocolors_1.default.red('Failed to resolve git commit SHA for shared run title.'));
|
|
64
|
+
console.log(constants_js_1.APP_PREFIX, 'Please run the tests inside a Git repository or set TESTOMATIO_TITLE explicitly.');
|
|
65
|
+
}
|
|
66
|
+
}
|
|
50
67
|
this.groupTitle = params.groupTitle || process.env.TESTOMATIO_RUNGROUP_TITLE;
|
|
51
68
|
this.env = process.env.TESTOMATIO_ENV;
|
|
52
69
|
this.label = process.env.TESTOMATIO_LABEL;
|
|
@@ -121,15 +138,19 @@ class TestomatioPipe {
|
|
|
121
138
|
async prepareRun(opts) {
|
|
122
139
|
if (!this.isEnabled)
|
|
123
140
|
return [];
|
|
124
|
-
const
|
|
141
|
+
const clearOptions = (0, pipe_utils_js_1.parseFilterParams)(opts);
|
|
142
|
+
if (!clearOptions) {
|
|
143
|
+
return [];
|
|
144
|
+
}
|
|
145
|
+
const { type, id } = clearOptions;
|
|
125
146
|
try {
|
|
126
147
|
const q = (0, pipe_utils_js_1.generateFilterRequestParams)({
|
|
127
148
|
type,
|
|
128
149
|
id,
|
|
129
|
-
apiKey: this
|
|
150
|
+
apiKey: this?.apiKey?.trim(),
|
|
130
151
|
});
|
|
131
152
|
if (!q) {
|
|
132
|
-
return;
|
|
153
|
+
return [];
|
|
133
154
|
}
|
|
134
155
|
const resp = await this.client.request({
|
|
135
156
|
method: 'GET',
|