testbeats 2.4.2 → 2.5.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/package.json +1 -1
- package/src/beats/beats.js +0 -16
- package/src/commands/base.command.js +5 -1
- package/src/commands/manual-sync.command.js +32 -269
- package/src/commands/publish.command.js +3 -24
- package/src/index.d.ts +2 -0
- package/src/manual/comparator.js +71 -0
- package/src/manual/factory.js +60 -0
- package/src/manual/hasher.js +93 -0
- package/src/manual/index.js +18 -0
- package/src/manual/orchestrator.js +124 -0
- package/src/manual/parsers/base.js +10 -32
- package/src/manual/parsers/gherkin.js +31 -19
- package/src/manual/project-resolver.js +33 -0
- package/src/manual/scanner.js +127 -0
- package/src/manual/synchronizer.js +329 -0
- package/src/utils/config.builder.js +91 -16
- package/src/manual/sync.helper.js +0 -193
|
@@ -0,0 +1,329 @@
|
|
|
1
|
+
const logger = require('../utils/logger');
|
|
2
|
+
|
|
3
|
+
/**
|
|
4
|
+
* Result of a synchronization operation
|
|
5
|
+
* @typedef {Object} SyncResult
|
|
6
|
+
* @property {boolean} success - Overall success status
|
|
7
|
+
* @property {number} foldersProcessed - Number of folders processed
|
|
8
|
+
* @property {number} testSuitesProcessed - Number of test suites processed
|
|
9
|
+
* @property {Array<{type: string, name: string, error: string}>} errors - List of errors encountered
|
|
10
|
+
*/
|
|
11
|
+
|
|
12
|
+
/**
|
|
13
|
+
* Synchronizes manual test resources with the server
|
|
14
|
+
*/
|
|
15
|
+
class ManualTestSynchronizer {
|
|
16
|
+
/**
|
|
17
|
+
* @param {import('../beats/beats.api').BeatsApi} beatsApi
|
|
18
|
+
*/
|
|
19
|
+
constructor(beatsApi) {
|
|
20
|
+
this.beatsApi = beatsApi;
|
|
21
|
+
}
|
|
22
|
+
|
|
23
|
+
/**
|
|
24
|
+
* Sync the manual resources
|
|
25
|
+
* @param {import('../types').IManualTestFolder} structure - Local folder structure
|
|
26
|
+
* @param {import('../types').IManualSyncCompareResponse} compareResult - Comparison result from server
|
|
27
|
+
* @param {string} projectId - Project ID
|
|
28
|
+
* @returns {Promise<SyncResult>} Synchronization result
|
|
29
|
+
*/
|
|
30
|
+
async sync(structure, compareResult, projectId) {
|
|
31
|
+
const errors = [];
|
|
32
|
+
let foldersProcessed = 0;
|
|
33
|
+
let testSuitesProcessed = 0;
|
|
34
|
+
|
|
35
|
+
const enrichedStructure = this.#mergeStructure(structure, compareResult);
|
|
36
|
+
const syncResult = await this.#syncFolders(enrichedStructure.folders, projectId, errors);
|
|
37
|
+
foldersProcessed = syncResult.foldersProcessed;
|
|
38
|
+
testSuitesProcessed = syncResult.testSuitesProcessed;
|
|
39
|
+
|
|
40
|
+
return {
|
|
41
|
+
success: errors.length === 0,
|
|
42
|
+
foldersProcessed,
|
|
43
|
+
testSuitesProcessed,
|
|
44
|
+
errors
|
|
45
|
+
};
|
|
46
|
+
}
|
|
47
|
+
|
|
48
|
+
/**
|
|
49
|
+
* Merge the structure with the compare result (returns new structure)
|
|
50
|
+
* @param {import('../types').IManualTestFolder} structure
|
|
51
|
+
* @param {import('../types').IManualSyncCompareResponse} compareResult
|
|
52
|
+
* @returns {import('../types').IManualTestFolder} Enriched structure
|
|
53
|
+
*/
|
|
54
|
+
#mergeStructure(structure, compareResult) {
|
|
55
|
+
const enrichedFolders = [];
|
|
56
|
+
|
|
57
|
+
for (const localFolder of structure.folders) {
|
|
58
|
+
const serverFolder = compareResult.folders.find(f => f.name === localFolder.name);
|
|
59
|
+
if (serverFolder) {
|
|
60
|
+
enrichedFolders.push(this.#mergeFolder(localFolder, serverFolder));
|
|
61
|
+
} else {
|
|
62
|
+
enrichedFolders.push(localFolder);
|
|
63
|
+
}
|
|
64
|
+
}
|
|
65
|
+
|
|
66
|
+
for (const serverFolder of compareResult.folders) {
|
|
67
|
+
const localFolder = structure.folders.find(f => f.name === serverFolder.name);
|
|
68
|
+
if (!localFolder) {
|
|
69
|
+
enrichedFolders.push(this.#convertServerFolderToLocal(serverFolder));
|
|
70
|
+
}
|
|
71
|
+
}
|
|
72
|
+
|
|
73
|
+
return {
|
|
74
|
+
...structure,
|
|
75
|
+
folders: enrichedFolders
|
|
76
|
+
};
|
|
77
|
+
}
|
|
78
|
+
|
|
79
|
+
/**
|
|
80
|
+
* Merge server folder metadata into local folder (returns new folder)
|
|
81
|
+
* @param {import('../types').IManualTestFolder} localFolder - Local folder with full data
|
|
82
|
+
* @param {import('../types').IManualSyncCompareResponseFolder} serverFolder - Server folder with metadata
|
|
83
|
+
* @returns {import('../types').IManualTestFolder} Merged folder
|
|
84
|
+
*/
|
|
85
|
+
#mergeFolder(localFolder, serverFolder) {
|
|
86
|
+
const enrichedTestSuites = [];
|
|
87
|
+
const enrichedSubFolders = [];
|
|
88
|
+
const serverTestSuites = serverFolder.test_suites || [];
|
|
89
|
+
const serverFolders = serverFolder.folders || [];
|
|
90
|
+
|
|
91
|
+
for (const localSuite of localFolder.test_suites) {
|
|
92
|
+
const serverSuite = serverTestSuites.find(s => s.name === localSuite.name);
|
|
93
|
+
if (serverSuite) {
|
|
94
|
+
enrichedTestSuites.push(this.#mergeTestSuite(localSuite, serverSuite));
|
|
95
|
+
} else {
|
|
96
|
+
enrichedTestSuites.push(localSuite);
|
|
97
|
+
}
|
|
98
|
+
}
|
|
99
|
+
|
|
100
|
+
for (const serverSuite of serverTestSuites) {
|
|
101
|
+
const localSuite = localFolder.test_suites.find(s => s.name === serverSuite.name);
|
|
102
|
+
if (!localSuite) {
|
|
103
|
+
enrichedTestSuites.push(this.#convertServerSuiteToLocal(serverSuite));
|
|
104
|
+
}
|
|
105
|
+
}
|
|
106
|
+
|
|
107
|
+
for (const localSubFolder of localFolder.folders) {
|
|
108
|
+
const serverSubFolder = serverFolders.find(f => f.name === localSubFolder.name);
|
|
109
|
+
if (serverSubFolder) {
|
|
110
|
+
enrichedSubFolders.push(this.#mergeFolder(localSubFolder, serverSubFolder));
|
|
111
|
+
} else {
|
|
112
|
+
enrichedSubFolders.push(localSubFolder);
|
|
113
|
+
}
|
|
114
|
+
}
|
|
115
|
+
|
|
116
|
+
for (const serverSubFolder of serverFolders) {
|
|
117
|
+
const localSubFolder = localFolder.folders.find(f => f.name === serverSubFolder.name);
|
|
118
|
+
if (!localSubFolder) {
|
|
119
|
+
enrichedSubFolders.push(this.#convertServerFolderToLocal(serverSubFolder));
|
|
120
|
+
}
|
|
121
|
+
}
|
|
122
|
+
|
|
123
|
+
return {
|
|
124
|
+
...localFolder,
|
|
125
|
+
id: serverFolder.id,
|
|
126
|
+
type: serverFolder.type,
|
|
127
|
+
test_suites: enrichedTestSuites,
|
|
128
|
+
folders: enrichedSubFolders
|
|
129
|
+
};
|
|
130
|
+
}
|
|
131
|
+
|
|
132
|
+
/**
|
|
133
|
+
* Merge server test suite metadata into local test suite (returns new suite)
|
|
134
|
+
* @param {import('../types').IManualTestSuite} localSuite - Local test suite with full data
|
|
135
|
+
* @param {import('../types').IManualSyncCompareResponseTestSuite} serverSuite - Server suite with metadata
|
|
136
|
+
* @returns {import('../types').IManualTestSuite} Merged test suite
|
|
137
|
+
*/
|
|
138
|
+
#mergeTestSuite(localSuite, serverSuite) {
|
|
139
|
+
return {
|
|
140
|
+
...localSuite,
|
|
141
|
+
id: serverSuite.id,
|
|
142
|
+
type: serverSuite.type
|
|
143
|
+
};
|
|
144
|
+
}
|
|
145
|
+
|
|
146
|
+
/**
|
|
147
|
+
* Convert server folder to local folder structure (for delete operations)
|
|
148
|
+
* @param {import('../types').IManualSyncCompareResponseFolder} serverFolder - Server folder
|
|
149
|
+
* @returns {import('../types').IManualTestFolder} Local folder structure
|
|
150
|
+
*/
|
|
151
|
+
#convertServerFolderToLocal(serverFolder) {
|
|
152
|
+
return {
|
|
153
|
+
name: serverFolder.name,
|
|
154
|
+
path: '',
|
|
155
|
+
hash: serverFolder.hash,
|
|
156
|
+
id: serverFolder.id,
|
|
157
|
+
type: serverFolder.type,
|
|
158
|
+
test_suites: serverFolder.test_suites?.map(s => this.#convertServerSuiteToLocal(s)) || [],
|
|
159
|
+
folders: serverFolder.folders?.map(f => this.#convertServerFolderToLocal(f)) || []
|
|
160
|
+
};
|
|
161
|
+
}
|
|
162
|
+
|
|
163
|
+
/**
|
|
164
|
+
* Convert server test suite to local test suite structure (for delete operations)
|
|
165
|
+
* @param {import('../types').IManualSyncCompareResponseTestSuite} serverSuite - Server suite
|
|
166
|
+
* @returns {import('../types').IManualTestSuite} Local test suite structure
|
|
167
|
+
*/
|
|
168
|
+
#convertServerSuiteToLocal(serverSuite) {
|
|
169
|
+
return {
|
|
170
|
+
name: serverSuite.name,
|
|
171
|
+
tags: [],
|
|
172
|
+
before_each: [],
|
|
173
|
+
test_cases: [],
|
|
174
|
+
hash: serverSuite.hash,
|
|
175
|
+
id: serverSuite.id,
|
|
176
|
+
type: serverSuite.type
|
|
177
|
+
};
|
|
178
|
+
}
|
|
179
|
+
|
|
180
|
+
/**
|
|
181
|
+
* Sync folders recursively
|
|
182
|
+
* @param {Array<import('../types').IManualTestFolder>} folders
|
|
183
|
+
* @param {string} projectId
|
|
184
|
+
* @param {Array} errors - Array to collect errors
|
|
185
|
+
* @returns {Promise<{foldersProcessed: number, testSuitesProcessed: number}>}
|
|
186
|
+
*/
|
|
187
|
+
async #syncFolders(folders, projectId, errors) {
|
|
188
|
+
let foldersProcessed = 0;
|
|
189
|
+
let testSuitesProcessed = 0;
|
|
190
|
+
|
|
191
|
+
if (folders.length === 0) {
|
|
192
|
+
return { foldersProcessed, testSuitesProcessed };
|
|
193
|
+
}
|
|
194
|
+
|
|
195
|
+
const payload = {
|
|
196
|
+
project_id: projectId,
|
|
197
|
+
folders: folders.map(folder => ({
|
|
198
|
+
type: folder.type,
|
|
199
|
+
id: folder.id,
|
|
200
|
+
name: folder.name,
|
|
201
|
+
hash: folder.hash,
|
|
202
|
+
parent_folder_id: folder.parent_folder_id,
|
|
203
|
+
})),
|
|
204
|
+
};
|
|
205
|
+
|
|
206
|
+
try {
|
|
207
|
+
const response = await this.beatsApi.syncManualFolders(payload);
|
|
208
|
+
const results = response.results;
|
|
209
|
+
|
|
210
|
+
for (let i = 0; i < results.length; i++) {
|
|
211
|
+
const result = results[i];
|
|
212
|
+
const folder = folders[i];
|
|
213
|
+
|
|
214
|
+
if (result.success) {
|
|
215
|
+
logger.info(`✅ Folder '${result.name}' synced successfully`);
|
|
216
|
+
foldersProcessed++;
|
|
217
|
+
|
|
218
|
+
folder.id = folder.id || result.id;
|
|
219
|
+
|
|
220
|
+
if (folder.test_suites && folder.test_suites.length > 0) {
|
|
221
|
+
const testSuitesWithFolderId = folder.test_suites.map(ts => ({
|
|
222
|
+
...ts,
|
|
223
|
+
folder_id: folder.id
|
|
224
|
+
}));
|
|
225
|
+
|
|
226
|
+
const suiteResult = await this.#syncTestSuites(testSuitesWithFolderId, projectId, errors);
|
|
227
|
+
testSuitesProcessed += suiteResult.testSuitesProcessed;
|
|
228
|
+
}
|
|
229
|
+
|
|
230
|
+
if (folder.folders && folder.folders.length > 0) {
|
|
231
|
+
const subfoldersWithParent = folder.folders.map(sf => ({
|
|
232
|
+
...sf,
|
|
233
|
+
parent_folder_id: folder.id
|
|
234
|
+
}));
|
|
235
|
+
|
|
236
|
+
const subResult = await this.#syncFolders(subfoldersWithParent, projectId, errors);
|
|
237
|
+
foldersProcessed += subResult.foldersProcessed;
|
|
238
|
+
testSuitesProcessed += subResult.testSuitesProcessed;
|
|
239
|
+
}
|
|
240
|
+
} else {
|
|
241
|
+
const error = {
|
|
242
|
+
type: 'folder',
|
|
243
|
+
name: result.name,
|
|
244
|
+
error: result.error || 'Unknown error'
|
|
245
|
+
};
|
|
246
|
+
errors.push(error);
|
|
247
|
+
logger.error(`❌ Folder '${result.name}' failed to sync: ${error.error}`);
|
|
248
|
+
}
|
|
249
|
+
}
|
|
250
|
+
} catch (error) {
|
|
251
|
+
const syncError = {
|
|
252
|
+
type: 'folder_batch',
|
|
253
|
+
name: 'batch',
|
|
254
|
+
error: error.message
|
|
255
|
+
};
|
|
256
|
+
errors.push(syncError);
|
|
257
|
+
logger.error(`❌ Failed to sync folder batch: ${error.message}`);
|
|
258
|
+
}
|
|
259
|
+
|
|
260
|
+
return { foldersProcessed, testSuitesProcessed };
|
|
261
|
+
}
|
|
262
|
+
|
|
263
|
+
/**
|
|
264
|
+
* Sync test suites
|
|
265
|
+
* @param {Array<import('../types').IManualTestSuite>} testSuites
|
|
266
|
+
* @param {string} projectId
|
|
267
|
+
* @param {Array} errors - Array to collect errors
|
|
268
|
+
* @returns {Promise<{testSuitesProcessed: number}>}
|
|
269
|
+
*/
|
|
270
|
+
async #syncTestSuites(testSuites, projectId, errors) {
|
|
271
|
+
let testSuitesProcessed = 0;
|
|
272
|
+
|
|
273
|
+
if (testSuites.length === 0) {
|
|
274
|
+
return { testSuitesProcessed };
|
|
275
|
+
}
|
|
276
|
+
|
|
277
|
+
const payload = {
|
|
278
|
+
project_id: projectId,
|
|
279
|
+
suites: testSuites.map(testSuite => ({
|
|
280
|
+
type: testSuite.type,
|
|
281
|
+
name: testSuite.name,
|
|
282
|
+
folder_id: testSuite.folder_id,
|
|
283
|
+
hash: testSuite.hash,
|
|
284
|
+
tags: testSuite.tags,
|
|
285
|
+
before_each: testSuite.before_each.map(step => step.name),
|
|
286
|
+
test_cases: testSuite.test_cases.map(tc => ({
|
|
287
|
+
name: tc.name,
|
|
288
|
+
type: tc.type,
|
|
289
|
+
tags: tc.tags,
|
|
290
|
+
steps: tc.steps.map(step => step.name),
|
|
291
|
+
hash: tc.hash,
|
|
292
|
+
})),
|
|
293
|
+
})),
|
|
294
|
+
};
|
|
295
|
+
|
|
296
|
+
try {
|
|
297
|
+
const response = await this.beatsApi.syncManualTestSuites(payload);
|
|
298
|
+
const results = response.results;
|
|
299
|
+
|
|
300
|
+
for (const result of results) {
|
|
301
|
+
if (result.success) {
|
|
302
|
+
logger.info(`✅ Test Suite '${result.name}' synced successfully`);
|
|
303
|
+
testSuitesProcessed++;
|
|
304
|
+
} else {
|
|
305
|
+
const error = {
|
|
306
|
+
type: 'test_suite',
|
|
307
|
+
name: result.name,
|
|
308
|
+
error: result.error || 'Unknown error'
|
|
309
|
+
};
|
|
310
|
+
errors.push(error);
|
|
311
|
+
logger.error(`❌ Test Suite '${result.name}' failed to sync: ${error.error}`);
|
|
312
|
+
}
|
|
313
|
+
}
|
|
314
|
+
} catch (error) {
|
|
315
|
+
const syncError = {
|
|
316
|
+
type: 'test_suite_batch',
|
|
317
|
+
name: 'batch',
|
|
318
|
+
error: error.message
|
|
319
|
+
};
|
|
320
|
+
errors.push(syncError);
|
|
321
|
+
logger.error(`❌ Failed to sync test suite batch: ${error.message}`);
|
|
322
|
+
}
|
|
323
|
+
|
|
324
|
+
return { testSuitesProcessed };
|
|
325
|
+
}
|
|
326
|
+
}
|
|
327
|
+
|
|
328
|
+
module.exports = { ManualTestSynchronizer };
|
|
329
|
+
|
|
@@ -1,47 +1,122 @@
|
|
|
1
1
|
const path = require('path');
|
|
2
2
|
const logger = require('./logger');
|
|
3
3
|
|
|
4
|
+
const DEFAULT_PROJECT = 'demo-project';
|
|
5
|
+
|
|
4
6
|
class ConfigBuilder {
|
|
5
7
|
|
|
6
8
|
/**
|
|
7
9
|
* @param {import('../index').CommandLineOptions} opts
|
|
10
|
+
* @param {import('../helpers/ci').ICIInfo} ci
|
|
11
|
+
* @param {Record<string, string | undefined>} env
|
|
8
12
|
*/
|
|
9
|
-
constructor(opts) {
|
|
13
|
+
constructor(opts, ci, env = process.env) {
|
|
10
14
|
this.opts = opts;
|
|
15
|
+
this.ci = ci;
|
|
16
|
+
this.env = env;
|
|
17
|
+
/** @type {import('../index').PublishConfig} */
|
|
18
|
+
this.config = {};
|
|
11
19
|
}
|
|
12
20
|
|
|
13
21
|
build() {
|
|
14
22
|
if (!this.opts) {
|
|
15
23
|
return;
|
|
16
24
|
}
|
|
17
|
-
|
|
18
|
-
|
|
25
|
+
logger.info('🏗 Building config...')
|
|
26
|
+
switch (typeof this.opts.config) {
|
|
27
|
+
case 'object':
|
|
28
|
+
this.#buildFromConfigFile();
|
|
29
|
+
break;
|
|
30
|
+
case 'string':
|
|
31
|
+
this.#buildFromConfigFilePath();
|
|
32
|
+
break;
|
|
33
|
+
default:
|
|
34
|
+
this.#buildFromCommandLineOptions();
|
|
35
|
+
break;
|
|
19
36
|
}
|
|
20
|
-
|
|
21
|
-
|
|
37
|
+
}
|
|
38
|
+
|
|
39
|
+
#buildFromConfigFile() {
|
|
40
|
+
this.config = this.opts.config;
|
|
41
|
+
this.#buildBeats();
|
|
42
|
+
}
|
|
43
|
+
|
|
44
|
+
#buildFromConfigFilePath() {
|
|
45
|
+
const cwd = process.cwd();
|
|
46
|
+
const file_path = path.join(cwd, this.opts.config);
|
|
47
|
+
try {
|
|
48
|
+
const config_json = require(file_path);
|
|
49
|
+
this.opts.config = config_json;
|
|
50
|
+
this.#buildFromConfigFile();
|
|
51
|
+
} catch (error) {
|
|
52
|
+
throw new Error(`Failed to read config file: '${file_path}' with error: '${error.message}'`);
|
|
22
53
|
}
|
|
54
|
+
}
|
|
23
55
|
|
|
24
|
-
|
|
25
|
-
|
|
56
|
+
|
|
57
|
+
|
|
58
|
+
#buildFromCommandLineOptions() {
|
|
59
|
+
this.opts.config = {};
|
|
60
|
+
this.config = this.opts.config;
|
|
26
61
|
this.#buildBeats();
|
|
27
62
|
this.#buildResults();
|
|
28
63
|
this.#buildTargets();
|
|
29
64
|
this.#buildExtensions();
|
|
65
|
+
}
|
|
30
66
|
|
|
31
|
-
|
|
67
|
+
#buildBeats() {
|
|
68
|
+
this.#setProject();
|
|
69
|
+
this.#setRun();
|
|
70
|
+
this.#setApiKey();
|
|
71
|
+
}
|
|
32
72
|
|
|
33
|
-
|
|
73
|
+
#setProject() {
|
|
74
|
+
if (this.opts.project) {
|
|
75
|
+
this.config.project = this.opts.project;
|
|
76
|
+
return;
|
|
77
|
+
}
|
|
78
|
+
if (this.env.TEST_BEATS_PROJECT || this.env.TESTBEATS_PROJECT) {
|
|
79
|
+
this.config.project = this.env.TEST_BEATS_PROJECT || this.env.TESTBEATS_PROJECT;
|
|
80
|
+
return;
|
|
81
|
+
}
|
|
82
|
+
if (this.config.project) {
|
|
83
|
+
return;
|
|
84
|
+
}
|
|
85
|
+
if (this.ci && this.ci.repository_name) {
|
|
86
|
+
this.config.project = this.ci.repository_name;
|
|
87
|
+
return;
|
|
88
|
+
}
|
|
89
|
+
|
|
90
|
+
this.config.project = DEFAULT_PROJECT;
|
|
34
91
|
}
|
|
35
92
|
|
|
36
|
-
#
|
|
37
|
-
|
|
38
|
-
|
|
93
|
+
#setRun() {
|
|
94
|
+
if (this.opts.run) {
|
|
95
|
+
this.config.run = this.opts.run;
|
|
96
|
+
return;
|
|
97
|
+
}
|
|
98
|
+
if (this.env.TEST_BEATS_RUN || this.env.TESTBEATS_RUN) {
|
|
99
|
+
this.config.run = this.env.TEST_BEATS_RUN || this.env.TESTBEATS_RUN;
|
|
100
|
+
return;
|
|
101
|
+
}
|
|
102
|
+
if (this.config.run) {
|
|
103
|
+
return;
|
|
104
|
+
}
|
|
105
|
+
if (this.ci && this.ci.build_name) {
|
|
106
|
+
this.config.run = this.ci.build_name;
|
|
107
|
+
return;
|
|
108
|
+
}
|
|
39
109
|
}
|
|
40
110
|
|
|
41
|
-
#
|
|
42
|
-
|
|
43
|
-
|
|
44
|
-
|
|
111
|
+
#setApiKey() {
|
|
112
|
+
if (this.opts['api-key']) {
|
|
113
|
+
this.config.api_key = this.opts['api-key'];
|
|
114
|
+
return;
|
|
115
|
+
}
|
|
116
|
+
if (this.env.TEST_BEATS_API_KEY || this.env.TESTBEATS_API_KEY) {
|
|
117
|
+
this.config.api_key = this.env.TEST_BEATS_API_KEY || this.env.TESTBEATS_API_KEY;
|
|
118
|
+
return;
|
|
119
|
+
}
|
|
45
120
|
}
|
|
46
121
|
|
|
47
122
|
#buildResults() {
|
|
@@ -1,193 +0,0 @@
|
|
|
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
|
-
* Hash a folder
|
|
61
|
-
* @param {import('../types').IManualTestFolder} folder
|
|
62
|
-
* @returns {string} Hash string
|
|
63
|
-
*/
|
|
64
|
-
hashFolder(folder) {
|
|
65
|
-
return this.generateHash({
|
|
66
|
-
name: folder.name,
|
|
67
|
-
path: folder.path,
|
|
68
|
-
test_suites: folder.test_suites.map(testSuite => testSuite.hash),
|
|
69
|
-
folders: folder.folders.map(subFolder => subFolder.hash)
|
|
70
|
-
});
|
|
71
|
-
}
|
|
72
|
-
|
|
73
|
-
/**
|
|
74
|
-
* Hash a test suite
|
|
75
|
-
* @param {import('../types').IManualTestSuite} testSuite
|
|
76
|
-
* @returns {string} Hash string
|
|
77
|
-
*/
|
|
78
|
-
hashTestSuite(testSuite) {
|
|
79
|
-
return this.generateHash({
|
|
80
|
-
name: testSuite.name,
|
|
81
|
-
tags: testSuite.tags,
|
|
82
|
-
before_each: testSuite.before_each,
|
|
83
|
-
test_cases: testSuite.test_cases.map(testCase => testCase.hash)
|
|
84
|
-
});
|
|
85
|
-
}
|
|
86
|
-
|
|
87
|
-
|
|
88
|
-
/**
|
|
89
|
-
* Scan directory recursively and build folder structure
|
|
90
|
-
* @param {string} directoryPath - Path to scan
|
|
91
|
-
* @returns {Object} Folder structure with test suites and hashes
|
|
92
|
-
*/
|
|
93
|
-
async scanDirectory(directoryPath) {
|
|
94
|
-
const absolutePath = path.resolve(directoryPath);
|
|
95
|
-
|
|
96
|
-
if (!fs.existsSync(absolutePath)) {
|
|
97
|
-
throw new Error(`Directory does not exist: ${directoryPath}`);
|
|
98
|
-
}
|
|
99
|
-
|
|
100
|
-
if (!fs.statSync(absolutePath).isDirectory()) {
|
|
101
|
-
throw new Error(`Path is not a directory: ${directoryPath}`);
|
|
102
|
-
}
|
|
103
|
-
|
|
104
|
-
return this.buildFolderStructure(absolutePath, directoryPath);
|
|
105
|
-
}
|
|
106
|
-
|
|
107
|
-
/**
|
|
108
|
-
* Build folder structure recursively
|
|
109
|
-
* @param {string} absolutePath - Absolute path for file operations
|
|
110
|
-
* @param {string} relativePath - Relative path for output
|
|
111
|
-
* @returns {import('../types').IManualTestFolder} Folder structure
|
|
112
|
-
*/
|
|
113
|
-
buildFolderStructure(absolutePath, relativePath) {
|
|
114
|
-
const folderName = path.basename(absolutePath);
|
|
115
|
-
const items = fs.readdirSync(absolutePath);
|
|
116
|
-
|
|
117
|
-
/**
|
|
118
|
-
* @type {import('../types').IManualTestFolder}
|
|
119
|
-
*/
|
|
120
|
-
const structure = {
|
|
121
|
-
name: folderName,
|
|
122
|
-
path: relativePath,
|
|
123
|
-
test_suites: [],
|
|
124
|
-
folders: []
|
|
125
|
-
};
|
|
126
|
-
|
|
127
|
-
for (const item of items) {
|
|
128
|
-
const itemPath = path.join(absolutePath, item);
|
|
129
|
-
const itemRelativePath = path.join(relativePath, item);
|
|
130
|
-
const stats = fs.statSync(itemPath);
|
|
131
|
-
|
|
132
|
-
if (stats.isDirectory()) {
|
|
133
|
-
// Recursively process subdirectories
|
|
134
|
-
const subFolder = this.buildFolderStructure(itemPath, itemRelativePath);
|
|
135
|
-
structure.folders.push(subFolder);
|
|
136
|
-
} else if (this.isGherkinFile(item)) {
|
|
137
|
-
// Parse gherkin files
|
|
138
|
-
try {
|
|
139
|
-
const testSuite = this.parseGherkinFile(itemPath, itemRelativePath);
|
|
140
|
-
testSuite.hash = this.hashTestSuite(testSuite);
|
|
141
|
-
structure.test_suites.push(testSuite);
|
|
142
|
-
} catch (error) {
|
|
143
|
-
console.warn(`Warning: Failed to parse ${itemPath}: ${error.message}`);
|
|
144
|
-
}
|
|
145
|
-
}
|
|
146
|
-
}
|
|
147
|
-
structure.hash = this.hashFolder(structure);
|
|
148
|
-
if (structure.test_suites.length > 0) {
|
|
149
|
-
const defaultFolder = {
|
|
150
|
-
name: 'default',
|
|
151
|
-
path: 'default',
|
|
152
|
-
hash: '',
|
|
153
|
-
test_suites: structure.test_suites,
|
|
154
|
-
folders: []
|
|
155
|
-
};
|
|
156
|
-
defaultFolder.hash = this.hashFolder(defaultFolder);
|
|
157
|
-
structure.folders.push(defaultFolder);
|
|
158
|
-
structure.test_suites = [];
|
|
159
|
-
}
|
|
160
|
-
|
|
161
|
-
return structure;
|
|
162
|
-
}
|
|
163
|
-
|
|
164
|
-
/**
|
|
165
|
-
* Check if file is a gherkin file
|
|
166
|
-
* @param {string} filename - Filename to check
|
|
167
|
-
* @returns {boolean} True if gherkin file
|
|
168
|
-
*/
|
|
169
|
-
isGherkinFile(filename) {
|
|
170
|
-
return filename.endsWith('.feature');
|
|
171
|
-
}
|
|
172
|
-
|
|
173
|
-
/**
|
|
174
|
-
* Parse a gherkin file and format output
|
|
175
|
-
* @param {string} filePath - Path to gherkin file
|
|
176
|
-
* @param {string} relativePath - Relative path for output
|
|
177
|
-
* @returns {import('../types').IManualTestSuite} Formatted test suite
|
|
178
|
-
*/
|
|
179
|
-
parseGherkinFile(filePath, relativePath) {
|
|
180
|
-
const parsed = this.parser.parse(filePath);
|
|
181
|
-
|
|
182
|
-
return {
|
|
183
|
-
name: parsed.name,
|
|
184
|
-
type: parsed.type,
|
|
185
|
-
tags: parsed.tags,
|
|
186
|
-
before_each: parsed.before_each,
|
|
187
|
-
path: relativePath,
|
|
188
|
-
test_cases: parsed.test_cases || [] // Map 'cases' to 'test_cases' for consistency
|
|
189
|
-
};
|
|
190
|
-
}
|
|
191
|
-
}
|
|
192
|
-
|
|
193
|
-
module.exports = { ManualSyncHelper };
|