@elisra-devops/docgen-data-provider 1.18.0 → 1.19.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/bin/helpers/tfs.d.ts +15 -0
- package/bin/helpers/tfs.js +148 -130
- package/bin/helpers/tfs.js.map +1 -1
- package/bin/modules/TestDataProvider.d.ts +8 -1
- package/bin/modules/TestDataProvider.js +165 -83
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/package.json +1 -1
- package/src/helpers/tfs.ts +166 -131
- package/src/modules/TestDataProvider.ts +228 -126
|
@@ -1,21 +1,18 @@
|
|
|
1
1
|
import { TFSServices } from '../helpers/tfs';
|
|
2
|
-
import {
|
|
3
|
-
import {
|
|
4
|
-
import { Query, TestSteps, createBugRelation, createRequirementRelation } from '../models/tfs-data';
|
|
5
|
-
import { QueryType } from '../models/tfs-data';
|
|
6
|
-
import { QueryAllTypes } from '../models/tfs-data';
|
|
7
|
-
import { Column } from '../models/tfs-data';
|
|
8
|
-
import { value } from '../models/tfs-data';
|
|
2
|
+
import { Helper, suiteData } from '../helpers/helper';
|
|
3
|
+
import { TestSteps, createRequirementRelation } from '../models/tfs-data';
|
|
9
4
|
import { TestCase } from '../models/tfs-data';
|
|
10
5
|
import * as xml2js from 'xml2js';
|
|
11
|
-
|
|
12
6
|
import logger from '../utils/logger';
|
|
13
7
|
import TestStepParserHelper from '../utils/testStepParserHelper';
|
|
8
|
+
const pLimit = require('p-limit');
|
|
14
9
|
|
|
15
10
|
export default class TestDataProvider {
|
|
16
11
|
orgUrl: string = '';
|
|
17
12
|
token: string = '';
|
|
18
13
|
private testStepParserHelper: TestStepParserHelper;
|
|
14
|
+
private cache = new Map<string, any>(); // Cache for API responses
|
|
15
|
+
private limit = pLimit(10);
|
|
19
16
|
|
|
20
17
|
constructor(orgUrl: string, token: string) {
|
|
21
18
|
this.orgUrl = orgUrl;
|
|
@@ -23,26 +20,47 @@ export default class TestDataProvider {
|
|
|
23
20
|
this.testStepParserHelper = new TestStepParserHelper(orgUrl, token);
|
|
24
21
|
}
|
|
25
22
|
|
|
23
|
+
private async fetchWithCache(url: string, ttlMs = 60000): Promise<any> {
|
|
24
|
+
if (this.cache.has(url)) {
|
|
25
|
+
const cached = this.cache.get(url);
|
|
26
|
+
if (cached.timestamp + ttlMs > Date.now()) {
|
|
27
|
+
return cached.data;
|
|
28
|
+
}
|
|
29
|
+
}
|
|
30
|
+
|
|
31
|
+
try {
|
|
32
|
+
const result = await TFSServices.getItemContent(url, this.token);
|
|
33
|
+
|
|
34
|
+
this.cache.set(url, {
|
|
35
|
+
data: result,
|
|
36
|
+
timestamp: Date.now(),
|
|
37
|
+
});
|
|
38
|
+
|
|
39
|
+
return result;
|
|
40
|
+
} catch (error: any) {
|
|
41
|
+
logger.error(`Error fetching ${url}: ${error.message}`);
|
|
42
|
+
throw error;
|
|
43
|
+
}
|
|
44
|
+
}
|
|
45
|
+
|
|
26
46
|
async GetTestSuiteByTestCase(testCaseId: string): Promise<any> {
|
|
27
47
|
let url = `${this.orgUrl}/_apis/testplan/suites?testCaseId=${testCaseId}`;
|
|
28
|
-
|
|
29
|
-
return testCaseData;
|
|
48
|
+
return this.fetchWithCache(url);
|
|
30
49
|
}
|
|
31
50
|
|
|
32
|
-
//get all test plans in the project
|
|
33
51
|
async GetTestPlans(project: string): Promise<string> {
|
|
34
52
|
let testPlanUrl: string = `${this.orgUrl}${project}/_apis/test/plans`;
|
|
35
|
-
return
|
|
53
|
+
return this.fetchWithCache(testPlanUrl);
|
|
36
54
|
}
|
|
37
|
-
//async get data test
|
|
38
55
|
|
|
39
|
-
// get all test suits in projct test plan
|
|
40
56
|
async GetTestSuites(project: string, planId: string): Promise<any> {
|
|
41
57
|
let testsuitesUrl: string = this.orgUrl + project + '/_apis/test/Plans/' + planId + '/suites';
|
|
42
58
|
try {
|
|
43
|
-
|
|
44
|
-
|
|
45
|
-
|
|
59
|
+
return this.fetchWithCache(testsuitesUrl);
|
|
60
|
+
} catch (e) {
|
|
61
|
+
logger.error(`Failed to get test suites: ${e}`);
|
|
62
|
+
return null;
|
|
63
|
+
}
|
|
46
64
|
}
|
|
47
65
|
|
|
48
66
|
async GetTestSuitesForPlan(project: string, planid: string): Promise<any> {
|
|
@@ -54,20 +72,16 @@ export default class TestDataProvider {
|
|
|
54
72
|
}
|
|
55
73
|
let url =
|
|
56
74
|
this.orgUrl + '/' + project + '/_api/_testManagement/GetTestSuitesForPlan?__v=5&planId=' + planid;
|
|
57
|
-
|
|
58
|
-
return suites;
|
|
75
|
+
return this.fetchWithCache(url);
|
|
59
76
|
}
|
|
60
77
|
|
|
61
78
|
async GetTestSuitesByPlan(project: string, planId: string, recursive: boolean): Promise<any> {
|
|
62
79
|
let suiteId = Number(planId) + 1;
|
|
63
|
-
|
|
64
|
-
return suites;
|
|
80
|
+
return this.GetTestSuiteById(project, planId, suiteId.toString(), recursive);
|
|
65
81
|
}
|
|
66
|
-
//gets all testsuits recorsivly under test suite
|
|
67
82
|
|
|
68
83
|
async GetTestSuiteById(project: string, planId: string, suiteId: string, recursive: boolean): Promise<any> {
|
|
69
84
|
let testSuites = await this.GetTestSuitesForPlan(project, planId);
|
|
70
|
-
// GetTestSuites(project, planId);
|
|
71
85
|
Helper.suitList = [];
|
|
72
86
|
let dataSuites: any = Helper.findSuitesRecursive(
|
|
73
87
|
planId,
|
|
@@ -78,17 +92,14 @@ export default class TestDataProvider {
|
|
|
78
92
|
recursive
|
|
79
93
|
);
|
|
80
94
|
Helper.first = true;
|
|
81
|
-
// let levledSuites: any = Helper.buildSuiteslevel(dataSuites);
|
|
82
|
-
|
|
83
95
|
return dataSuites;
|
|
84
96
|
}
|
|
85
|
-
//gets all testcase under test suite acording to recursive flag
|
|
86
97
|
|
|
87
98
|
async GetTestCasesBySuites(
|
|
88
99
|
project: string,
|
|
89
100
|
planId: string,
|
|
90
101
|
suiteId: string,
|
|
91
|
-
|
|
102
|
+
recursive: boolean,
|
|
92
103
|
includeRequirements: boolean,
|
|
93
104
|
CustomerRequirementId: boolean,
|
|
94
105
|
stepResultDetailsMap?: Map<string, any>
|
|
@@ -100,23 +111,38 @@ export default class TestDataProvider {
|
|
|
100
111
|
project,
|
|
101
112
|
planId,
|
|
102
113
|
suiteId,
|
|
103
|
-
|
|
114
|
+
recursive
|
|
104
115
|
);
|
|
105
|
-
for (let i = 0; i < suitesTestCasesList.length; i++) {
|
|
106
|
-
let testCases: any = await this.GetTestCases(project, planId, suitesTestCasesList[i].id);
|
|
107
|
-
let testCseseWithSteps: any = await this.StructureTestCase(
|
|
108
|
-
project,
|
|
109
|
-
testCases,
|
|
110
|
-
suitesTestCasesList[i],
|
|
111
|
-
includeRequirements,
|
|
112
|
-
CustomerRequirementId,
|
|
113
|
-
requirementToTestCaseTraceMap,
|
|
114
|
-
testCaseToRequirementsTraceMap,
|
|
115
|
-
stepResultDetailsMap
|
|
116
|
-
);
|
|
117
116
|
|
|
118
|
-
|
|
119
|
-
|
|
117
|
+
// Create array of promises that each return their test cases
|
|
118
|
+
const testCaseListPromises = suitesTestCasesList.map((suite) =>
|
|
119
|
+
this.limit(async () => {
|
|
120
|
+
try {
|
|
121
|
+
const testCases = await this.GetTestCases(project, planId, suite.id);
|
|
122
|
+
|
|
123
|
+
const testCasesWithSteps = await this.StructureTestCase(
|
|
124
|
+
project,
|
|
125
|
+
testCases,
|
|
126
|
+
suite,
|
|
127
|
+
includeRequirements,
|
|
128
|
+
CustomerRequirementId,
|
|
129
|
+
requirementToTestCaseTraceMap,
|
|
130
|
+
testCaseToRequirementsTraceMap,
|
|
131
|
+
stepResultDetailsMap
|
|
132
|
+
);
|
|
133
|
+
|
|
134
|
+
// Return the results instead of modifying shared array
|
|
135
|
+
return testCasesWithSteps || [];
|
|
136
|
+
} catch (error) {
|
|
137
|
+
logger.error(`Error processing suite ${suite.id}: ${error}`);
|
|
138
|
+
return []; // Return empty array on error
|
|
139
|
+
}
|
|
140
|
+
})
|
|
141
|
+
);
|
|
142
|
+
|
|
143
|
+
// Wait for all promises and only then combine the results
|
|
144
|
+
const results = await Promise.all(testCaseListPromises);
|
|
145
|
+
testCasesList = results.flat(); // Combine all results into a single array
|
|
120
146
|
|
|
121
147
|
return { testCasesList, requirementToTestCaseTraceMap, testCaseToRequirementsTraceMap };
|
|
122
148
|
}
|
|
@@ -131,96 +157,166 @@ export default class TestDataProvider {
|
|
|
131
157
|
testCaseToRequirementsTraceMap: Map<string, string[]>,
|
|
132
158
|
stepResultDetailsMap?: Map<string, any>
|
|
133
159
|
): Promise<Array<any>> {
|
|
134
|
-
|
|
135
|
-
|
|
136
|
-
|
|
160
|
+
const baseUrl = this.orgUrl + project + '/_workitems/edit/';
|
|
161
|
+
const testCasesUrlList: Array<any> = [];
|
|
162
|
+
|
|
163
|
+
logger.debug(`Structuring test cases for ${project} suite: ${suite.id}:${suite.name}`);
|
|
164
|
+
|
|
137
165
|
try {
|
|
138
|
-
if (!testCases) {
|
|
139
|
-
|
|
166
|
+
if (!testCases || !testCases.value || testCases.count === 0) {
|
|
167
|
+
logger.warn(`No test cases found for suite: ${suite.id}`);
|
|
168
|
+
return [];
|
|
140
169
|
}
|
|
141
170
|
|
|
142
|
-
|
|
143
|
-
|
|
144
|
-
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
151
|
-
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
|
|
156
|
-
|
|
157
|
-
|
|
158
|
-
|
|
159
|
-
|
|
160
|
-
|
|
161
|
-
|
|
162
|
-
|
|
163
|
-
|
|
164
|
-
|
|
165
|
-
|
|
166
|
-
|
|
167
|
-
|
|
168
|
-
|
|
169
|
-
|
|
170
|
-
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
176
|
-
|
|
177
|
-
|
|
178
|
-
|
|
179
|
-
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
|
|
183
|
-
|
|
184
|
-
|
|
185
|
-
|
|
186
|
-
|
|
187
|
-
|
|
188
|
-
|
|
189
|
-
|
|
190
|
-
|
|
191
|
-
|
|
192
|
-
|
|
193
|
-
|
|
194
|
-
|
|
195
|
-
|
|
196
|
-
|
|
197
|
-
|
|
198
|
-
|
|
199
|
-
|
|
200
|
-
|
|
201
|
-
|
|
202
|
-
|
|
203
|
-
|
|
204
|
-
|
|
205
|
-
|
|
206
|
-
|
|
207
|
-
|
|
208
|
-
|
|
171
|
+
// Step 1: Prepare all test case fetch requests
|
|
172
|
+
const testCaseRequests = testCases.value.map((item: any) => {
|
|
173
|
+
const testCaseId = item.testCase.id.toString();
|
|
174
|
+
const stepDetailObject = stepResultDetailsMap?.get(testCaseId);
|
|
175
|
+
|
|
176
|
+
const url = !stepDetailObject?.testCaseRevision
|
|
177
|
+
? item.testCase.url + '?$expand=All'
|
|
178
|
+
: `${item.testCase.url}/revisions/${stepDetailObject.testCaseRevision}?$expand=All`;
|
|
179
|
+
|
|
180
|
+
return {
|
|
181
|
+
url,
|
|
182
|
+
testCaseId,
|
|
183
|
+
stepDetailObject,
|
|
184
|
+
};
|
|
185
|
+
});
|
|
186
|
+
|
|
187
|
+
// Step 2: Fetch all test cases in parallel using limit to control concurrency
|
|
188
|
+
logger.debug(`Fetching ${testCaseRequests.length} test cases concurrently`);
|
|
189
|
+
const testCaseResults = await Promise.all(
|
|
190
|
+
testCaseRequests.map((request: any) =>
|
|
191
|
+
this.limit(async () => {
|
|
192
|
+
try {
|
|
193
|
+
const test = await this.fetchWithCache(request.url);
|
|
194
|
+
return {
|
|
195
|
+
test,
|
|
196
|
+
testCaseId: request.testCaseId,
|
|
197
|
+
stepDetailObject: request.stepDetailObject,
|
|
198
|
+
};
|
|
199
|
+
} catch (error) {
|
|
200
|
+
logger.error(`Error fetching test case ${request.testCaseId}: ${error}`);
|
|
201
|
+
return null;
|
|
202
|
+
}
|
|
203
|
+
})
|
|
204
|
+
)
|
|
205
|
+
);
|
|
206
|
+
|
|
207
|
+
// Step 3: Process test cases and collect relation URLs
|
|
208
|
+
const validResults = testCaseResults.filter((result) => result !== null);
|
|
209
|
+
const relationRequests: Array<{ url: string; testCaseIndex: number }> = [];
|
|
210
|
+
|
|
211
|
+
const testCaseObjects = await Promise.all(
|
|
212
|
+
validResults.map(async (result, index) => {
|
|
213
|
+
try {
|
|
214
|
+
const test = result!.test;
|
|
215
|
+
const testCase = new TestCase();
|
|
216
|
+
|
|
217
|
+
// Build test case object
|
|
218
|
+
testCase.title = test.fields['System.Title'];
|
|
219
|
+
testCase.area = test.fields['System.AreaPath'];
|
|
220
|
+
testCase.description = test.fields['System.Description'];
|
|
221
|
+
testCase.url = baseUrl + test.id;
|
|
222
|
+
testCase.id = test.id;
|
|
223
|
+
testCase.suit = suite.id;
|
|
224
|
+
|
|
225
|
+
// Handle steps
|
|
226
|
+
if (!result!.stepDetailObject && test.fields['Microsoft.VSTS.TCM.Steps'] != null) {
|
|
227
|
+
testCase.steps = await this.testStepParserHelper.parseTestSteps(
|
|
228
|
+
test.fields['Microsoft.VSTS.TCM.Steps'],
|
|
229
|
+
new Map<number, number>()
|
|
230
|
+
);
|
|
231
|
+
} else if (result!.stepDetailObject) {
|
|
232
|
+
testCase.steps = result!.stepDetailObject.stepList;
|
|
233
|
+
testCase.caseEvidenceAttachments = result!.stepDetailObject.caseEvidenceAttachments;
|
|
234
|
+
}
|
|
235
|
+
|
|
236
|
+
// Collect relation URLs for batch processing
|
|
237
|
+
if (test.relations) {
|
|
238
|
+
test.relations.forEach((relation: any) => {
|
|
239
|
+
if (relation.url.includes('/workItems/')) {
|
|
240
|
+
relationRequests.push({
|
|
241
|
+
url: relation.url,
|
|
242
|
+
testCaseIndex: index,
|
|
243
|
+
});
|
|
209
244
|
}
|
|
210
|
-
}
|
|
245
|
+
});
|
|
211
246
|
}
|
|
247
|
+
|
|
248
|
+
return testCase;
|
|
249
|
+
} catch (error) {
|
|
250
|
+
logger.error(`Error processing test case ${result!.testCaseId}: ${error}`);
|
|
251
|
+
return null;
|
|
212
252
|
}
|
|
213
|
-
|
|
214
|
-
|
|
215
|
-
|
|
216
|
-
|
|
217
|
-
|
|
218
|
-
|
|
253
|
+
})
|
|
254
|
+
);
|
|
255
|
+
|
|
256
|
+
// Filter out any errors during test case processing
|
|
257
|
+
const validTestCases = testCaseObjects.filter((tc) => tc !== null) as TestCase[];
|
|
258
|
+
|
|
259
|
+
// Step 4: Fetch all relations concurrently
|
|
260
|
+
if (relationRequests.length > 0) {
|
|
261
|
+
logger.debug(`Fetching ${relationRequests.length} relations concurrently`);
|
|
262
|
+
const relationResults = await Promise.all(
|
|
263
|
+
relationRequests.map((request) =>
|
|
264
|
+
this.limit(async () => {
|
|
265
|
+
try {
|
|
266
|
+
const relatedItemContent = await this.fetchWithCache(request.url);
|
|
267
|
+
return {
|
|
268
|
+
content: relatedItemContent,
|
|
269
|
+
testCaseIndex: request.testCaseIndex,
|
|
270
|
+
};
|
|
271
|
+
} catch (error) {
|
|
272
|
+
logger.error(`Failed to fetch relation: ${error}`);
|
|
273
|
+
return null;
|
|
274
|
+
}
|
|
275
|
+
})
|
|
276
|
+
)
|
|
277
|
+
);
|
|
278
|
+
|
|
279
|
+
// Step 5: Process relations and update test cases
|
|
280
|
+
relationResults
|
|
281
|
+
.filter((result) => result !== null)
|
|
282
|
+
.forEach((result) => {
|
|
283
|
+
const relatedItemContent = result!.content;
|
|
284
|
+
const testCase = validTestCases[result!.testCaseIndex];
|
|
285
|
+
|
|
286
|
+
// Only process requirement relations
|
|
287
|
+
if (relatedItemContent.fields['System.WorkItemType'] === 'Requirement') {
|
|
288
|
+
const newRequirementRelation = this.createNewRequirement(
|
|
289
|
+
CustomerRequirementId,
|
|
290
|
+
relatedItemContent
|
|
291
|
+
);
|
|
292
|
+
|
|
293
|
+
const stringifiedTestCase = JSON.stringify({
|
|
294
|
+
id: testCase.id,
|
|
295
|
+
title: testCase.title,
|
|
296
|
+
});
|
|
297
|
+
const stringifiedRequirement = JSON.stringify(newRequirementRelation);
|
|
298
|
+
|
|
299
|
+
// Update trace maps
|
|
300
|
+
this.addToMap(requirementToTestCaseTraceMap, stringifiedRequirement, stringifiedTestCase);
|
|
301
|
+
this.addToMap(testCaseToRequirementsTraceMap, stringifiedTestCase, stringifiedRequirement);
|
|
302
|
+
|
|
303
|
+
// Add to test case relations if needed
|
|
304
|
+
if (includeRequirements) {
|
|
305
|
+
testCase.relations.push(newRequirementRelation);
|
|
306
|
+
}
|
|
307
|
+
}
|
|
308
|
+
});
|
|
219
309
|
}
|
|
310
|
+
|
|
311
|
+
// Add all valid test cases to the result list
|
|
312
|
+
testCasesUrlList.push(...validTestCases);
|
|
220
313
|
} catch (err: any) {
|
|
221
|
-
logger.error(`Error: ${err.message} while
|
|
314
|
+
logger.error(`Error: ${err.message} while structuring test cases for suite ${suite.id}`);
|
|
222
315
|
}
|
|
223
316
|
|
|
317
|
+
logger.info(
|
|
318
|
+
`StructureTestCase for suite ${suite.id} completed with ${testCasesUrlList.length} test cases`
|
|
319
|
+
);
|
|
224
320
|
return testCasesUrlList;
|
|
225
321
|
}
|
|
226
322
|
|
|
@@ -275,16 +371,14 @@ export default class TestDataProvider {
|
|
|
275
371
|
async GetTestCases(project: string, planId: string, suiteId: string): Promise<any> {
|
|
276
372
|
let testCaseUrl: string =
|
|
277
373
|
this.orgUrl + project + '/_apis/test/Plans/' + planId + '/suites/' + suiteId + '/testcases/';
|
|
278
|
-
let testCases: any = await
|
|
374
|
+
let testCases: any = await this.fetchWithCache(testCaseUrl);
|
|
279
375
|
logger.debug(`test cases for plan ${planId} and ${suiteId} were ${testCases ? 'found' : 'not found'}`);
|
|
280
376
|
return testCases;
|
|
281
377
|
}
|
|
282
378
|
|
|
283
|
-
//gets all test point in a test case
|
|
284
379
|
async GetTestPoint(project: string, planId: string, suiteId: string, testCaseId: string): Promise<any> {
|
|
285
380
|
let testPointUrl: string = `${this.orgUrl}${project}/_apis/test/Plans/${planId}/Suites/${suiteId}/points?testCaseId=${testCaseId}`;
|
|
286
|
-
|
|
287
|
-
return testPoints;
|
|
381
|
+
return this.fetchWithCache(testPointUrl);
|
|
288
382
|
}
|
|
289
383
|
|
|
290
384
|
async CreateTestRun(
|
|
@@ -415,4 +509,12 @@ export default class TestDataProvider {
|
|
|
415
509
|
}
|
|
416
510
|
map.get(key)?.push(value);
|
|
417
511
|
};
|
|
512
|
+
|
|
513
|
+
/**
|
|
514
|
+
* Clears the cache to free memory
|
|
515
|
+
*/
|
|
516
|
+
public clearCache(): void {
|
|
517
|
+
this.cache.clear();
|
|
518
|
+
logger.debug('Cache cleared');
|
|
519
|
+
}
|
|
418
520
|
}
|