@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.
@@ -1,21 +1,18 @@
1
1
  import { TFSServices } from '../helpers/tfs';
2
- import { Workitem } from '../models/tfs-data';
3
- import { Helper, suiteData, Links, Trace, Relations } from '../helpers/helper';
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
- let testCaseData = await TFSServices.getItemContent(url, this.token);
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 TFSServices.getItemContent(testPlanUrl, this.token);
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
- let testSuites = await TFSServices.getItemContent(testsuitesUrl, this.token);
44
- return testSuites;
45
- } catch (e) {}
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
- let suites = await TFSServices.getItemContent(url, this.token);
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
- let suites = await this.GetTestSuiteById(project, planId, suiteId.toString(), recursive);
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
- recursiv: boolean,
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
- recursiv
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
- if (testCseseWithSteps.length > 0) testCasesList = [...testCasesList, ...testCseseWithSteps];
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
- let url = this.orgUrl + project + '/_workitems/edit/';
135
- let testCasesUrlList: Array<any> = new Array<any>();
136
- logger.debug(`Trying to structure Test case for ${project} suite: ${suite.id}:${suite.name}`);
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
- throw new Error('test cases were not found');
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
- for (let i = 0; i < testCases.count; i++) {
143
- try {
144
- let stepDetailObject =
145
- stepResultDetailsMap?.get(testCases.value[i].testCase.id.toString()) || undefined;
146
-
147
- let newurl = !stepDetailObject?.testCaseRevision
148
- ? testCases.value[i].testCase.url + '?$expand=All'
149
- : `${testCases.value[i].testCase.url}/revisions/${stepDetailObject.testCaseRevision}?$expand=All`;
150
- let test: any = await TFSServices.getItemContent(newurl, this.token);
151
- let testCase: TestCase = new TestCase();
152
-
153
- testCase.title = test.fields['System.Title'];
154
- testCase.area = test.fields['System.AreaPath'];
155
- testCase.description = test.fields['System.Description'];
156
- testCase.url = url + test.id;
157
- //testCase.steps = test.fields["Microsoft.VSTS.TCM.Steps"];
158
- testCase.id = test.id;
159
- testCase.suit = suite.id;
160
-
161
- if (!stepDetailObject && test.fields['Microsoft.VSTS.TCM.Steps'] != null) {
162
- let steps = await this.testStepParserHelper.parseTestSteps(
163
- test.fields['Microsoft.VSTS.TCM.Steps'],
164
- new Map<number, number>()
165
- );
166
- testCase.steps = steps;
167
- //In case its already parsed during the STR
168
- } else if (stepDetailObject) {
169
- testCase.steps = stepDetailObject.stepList;
170
- testCase.caseEvidenceAttachments = stepDetailObject.caseEvidenceAttachments;
171
- }
172
- if (test.relations) {
173
- for (const relation of test.relations) {
174
- // Only proceed if the URL contains 'workItems'
175
- if (relation.url.includes('/workItems/')) {
176
- try {
177
- let relatedItemContent: any = await TFSServices.getItemContent(relation.url, this.token);
178
- // Check if the WorkItemType is "Requirement" before adding to relations
179
- if (relatedItemContent.fields['System.WorkItemType'] === 'Requirement') {
180
- const newRequirementRelation = this.createNewRequirement(
181
- CustomerRequirementId,
182
- relatedItemContent
183
- );
184
-
185
- const stringifiedTestCase = JSON.stringify({
186
- id: testCase.id,
187
- title: testCase.title,
188
- });
189
- const stringifiedRequirement = JSON.stringify(newRequirementRelation);
190
-
191
- // Add the test case to the requirement-to-test-case trace map
192
- this.addToMap(requirementToTestCaseTraceMap, stringifiedRequirement, stringifiedTestCase);
193
-
194
- // Add the requirement to the test-case-to-requirements trace map
195
- this.addToMap(
196
- testCaseToRequirementsTraceMap,
197
- stringifiedTestCase,
198
- stringifiedRequirement
199
- );
200
-
201
- if (includeRequirements) {
202
- testCase.relations.push(newRequirementRelation);
203
- }
204
- }
205
- } catch (fetchError) {
206
- // Log error silently or handle as needed
207
- console.error('Failed to fetch relation content', fetchError);
208
- logger.error(`Failed to fetch relation content for URL ${relation.url}: ${fetchError}`);
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
- testCasesUrlList.push(testCase);
214
- } catch {
215
- const errorMsg = `ran into an issue while retriving testCase ${testCases.value[i].testCase.id}`;
216
- logger.error(`errorMsg`);
217
- throw new Error(errorMsg);
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 trying to structure testCases for test suite ${suite.id}`);
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 TFSServices.getItemContent(testCaseUrl, this.token);
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
- let testPoints: any = await TFSServices.getItemContent(testPointUrl, this.token);
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
  }