@elisra-devops/docgen-data-provider 1.27.0 → 1.29.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/models/tfs-data.d.ts +6 -0
- package/bin/models/tfs-data.js.map +1 -1
- package/bin/modules/ResultDataProvider.d.ts +13 -2
- package/bin/modules/ResultDataProvider.js +140 -25
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/modules/TestDataProvider.js +3 -2
- package/bin/modules/TestDataProvider.js.map +1 -1
- package/bin/modules/TicketsDataProvider.d.ts +40 -5
- package/bin/modules/TicketsDataProvider.js +67 -19
- package/bin/modules/TicketsDataProvider.js.map +1 -1
- package/bin/modules/test/ResultDataProvider.test.js +67 -101
- package/bin/modules/test/ResultDataProvider.test.js.map +1 -1
- package/bin/modules/test/testDataProvider.test.js +13 -15
- package/bin/modules/test/testDataProvider.test.js.map +1 -1
- package/bin/utils/DataProviderUtils.d.ts +11 -0
- package/bin/utils/DataProviderUtils.js +21 -0
- package/bin/utils/DataProviderUtils.js.map +1 -0
- package/package.json +1 -1
- package/src/models/tfs-data.ts +7 -0
- package/src/modules/ResultDataProvider.ts +193 -43
- package/src/modules/TestDataProvider.ts +10 -5
- package/src/modules/TicketsDataProvider.ts +86 -25
- package/src/modules/test/ResultDataProvider.test.ts +511 -550
- package/src/modules/test/testDataProvider.test.ts +19 -18
- package/src/utils/DataProviderUtils.ts +16 -0
|
@@ -1,8 +1,8 @@
|
|
|
1
|
-
import
|
|
1
|
+
import DataProviderUtils from '../utils/DataProviderUtils';
|
|
2
2
|
import { TFSServices } from '../helpers/tfs';
|
|
3
|
-
import { TestSteps } from '../models/tfs-data';
|
|
3
|
+
import { OpenPcrRequest, TestSteps } from '../models/tfs-data';
|
|
4
4
|
import logger from '../utils/logger';
|
|
5
|
-
import
|
|
5
|
+
import Utils from '../utils/testStepParserHelper';
|
|
6
6
|
const pLimit = require('p-limit');
|
|
7
7
|
/**
|
|
8
8
|
* Provides methods to fetch, process, and summarize test data from Azure DevOps.
|
|
@@ -30,11 +30,11 @@ export default class ResultDataProvider {
|
|
|
30
30
|
orgUrl: string = '';
|
|
31
31
|
token: string = '';
|
|
32
32
|
private limit = pLimit(10);
|
|
33
|
-
private testStepParserHelper:
|
|
33
|
+
private testStepParserHelper: Utils;
|
|
34
34
|
constructor(orgUrl: string, token: string) {
|
|
35
35
|
this.orgUrl = orgUrl;
|
|
36
36
|
this.token = token;
|
|
37
|
-
this.testStepParserHelper = new
|
|
37
|
+
this.testStepParserHelper = new Utils(orgUrl, token);
|
|
38
38
|
}
|
|
39
39
|
|
|
40
40
|
/**
|
|
@@ -46,14 +46,20 @@ export default class ResultDataProvider {
|
|
|
46
46
|
selectedSuiteIds?: number[],
|
|
47
47
|
addConfiguration: boolean = false,
|
|
48
48
|
isHierarchyGroupName: boolean = false,
|
|
49
|
-
|
|
49
|
+
openPcrRequest: OpenPcrRequest | null = null,
|
|
50
50
|
includeTestLog: boolean = false,
|
|
51
51
|
stepExecution?: any,
|
|
52
52
|
stepAnalysis?: any,
|
|
53
53
|
includeHardCopyRun: boolean = false
|
|
54
|
-
): Promise<
|
|
54
|
+
): Promise<{
|
|
55
|
+
combinedResults: any[];
|
|
56
|
+
openPcrToTestCaseTraceMap: Map<string, string[]>;
|
|
57
|
+
testCaseToOpenPcrTraceMap: Map<string, string[]>;
|
|
58
|
+
}> {
|
|
55
59
|
const combinedResults: any[] = [];
|
|
56
60
|
try {
|
|
61
|
+
const openPcrToTestCaseTraceMap = new Map<string, string[]>();
|
|
62
|
+
const testCaseToOpenPcrTraceMap = new Map<string, string[]>();
|
|
57
63
|
// Fetch test suites
|
|
58
64
|
const suites = await this.fetchTestSuites(
|
|
59
65
|
testPlanId,
|
|
@@ -114,7 +120,7 @@ export default class ResultDataProvider {
|
|
|
114
120
|
});
|
|
115
121
|
|
|
116
122
|
// 3. Calculate Detailed Results Summary
|
|
117
|
-
const testData = await this.fetchTestData(suites, projectName, testPlanId);
|
|
123
|
+
const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
|
|
118
124
|
const runResults = await this.fetchAllResultData(testData, projectName);
|
|
119
125
|
const detailedStepResultsSummary = this.alignStepsWithIterations(testData, runResults)?.filter(
|
|
120
126
|
(results) => results !== null
|
|
@@ -131,9 +137,14 @@ export default class ResultDataProvider {
|
|
|
131
137
|
skin: 'detailed-test-result-table',
|
|
132
138
|
});
|
|
133
139
|
|
|
134
|
-
if (
|
|
140
|
+
if (openPcrRequest?.openPcrMode === 'linked') {
|
|
135
141
|
//5. Open PCRs data (only if enabled)
|
|
136
|
-
await this.fetchOpenPcrData(
|
|
142
|
+
await this.fetchOpenPcrData(
|
|
143
|
+
testResultsSummary,
|
|
144
|
+
projectName,
|
|
145
|
+
openPcrToTestCaseTraceMap,
|
|
146
|
+
testCaseToOpenPcrTraceMap
|
|
147
|
+
);
|
|
137
148
|
}
|
|
138
149
|
|
|
139
150
|
//6. Test Log (only if enabled)
|
|
@@ -164,7 +175,7 @@ export default class ResultDataProvider {
|
|
|
164
175
|
if (stepExecution && stepExecution.isEnabled) {
|
|
165
176
|
const mappedAnalysisData =
|
|
166
177
|
stepExecution.generateAttachments.isEnabled &&
|
|
167
|
-
|
|
178
|
+
stepExecution.generateAttachments.runAttachmentMode !== 'planOnly'
|
|
168
179
|
? runResults.filter((result) => result.iteration?.attachments?.length > 0)
|
|
169
180
|
: [];
|
|
170
181
|
const mappedAnalysisResultData =
|
|
@@ -181,7 +192,7 @@ export default class ResultDataProvider {
|
|
|
181
192
|
});
|
|
182
193
|
}
|
|
183
194
|
|
|
184
|
-
return combinedResults;
|
|
195
|
+
return { combinedResults, openPcrToTestCaseTraceMap, testCaseToOpenPcrTraceMap };
|
|
185
196
|
} catch (error: any) {
|
|
186
197
|
logger.error(`Error during getCombinedResultsSummary: ${error.message}`);
|
|
187
198
|
if (error.response) {
|
|
@@ -209,6 +220,7 @@ export default class ResultDataProvider {
|
|
|
209
220
|
projectName: string,
|
|
210
221
|
selectedSuiteIds: number[],
|
|
211
222
|
selectedFields: string[],
|
|
223
|
+
allowCrossTestPlan: boolean,
|
|
212
224
|
enableRunTestCaseFilter: boolean,
|
|
213
225
|
enableRunStepStatusFilter: boolean
|
|
214
226
|
) {
|
|
@@ -218,17 +230,21 @@ export default class ResultDataProvider {
|
|
|
218
230
|
);
|
|
219
231
|
logger.debug(`Selected suite IDs: ${selectedSuiteIds}`);
|
|
220
232
|
try {
|
|
233
|
+
logger.debug(`Fetching Plan info for test plan ID: ${testPlanId}, project name: ${projectName}`);
|
|
221
234
|
const plan = await this.fetchTestPlanName(testPlanId, projectName);
|
|
235
|
+
logger.debug(`Fetching Test suites for test plan ID: ${testPlanId}, project name: ${projectName}`);
|
|
222
236
|
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
223
|
-
|
|
237
|
+
logger.debug(`Fetching test data for test plan ID: ${testPlanId}, project name: ${projectName}`);
|
|
238
|
+
const testData = await this.fetchTestData(suites, projectName, testPlanId, allowCrossTestPlan);
|
|
239
|
+
logger.debug(`Fetching Run results for test data, project name: ${projectName}`);
|
|
224
240
|
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, selectedFields);
|
|
241
|
+
logger.debug(`Aligning steps with iterations for test reporter results`);
|
|
225
242
|
const testReporterData = this.alignStepsWithIterationsTestReporter(
|
|
226
243
|
testData,
|
|
227
244
|
runResults,
|
|
228
245
|
selectedFields,
|
|
229
246
|
!enableRunTestCaseFilter
|
|
230
247
|
);
|
|
231
|
-
|
|
232
248
|
// Apply filters sequentially based on enabled flags
|
|
233
249
|
let filteredResults = testReporterData;
|
|
234
250
|
|
|
@@ -406,13 +422,85 @@ export default class ResultDataProvider {
|
|
|
406
422
|
: `${parts[1]}/${parts[parts.length - 1]}`;
|
|
407
423
|
}
|
|
408
424
|
|
|
425
|
+
private async fetchCrossTestPoints(projectName: string, testCaseIds: any[]): Promise<any[]> {
|
|
426
|
+
try {
|
|
427
|
+
const url = `${this.orgUrl}${projectName}/_apis/test/points?api-version=6.0`
|
|
428
|
+
if (testCaseIds.length === 0) {
|
|
429
|
+
return []
|
|
430
|
+
}
|
|
431
|
+
|
|
432
|
+
const requestBody: any = {
|
|
433
|
+
PointsFilter: {
|
|
434
|
+
TestcaseIds: testCaseIds,
|
|
435
|
+
},
|
|
436
|
+
};
|
|
437
|
+
|
|
438
|
+
const { data: value } = await TFSServices.postRequest(url, this.token, 'Post', requestBody, null)
|
|
439
|
+
if (!value || !value.points || !Array.isArray(value.points)) {
|
|
440
|
+
logger.warn("No test points found or invalid response format");
|
|
441
|
+
return [];
|
|
442
|
+
}
|
|
443
|
+
|
|
444
|
+
// Group test points by test case ID
|
|
445
|
+
const pointsByTestCase = new Map();
|
|
446
|
+
value.points.forEach((point: any) => {
|
|
447
|
+
const testCaseId = point.testCase.id;
|
|
448
|
+
|
|
449
|
+
if (!pointsByTestCase.has(testCaseId)) {
|
|
450
|
+
pointsByTestCase.set(testCaseId, []);
|
|
451
|
+
}
|
|
452
|
+
|
|
453
|
+
pointsByTestCase.get(testCaseId).push(point);
|
|
454
|
+
});
|
|
455
|
+
|
|
456
|
+
// For each test case, find the point with the most recent run
|
|
457
|
+
const latestPoints: any[] = [];
|
|
458
|
+
|
|
459
|
+
for (const [testCaseId, points] of pointsByTestCase.entries()) {
|
|
460
|
+
// Sort by lastTestRun.id (descending), then by lastResult.id (descending)
|
|
461
|
+
const sortedPoints = points.sort((a: any, b: any) => {
|
|
462
|
+
// Parse IDs as numbers for proper comparison
|
|
463
|
+
const aRunId = parseInt(a.lastTestRun?.id || '0');
|
|
464
|
+
const bRunId = parseInt(b.lastTestRun?.id || '0');
|
|
465
|
+
|
|
466
|
+
if (aRunId !== bRunId) {
|
|
467
|
+
return bRunId - aRunId; // Sort by run ID first (descending)
|
|
468
|
+
}
|
|
469
|
+
|
|
470
|
+
const aResultId = parseInt(a.lastResult?.id || '0');
|
|
471
|
+
const bResultId = parseInt(b.lastResult?.id || '0');
|
|
472
|
+
return bResultId - aResultId; // Then by result ID (descending)
|
|
473
|
+
});
|
|
474
|
+
|
|
475
|
+
// Take the first item (most recent)
|
|
476
|
+
latestPoints.push(sortedPoints[0]);
|
|
477
|
+
}
|
|
478
|
+
|
|
479
|
+
// Fetch detailed information for each test point and map to required format
|
|
480
|
+
const detailedPoints = await Promise.all(
|
|
481
|
+
latestPoints.map(async (point: any) => {
|
|
482
|
+
const url = `${point.url}?witFields=Microsoft.VSTS.TCM.Steps&includePointDetails=true`
|
|
483
|
+
const detailedPoint = await TFSServices.getItemContent(url, this.token);
|
|
484
|
+
return this.mapTestPointForCrossPlans(detailedPoint, projectName);
|
|
485
|
+
// return this.mapTestPointForCrossPlans(detailedPoint, projectName);
|
|
486
|
+
})
|
|
487
|
+
);
|
|
488
|
+
return detailedPoints
|
|
489
|
+
} catch (err: any) {
|
|
490
|
+
logger.error(`Error during fetching Cross Test Points: ${err.message}`);
|
|
491
|
+
logger.error(`Error stack: ${err.stack}`);
|
|
492
|
+
return [];
|
|
493
|
+
}
|
|
494
|
+
|
|
495
|
+
}
|
|
496
|
+
|
|
409
497
|
/**
|
|
410
498
|
* Fetches test points by suite ID.
|
|
411
499
|
*/
|
|
412
500
|
private async fetchTestPoints(
|
|
413
501
|
projectName: string,
|
|
414
502
|
testPlanId: string,
|
|
415
|
-
testSuiteId: string
|
|
503
|
+
testSuiteId: string,
|
|
416
504
|
): Promise<any[]> {
|
|
417
505
|
try {
|
|
418
506
|
const url = `${this.orgUrl}${projectName}/_apis/testplan/Plans/${testPlanId}/Suites/${testSuiteId}/TestPoint?includePointDetails=true`;
|
|
@@ -441,6 +529,39 @@ export default class ResultDataProvider {
|
|
|
441
529
|
};
|
|
442
530
|
}
|
|
443
531
|
|
|
532
|
+
|
|
533
|
+
/**
|
|
534
|
+
* Maps raw test point data to a simplified object.
|
|
535
|
+
*/
|
|
536
|
+
private mapTestPointForCrossPlans(testPoint: any, projectName: string): any {
|
|
537
|
+
return {
|
|
538
|
+
testCaseId: testPoint.testCase.id,
|
|
539
|
+
testCaseName: testPoint.testCase.name,
|
|
540
|
+
testCaseUrl: `${this.orgUrl}${projectName}/_workitems/edit/${testPoint.testCase.id}`,
|
|
541
|
+
configurationName: testPoint.configuration?.name,
|
|
542
|
+
outcome: testPoint.outcome || 'Not Run',
|
|
543
|
+
lastRunId: testPoint.lastTestRun?.id,
|
|
544
|
+
lastResultId: testPoint.lastResult?.id,
|
|
545
|
+
lastResultDetails: testPoint.lastResultDetails || {
|
|
546
|
+
"duration": 0,
|
|
547
|
+
"dateCompleted": "0000-00-00T00:00:00.000Z",
|
|
548
|
+
"runBy": { "displayName": "No tester", "id": "00000000-0000-0000-0000-000000000000" }
|
|
549
|
+
},
|
|
550
|
+
};
|
|
551
|
+
}
|
|
552
|
+
|
|
553
|
+
// Helper method to get all test points for a test case
|
|
554
|
+
async getTestPointsForTestCases(projectName: string, testCaseId: string[]): Promise<any> {
|
|
555
|
+
const url = `${this.orgUrl}${projectName}/_apis/test/points`;
|
|
556
|
+
const requestBody = {
|
|
557
|
+
PointsFilter: {
|
|
558
|
+
TestcaseIds: testCaseId,
|
|
559
|
+
},
|
|
560
|
+
};
|
|
561
|
+
|
|
562
|
+
return await TFSServices.postRequest(url, this.token, 'Post', requestBody, null);
|
|
563
|
+
}
|
|
564
|
+
|
|
444
565
|
/**
|
|
445
566
|
* Fetches test cases by suite ID.
|
|
446
567
|
*/
|
|
@@ -464,6 +585,10 @@ export default class ResultDataProvider {
|
|
|
464
585
|
selectedFields?: string[]
|
|
465
586
|
): Promise<any> {
|
|
466
587
|
try {
|
|
588
|
+
if (runId === '0' || resultId === '0') {
|
|
589
|
+
logger.warn(`Invalid runId or resultId: ${runId}, ${resultId}`);
|
|
590
|
+
return null;
|
|
591
|
+
}
|
|
467
592
|
const url = `${this.orgUrl}${projectName}/_apis/test/runs/${runId}/results/${resultId}?detailsToInclude=Iterations`;
|
|
468
593
|
const resultData = await TFSServices.getItemContent(url, this.token);
|
|
469
594
|
|
|
@@ -478,9 +603,6 @@ export default class ResultDataProvider {
|
|
|
478
603
|
let relatedRequirements: any[] = [];
|
|
479
604
|
let relatedBugs: any[] = [];
|
|
480
605
|
let relatedCRs: any[] = [];
|
|
481
|
-
//TODO: Add CR support as well, and also add the logic to fetch the CR details
|
|
482
|
-
// TODO: Add logic for grabbing the relations from cross projects
|
|
483
|
-
|
|
484
606
|
// Process selected fields if provided
|
|
485
607
|
if (selectedFields?.length && isTestReporter) {
|
|
486
608
|
const filtered = selectedFields
|
|
@@ -610,8 +732,8 @@ export default class ResultDataProvider {
|
|
|
610
732
|
return actionResult.outcome === 'Unspecified'
|
|
611
733
|
? 'Not Run'
|
|
612
734
|
: actionResult.outcome !== 'Not Run'
|
|
613
|
-
|
|
614
|
-
|
|
735
|
+
? actionResult.outcome
|
|
736
|
+
: '';
|
|
615
737
|
}
|
|
616
738
|
|
|
617
739
|
/**
|
|
@@ -645,6 +767,7 @@ export default class ResultDataProvider {
|
|
|
645
767
|
}
|
|
646
768
|
): any[] {
|
|
647
769
|
const detailedResults: any[] = [];
|
|
770
|
+
|
|
648
771
|
if (!iterations || iterations?.length === 0) {
|
|
649
772
|
return detailedResults;
|
|
650
773
|
}
|
|
@@ -658,7 +781,7 @@ export default class ResultDataProvider {
|
|
|
658
781
|
|
|
659
782
|
for (const testItem of testData) {
|
|
660
783
|
for (const point of testItem.testPointsItems) {
|
|
661
|
-
const testCase = testItem.testCasesItems.find((tc: any) => tc.workItem.id === point.testCaseId);
|
|
784
|
+
const testCase = testItem.testCasesItems.find((tc: any) => Number(tc.workItem.id) === Number(point.testCaseId));
|
|
662
785
|
if (!testCase) continue;
|
|
663
786
|
|
|
664
787
|
if (testCase.workItem.workItemFields.length === 0) {
|
|
@@ -747,13 +870,13 @@ export default class ResultDataProvider {
|
|
|
747
870
|
resultObjectsToAdd.length > 0
|
|
748
871
|
? detailedResults.push(...resultObjectsToAdd)
|
|
749
872
|
: detailedResults.push(
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
|
|
754
|
-
|
|
755
|
-
|
|
756
|
-
|
|
873
|
+
options.createResultObject({
|
|
874
|
+
testItem,
|
|
875
|
+
point,
|
|
876
|
+
fetchedTestCase,
|
|
877
|
+
filteredFields,
|
|
878
|
+
})
|
|
879
|
+
);
|
|
757
880
|
}
|
|
758
881
|
}
|
|
759
882
|
|
|
@@ -818,17 +941,25 @@ export default class ResultDataProvider {
|
|
|
818
941
|
/**
|
|
819
942
|
* Fetches test data for all suites, including test points and test cases.
|
|
820
943
|
*/
|
|
821
|
-
private async fetchTestData(
|
|
944
|
+
private async fetchTestData(
|
|
945
|
+
suites: any[],
|
|
946
|
+
projectName: string,
|
|
947
|
+
testPlanId: string,
|
|
948
|
+
fetchCrossPlans: boolean = false
|
|
949
|
+
): Promise<any[]> {
|
|
822
950
|
return await Promise.all(
|
|
823
951
|
suites.map((suite) =>
|
|
824
952
|
this.limit(async () => {
|
|
825
953
|
try {
|
|
826
|
-
|
|
954
|
+
|
|
827
955
|
const testCasesItems = await this.fetchTestCasesBySuiteId(
|
|
828
956
|
projectName,
|
|
829
957
|
testPlanId,
|
|
830
958
|
suite.testSuiteId
|
|
831
959
|
);
|
|
960
|
+
const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
|
|
961
|
+
const testPointsItems = !fetchCrossPlans ? await this.fetchTestPoints(projectName, testPlanId, suite.testSuiteId) : await this.fetchCrossTestPoints(projectName, testCaseIds);
|
|
962
|
+
|
|
832
963
|
return { ...suite, testPointsItems, testCasesItems };
|
|
833
964
|
} catch (error: any) {
|
|
834
965
|
logger.error(`Error occurred for suite ${suite.testSuiteId}: ${error.message}`);
|
|
@@ -921,6 +1052,13 @@ export default class ResultDataProvider {
|
|
|
921
1052
|
additionalArgs: any[] = []
|
|
922
1053
|
): Promise<any> {
|
|
923
1054
|
const { lastRunId, lastResultId } = point;
|
|
1055
|
+
if (!lastRunId || !lastResultId) {
|
|
1056
|
+
logger.warn(`Invalid lastRunId or lastResultId for point: ${JSON.stringify(point)}`);
|
|
1057
|
+
return null;
|
|
1058
|
+
} else if (lastRunId === '0' || lastResultId === '0') {
|
|
1059
|
+
logger.warn(`Invalid lastRunId or lastResultId: ${lastRunId}, ${lastResultId}`);
|
|
1060
|
+
return null;
|
|
1061
|
+
}
|
|
924
1062
|
const resultData = await fetchResultMethod(
|
|
925
1063
|
projectName,
|
|
926
1064
|
lastRunId.toString(),
|
|
@@ -1099,21 +1237,32 @@ export default class ResultDataProvider {
|
|
|
1099
1237
|
* Fetching Open PCRs data
|
|
1100
1238
|
*/
|
|
1101
1239
|
|
|
1102
|
-
private async fetchOpenPcrData(
|
|
1240
|
+
private async fetchOpenPcrData(
|
|
1241
|
+
testItems: any[],
|
|
1242
|
+
projectName: string,
|
|
1243
|
+
openPcrToTestCaseTraceMap: Map<string, string[]>,
|
|
1244
|
+
testCaseToOpenPcrTraceMap: Map<string, string[]>
|
|
1245
|
+
) {
|
|
1103
1246
|
const linkedWorkItems = await this.fetchLinkedWi(projectName, testItems);
|
|
1104
|
-
const
|
|
1105
|
-
|
|
1106
|
-
.
|
|
1107
|
-
|
|
1108
|
-
|
|
1109
|
-
|
|
1110
|
-
|
|
1111
|
-
// Add openPCR to combined results
|
|
1112
|
-
combinedResults.push({
|
|
1113
|
-
contentControl: 'open-pcr-content-control',
|
|
1114
|
-
data: flatOpenPcrsItems,
|
|
1115
|
-
skin: 'open-pcr-table',
|
|
1247
|
+
for (const wi of linkedWorkItems) {
|
|
1248
|
+
const { linkItems, ...restItem } = wi;
|
|
1249
|
+
const stringifiedTestCase = JSON.stringify({
|
|
1250
|
+
id: restItem.testId,
|
|
1251
|
+
title: restItem.testName,
|
|
1252
|
+
testCaseUrl: restItem.testCaseUrl,
|
|
1253
|
+
runStatus: restItem.runStatus,
|
|
1116
1254
|
});
|
|
1255
|
+
for (const linkedItem of linkItems) {
|
|
1256
|
+
const stringifiedPcr = JSON.stringify({
|
|
1257
|
+
pcrId: linkedItem.pcrId,
|
|
1258
|
+
workItemType: linkedItem.workItemType,
|
|
1259
|
+
title: linkedItem.title,
|
|
1260
|
+
severity: linkedItem.severity,
|
|
1261
|
+
pcrUrl: linkedItem.pcrUrl,
|
|
1262
|
+
});
|
|
1263
|
+
DataProviderUtils.addToTraceMap(openPcrToTestCaseTraceMap, stringifiedPcr, stringifiedTestCase);
|
|
1264
|
+
DataProviderUtils.addToTraceMap(testCaseToOpenPcrTraceMap, stringifiedTestCase, stringifiedPcr);
|
|
1265
|
+
}
|
|
1117
1266
|
}
|
|
1118
1267
|
}
|
|
1119
1268
|
|
|
@@ -1323,6 +1472,7 @@ export default class ResultDataProvider {
|
|
|
1323
1472
|
testGroupName: testPoint.testGroupName,
|
|
1324
1473
|
testId: testPoint.testCaseId,
|
|
1325
1474
|
testName: testPoint.testCaseName,
|
|
1475
|
+
testCaseUrl: testPoint.testCaseUrl,
|
|
1326
1476
|
runStatus: !includeHardCopyRun ? this.convertRunStatus(testPoint.outcome) : '',
|
|
1327
1477
|
};
|
|
1328
1478
|
|
|
@@ -4,20 +4,21 @@ import { TestSteps, createMomRelation, createRequirementRelation } from '../mode
|
|
|
4
4
|
import { TestCase } from '../models/tfs-data';
|
|
5
5
|
import * as xml2js from 'xml2js';
|
|
6
6
|
import logger from '../utils/logger';
|
|
7
|
-
import
|
|
7
|
+
import Utils from '../utils/testStepParserHelper';
|
|
8
|
+
import DataProviderUtils from '../utils/DataProviderUtils';
|
|
8
9
|
const pLimit = require('p-limit');
|
|
9
10
|
|
|
10
11
|
export default class TestDataProvider {
|
|
11
12
|
orgUrl: string = '';
|
|
12
13
|
token: string = '';
|
|
13
|
-
private testStepParserHelper:
|
|
14
|
+
private testStepParserHelper: Utils;
|
|
14
15
|
private cache = new Map<string, any>(); // Cache for API responses
|
|
15
16
|
private limit = pLimit(10);
|
|
16
17
|
|
|
17
18
|
constructor(orgUrl: string, token: string) {
|
|
18
19
|
this.orgUrl = orgUrl;
|
|
19
20
|
this.token = token;
|
|
20
|
-
this.testStepParserHelper = new
|
|
21
|
+
this.testStepParserHelper = new Utils(orgUrl, token);
|
|
21
22
|
}
|
|
22
23
|
|
|
23
24
|
private async fetchWithCache(url: string, ttlMs = 60000): Promise<any> {
|
|
@@ -226,10 +227,14 @@ export default class TestDataProvider {
|
|
|
226
227
|
const stringifiedRequirement = JSON.stringify(newRequirementRelation);
|
|
227
228
|
|
|
228
229
|
// Add the test case to the requirement-to-test-case trace map
|
|
229
|
-
|
|
230
|
+
DataProviderUtils.addToTraceMap(
|
|
231
|
+
requirementToTestCaseTraceMap,
|
|
232
|
+
stringifiedRequirement,
|
|
233
|
+
stringifiedTestCase
|
|
234
|
+
);
|
|
230
235
|
|
|
231
236
|
// Add the requirement to the test-case-to-requirements trace map
|
|
232
|
-
|
|
237
|
+
DataProviderUtils.addToTraceMap(
|
|
233
238
|
testCaseToRequirementsTraceMap,
|
|
234
239
|
stringifiedTestCase,
|
|
235
240
|
stringifiedRequirement
|
|
@@ -120,9 +120,11 @@ export default class TicketsDataProvider {
|
|
|
120
120
|
logger.debug(`doctype: ${docType}`);
|
|
121
121
|
switch (docType) {
|
|
122
122
|
case 'STD':
|
|
123
|
-
return await this.
|
|
123
|
+
return await this.fetchLinkedReqTestQueries(queries, false);
|
|
124
124
|
case 'STR':
|
|
125
|
-
|
|
125
|
+
const reqTestTrees = await this.fetchLinkedReqTestQueries(queries, false);
|
|
126
|
+
const openPcrTestTrees = await this.fetchLinkedOpenPcrTestQueries(queries, false);
|
|
127
|
+
return { reqTestTrees, openPcrTestTrees };
|
|
126
128
|
case 'SVD':
|
|
127
129
|
return await this.fetchAnyQueries(queries);
|
|
128
130
|
default:
|
|
@@ -140,13 +142,41 @@ export default class TicketsDataProvider {
|
|
|
140
142
|
* @param onlyTestReq get only test req
|
|
141
143
|
* @returns ReqTestTree and TestReqTree
|
|
142
144
|
*/
|
|
143
|
-
private async
|
|
144
|
-
const { tree1: reqTestTree, tree2: testReqTree } = await this.
|
|
145
|
+
private async fetchLinkedReqTestQueries(queries: any, onlyTestReq: boolean = false) {
|
|
146
|
+
const { tree1: reqTestTree, tree2: testReqTree } = await this.structureFetchedQueries(
|
|
145
147
|
queries,
|
|
146
|
-
onlyTestReq
|
|
148
|
+
onlyTestReq,
|
|
149
|
+
null,
|
|
150
|
+
['Requirement'],
|
|
151
|
+
['Test Case']
|
|
147
152
|
);
|
|
148
153
|
return { reqTestTree, testReqTree };
|
|
149
154
|
}
|
|
155
|
+
|
|
156
|
+
/**
|
|
157
|
+
* Fetches and structures linked queries related to open PCR (Problem Change Request) tests.
|
|
158
|
+
*
|
|
159
|
+
* This method retrieves and organizes the relationships between "Test Case" and
|
|
160
|
+
* other entities such as "Bug" and "Change Request" into two tree structures.
|
|
161
|
+
*
|
|
162
|
+
* @param queries - The input queries to be processed and structured.
|
|
163
|
+
* @param onlySourceSide - A flag indicating whether to process only the source side of the queries.
|
|
164
|
+
* Defaults to `false`.
|
|
165
|
+
* @returns An object containing two tree structures:
|
|
166
|
+
* - `OpenPcrToTestTree`: The tree representing the relationship from Open PCR to Test Case.
|
|
167
|
+
* - `TestToOpenPcrTree`: The tree representing the relationship from Test Case to Open PCR.
|
|
168
|
+
*/
|
|
169
|
+
private async fetchLinkedOpenPcrTestQueries(queries: any, onlySourceSide: boolean = false) {
|
|
170
|
+
const { tree1: OpenPcrToTestTree, tree2: TestToOpenPcrTree } = await this.structureFetchedQueries(
|
|
171
|
+
queries,
|
|
172
|
+
onlySourceSide,
|
|
173
|
+
null,
|
|
174
|
+
['Bug', 'Change Request'],
|
|
175
|
+
['Test Case']
|
|
176
|
+
);
|
|
177
|
+
return { OpenPcrToTestTree, TestToOpenPcrTree };
|
|
178
|
+
}
|
|
179
|
+
|
|
150
180
|
private async fetchAnyQueries(queries: any) {
|
|
151
181
|
const { tree1: systemOverviewQueryTree, tree2: knownBugsQueryTree } = await this.structureAllQueryPath(
|
|
152
182
|
queries
|
|
@@ -756,12 +786,25 @@ export default class TicketsDataProvider {
|
|
|
756
786
|
}
|
|
757
787
|
|
|
758
788
|
/**
|
|
759
|
-
* Recursively structures
|
|
789
|
+
* Recursively structures fetched queries into two hierarchical trees (tree1 and tree2)
|
|
790
|
+
* based on specific conditions. It processes both leaf and non-leaf nodes, fetching
|
|
791
|
+
* children if necessary, and builds the trees by matching source and target conditions.
|
|
792
|
+
*
|
|
793
|
+
* @param rootQuery - The root query object to process. It may contain children or be a leaf node.
|
|
794
|
+
* @param onlyTestReq - A boolean flag indicating whether to exclude requirement-to-test-case queries.
|
|
795
|
+
* @param parentId - The ID of the parent node, used to maintain the hierarchy. Defaults to `null`.
|
|
796
|
+
* @param sources - An array of source strings used to match queries.
|
|
797
|
+
* @param targets - An array of target strings used to match queries.
|
|
798
|
+
* @returns A promise that resolves to an object containing two trees (`tree1` and `tree2`),
|
|
799
|
+
* or `null` for each tree if no valid nodes are found.
|
|
800
|
+
* @throws Logs an error if an exception occurs during processing.
|
|
760
801
|
*/
|
|
761
|
-
private async
|
|
802
|
+
private async structureFetchedQueries(
|
|
762
803
|
rootQuery: any,
|
|
763
804
|
onlyTestReq: boolean,
|
|
764
|
-
parentId: any = null
|
|
805
|
+
parentId: any = null,
|
|
806
|
+
sources: string[],
|
|
807
|
+
targets: string[]
|
|
765
808
|
): Promise<any> {
|
|
766
809
|
try {
|
|
767
810
|
if (!rootQuery.hasChildren) {
|
|
@@ -770,7 +813,7 @@ export default class TicketsDataProvider {
|
|
|
770
813
|
let tree1Node = null;
|
|
771
814
|
let tree2Node = null;
|
|
772
815
|
// Check if the query is a requirement to test case query
|
|
773
|
-
if (!onlyTestReq && this.
|
|
816
|
+
if (!onlyTestReq && this.matchesSourceTargetCondition(wiql, sources, targets)) {
|
|
774
817
|
tree1Node = {
|
|
775
818
|
id: rootQuery.id,
|
|
776
819
|
pId: parentId,
|
|
@@ -781,7 +824,7 @@ export default class TicketsDataProvider {
|
|
|
781
824
|
isValidQuery: true,
|
|
782
825
|
};
|
|
783
826
|
}
|
|
784
|
-
if (this.
|
|
827
|
+
if (this.matchesSourceTargetCondition(wiql, targets, sources)) {
|
|
785
828
|
tree2Node = {
|
|
786
829
|
id: rootQuery.id,
|
|
787
830
|
pId: parentId,
|
|
@@ -802,13 +845,15 @@ export default class TicketsDataProvider {
|
|
|
802
845
|
const queryUrl = `${rootQuery.url}?$depth=2&$expand=all`;
|
|
803
846
|
const currentQuery = await TFSServices.getItemContent(queryUrl, this.token);
|
|
804
847
|
return currentQuery
|
|
805
|
-
? await this.
|
|
848
|
+
? await this.structureFetchedQueries(currentQuery, onlyTestReq, currentQuery.id, sources, targets)
|
|
806
849
|
: { tree1: null, tree2: null };
|
|
807
850
|
}
|
|
808
851
|
|
|
809
852
|
// Process children recursively
|
|
810
853
|
const childResults = await Promise.all(
|
|
811
|
-
rootQuery.children.map((child: any) =>
|
|
854
|
+
rootQuery.children.map((child: any) =>
|
|
855
|
+
this.structureFetchedQueries(child, onlyTestReq, rootQuery.id, sources, targets)
|
|
856
|
+
)
|
|
812
857
|
);
|
|
813
858
|
|
|
814
859
|
// Build tree1
|
|
@@ -848,20 +893,36 @@ export default class TicketsDataProvider {
|
|
|
848
893
|
}
|
|
849
894
|
}
|
|
850
895
|
|
|
851
|
-
|
|
852
|
-
|
|
853
|
-
|
|
854
|
-
|
|
855
|
-
|
|
856
|
-
|
|
857
|
-
|
|
896
|
+
/**
|
|
897
|
+
* Determines whether the given WIQL (Work Item Query Language) string matches the specified
|
|
898
|
+
* source and target conditions. It checks if the WIQL contains references to the specified
|
|
899
|
+
* source and target work item types.
|
|
900
|
+
*
|
|
901
|
+
* @param wiql - The WIQL string to evaluate.
|
|
902
|
+
* @param source - An array of source work item types to check for in the WIQL.
|
|
903
|
+
* @param target - An array of target work item types to check for in the WIQL.
|
|
904
|
+
* @returns A boolean indicating whether the WIQL includes at least one source work item type
|
|
905
|
+
* and at least one target work item type.
|
|
906
|
+
*/
|
|
907
|
+
private matchesSourceTargetCondition(wiql: string, source: string[], target: string[]): boolean {
|
|
908
|
+
let isSourceIncluded = false;
|
|
909
|
+
let isTargetIncluded = false;
|
|
910
|
+
|
|
911
|
+
for (const src of source) {
|
|
912
|
+
if (wiql.includes(`Source.[System.WorkItemType] = '${src}'`)) {
|
|
913
|
+
isSourceIncluded = true;
|
|
914
|
+
break;
|
|
915
|
+
}
|
|
916
|
+
}
|
|
858
917
|
|
|
859
|
-
|
|
860
|
-
|
|
861
|
-
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
|
|
918
|
+
for (const tgt of target) {
|
|
919
|
+
if (wiql.includes(`Target.[System.WorkItemType] = '${tgt}'`)) {
|
|
920
|
+
isTargetIncluded = true;
|
|
921
|
+
break;
|
|
922
|
+
}
|
|
923
|
+
}
|
|
924
|
+
|
|
925
|
+
return isSourceIncluded && isTargetIncluded;
|
|
865
926
|
}
|
|
866
927
|
|
|
867
928
|
// check if the query is a bug query
|