testbeats 2.2.8 ā 2.2.9
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/package.json +3 -2
- package/src/cli.js +18 -0
- package/src/commands/manual-sync.command.js +79 -0
- package/src/helpers/constants.js +1 -0
- package/src/index.d.ts +7 -2
- package/src/manual/parsers/gherkin.js +237 -0
- package/src/manual/sync.helper.js +148 -0
- package/src/targets/github-output.target.js +64 -0
- package/src/targets/index.js +3 -0
package/package.json
CHANGED
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
{
|
|
2
2
|
"name": "testbeats",
|
|
3
|
-
"version": "2.2.
|
|
3
|
+
"version": "2.2.9",
|
|
4
4
|
"description": "Publish test results to Microsoft Teams, Google Chat, Slack and InfluxDB",
|
|
5
5
|
"main": "src/index.js",
|
|
6
6
|
"types": "./src/index.d.ts",
|
|
@@ -50,13 +50,14 @@
|
|
|
50
50
|
"dotenv": "^16.4.5",
|
|
51
51
|
"form-data-lite": "^1.0.3",
|
|
52
52
|
"influxdb-lite": "^1.0.0",
|
|
53
|
+
"object-hash": "^3.0.0",
|
|
53
54
|
"performance-results-parser": "latest",
|
|
54
55
|
"phin-retry": "^1.0.3",
|
|
55
56
|
"pretty-ms": "^7.0.1",
|
|
56
57
|
"prompts": "^2.4.2",
|
|
57
58
|
"rosters": "0.0.1",
|
|
58
59
|
"sade": "^1.8.1",
|
|
59
|
-
"test-results-parser": "0.2.
|
|
60
|
+
"test-results-parser": "0.2.9"
|
|
60
61
|
},
|
|
61
62
|
"devDependencies": {
|
|
62
63
|
"c8": "^10.1.2",
|
package/src/cli.js
CHANGED
|
@@ -6,6 +6,7 @@ const sade = require('sade');
|
|
|
6
6
|
const prog = sade('testbeats');
|
|
7
7
|
const { PublishCommand } = require('./commands/publish.command');
|
|
8
8
|
const { GenerateConfigCommand } = require('./commands/generate-config.command');
|
|
9
|
+
const { ManualSyncCommand } = require('./commands/manual-sync.command');
|
|
9
10
|
const logger = require('./utils/logger');
|
|
10
11
|
const pkg = require('../package.json');
|
|
11
12
|
|
|
@@ -63,4 +64,21 @@ prog.command('init')
|
|
|
63
64
|
}
|
|
64
65
|
});
|
|
65
66
|
|
|
67
|
+
// Command to sync manual test cases from gherkin files
|
|
68
|
+
prog.command('manual sync')
|
|
69
|
+
.describe('Sync manual test cases from gherkin files in a directory')
|
|
70
|
+
.option('-p, --path', 'Path to directory to sync (default: current directory)')
|
|
71
|
+
.example('manual sync')
|
|
72
|
+
.example('manual sync --path ./tests/features')
|
|
73
|
+
.action(async (opts) => {
|
|
74
|
+
try {
|
|
75
|
+
logger.setLevel(opts.logLevel);
|
|
76
|
+
const manual_sync_command = new ManualSyncCommand(opts);
|
|
77
|
+
await manual_sync_command.execute();
|
|
78
|
+
} catch (error) {
|
|
79
|
+
logger.error(`Manual sync failed: ${error.message}`);
|
|
80
|
+
process.exit(1);
|
|
81
|
+
}
|
|
82
|
+
});
|
|
83
|
+
|
|
66
84
|
prog.parse(process.argv);
|
|
@@ -0,0 +1,79 @@
|
|
|
1
|
+
const path = require('path');
|
|
2
|
+
const { ManualSyncHelper } = require('../manual/sync.helper');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
|
|
5
|
+
class ManualSyncCommand {
|
|
6
|
+
/**
|
|
7
|
+
* @param {Object} opts - Command options
|
|
8
|
+
*/
|
|
9
|
+
constructor(opts) {
|
|
10
|
+
this.opts = opts;
|
|
11
|
+
this.syncHelper = new ManualSyncHelper();
|
|
12
|
+
}
|
|
13
|
+
|
|
14
|
+
/**
|
|
15
|
+
* Execute the manual sync command
|
|
16
|
+
*/
|
|
17
|
+
async execute() {
|
|
18
|
+
try {
|
|
19
|
+
logger.info('š Starting manual test case sync...');
|
|
20
|
+
|
|
21
|
+
const targetPath = this.opts.path || '.';
|
|
22
|
+
logger.info(`š Scanning directory: ${targetPath}`);
|
|
23
|
+
|
|
24
|
+
const result = await this.syncHelper.scanDirectory(targetPath);
|
|
25
|
+
|
|
26
|
+
// Format output as specified in requirements
|
|
27
|
+
const output = {
|
|
28
|
+
folders: [result]
|
|
29
|
+
};
|
|
30
|
+
|
|
31
|
+
// Display results
|
|
32
|
+
this.displayResults(output);
|
|
33
|
+
|
|
34
|
+
logger.info('ā
Manual sync completed successfully!');
|
|
35
|
+
|
|
36
|
+
return output;
|
|
37
|
+
|
|
38
|
+
} catch (error) {
|
|
39
|
+
logger.error(`ā Manual sync failed: ${error.message}`);
|
|
40
|
+
throw error;
|
|
41
|
+
}
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
/**
|
|
45
|
+
* Display sync results in a readable format
|
|
46
|
+
* @param {Object} output - Sync results
|
|
47
|
+
*/
|
|
48
|
+
displayResults(output) {
|
|
49
|
+
logger.info('\nš Sync Results:');
|
|
50
|
+
this.displayFolderRecursive(output.folders[0], 0);
|
|
51
|
+
}
|
|
52
|
+
|
|
53
|
+
/**
|
|
54
|
+
* Recursively display folder structure
|
|
55
|
+
* @param {Object} folder - Folder object
|
|
56
|
+
* @param {number} depth - Current depth for indentation
|
|
57
|
+
*/
|
|
58
|
+
displayFolderRecursive(folder, depth) {
|
|
59
|
+
const indent = ' '.repeat(depth);
|
|
60
|
+
|
|
61
|
+
logger.info(`${indent}š ${folder.name} (${folder.path})`);
|
|
62
|
+
|
|
63
|
+
if (folder.test_suites.length > 0) {
|
|
64
|
+
logger.info(`${indent} š Test Suites: ${folder.test_suites.length}`);
|
|
65
|
+
folder.test_suites.forEach(suite => {
|
|
66
|
+
logger.info(`${indent} ⢠${suite.name} (${suite.test_cases.length} test cases)`);
|
|
67
|
+
});
|
|
68
|
+
}
|
|
69
|
+
|
|
70
|
+
if (folder.folders.length > 0) {
|
|
71
|
+
logger.info(`${indent} š Subfolders: ${folder.folders.length}`);
|
|
72
|
+
folder.folders.forEach(subFolder => {
|
|
73
|
+
this.displayFolderRecursive(subFolder, depth + 1);
|
|
74
|
+
});
|
|
75
|
+
}
|
|
76
|
+
}
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
module.exports = { ManualSyncCommand };
|
package/src/helpers/constants.js
CHANGED
package/src/index.d.ts
CHANGED
|
@@ -10,7 +10,7 @@ export interface ITarget {
|
|
|
10
10
|
name: TargetName;
|
|
11
11
|
enable?: string | boolean;
|
|
12
12
|
condition?: Condition;
|
|
13
|
-
inputs?: SlackInputs | TeamsInputs | ChatInputs | IGitHubInputs | ICustomTargetInputs | InfluxDBTargetInputs;
|
|
13
|
+
inputs?: SlackInputs | TeamsInputs | ChatInputs | IGitHubInputs | IGitHubOutputInputs | ICustomTargetInputs | InfluxDBTargetInputs;
|
|
14
14
|
extensions?: IExtension[];
|
|
15
15
|
}
|
|
16
16
|
|
|
@@ -25,7 +25,7 @@ export interface IExtension {
|
|
|
25
25
|
|
|
26
26
|
export type ExtensionName = 'report-portal-analysis' | 'hyperlinks' | 'mentions' | 'report-portal-history' | 'quick-chart-test-summary' | 'metadata' | 'ci-info' | 'custom' | 'ai-failure-summary';
|
|
27
27
|
export type Hook = 'start' | 'end' | 'after-summary';
|
|
28
|
-
export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'custom' | 'delay';
|
|
28
|
+
export type TargetName = 'slack' | 'teams' | 'chat' | 'github' | 'github-output' | 'custom' | 'delay';
|
|
29
29
|
export type PublishReportType = 'test-summary' | 'test-summary-slim' | 'failure-details';
|
|
30
30
|
|
|
31
31
|
export interface ConditionFunctionContext {
|
|
@@ -251,6 +251,11 @@ export interface IGitHubInputs extends TargetInputs {
|
|
|
251
251
|
pull_number?: string;
|
|
252
252
|
}
|
|
253
253
|
|
|
254
|
+
export interface IGitHubOutputInputs {
|
|
255
|
+
output_file?: string;
|
|
256
|
+
key?: string;
|
|
257
|
+
}
|
|
258
|
+
|
|
254
259
|
export interface InfluxDBTargetInputs {
|
|
255
260
|
url: string;
|
|
256
261
|
version?: string;
|
|
@@ -0,0 +1,237 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Simple and extendable Gherkin parser for Cucumber feature files
|
|
5
|
+
* Parses .feature files and returns structured test suite objects
|
|
6
|
+
*/
|
|
7
|
+
class GherkinParser {
|
|
8
|
+
constructor() {
|
|
9
|
+
/** @type {string[]} Supported step keywords */
|
|
10
|
+
this.stepKeywords = ['Given', 'When', 'Then', 'And', 'But'];
|
|
11
|
+
}
|
|
12
|
+
|
|
13
|
+
/**
|
|
14
|
+
* @param {string} file_path
|
|
15
|
+
* @returns {Object} Parsed test suite structure
|
|
16
|
+
*/
|
|
17
|
+
parse(file_path) {
|
|
18
|
+
try {
|
|
19
|
+
const content = fs.readFileSync(file_path, 'utf8');
|
|
20
|
+
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
21
|
+
|
|
22
|
+
return this.parseLines(lines);
|
|
23
|
+
} catch (error) {
|
|
24
|
+
throw new Error(`Failed to parse Gherkin file: ${error.message}`);
|
|
25
|
+
}
|
|
26
|
+
}
|
|
27
|
+
|
|
28
|
+
/**
|
|
29
|
+
* Parse lines and build the test suite structure
|
|
30
|
+
* @param {string[]} lines
|
|
31
|
+
* @returns {Object}
|
|
32
|
+
*/
|
|
33
|
+
parseLines(lines) {
|
|
34
|
+
const testSuite = {
|
|
35
|
+
name: '',
|
|
36
|
+
type: 'feature',
|
|
37
|
+
tags: [],
|
|
38
|
+
beforeEach: [],
|
|
39
|
+
cases: []
|
|
40
|
+
};
|
|
41
|
+
|
|
42
|
+
let currentFeature = null;
|
|
43
|
+
let currentBackground = null;
|
|
44
|
+
let currentScenario = null;
|
|
45
|
+
let pendingFeatureTags = [];
|
|
46
|
+
let pendingScenarioTags = [];
|
|
47
|
+
|
|
48
|
+
for (let i = 0; i < lines.length; i++) {
|
|
49
|
+
const line = lines[i];
|
|
50
|
+
|
|
51
|
+
if (line.startsWith('@')) {
|
|
52
|
+
// Handle tags
|
|
53
|
+
const tags = this.parseTags(line);
|
|
54
|
+
|
|
55
|
+
// Look ahead to see if tags belong to Feature or Scenario
|
|
56
|
+
if (i + 1 < lines.length && lines[i + 1].startsWith('Feature:')) {
|
|
57
|
+
// Tags belong to Feature
|
|
58
|
+
pendingFeatureTags = tags;
|
|
59
|
+
} else if (i + 1 < lines.length && lines[i + 1].startsWith('Scenario:')) {
|
|
60
|
+
// Tags belong to Scenario
|
|
61
|
+
pendingScenarioTags = tags;
|
|
62
|
+
}
|
|
63
|
+
} else if (line.startsWith('Feature:')) {
|
|
64
|
+
// Parse Feature
|
|
65
|
+
const description = this.parseMultiLineDescription(lines, i + 1);
|
|
66
|
+
currentFeature = this.parseFeature(line, description);
|
|
67
|
+
testSuite.name = currentFeature.name;
|
|
68
|
+
testSuite.tags = pendingFeatureTags.map(tag => tag.name);
|
|
69
|
+
pendingFeatureTags = [];
|
|
70
|
+
i += description.split('\n').length; // Skip all description lines
|
|
71
|
+
} else if (line.startsWith('Background:')) {
|
|
72
|
+
// Parse Background
|
|
73
|
+
currentBackground = this.parseBackground();
|
|
74
|
+
testSuite.beforeEach.push(currentBackground);
|
|
75
|
+
} else if (line.startsWith('Scenario:')) {
|
|
76
|
+
// Parse Scenario
|
|
77
|
+
currentScenario = this.parseScenario(line);
|
|
78
|
+
currentScenario.tags = pendingScenarioTags.map(tag => tag.name);
|
|
79
|
+
testSuite.cases.push(currentScenario);
|
|
80
|
+
pendingScenarioTags = [];
|
|
81
|
+
currentBackground = null; // Reset Background context when Scenario starts
|
|
82
|
+
} else if (this.isStep(line)) {
|
|
83
|
+
// Parse Step
|
|
84
|
+
const step = this.parseStep(line);
|
|
85
|
+
|
|
86
|
+
// Determine where to add the step based on current context
|
|
87
|
+
if (currentBackground && currentBackground.steps) {
|
|
88
|
+
// Add step to Background
|
|
89
|
+
currentBackground.steps.push(step);
|
|
90
|
+
} else if (currentScenario && currentScenario.steps) {
|
|
91
|
+
// Add step to Scenario
|
|
92
|
+
currentScenario.steps.push(step);
|
|
93
|
+
}
|
|
94
|
+
}
|
|
95
|
+
}
|
|
96
|
+
|
|
97
|
+
return testSuite;
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
/**
|
|
101
|
+
* Parse tags from a line
|
|
102
|
+
* @param {string} line
|
|
103
|
+
* @returns {Array}
|
|
104
|
+
*/
|
|
105
|
+
parseTags(line) {
|
|
106
|
+
const tagMatches = line.match(/@\w+/g);
|
|
107
|
+
return tagMatches ? tagMatches.map(tag => ({ name: tag })) : [];
|
|
108
|
+
}
|
|
109
|
+
|
|
110
|
+
/**
|
|
111
|
+
* Parse multi-line description starting from a given line index
|
|
112
|
+
* @param {string[]} lines
|
|
113
|
+
* @param {number} startIndex
|
|
114
|
+
* @returns {string}
|
|
115
|
+
*/
|
|
116
|
+
parseMultiLineDescription(lines, startIndex) {
|
|
117
|
+
const descriptionLines = [];
|
|
118
|
+
|
|
119
|
+
for (let i = startIndex; i < lines.length; i++) {
|
|
120
|
+
const line = lines[i];
|
|
121
|
+
|
|
122
|
+
// Stop if we hit a keyword or step
|
|
123
|
+
if (line.startsWith('Background:') ||
|
|
124
|
+
line.startsWith('Scenario:') ||
|
|
125
|
+
line.startsWith('@') ||
|
|
126
|
+
this.isStep(line)) {
|
|
127
|
+
break;
|
|
128
|
+
}
|
|
129
|
+
|
|
130
|
+
// Add non-empty lines to description
|
|
131
|
+
if (line.trim().length > 0) {
|
|
132
|
+
descriptionLines.push(line.trim());
|
|
133
|
+
}
|
|
134
|
+
}
|
|
135
|
+
|
|
136
|
+
return descriptionLines.join('\n');
|
|
137
|
+
}
|
|
138
|
+
|
|
139
|
+
/**
|
|
140
|
+
* Parse Feature line
|
|
141
|
+
* @param {string} line
|
|
142
|
+
* @param {string} description
|
|
143
|
+
* @returns {Object}
|
|
144
|
+
*/
|
|
145
|
+
parseFeature(line, description) {
|
|
146
|
+
const name = line.replace('Feature:', '').trim();
|
|
147
|
+
return {
|
|
148
|
+
type: 'Feature',
|
|
149
|
+
tags: [],
|
|
150
|
+
keyword: 'Feature',
|
|
151
|
+
name: name,
|
|
152
|
+
description: description.trim(),
|
|
153
|
+
children: []
|
|
154
|
+
};
|
|
155
|
+
}
|
|
156
|
+
|
|
157
|
+
/**
|
|
158
|
+
* Parse Background line
|
|
159
|
+
* @returns {Object}
|
|
160
|
+
*/
|
|
161
|
+
parseBackground() {
|
|
162
|
+
return {
|
|
163
|
+
name: '',
|
|
164
|
+
type: 'background',
|
|
165
|
+
steps: []
|
|
166
|
+
};
|
|
167
|
+
}
|
|
168
|
+
|
|
169
|
+
/**
|
|
170
|
+
* Parse Scenario line
|
|
171
|
+
* @param {string} line
|
|
172
|
+
* @returns {Object}
|
|
173
|
+
*/
|
|
174
|
+
parseScenario(line) {
|
|
175
|
+
const name = line.replace('Scenario:', '').trim();
|
|
176
|
+
return {
|
|
177
|
+
name: name,
|
|
178
|
+
type: 'scenario',
|
|
179
|
+
tags: [],
|
|
180
|
+
steps: []
|
|
181
|
+
};
|
|
182
|
+
}
|
|
183
|
+
|
|
184
|
+
/**
|
|
185
|
+
* Check if a line is a step
|
|
186
|
+
* @param {string} line
|
|
187
|
+
* @returns {boolean}
|
|
188
|
+
*/
|
|
189
|
+
isStep(line) {
|
|
190
|
+
return this.stepKeywords.some(keyword =>
|
|
191
|
+
line.startsWith(keyword + ' ') ||
|
|
192
|
+
line.startsWith('And ') ||
|
|
193
|
+
line.startsWith('But ')
|
|
194
|
+
);
|
|
195
|
+
}
|
|
196
|
+
|
|
197
|
+
/**
|
|
198
|
+
* Parse a step line
|
|
199
|
+
* @param {string} line
|
|
200
|
+
* @returns {Object}
|
|
201
|
+
*/
|
|
202
|
+
parseStep(line) {
|
|
203
|
+
return {
|
|
204
|
+
name: line.trim()
|
|
205
|
+
};
|
|
206
|
+
}
|
|
207
|
+
|
|
208
|
+
/**
|
|
209
|
+
* Parse from string content instead of file
|
|
210
|
+
* @param {string} content
|
|
211
|
+
* @returns {Object} Parsed test suite structure
|
|
212
|
+
*/
|
|
213
|
+
parseString(content) {
|
|
214
|
+
try {
|
|
215
|
+
const lines = content.split('\n').map(line => line.trim()).filter(line => line.length > 0);
|
|
216
|
+
return this.parseLines(lines);
|
|
217
|
+
} catch (error) {
|
|
218
|
+
throw new Error(`Failed to parse Gherkin content: ${error.message}`);
|
|
219
|
+
}
|
|
220
|
+
}
|
|
221
|
+
|
|
222
|
+
/**
|
|
223
|
+
* Validate if the parsed document has required structure
|
|
224
|
+
* @param {Object} document
|
|
225
|
+
* @returns {boolean}
|
|
226
|
+
*/
|
|
227
|
+
validate(document) {
|
|
228
|
+
return document &&
|
|
229
|
+
document.name &&
|
|
230
|
+
document.type === 'feature' &&
|
|
231
|
+
Array.isArray(document.tags) &&
|
|
232
|
+
Array.isArray(document.beforeEach) &&
|
|
233
|
+
Array.isArray(document.cases);
|
|
234
|
+
}
|
|
235
|
+
}
|
|
236
|
+
|
|
237
|
+
module.exports = { GherkinParser };
|
|
@@ -0,0 +1,148 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const hash = require('object-hash');
|
|
4
|
+
const { GherkinParser } = require('./parsers/gherkin');
|
|
5
|
+
|
|
6
|
+
class ManualSyncHelper {
|
|
7
|
+
constructor() {
|
|
8
|
+
this.parser = new GherkinParser();
|
|
9
|
+
}
|
|
10
|
+
|
|
11
|
+
/**
|
|
12
|
+
* Generate hash for an object
|
|
13
|
+
* @param {Object} obj - Object to hash
|
|
14
|
+
* @returns {string} Hash string
|
|
15
|
+
*/
|
|
16
|
+
generateHash(obj) {
|
|
17
|
+
return hash(obj, { algorithm: 'md5' });
|
|
18
|
+
}
|
|
19
|
+
|
|
20
|
+
/**
|
|
21
|
+
* Add hash to test case
|
|
22
|
+
* @param {Object} testCase - Test case object
|
|
23
|
+
* @returns {Object} Test case with hash
|
|
24
|
+
*/
|
|
25
|
+
addTestCaseHash(testCase) {
|
|
26
|
+
return {
|
|
27
|
+
...testCase,
|
|
28
|
+
hash: this.generateHash(testCase)
|
|
29
|
+
};
|
|
30
|
+
}
|
|
31
|
+
|
|
32
|
+
/**
|
|
33
|
+
* Add hash to test suite
|
|
34
|
+
* @param {Object} testSuite - Test suite object
|
|
35
|
+
* @returns {Object} Test suite with hash
|
|
36
|
+
*/
|
|
37
|
+
addTestSuiteHash(testSuite) {
|
|
38
|
+
return {
|
|
39
|
+
...testSuite,
|
|
40
|
+
hash: this.generateHash(testSuite),
|
|
41
|
+
test_cases: testSuite.test_cases.map(testCase => this.addTestCaseHash(testCase))
|
|
42
|
+
};
|
|
43
|
+
}
|
|
44
|
+
|
|
45
|
+
/**
|
|
46
|
+
* Add hash to folder
|
|
47
|
+
* @param {Object} folder - Folder object
|
|
48
|
+
* @returns {Object} Folder with hash
|
|
49
|
+
*/
|
|
50
|
+
addFolderHash(folder) {
|
|
51
|
+
return {
|
|
52
|
+
...folder,
|
|
53
|
+
hash: this.generateHash(folder),
|
|
54
|
+
test_suites: folder.test_suites.map(testSuite => this.addTestSuiteHash(testSuite)),
|
|
55
|
+
folders: folder.folders.map(subFolder => this.addFolderHash(subFolder))
|
|
56
|
+
};
|
|
57
|
+
}
|
|
58
|
+
|
|
59
|
+
/**
|
|
60
|
+
* Scan directory recursively and build folder structure
|
|
61
|
+
* @param {string} directoryPath - Path to scan
|
|
62
|
+
* @returns {Object} Folder structure with test suites and hashes
|
|
63
|
+
*/
|
|
64
|
+
async scanDirectory(directoryPath) {
|
|
65
|
+
const absolutePath = path.resolve(directoryPath);
|
|
66
|
+
|
|
67
|
+
if (!fs.existsSync(absolutePath)) {
|
|
68
|
+
throw new Error(`Directory does not exist: ${directoryPath}`);
|
|
69
|
+
}
|
|
70
|
+
|
|
71
|
+
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
72
|
+
throw new Error(`Path is not a directory: ${directoryPath}`);
|
|
73
|
+
}
|
|
74
|
+
|
|
75
|
+
const structure = this.buildFolderStructure(absolutePath, directoryPath);
|
|
76
|
+
return this.addFolderHash(structure);
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Build folder structure recursively
|
|
81
|
+
* @param {string} absolutePath - Absolute path for file operations
|
|
82
|
+
* @param {string} relativePath - Relative path for output
|
|
83
|
+
* @returns {Object} Folder structure
|
|
84
|
+
*/
|
|
85
|
+
buildFolderStructure(absolutePath, relativePath) {
|
|
86
|
+
const folderName = path.basename(absolutePath);
|
|
87
|
+
const items = fs.readdirSync(absolutePath);
|
|
88
|
+
|
|
89
|
+
const structure = {
|
|
90
|
+
name: folderName,
|
|
91
|
+
path: relativePath,
|
|
92
|
+
test_suites: [],
|
|
93
|
+
folders: []
|
|
94
|
+
};
|
|
95
|
+
|
|
96
|
+
for (const item of items) {
|
|
97
|
+
const itemPath = path.join(absolutePath, item);
|
|
98
|
+
const itemRelativePath = path.join(relativePath, item);
|
|
99
|
+
const stats = fs.statSync(itemPath);
|
|
100
|
+
|
|
101
|
+
if (stats.isDirectory()) {
|
|
102
|
+
// Recursively process subdirectories
|
|
103
|
+
const subFolder = this.buildFolderStructure(itemPath, itemRelativePath);
|
|
104
|
+
structure.folders.push(subFolder);
|
|
105
|
+
} else if (this.isGherkinFile(item)) {
|
|
106
|
+
// Parse gherkin files
|
|
107
|
+
try {
|
|
108
|
+
const testSuite = this.parseGherkinFile(itemPath, itemRelativePath);
|
|
109
|
+
structure.test_suites.push(testSuite);
|
|
110
|
+
} catch (error) {
|
|
111
|
+
console.warn(`Warning: Failed to parse ${itemPath}: ${error.message}`);
|
|
112
|
+
}
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
return structure;
|
|
117
|
+
}
|
|
118
|
+
|
|
119
|
+
/**
|
|
120
|
+
* Check if file is a gherkin file
|
|
121
|
+
* @param {string} filename - Filename to check
|
|
122
|
+
* @returns {boolean} True if gherkin file
|
|
123
|
+
*/
|
|
124
|
+
isGherkinFile(filename) {
|
|
125
|
+
return filename.endsWith('.feature');
|
|
126
|
+
}
|
|
127
|
+
|
|
128
|
+
/**
|
|
129
|
+
* Parse a gherkin file and format output
|
|
130
|
+
* @param {string} filePath - Path to gherkin file
|
|
131
|
+
* @param {string} relativePath - Relative path for output
|
|
132
|
+
* @returns {Object} Formatted test suite
|
|
133
|
+
*/
|
|
134
|
+
parseGherkinFile(filePath, relativePath) {
|
|
135
|
+
const parsed = this.parser.parse(filePath);
|
|
136
|
+
|
|
137
|
+
return {
|
|
138
|
+
name: parsed.name,
|
|
139
|
+
type: parsed.type,
|
|
140
|
+
tags: parsed.tags,
|
|
141
|
+
beforeEach: parsed.beforeEach,
|
|
142
|
+
path: relativePath,
|
|
143
|
+
test_cases: parsed.cases || [] // Map 'cases' to 'test_cases' for consistency
|
|
144
|
+
};
|
|
145
|
+
}
|
|
146
|
+
}
|
|
147
|
+
|
|
148
|
+
module.exports = { ManualSyncHelper };
|
|
@@ -0,0 +1,64 @@
|
|
|
1
|
+
const fs = require('fs');
|
|
2
|
+
const path = require('path');
|
|
3
|
+
const logger = require('../utils/logger');
|
|
4
|
+
const { BaseTarget } = require('./base.target');
|
|
5
|
+
const context = require('../utils/context.utils');
|
|
6
|
+
|
|
7
|
+
const DEFAULT_INPUTS = {
|
|
8
|
+
output_file: process.env.GITHUB_OUTPUT, // Path to output file, defaults to GITHUB_OUTPUT env var
|
|
9
|
+
key: 'testbeats' // Key name for the output
|
|
10
|
+
};
|
|
11
|
+
|
|
12
|
+
const default_options = {
|
|
13
|
+
condition: 'passOrFail'
|
|
14
|
+
};
|
|
15
|
+
|
|
16
|
+
class GitHubOutputTarget extends BaseTarget {
|
|
17
|
+
constructor({ target }) {
|
|
18
|
+
super({ target });
|
|
19
|
+
}
|
|
20
|
+
|
|
21
|
+
async run({ result, target }) {
|
|
22
|
+
this.result = result;
|
|
23
|
+
this.setTargetInputs(target);
|
|
24
|
+
|
|
25
|
+
logger.info(`š Writing results to GitHub Actions outputs...`);
|
|
26
|
+
return await this.writeToGitHubOutput({ target, result });
|
|
27
|
+
}
|
|
28
|
+
|
|
29
|
+
setTargetInputs(target) {
|
|
30
|
+
target.inputs = Object.assign({}, DEFAULT_INPUTS, target.inputs);
|
|
31
|
+
}
|
|
32
|
+
|
|
33
|
+
async writeToGitHubOutput({ target, result }) {
|
|
34
|
+
const outputFile = target.inputs.output_file || process.env.GITHUB_OUTPUT;
|
|
35
|
+
|
|
36
|
+
if (!outputFile) {
|
|
37
|
+
throw new Error('GitHub output file path is required. Set GITHUB_OUTPUT environment variable or provide output_file in target inputs.');
|
|
38
|
+
}
|
|
39
|
+
|
|
40
|
+
// Ensure the directory exists
|
|
41
|
+
const outputDir = path.dirname(outputFile);
|
|
42
|
+
if (!fs.existsSync(outputDir)) {
|
|
43
|
+
fs.mkdirSync(outputDir, { recursive: true });
|
|
44
|
+
}
|
|
45
|
+
|
|
46
|
+
const lines = []
|
|
47
|
+
lines.push(`${target.inputs.key}_results=${JSON.stringify(result)}`)
|
|
48
|
+
lines.push(`${target.inputs.key}_stores=${JSON.stringify(context.stores)}`)
|
|
49
|
+
const outputContent = lines.join('\n');
|
|
50
|
+
|
|
51
|
+
fs.appendFileSync(outputFile, outputContent);
|
|
52
|
+
|
|
53
|
+
logger.info(`ā
Successfully wrote results to ${outputFile}`);
|
|
54
|
+
return { success: true, key: target.inputs.key };
|
|
55
|
+
}
|
|
56
|
+
|
|
57
|
+
async handleErrors({ target, errors }) {
|
|
58
|
+
logger.error('GitHub Output target errors:', errors);
|
|
59
|
+
}
|
|
60
|
+
}
|
|
61
|
+
|
|
62
|
+
module.exports = {
|
|
63
|
+
GitHubOutputTarget
|
|
64
|
+
};
|
package/src/targets/index.js
CHANGED
|
@@ -2,6 +2,7 @@ const teams = require('./teams');
|
|
|
2
2
|
const slack = require('./slack');
|
|
3
3
|
const chat = require('./chat');
|
|
4
4
|
const { GitHubTarget} = require('./github.target');
|
|
5
|
+
const { GitHubOutputTarget } = require('./github-output.target');
|
|
5
6
|
const { CustomTarget } = require('./custom.target');
|
|
6
7
|
const { DelayTarget } = require('./delay.target');
|
|
7
8
|
const { HttpTarget } = require('./http.target');
|
|
@@ -19,6 +20,8 @@ function getTargetRunner(target) {
|
|
|
19
20
|
return chat;
|
|
20
21
|
case TARGET.GITHUB:
|
|
21
22
|
return new GitHubTarget({ target });
|
|
23
|
+
case TARGET.GITHUB_OUTPUT:
|
|
24
|
+
return new GitHubOutputTarget({ target });
|
|
22
25
|
case TARGET.CUSTOM:
|
|
23
26
|
return new CustomTarget({ target });
|
|
24
27
|
case TARGET.DELAY:
|