@elisra-devops/docgen-data-provider 1.88.0 → 1.90.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/mewp-reporting.d.ts +0 -4
- package/bin/modules/ResultDataProvider.d.ts +8 -7
- package/bin/modules/ResultDataProvider.js +153 -234
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +50 -18
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/package.json +1 -1
- package/src/models/mewp-reporting.ts +0 -5
- package/src/modules/ResultDataProvider.ts +185 -254
- package/src/tests/modules/ResultDataProvider.test.ts +73 -19
package/package.json
CHANGED
|
@@ -13,15 +13,10 @@ export interface MewpExternalFileRef {
|
|
|
13
13
|
}
|
|
14
14
|
|
|
15
15
|
export interface MewpCoverageRequestOptions {
|
|
16
|
-
useRelFallback?: boolean;
|
|
17
16
|
externalBugsFile?: MewpExternalFileRef | null;
|
|
18
17
|
externalL3L4File?: MewpExternalFileRef | null;
|
|
19
18
|
}
|
|
20
19
|
|
|
21
|
-
export interface MewpInternalValidationRequestOptions {
|
|
22
|
-
useRelFallback?: boolean;
|
|
23
|
-
}
|
|
24
|
-
|
|
25
20
|
export interface MewpRequirementStepSummary {
|
|
26
21
|
passed: number;
|
|
27
22
|
failed: number;
|
|
@@ -13,7 +13,6 @@ import type {
|
|
|
13
13
|
MewpExternalTableValidationResult,
|
|
14
14
|
MewpCoverageRow,
|
|
15
15
|
MewpInternalValidationFlatPayload,
|
|
16
|
-
MewpInternalValidationRequestOptions,
|
|
17
16
|
MewpInternalValidationRow,
|
|
18
17
|
MewpL2RequirementFamily,
|
|
19
18
|
MewpL2RequirementWorkItem,
|
|
@@ -446,8 +445,7 @@ export default class ResultDataProvider {
|
|
|
446
445
|
const testData = await this.fetchMewpScopedTestData(
|
|
447
446
|
testPlanId,
|
|
448
447
|
projectName,
|
|
449
|
-
selectedSuiteIds
|
|
450
|
-
!!options?.useRelFallback
|
|
448
|
+
selectedSuiteIds
|
|
451
449
|
);
|
|
452
450
|
|
|
453
451
|
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
@@ -734,8 +732,7 @@ export default class ResultDataProvider {
|
|
|
734
732
|
testPlanId: string,
|
|
735
733
|
projectName: string,
|
|
736
734
|
selectedSuiteIds: number[] | undefined,
|
|
737
|
-
linkedQueryRequest?: any
|
|
738
|
-
options?: MewpInternalValidationRequestOptions
|
|
735
|
+
linkedQueryRequest?: any
|
|
739
736
|
): Promise<MewpInternalValidationFlatPayload> {
|
|
740
737
|
const defaultPayload: MewpInternalValidationFlatPayload = {
|
|
741
738
|
sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
|
|
@@ -748,8 +745,7 @@ export default class ResultDataProvider {
|
|
|
748
745
|
const testData = await this.fetchMewpScopedTestData(
|
|
749
746
|
testPlanId,
|
|
750
747
|
projectName,
|
|
751
|
-
selectedSuiteIds
|
|
752
|
-
!!options?.useRelFallback
|
|
748
|
+
selectedSuiteIds
|
|
753
749
|
);
|
|
754
750
|
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
755
751
|
const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
|
|
@@ -808,34 +804,6 @@ export default class ResultDataProvider {
|
|
|
808
804
|
|
|
809
805
|
for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
|
|
810
806
|
diagnostics.totalTestCases += 1;
|
|
811
|
-
const logList = (items: Iterable<string>, max = 12): string => {
|
|
812
|
-
const values = [...items].map((item) => String(item || '').trim()).filter((item) => !!item);
|
|
813
|
-
const shown = values.slice(0, max);
|
|
814
|
-
const suffix = values.length > max ? ` ...(+${values.length - max} more)` : '';
|
|
815
|
-
return shown.join(', ') + suffix;
|
|
816
|
-
};
|
|
817
|
-
const logByFamily = (items: Iterable<string>, maxFamilies = 8, maxMembers = 10): string => {
|
|
818
|
-
const map = new Map<string, Set<string>>();
|
|
819
|
-
for (const item of items || []) {
|
|
820
|
-
const normalized = this.normalizeMewpRequirementCodeWithSuffix(String(item || ''));
|
|
821
|
-
if (!normalized) continue;
|
|
822
|
-
const base = this.toRequirementKey(normalized) || normalized;
|
|
823
|
-
if (!map.has(base)) map.set(base, new Set<string>());
|
|
824
|
-
map.get(base)!.add(normalized);
|
|
825
|
-
}
|
|
826
|
-
const entries = [...map.entries()]
|
|
827
|
-
.sort((a, b) => a[0].localeCompare(b[0]))
|
|
828
|
-
.slice(0, maxFamilies)
|
|
829
|
-
.map(([base, members]) => {
|
|
830
|
-
const sortedMembers = [...members].sort((a, b) => a.localeCompare(b));
|
|
831
|
-
const shownMembers = sortedMembers.slice(0, maxMembers);
|
|
832
|
-
const membersSuffix =
|
|
833
|
-
sortedMembers.length > maxMembers ? ` ...(+${sortedMembers.length - maxMembers} more)` : '';
|
|
834
|
-
return `${base}=[${shownMembers.join(', ')}${membersSuffix}]`;
|
|
835
|
-
});
|
|
836
|
-
const suffix = map.size > maxFamilies ? ` ...(+${map.size - maxFamilies} families)` : '';
|
|
837
|
-
return entries.join(' | ') + suffix;
|
|
838
|
-
};
|
|
839
807
|
const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
|
|
840
808
|
const parsedSteps =
|
|
841
809
|
stepsXml && String(stepsXml).trim() !== ''
|
|
@@ -888,15 +856,6 @@ export default class ResultDataProvider {
|
|
|
888
856
|
const linkedBaseKeys = new Set<string>(
|
|
889
857
|
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
890
858
|
);
|
|
891
|
-
const mentionStepSample = mentionEntries
|
|
892
|
-
.slice(0, 8)
|
|
893
|
-
.map((entry) => `${entry.stepRef}=[${logList(entry.codes, 6)}]`)
|
|
894
|
-
.join(' | ');
|
|
895
|
-
logger.debug(
|
|
896
|
-
`MEWP internal validation trace: testCaseId=${testCaseId} ` +
|
|
897
|
-
`mentionSteps=${mentionEntries.length} mentionStepSample='${mentionStepSample}' ` +
|
|
898
|
-
`mentionedL2ByFamily='${logByFamily(mentionedL2Only)}' linkedByFamily='${logByFamily(linkedFullCodes)}'`
|
|
899
|
-
);
|
|
900
859
|
|
|
901
860
|
const mentionedCodesByBase = new Map<string, Set<string>>();
|
|
902
861
|
for (const code of mentionedL2Only) {
|
|
@@ -918,89 +877,49 @@ export default class ResultDataProvider {
|
|
|
918
877
|
const mentionedCodesList = [...mentionedCodes];
|
|
919
878
|
const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
|
|
920
879
|
const mentionedSpecificMembers = mentionedCodesList.filter((code) => /-\d+$/.test(code));
|
|
921
|
-
let familyDecision = 'no-action';
|
|
922
|
-
let familyTargetCodes: string[] = [];
|
|
923
|
-
let familyMissingCodes: string[] = [];
|
|
924
|
-
let familyLinkedCodes: string[] = [];
|
|
925
|
-
let familyAllCodes: string[] = [];
|
|
926
880
|
|
|
927
881
|
if (familyCodes?.size) {
|
|
928
|
-
familyAllCodes = [...familyCodes].sort((a, b) => a.localeCompare(b));
|
|
929
|
-
familyLinkedCodes = familyAllCodes.filter((code) => linkedFullCodes.has(code));
|
|
930
882
|
// Base mention ("SR0054") validates against child coverage when children exist.
|
|
931
883
|
// If no child variants exist, fallback to the single standalone requirement code.
|
|
932
884
|
if (hasBaseMention) {
|
|
933
885
|
const familyCodesList = [...familyCodes];
|
|
934
886
|
const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
|
|
935
887
|
const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
|
|
936
|
-
familyTargetCodes = [...targetFamilyCodes].sort((a, b) => a.localeCompare(b));
|
|
937
888
|
const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
|
|
938
|
-
familyMissingCodes = [...missingInTargetFamily].sort((a, b) => a.localeCompare(b));
|
|
939
889
|
|
|
940
890
|
if (missingInTargetFamily.length > 0) {
|
|
941
891
|
const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
|
|
942
892
|
if (!hasAnyLinkedInFamily) {
|
|
943
893
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
944
|
-
familyDecision = 'base-mentioned-family-uncovered';
|
|
945
894
|
} else {
|
|
946
895
|
for (const code of missingInTargetFamily) {
|
|
947
896
|
missingFamilyMembers.add(code);
|
|
948
897
|
}
|
|
949
|
-
familyDecision = 'base-mentioned-family-partial-missing-children';
|
|
950
898
|
}
|
|
951
|
-
} else {
|
|
952
|
-
familyDecision = 'base-mentioned-family-fully-covered';
|
|
953
899
|
}
|
|
954
|
-
logger.debug(
|
|
955
|
-
`MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
|
|
956
|
-
`mode=baseMention mentioned='${logList(mentionedCodesList)}' familyAll='${logList(familyAllCodes)}' ` +
|
|
957
|
-
`target='${logList(familyTargetCodes)}' linked='${logList(familyLinkedCodes)}' ` +
|
|
958
|
-
`missing='${logList(familyMissingCodes)}' decision=${familyDecision}`
|
|
959
|
-
);
|
|
960
900
|
continue;
|
|
961
901
|
}
|
|
962
902
|
|
|
963
903
|
// Specific mention ("SR0054-1") validates as exact-match only.
|
|
964
|
-
const missingSpecificMembers: string[] = [];
|
|
965
904
|
for (const code of mentionedSpecificMembers) {
|
|
966
905
|
if (!linkedFullCodes.has(code)) {
|
|
967
906
|
missingSpecificMentionedNoFamily.add(code);
|
|
968
|
-
missingSpecificMembers.push(code);
|
|
969
907
|
}
|
|
970
908
|
}
|
|
971
|
-
familyDecision =
|
|
972
|
-
missingSpecificMembers.length > 0
|
|
973
|
-
? 'specific-mentioned-exact-missing'
|
|
974
|
-
: 'specific-mentioned-exact-covered';
|
|
975
|
-
logger.debug(
|
|
976
|
-
`MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
|
|
977
|
-
`mode=specificMention mentioned='${logList(mentionedCodesList)}' familyAll='${logList(familyAllCodes)}' ` +
|
|
978
|
-
`linked='${logList(familyLinkedCodes)}' missingSpecific='${logList(missingSpecificMembers)}' ` +
|
|
979
|
-
`decision=${familyDecision}`
|
|
980
|
-
);
|
|
981
909
|
continue;
|
|
982
910
|
}
|
|
983
911
|
|
|
984
912
|
// Fallback path when family data is unavailable for this base key.
|
|
985
|
-
const fallbackMissingSpecific: string[] = [];
|
|
986
|
-
let fallbackBaseMissing = false;
|
|
987
913
|
for (const code of mentionedCodes) {
|
|
988
914
|
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
989
915
|
if (hasSpecificSuffix) {
|
|
990
916
|
if (!linkedFullCodes.has(code)) {
|
|
991
917
|
missingSpecificMentionedNoFamily.add(code);
|
|
992
|
-
fallbackMissingSpecific.push(code);
|
|
993
918
|
}
|
|
994
919
|
} else if (!linkedBaseKeys.has(baseKey)) {
|
|
995
920
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
996
|
-
fallbackBaseMissing = true;
|
|
997
921
|
}
|
|
998
922
|
}
|
|
999
|
-
logger.debug(
|
|
1000
|
-
`MEWP internal validation family decision: testCaseId=${testCaseId} base=${baseKey} ` +
|
|
1001
|
-
`mode=noFamilyData mentioned='${logList(mentionedCodesList)}' linkedBasePresent=${linkedBaseKeys.has(baseKey)} ` +
|
|
1002
|
-
`missingSpecific='${logList(fallbackMissingSpecific)}' missingBase=${fallbackBaseMissing}`
|
|
1003
|
-
);
|
|
1004
923
|
}
|
|
1005
924
|
|
|
1006
925
|
// Direction B is family-based: if any member of a family is mentioned in Expected Result,
|
|
@@ -1063,16 +982,8 @@ export default class ResultDataProvider {
|
|
|
1063
982
|
const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
|
|
1064
983
|
return `${stepRef}: ${groupedRequirementList}`;
|
|
1065
984
|
})
|
|
1066
|
-
.join('
|
|
985
|
+
.join('\n');
|
|
1067
986
|
const linkedButNotMentioned = this.formatRequirementCodesGroupedByFamily(sortedExtraLinked);
|
|
1068
|
-
const rawMentionedByStepForLog = [...mentionedButNotLinkedByStep.entries()]
|
|
1069
|
-
.map(([stepRef, requirementIds]) => `${stepRef}=[${logList(requirementIds, 8)}]`)
|
|
1070
|
-
.join(' | ');
|
|
1071
|
-
logger.debug(
|
|
1072
|
-
`MEWP internal validation grouped diagnostics: testCaseId=${testCaseId} ` +
|
|
1073
|
-
`rawMentionedByStep='${rawMentionedByStepForLog}' groupedMentioned='${mentionedButNotLinked}' ` +
|
|
1074
|
-
`rawLinkedOnlyByFamily='${logByFamily(sortedExtraLinked)}' groupedLinkedOnly='${linkedButNotMentioned}'`
|
|
1075
|
-
);
|
|
1076
987
|
const validationStatus: 'Pass' | 'Fail' =
|
|
1077
988
|
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
1078
989
|
if (validationStatus === 'Fail') diagnostics.failingRows += 1;
|
|
@@ -1534,139 +1445,10 @@ export default class ResultDataProvider {
|
|
|
1534
1445
|
private async fetchMewpScopedTestData(
|
|
1535
1446
|
testPlanId: string,
|
|
1536
1447
|
projectName: string,
|
|
1537
|
-
selectedSuiteIds: number[] | undefined
|
|
1538
|
-
useRelFallback: boolean
|
|
1448
|
+
selectedSuiteIds: number[] | undefined
|
|
1539
1449
|
): Promise<any[]> {
|
|
1540
|
-
|
|
1541
|
-
|
|
1542
|
-
return this.fetchTestData(suites, projectName, testPlanId, false);
|
|
1543
|
-
}
|
|
1544
|
-
|
|
1545
|
-
const selectedSuites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1546
|
-
const selectedRel = this.resolveMaxRelNumberFromSuites(selectedSuites);
|
|
1547
|
-
if (selectedRel <= 0) {
|
|
1548
|
-
return this.fetchTestData(selectedSuites, projectName, testPlanId, false);
|
|
1549
|
-
}
|
|
1550
|
-
|
|
1551
|
-
const allSuites = await this.fetchTestSuites(testPlanId, projectName, undefined, true);
|
|
1552
|
-
const relScopedSuites = allSuites.filter((suite) => {
|
|
1553
|
-
const rel = this.extractRelNumberFromSuite(suite);
|
|
1554
|
-
return rel > 0 && rel <= selectedRel;
|
|
1555
|
-
});
|
|
1556
|
-
const suitesForFetch = relScopedSuites.length > 0 ? relScopedSuites : selectedSuites;
|
|
1557
|
-
const rawTestData = await this.fetchTestData(suitesForFetch, projectName, testPlanId, false);
|
|
1558
|
-
return this.reduceToLatestRelRunPerTestCase(rawTestData);
|
|
1559
|
-
}
|
|
1560
|
-
|
|
1561
|
-
private extractRelNumberFromSuite(suite: any): number {
|
|
1562
|
-
const candidates = [
|
|
1563
|
-
suite?.suiteName,
|
|
1564
|
-
suite?.parentSuiteName,
|
|
1565
|
-
suite?.suitePath,
|
|
1566
|
-
suite?.testGroupName,
|
|
1567
|
-
];
|
|
1568
|
-
const pattern = /(?:^|[^a-z0-9])rel\s*([0-9]+)/i;
|
|
1569
|
-
for (const item of candidates) {
|
|
1570
|
-
const match = pattern.exec(String(item || ''));
|
|
1571
|
-
if (!match) continue;
|
|
1572
|
-
const parsed = Number(match[1]);
|
|
1573
|
-
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1574
|
-
return parsed;
|
|
1575
|
-
}
|
|
1576
|
-
}
|
|
1577
|
-
return 0;
|
|
1578
|
-
}
|
|
1579
|
-
|
|
1580
|
-
private resolveMaxRelNumberFromSuites(suites: any[]): number {
|
|
1581
|
-
let maxRel = 0;
|
|
1582
|
-
for (const suite of suites || []) {
|
|
1583
|
-
const rel = this.extractRelNumberFromSuite(suite);
|
|
1584
|
-
if (rel > maxRel) maxRel = rel;
|
|
1585
|
-
}
|
|
1586
|
-
return maxRel;
|
|
1587
|
-
}
|
|
1588
|
-
|
|
1589
|
-
private reduceToLatestRelRunPerTestCase(testData: any[]): any[] {
|
|
1590
|
-
type Candidate = {
|
|
1591
|
-
point: any;
|
|
1592
|
-
rel: number;
|
|
1593
|
-
runId: number;
|
|
1594
|
-
resultId: number;
|
|
1595
|
-
hasRun: boolean;
|
|
1596
|
-
};
|
|
1597
|
-
|
|
1598
|
-
const candidatesByTestCase = new Map<number, Candidate[]>();
|
|
1599
|
-
const testCaseDefinitionById = new Map<number, any>();
|
|
1600
|
-
|
|
1601
|
-
for (const suite of testData || []) {
|
|
1602
|
-
const rel = this.extractRelNumberFromSuite(suite);
|
|
1603
|
-
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1604
|
-
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1605
|
-
|
|
1606
|
-
for (const testCase of testCasesItems) {
|
|
1607
|
-
const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1608
|
-
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1609
|
-
if (!testCaseDefinitionById.has(testCaseId)) {
|
|
1610
|
-
testCaseDefinitionById.set(testCaseId, testCase);
|
|
1611
|
-
}
|
|
1612
|
-
}
|
|
1613
|
-
|
|
1614
|
-
for (const point of testPointsItems) {
|
|
1615
|
-
const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
|
|
1616
|
-
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1617
|
-
|
|
1618
|
-
const runId = Number(point?.lastRunId || 0);
|
|
1619
|
-
const resultId = Number(point?.lastResultId || 0);
|
|
1620
|
-
const hasRun = runId > 0 && resultId > 0;
|
|
1621
|
-
if (!candidatesByTestCase.has(testCaseId)) {
|
|
1622
|
-
candidatesByTestCase.set(testCaseId, []);
|
|
1623
|
-
}
|
|
1624
|
-
candidatesByTestCase.get(testCaseId)!.push({
|
|
1625
|
-
point,
|
|
1626
|
-
rel,
|
|
1627
|
-
runId,
|
|
1628
|
-
resultId,
|
|
1629
|
-
hasRun,
|
|
1630
|
-
});
|
|
1631
|
-
}
|
|
1632
|
-
}
|
|
1633
|
-
|
|
1634
|
-
const selectedPoints: any[] = [];
|
|
1635
|
-
const selectedTestCaseIds = new Set<number>();
|
|
1636
|
-
for (const [testCaseId, candidates] of candidatesByTestCase.entries()) {
|
|
1637
|
-
const sorted = [...candidates].sort((a, b) => {
|
|
1638
|
-
if (a.hasRun !== b.hasRun) return a.hasRun ? -1 : 1;
|
|
1639
|
-
if (a.rel !== b.rel) return b.rel - a.rel;
|
|
1640
|
-
if (a.runId !== b.runId) return b.runId - a.runId;
|
|
1641
|
-
return b.resultId - a.resultId;
|
|
1642
|
-
});
|
|
1643
|
-
const chosen = sorted[0];
|
|
1644
|
-
if (!chosen?.point) continue;
|
|
1645
|
-
selectedPoints.push(chosen.point);
|
|
1646
|
-
selectedTestCaseIds.add(testCaseId);
|
|
1647
|
-
}
|
|
1648
|
-
|
|
1649
|
-
const selectedTestCases: any[] = [];
|
|
1650
|
-
for (const testCaseId of selectedTestCaseIds) {
|
|
1651
|
-
const definition = testCaseDefinitionById.get(testCaseId);
|
|
1652
|
-
if (definition) {
|
|
1653
|
-
selectedTestCases.push(definition);
|
|
1654
|
-
}
|
|
1655
|
-
}
|
|
1656
|
-
|
|
1657
|
-
return [
|
|
1658
|
-
{
|
|
1659
|
-
testSuiteId: 'MEWP_REL_SCOPED',
|
|
1660
|
-
suiteId: 'MEWP_REL_SCOPED',
|
|
1661
|
-
suiteName: 'MEWP Rel Scoped',
|
|
1662
|
-
parentSuiteId: '',
|
|
1663
|
-
parentSuiteName: '',
|
|
1664
|
-
suitePath: 'MEWP Rel Scoped',
|
|
1665
|
-
testGroupName: 'MEWP Rel Scoped',
|
|
1666
|
-
testPointsItems: selectedPoints,
|
|
1667
|
-
testCasesItems: selectedTestCases,
|
|
1668
|
-
},
|
|
1669
|
-
];
|
|
1450
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1451
|
+
return this.fetchTestData(suites, projectName, testPlanId, false);
|
|
1670
1452
|
}
|
|
1671
1453
|
|
|
1672
1454
|
private async loadExternalBugsByTestCase(
|
|
@@ -2010,16 +1792,6 @@ export default class ResultDataProvider {
|
|
|
2010
1792
|
return out;
|
|
2011
1793
|
}
|
|
2012
1794
|
|
|
2013
|
-
private extractRequirementCodesFromExpectedSteps(steps: TestSteps[], includeSuffix: boolean): Set<string> {
|
|
2014
|
-
const out = new Set<string>();
|
|
2015
|
-
for (const step of Array.isArray(steps) ? steps : []) {
|
|
2016
|
-
if (step?.isSharedStepTitle) continue;
|
|
2017
|
-
const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
|
|
2018
|
-
codes.forEach((code) => out.add(code));
|
|
2019
|
-
}
|
|
2020
|
-
return out;
|
|
2021
|
-
}
|
|
2022
|
-
|
|
2023
1795
|
private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
|
|
2024
1796
|
const out = new Set<string>();
|
|
2025
1797
|
const source = this.normalizeRequirementStepText(text);
|
|
@@ -2862,7 +2634,7 @@ export default class ResultDataProvider {
|
|
|
2862
2634
|
if (sortedMembers.length <= 1) return sortedMembers[0];
|
|
2863
2635
|
return `${baseKey}: ${sortedMembers.join(', ')}`;
|
|
2864
2636
|
})
|
|
2865
|
-
.join('
|
|
2637
|
+
.join('\n');
|
|
2866
2638
|
}
|
|
2867
2639
|
|
|
2868
2640
|
private toMewpComparableText(value: any): string {
|
|
@@ -3183,6 +2955,140 @@ export default class ResultDataProvider {
|
|
|
3183
2955
|
return testCases;
|
|
3184
2956
|
}
|
|
3185
2957
|
|
|
2958
|
+
private attachSuiteTestCaseContextToPoints(testCasesItems: any[], testPointsItems: any[]): any[] {
|
|
2959
|
+
const points = Array.isArray(testPointsItems) ? testPointsItems : [];
|
|
2960
|
+
const testCases = Array.isArray(testCasesItems) ? testCasesItems : [];
|
|
2961
|
+
if (points.length === 0 || testCases.length === 0) return points;
|
|
2962
|
+
|
|
2963
|
+
const byTestCaseId = new Map<number, any>();
|
|
2964
|
+
for (const testCaseItem of testCases) {
|
|
2965
|
+
const testCaseId = Number(
|
|
2966
|
+
testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || 0
|
|
2967
|
+
);
|
|
2968
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || byTestCaseId.has(testCaseId)) continue;
|
|
2969
|
+
byTestCaseId.set(testCaseId, testCaseItem);
|
|
2970
|
+
}
|
|
2971
|
+
|
|
2972
|
+
return points.map((point: any) => {
|
|
2973
|
+
const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
|
|
2974
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return point;
|
|
2975
|
+
const suiteTestCase = byTestCaseId.get(testCaseId);
|
|
2976
|
+
if (!suiteTestCase) return point;
|
|
2977
|
+
return { ...point, suiteTestCase };
|
|
2978
|
+
});
|
|
2979
|
+
}
|
|
2980
|
+
|
|
2981
|
+
private extractWorkItemFieldsMap(workItemFields: any): Record<string, any> {
|
|
2982
|
+
const fields: Record<string, any> = {};
|
|
2983
|
+
if (!Array.isArray(workItemFields)) return fields;
|
|
2984
|
+
for (const field of workItemFields) {
|
|
2985
|
+
const keyCandidates = [field?.key, field?.name, field?.referenceName]
|
|
2986
|
+
.map((item) => String(item || '').trim())
|
|
2987
|
+
.filter((item) => !!item);
|
|
2988
|
+
for (const key of keyCandidates) {
|
|
2989
|
+
fields[key] = field?.value;
|
|
2990
|
+
}
|
|
2991
|
+
}
|
|
2992
|
+
return fields;
|
|
2993
|
+
}
|
|
2994
|
+
|
|
2995
|
+
private resolveSuiteTestCaseRevision(testCaseItem: any): number {
|
|
2996
|
+
const revisionCandidates = [
|
|
2997
|
+
testCaseItem?.workItem?.rev,
|
|
2998
|
+
testCaseItem?.workItem?.revision,
|
|
2999
|
+
testCaseItem?.workItem?.version,
|
|
3000
|
+
testCaseItem?.workItem?.workItemRevision,
|
|
3001
|
+
testCaseItem?.workItem?.workItemVersion,
|
|
3002
|
+
testCaseItem?.revision,
|
|
3003
|
+
testCaseItem?.workItemRevision,
|
|
3004
|
+
testCaseItem?.workItemVersion,
|
|
3005
|
+
];
|
|
3006
|
+
for (const candidate of revisionCandidates) {
|
|
3007
|
+
const parsed = Number(candidate || 0);
|
|
3008
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
3009
|
+
}
|
|
3010
|
+
return 0;
|
|
3011
|
+
}
|
|
3012
|
+
|
|
3013
|
+
private buildWorkItemSnapshotFromSuiteTestCase(
|
|
3014
|
+
testCaseItem: any,
|
|
3015
|
+
fallbackTestCaseId: number,
|
|
3016
|
+
fallbackTestCaseName: string = ''
|
|
3017
|
+
): any | null {
|
|
3018
|
+
if (!testCaseItem) return null;
|
|
3019
|
+
|
|
3020
|
+
const testCaseId = Number(
|
|
3021
|
+
testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || fallbackTestCaseId || 0
|
|
3022
|
+
);
|
|
3023
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return null;
|
|
3024
|
+
|
|
3025
|
+
const workItem = testCaseItem?.workItem || {};
|
|
3026
|
+
const stepsXml = this.extractStepsXmlFromTestCaseItem(testCaseItem);
|
|
3027
|
+
const fieldsFromList = this.extractWorkItemFieldsMap(workItem?.workItemFields);
|
|
3028
|
+
const fieldsFromMap = workItem?.fields || {};
|
|
3029
|
+
const fields = {
|
|
3030
|
+
...fieldsFromList,
|
|
3031
|
+
...fieldsFromMap,
|
|
3032
|
+
};
|
|
3033
|
+
|
|
3034
|
+
if (!fields['System.Title']) {
|
|
3035
|
+
const title = String(
|
|
3036
|
+
testCaseItem?.testCaseName || workItem?.name || testCaseItem?.name || fallbackTestCaseName || ''
|
|
3037
|
+
).trim();
|
|
3038
|
+
if (title) {
|
|
3039
|
+
fields['System.Title'] = title;
|
|
3040
|
+
}
|
|
3041
|
+
}
|
|
3042
|
+
if (stepsXml && !fields['Microsoft.VSTS.TCM.Steps']) {
|
|
3043
|
+
fields['Microsoft.VSTS.TCM.Steps'] = stepsXml;
|
|
3044
|
+
}
|
|
3045
|
+
|
|
3046
|
+
return {
|
|
3047
|
+
id: testCaseId,
|
|
3048
|
+
rev: this.resolveSuiteTestCaseRevision(testCaseItem) || 1,
|
|
3049
|
+
fields,
|
|
3050
|
+
relations: Array.isArray(workItem?.relations) ? workItem.relations : [],
|
|
3051
|
+
};
|
|
3052
|
+
}
|
|
3053
|
+
|
|
3054
|
+
private async fetchWorkItemByRevision(
|
|
3055
|
+
projectName: string,
|
|
3056
|
+
workItemId: number,
|
|
3057
|
+
revision: number,
|
|
3058
|
+
expandAll: boolean = false
|
|
3059
|
+
): Promise<any | null> {
|
|
3060
|
+
const id = Number(workItemId || 0);
|
|
3061
|
+
const rev = Number(revision || 0);
|
|
3062
|
+
if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(rev) || rev <= 0) return null;
|
|
3063
|
+
|
|
3064
|
+
const expandParam = expandAll ? '?$expand=all' : '';
|
|
3065
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
|
|
3066
|
+
try {
|
|
3067
|
+
return await TFSServices.getItemContent(url, this.token);
|
|
3068
|
+
} catch (error: any) {
|
|
3069
|
+
logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
|
|
3070
|
+
return null;
|
|
3071
|
+
}
|
|
3072
|
+
}
|
|
3073
|
+
|
|
3074
|
+
private async fetchWorkItemLatest(
|
|
3075
|
+
projectName: string,
|
|
3076
|
+
workItemId: number,
|
|
3077
|
+
expandAll: boolean = false
|
|
3078
|
+
): Promise<any | null> {
|
|
3079
|
+
const id = Number(workItemId || 0);
|
|
3080
|
+
if (!Number.isFinite(id) || id <= 0) return null;
|
|
3081
|
+
|
|
3082
|
+
const expandParam = expandAll ? '?$expand=all' : '';
|
|
3083
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
|
|
3084
|
+
try {
|
|
3085
|
+
return await TFSServices.getItemContent(url, this.token);
|
|
3086
|
+
} catch (error: any) {
|
|
3087
|
+
logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
|
|
3088
|
+
return null;
|
|
3089
|
+
}
|
|
3090
|
+
}
|
|
3091
|
+
|
|
3186
3092
|
/**
|
|
3187
3093
|
* Fetches result data based on the Work Item Test Reporter.
|
|
3188
3094
|
*
|
|
@@ -3217,13 +3123,37 @@ export default class ResultDataProvider {
|
|
|
3217
3123
|
logger.warn(`Invalid run result ${runId} or result ${resultId}`);
|
|
3218
3124
|
return null;
|
|
3219
3125
|
}
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3126
|
+
const suiteTestCaseItem = point?.suiteTestCase;
|
|
3127
|
+
const testCaseId = Number(
|
|
3128
|
+
point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
|
|
3129
|
+
);
|
|
3130
|
+
const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
|
|
3131
|
+
const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
|
|
3132
|
+
suiteTestCaseItem,
|
|
3133
|
+
testCaseId,
|
|
3134
|
+
String(point?.testCaseName || '')
|
|
3135
|
+
);
|
|
3136
|
+
let testCaseData = await this.fetchWorkItemByRevision(
|
|
3137
|
+
projectName,
|
|
3138
|
+
testCaseId,
|
|
3139
|
+
suiteTestCaseRevision,
|
|
3140
|
+
isTestReporter
|
|
3141
|
+
);
|
|
3142
|
+
if (!testCaseData) {
|
|
3143
|
+
testCaseData = fallbackSnapshot;
|
|
3144
|
+
}
|
|
3145
|
+
if (!testCaseData) {
|
|
3146
|
+
testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
|
|
3147
|
+
}
|
|
3148
|
+
if (!testCaseData) {
|
|
3149
|
+
logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
|
|
3150
|
+
return null;
|
|
3151
|
+
}
|
|
3222
3152
|
const newResultData: PlainTestResult = {
|
|
3223
3153
|
id: 0,
|
|
3224
3154
|
outcome: point.outcome,
|
|
3225
|
-
revision: testCaseData?.rev || 1,
|
|
3226
|
-
testCase: { id:
|
|
3155
|
+
revision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
|
|
3156
|
+
testCase: { id: String(testCaseId), name: point.testCaseName },
|
|
3227
3157
|
state: testCaseData?.fields?.['System.State'] || 'Active',
|
|
3228
3158
|
priority: testCaseData?.fields?.['Microsoft.VSTS.TCM.Priority'] || 0,
|
|
3229
3159
|
createdDate: testCaseData?.fields?.['System.CreatedDate'] || '0001-01-01T00:00:00',
|
|
@@ -3271,8 +3201,8 @@ export default class ResultDataProvider {
|
|
|
3271
3201
|
selectedFieldSet.clear();
|
|
3272
3202
|
return {
|
|
3273
3203
|
...newResultData,
|
|
3274
|
-
stepsResultXml: testCaseData
|
|
3275
|
-
testCaseRevision: testCaseData
|
|
3204
|
+
stepsResultXml: testCaseData?.fields?.['Microsoft.VSTS.TCM.Steps'] || undefined,
|
|
3205
|
+
testCaseRevision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
|
|
3276
3206
|
filteredFields,
|
|
3277
3207
|
relatedRequirements,
|
|
3278
3208
|
relatedBugs,
|
|
@@ -3442,13 +3372,6 @@ export default class ResultDataProvider {
|
|
|
3442
3372
|
);
|
|
3443
3373
|
}
|
|
3444
3374
|
|
|
3445
|
-
private isRunResultDebugEnabled(): boolean {
|
|
3446
|
-
return (
|
|
3447
|
-
String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '').toLowerCase() === 'true' ||
|
|
3448
|
-
String(process?.env?.DOCGEN_DEBUG_RUNRESULT ?? '') === '1'
|
|
3449
|
-
);
|
|
3450
|
-
}
|
|
3451
|
-
|
|
3452
3375
|
private extractCommentText(comment: AdoWorkItemComment): string {
|
|
3453
3376
|
const rendered = comment?.renderedText;
|
|
3454
3377
|
// In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
|
|
@@ -4210,9 +4133,17 @@ export default class ResultDataProvider {
|
|
|
4210
4133
|
suite.testSuiteId
|
|
4211
4134
|
);
|
|
4212
4135
|
const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
|
|
4213
|
-
const
|
|
4214
|
-
? await this.fetchTestPoints(
|
|
4136
|
+
const rawTestPointsItems = !fetchCrossPlans
|
|
4137
|
+
? await this.fetchTestPoints(
|
|
4138
|
+
projectName,
|
|
4139
|
+
testPlanId,
|
|
4140
|
+
suite.testSuiteId
|
|
4141
|
+
)
|
|
4215
4142
|
: await this.fetchCrossTestPoints(projectName, testCaseIds);
|
|
4143
|
+
const testPointsItems = this.attachSuiteTestCaseContextToPoints(
|
|
4144
|
+
testCasesItems,
|
|
4145
|
+
rawTestPointsItems
|
|
4146
|
+
);
|
|
4216
4147
|
|
|
4217
4148
|
return { ...suite, testPointsItems, testCasesItems };
|
|
4218
4149
|
} catch (error: any) {
|
|
@@ -5074,6 +5005,7 @@ export default class ResultDataProvider {
|
|
|
5074
5005
|
resultData.iterationDetails?.length > 0
|
|
5075
5006
|
? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
|
|
5076
5007
|
: undefined;
|
|
5008
|
+
const testOutcome = this.getTestOutcome(resultData);
|
|
5077
5009
|
|
|
5078
5010
|
if (!resultData?.testCase || !resultData?.testSuite) {
|
|
5079
5011
|
logger.debug(
|
|
@@ -5126,15 +5058,14 @@ export default class ResultDataProvider {
|
|
|
5126
5058
|
resultDataResponse.priority = resultData.priority;
|
|
5127
5059
|
break;
|
|
5128
5060
|
case 'testCaseResult':
|
|
5129
|
-
const outcome = this.getTestOutcome(resultData);
|
|
5130
5061
|
if (lastRunId === undefined || lastResultId === undefined) {
|
|
5131
5062
|
resultDataResponse.testCaseResult = {
|
|
5132
|
-
resultMessage: `${this.convertRunStatus(
|
|
5063
|
+
resultMessage: `${this.convertRunStatus(testOutcome)}`,
|
|
5133
5064
|
url: '',
|
|
5134
5065
|
};
|
|
5135
5066
|
} else {
|
|
5136
5067
|
resultDataResponse.testCaseResult = {
|
|
5137
|
-
resultMessage: `${this.convertRunStatus(
|
|
5068
|
+
resultMessage: `${this.convertRunStatus(testOutcome)} in Run ${lastRunId}`,
|
|
5138
5069
|
url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
|
|
5139
5070
|
};
|
|
5140
5071
|
}
|