@elisra-devops/docgen-data-provider 1.88.0 → 1.89.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 +2 -2
- package/bin/modules/ResultDataProvider.d.ts +7 -5
- package/bin/modules/ResultDataProvider.js +238 -240
- 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 +2 -2
- package/src/modules/ResultDataProvider.ts +278 -251
- package/src/tests/modules/ResultDataProvider.test.ts +73 -19
|
@@ -446,8 +446,7 @@ export default class ResultDataProvider {
|
|
|
446
446
|
const testData = await this.fetchMewpScopedTestData(
|
|
447
447
|
testPlanId,
|
|
448
448
|
projectName,
|
|
449
|
-
selectedSuiteIds
|
|
450
|
-
!!options?.useRelFallback
|
|
449
|
+
selectedSuiteIds
|
|
451
450
|
);
|
|
452
451
|
|
|
453
452
|
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
@@ -549,6 +548,9 @@ export default class ResultDataProvider {
|
|
|
549
548
|
const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
|
|
550
549
|
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
551
550
|
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
551
|
+
if (options?.debugMode) {
|
|
552
|
+
this.logMewpRunScenarioDebugMatrix(runResults, `coverage plan=${testPlanId}`);
|
|
553
|
+
}
|
|
552
554
|
for (const runResult of runResults) {
|
|
553
555
|
const testCaseId = this.extractMewpTestCaseId(runResult);
|
|
554
556
|
const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
@@ -748,8 +750,7 @@ export default class ResultDataProvider {
|
|
|
748
750
|
const testData = await this.fetchMewpScopedTestData(
|
|
749
751
|
testPlanId,
|
|
750
752
|
projectName,
|
|
751
|
-
selectedSuiteIds
|
|
752
|
-
!!options?.useRelFallback
|
|
753
|
+
selectedSuiteIds
|
|
753
754
|
);
|
|
754
755
|
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
755
756
|
const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
|
|
@@ -795,6 +796,16 @@ export default class ResultDataProvider {
|
|
|
795
796
|
`fromSuitePayload=${preloadedStepXmlCount} fromWorkItemFallback=${fallbackStepLoadStats.loadedFromFallback} ` +
|
|
796
797
|
`stepsXmlAvailable=${stepsXmlByTestCase.size} unresolved=${fallbackStepLoadStats.unresolvedCount}`
|
|
797
798
|
);
|
|
799
|
+
if (options?.debugMode) {
|
|
800
|
+
const debugRunResults = await this.fetchAllResultDataTestReporter(
|
|
801
|
+
testData,
|
|
802
|
+
projectName,
|
|
803
|
+
[],
|
|
804
|
+
false,
|
|
805
|
+
false
|
|
806
|
+
);
|
|
807
|
+
this.logMewpRunScenarioDebugMatrix(debugRunResults, `internal-validation plan=${testPlanId}`);
|
|
808
|
+
}
|
|
798
809
|
|
|
799
810
|
const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
|
|
800
811
|
const diagnostics = {
|
|
@@ -808,34 +819,6 @@ export default class ResultDataProvider {
|
|
|
808
819
|
|
|
809
820
|
for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
|
|
810
821
|
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
822
|
const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
|
|
840
823
|
const parsedSteps =
|
|
841
824
|
stepsXml && String(stepsXml).trim() !== ''
|
|
@@ -888,15 +871,6 @@ export default class ResultDataProvider {
|
|
|
888
871
|
const linkedBaseKeys = new Set<string>(
|
|
889
872
|
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
890
873
|
);
|
|
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
874
|
|
|
901
875
|
const mentionedCodesByBase = new Map<string, Set<string>>();
|
|
902
876
|
for (const code of mentionedL2Only) {
|
|
@@ -918,89 +892,49 @@ export default class ResultDataProvider {
|
|
|
918
892
|
const mentionedCodesList = [...mentionedCodes];
|
|
919
893
|
const hasBaseMention = mentionedCodesList.some((code) => !/-\d+$/.test(code));
|
|
920
894
|
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
895
|
|
|
927
896
|
if (familyCodes?.size) {
|
|
928
|
-
familyAllCodes = [...familyCodes].sort((a, b) => a.localeCompare(b));
|
|
929
|
-
familyLinkedCodes = familyAllCodes.filter((code) => linkedFullCodes.has(code));
|
|
930
897
|
// Base mention ("SR0054") validates against child coverage when children exist.
|
|
931
898
|
// If no child variants exist, fallback to the single standalone requirement code.
|
|
932
899
|
if (hasBaseMention) {
|
|
933
900
|
const familyCodesList = [...familyCodes];
|
|
934
901
|
const childFamilyCodes = familyCodesList.filter((code) => /-\d+$/.test(code));
|
|
935
902
|
const targetFamilyCodes = childFamilyCodes.length > 0 ? childFamilyCodes : familyCodesList;
|
|
936
|
-
familyTargetCodes = [...targetFamilyCodes].sort((a, b) => a.localeCompare(b));
|
|
937
903
|
const missingInTargetFamily = targetFamilyCodes.filter((code) => !linkedFullCodes.has(code));
|
|
938
|
-
familyMissingCodes = [...missingInTargetFamily].sort((a, b) => a.localeCompare(b));
|
|
939
904
|
|
|
940
905
|
if (missingInTargetFamily.length > 0) {
|
|
941
906
|
const hasAnyLinkedInFamily = familyCodesList.some((code) => linkedFullCodes.has(code));
|
|
942
907
|
if (!hasAnyLinkedInFamily) {
|
|
943
908
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
944
|
-
familyDecision = 'base-mentioned-family-uncovered';
|
|
945
909
|
} else {
|
|
946
910
|
for (const code of missingInTargetFamily) {
|
|
947
911
|
missingFamilyMembers.add(code);
|
|
948
912
|
}
|
|
949
|
-
familyDecision = 'base-mentioned-family-partial-missing-children';
|
|
950
913
|
}
|
|
951
|
-
} else {
|
|
952
|
-
familyDecision = 'base-mentioned-family-fully-covered';
|
|
953
914
|
}
|
|
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
915
|
continue;
|
|
961
916
|
}
|
|
962
917
|
|
|
963
918
|
// Specific mention ("SR0054-1") validates as exact-match only.
|
|
964
|
-
const missingSpecificMembers: string[] = [];
|
|
965
919
|
for (const code of mentionedSpecificMembers) {
|
|
966
920
|
if (!linkedFullCodes.has(code)) {
|
|
967
921
|
missingSpecificMentionedNoFamily.add(code);
|
|
968
|
-
missingSpecificMembers.push(code);
|
|
969
922
|
}
|
|
970
923
|
}
|
|
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
924
|
continue;
|
|
982
925
|
}
|
|
983
926
|
|
|
984
927
|
// Fallback path when family data is unavailable for this base key.
|
|
985
|
-
const fallbackMissingSpecific: string[] = [];
|
|
986
|
-
let fallbackBaseMissing = false;
|
|
987
928
|
for (const code of mentionedCodes) {
|
|
988
929
|
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
989
930
|
if (hasSpecificSuffix) {
|
|
990
931
|
if (!linkedFullCodes.has(code)) {
|
|
991
932
|
missingSpecificMentionedNoFamily.add(code);
|
|
992
|
-
fallbackMissingSpecific.push(code);
|
|
993
933
|
}
|
|
994
934
|
} else if (!linkedBaseKeys.has(baseKey)) {
|
|
995
935
|
missingBaseWhenFamilyUncovered.add(baseKey);
|
|
996
|
-
fallbackBaseMissing = true;
|
|
997
936
|
}
|
|
998
937
|
}
|
|
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
938
|
}
|
|
1005
939
|
|
|
1006
940
|
// Direction B is family-based: if any member of a family is mentioned in Expected Result,
|
|
@@ -1063,16 +997,8 @@ export default class ResultDataProvider {
|
|
|
1063
997
|
const groupedRequirementList = this.formatRequirementCodesGroupedByFamily(requirementIds);
|
|
1064
998
|
return `${stepRef}: ${groupedRequirementList}`;
|
|
1065
999
|
})
|
|
1066
|
-
.join('
|
|
1000
|
+
.join('\n');
|
|
1067
1001
|
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
1002
|
const validationStatus: 'Pass' | 'Fail' =
|
|
1077
1003
|
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
1078
1004
|
if (validationStatus === 'Fail') diagnostics.failingRows += 1;
|
|
@@ -1534,139 +1460,10 @@ export default class ResultDataProvider {
|
|
|
1534
1460
|
private async fetchMewpScopedTestData(
|
|
1535
1461
|
testPlanId: string,
|
|
1536
1462
|
projectName: string,
|
|
1537
|
-
selectedSuiteIds: number[] | undefined
|
|
1538
|
-
useRelFallback: boolean
|
|
1463
|
+
selectedSuiteIds: number[] | undefined
|
|
1539
1464
|
): 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
|
-
];
|
|
1465
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1466
|
+
return this.fetchTestData(suites, projectName, testPlanId, false);
|
|
1670
1467
|
}
|
|
1671
1468
|
|
|
1672
1469
|
private async loadExternalBugsByTestCase(
|
|
@@ -2010,16 +1807,6 @@ export default class ResultDataProvider {
|
|
|
2010
1807
|
return out;
|
|
2011
1808
|
}
|
|
2012
1809
|
|
|
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
1810
|
private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
|
|
2024
1811
|
const out = new Set<string>();
|
|
2025
1812
|
const source = this.normalizeRequirementStepText(text);
|
|
@@ -2862,7 +2649,7 @@ export default class ResultDataProvider {
|
|
|
2862
2649
|
if (sortedMembers.length <= 1) return sortedMembers[0];
|
|
2863
2650
|
return `${baseKey}: ${sortedMembers.join(', ')}`;
|
|
2864
2651
|
})
|
|
2865
|
-
.join('
|
|
2652
|
+
.join('\n');
|
|
2866
2653
|
}
|
|
2867
2654
|
|
|
2868
2655
|
private toMewpComparableText(value: any): string {
|
|
@@ -3183,6 +2970,140 @@ export default class ResultDataProvider {
|
|
|
3183
2970
|
return testCases;
|
|
3184
2971
|
}
|
|
3185
2972
|
|
|
2973
|
+
private attachSuiteTestCaseContextToPoints(testCasesItems: any[], testPointsItems: any[]): any[] {
|
|
2974
|
+
const points = Array.isArray(testPointsItems) ? testPointsItems : [];
|
|
2975
|
+
const testCases = Array.isArray(testCasesItems) ? testCasesItems : [];
|
|
2976
|
+
if (points.length === 0 || testCases.length === 0) return points;
|
|
2977
|
+
|
|
2978
|
+
const byTestCaseId = new Map<number, any>();
|
|
2979
|
+
for (const testCaseItem of testCases) {
|
|
2980
|
+
const testCaseId = Number(
|
|
2981
|
+
testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || 0
|
|
2982
|
+
);
|
|
2983
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || byTestCaseId.has(testCaseId)) continue;
|
|
2984
|
+
byTestCaseId.set(testCaseId, testCaseItem);
|
|
2985
|
+
}
|
|
2986
|
+
|
|
2987
|
+
return points.map((point: any) => {
|
|
2988
|
+
const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
|
|
2989
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return point;
|
|
2990
|
+
const suiteTestCase = byTestCaseId.get(testCaseId);
|
|
2991
|
+
if (!suiteTestCase) return point;
|
|
2992
|
+
return { ...point, suiteTestCase };
|
|
2993
|
+
});
|
|
2994
|
+
}
|
|
2995
|
+
|
|
2996
|
+
private extractWorkItemFieldsMap(workItemFields: any): Record<string, any> {
|
|
2997
|
+
const fields: Record<string, any> = {};
|
|
2998
|
+
if (!Array.isArray(workItemFields)) return fields;
|
|
2999
|
+
for (const field of workItemFields) {
|
|
3000
|
+
const keyCandidates = [field?.key, field?.name, field?.referenceName]
|
|
3001
|
+
.map((item) => String(item || '').trim())
|
|
3002
|
+
.filter((item) => !!item);
|
|
3003
|
+
for (const key of keyCandidates) {
|
|
3004
|
+
fields[key] = field?.value;
|
|
3005
|
+
}
|
|
3006
|
+
}
|
|
3007
|
+
return fields;
|
|
3008
|
+
}
|
|
3009
|
+
|
|
3010
|
+
private resolveSuiteTestCaseRevision(testCaseItem: any): number {
|
|
3011
|
+
const revisionCandidates = [
|
|
3012
|
+
testCaseItem?.workItem?.rev,
|
|
3013
|
+
testCaseItem?.workItem?.revision,
|
|
3014
|
+
testCaseItem?.workItem?.version,
|
|
3015
|
+
testCaseItem?.workItem?.workItemRevision,
|
|
3016
|
+
testCaseItem?.workItem?.workItemVersion,
|
|
3017
|
+
testCaseItem?.revision,
|
|
3018
|
+
testCaseItem?.workItemRevision,
|
|
3019
|
+
testCaseItem?.workItemVersion,
|
|
3020
|
+
];
|
|
3021
|
+
for (const candidate of revisionCandidates) {
|
|
3022
|
+
const parsed = Number(candidate || 0);
|
|
3023
|
+
if (Number.isFinite(parsed) && parsed > 0) return parsed;
|
|
3024
|
+
}
|
|
3025
|
+
return 0;
|
|
3026
|
+
}
|
|
3027
|
+
|
|
3028
|
+
private buildWorkItemSnapshotFromSuiteTestCase(
|
|
3029
|
+
testCaseItem: any,
|
|
3030
|
+
fallbackTestCaseId: number,
|
|
3031
|
+
fallbackTestCaseName: string = ''
|
|
3032
|
+
): any | null {
|
|
3033
|
+
if (!testCaseItem) return null;
|
|
3034
|
+
|
|
3035
|
+
const testCaseId = Number(
|
|
3036
|
+
testCaseItem?.workItem?.id || testCaseItem?.testCaseId || testCaseItem?.id || fallbackTestCaseId || 0
|
|
3037
|
+
);
|
|
3038
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return null;
|
|
3039
|
+
|
|
3040
|
+
const workItem = testCaseItem?.workItem || {};
|
|
3041
|
+
const stepsXml = this.extractStepsXmlFromTestCaseItem(testCaseItem);
|
|
3042
|
+
const fieldsFromList = this.extractWorkItemFieldsMap(workItem?.workItemFields);
|
|
3043
|
+
const fieldsFromMap = workItem?.fields || {};
|
|
3044
|
+
const fields = {
|
|
3045
|
+
...fieldsFromList,
|
|
3046
|
+
...fieldsFromMap,
|
|
3047
|
+
};
|
|
3048
|
+
|
|
3049
|
+
if (!fields['System.Title']) {
|
|
3050
|
+
const title = String(
|
|
3051
|
+
testCaseItem?.testCaseName || workItem?.name || testCaseItem?.name || fallbackTestCaseName || ''
|
|
3052
|
+
).trim();
|
|
3053
|
+
if (title) {
|
|
3054
|
+
fields['System.Title'] = title;
|
|
3055
|
+
}
|
|
3056
|
+
}
|
|
3057
|
+
if (stepsXml && !fields['Microsoft.VSTS.TCM.Steps']) {
|
|
3058
|
+
fields['Microsoft.VSTS.TCM.Steps'] = stepsXml;
|
|
3059
|
+
}
|
|
3060
|
+
|
|
3061
|
+
return {
|
|
3062
|
+
id: testCaseId,
|
|
3063
|
+
rev: this.resolveSuiteTestCaseRevision(testCaseItem) || 1,
|
|
3064
|
+
fields,
|
|
3065
|
+
relations: Array.isArray(workItem?.relations) ? workItem.relations : [],
|
|
3066
|
+
};
|
|
3067
|
+
}
|
|
3068
|
+
|
|
3069
|
+
private async fetchWorkItemByRevision(
|
|
3070
|
+
projectName: string,
|
|
3071
|
+
workItemId: number,
|
|
3072
|
+
revision: number,
|
|
3073
|
+
expandAll: boolean = false
|
|
3074
|
+
): Promise<any | null> {
|
|
3075
|
+
const id = Number(workItemId || 0);
|
|
3076
|
+
const rev = Number(revision || 0);
|
|
3077
|
+
if (!Number.isFinite(id) || id <= 0 || !Number.isFinite(rev) || rev <= 0) return null;
|
|
3078
|
+
|
|
3079
|
+
const expandParam = expandAll ? '?$expand=all' : '';
|
|
3080
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}/revisions/${rev}${expandParam}`;
|
|
3081
|
+
try {
|
|
3082
|
+
return await TFSServices.getItemContent(url, this.token);
|
|
3083
|
+
} catch (error: any) {
|
|
3084
|
+
logger.warn(`Failed to fetch work item ${id} by revision ${rev}: ${error?.message || error}`);
|
|
3085
|
+
return null;
|
|
3086
|
+
}
|
|
3087
|
+
}
|
|
3088
|
+
|
|
3089
|
+
private async fetchWorkItemLatest(
|
|
3090
|
+
projectName: string,
|
|
3091
|
+
workItemId: number,
|
|
3092
|
+
expandAll: boolean = false
|
|
3093
|
+
): Promise<any | null> {
|
|
3094
|
+
const id = Number(workItemId || 0);
|
|
3095
|
+
if (!Number.isFinite(id) || id <= 0) return null;
|
|
3096
|
+
|
|
3097
|
+
const expandParam = expandAll ? '?$expand=all' : '';
|
|
3098
|
+
const url = `${this.orgUrl}${projectName}/_apis/wit/workItems/${id}${expandParam}`;
|
|
3099
|
+
try {
|
|
3100
|
+
return await TFSServices.getItemContent(url, this.token);
|
|
3101
|
+
} catch (error: any) {
|
|
3102
|
+
logger.warn(`Failed to fetch latest work item ${id}: ${error?.message || error}`);
|
|
3103
|
+
return null;
|
|
3104
|
+
}
|
|
3105
|
+
}
|
|
3106
|
+
|
|
3186
3107
|
/**
|
|
3187
3108
|
* Fetches result data based on the Work Item Test Reporter.
|
|
3188
3109
|
*
|
|
@@ -3217,13 +3138,37 @@ export default class ResultDataProvider {
|
|
|
3217
3138
|
logger.warn(`Invalid run result ${runId} or result ${resultId}`);
|
|
3218
3139
|
return null;
|
|
3219
3140
|
}
|
|
3220
|
-
const
|
|
3221
|
-
const
|
|
3141
|
+
const suiteTestCaseItem = point?.suiteTestCase;
|
|
3142
|
+
const testCaseId = Number(
|
|
3143
|
+
point?.testCaseId || suiteTestCaseItem?.workItem?.id || suiteTestCaseItem?.testCaseId || 0
|
|
3144
|
+
);
|
|
3145
|
+
const suiteTestCaseRevision = this.resolveSuiteTestCaseRevision(suiteTestCaseItem);
|
|
3146
|
+
const fallbackSnapshot = this.buildWorkItemSnapshotFromSuiteTestCase(
|
|
3147
|
+
suiteTestCaseItem,
|
|
3148
|
+
testCaseId,
|
|
3149
|
+
String(point?.testCaseName || '')
|
|
3150
|
+
);
|
|
3151
|
+
let testCaseData = await this.fetchWorkItemByRevision(
|
|
3152
|
+
projectName,
|
|
3153
|
+
testCaseId,
|
|
3154
|
+
suiteTestCaseRevision,
|
|
3155
|
+
isTestReporter
|
|
3156
|
+
);
|
|
3157
|
+
if (!testCaseData) {
|
|
3158
|
+
testCaseData = fallbackSnapshot;
|
|
3159
|
+
}
|
|
3160
|
+
if (!testCaseData) {
|
|
3161
|
+
testCaseData = await this.fetchWorkItemLatest(projectName, testCaseId, isTestReporter);
|
|
3162
|
+
}
|
|
3163
|
+
if (!testCaseData) {
|
|
3164
|
+
logger.warn(`Could not resolve test case ${point.testCaseId} for runless point fallback.`);
|
|
3165
|
+
return null;
|
|
3166
|
+
}
|
|
3222
3167
|
const newResultData: PlainTestResult = {
|
|
3223
3168
|
id: 0,
|
|
3224
3169
|
outcome: point.outcome,
|
|
3225
|
-
revision: testCaseData?.rev || 1,
|
|
3226
|
-
testCase: { id:
|
|
3170
|
+
revision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
|
|
3171
|
+
testCase: { id: String(testCaseId), name: point.testCaseName },
|
|
3227
3172
|
state: testCaseData?.fields?.['System.State'] || 'Active',
|
|
3228
3173
|
priority: testCaseData?.fields?.['Microsoft.VSTS.TCM.Priority'] || 0,
|
|
3229
3174
|
createdDate: testCaseData?.fields?.['System.CreatedDate'] || '0001-01-01T00:00:00',
|
|
@@ -3271,8 +3216,8 @@ export default class ResultDataProvider {
|
|
|
3271
3216
|
selectedFieldSet.clear();
|
|
3272
3217
|
return {
|
|
3273
3218
|
...newResultData,
|
|
3274
|
-
stepsResultXml: testCaseData
|
|
3275
|
-
testCaseRevision: testCaseData
|
|
3219
|
+
stepsResultXml: testCaseData?.fields?.['Microsoft.VSTS.TCM.Steps'] || undefined,
|
|
3220
|
+
testCaseRevision: Number(testCaseData?.rev || suiteTestCaseRevision || 1),
|
|
3276
3221
|
filteredFields,
|
|
3277
3222
|
relatedRequirements,
|
|
3278
3223
|
relatedBugs,
|
|
@@ -3442,13 +3387,6 @@ export default class ResultDataProvider {
|
|
|
3442
3387
|
);
|
|
3443
3388
|
}
|
|
3444
3389
|
|
|
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
3390
|
private extractCommentText(comment: AdoWorkItemComment): string {
|
|
3453
3391
|
const rendered = comment?.renderedText;
|
|
3454
3392
|
// In Azure DevOps the `renderedText` field can be present but empty ("") even when `text` is populated.
|
|
@@ -3935,6 +3873,79 @@ export default class ResultDataProvider {
|
|
|
3935
3873
|
return this.fetchResultDataBasedOnWiBase(projectName, runId, resultId);
|
|
3936
3874
|
}
|
|
3937
3875
|
|
|
3876
|
+
private logMewpRunScenarioDebugMatrix(runResults: any[], contextLabel: string): void {
|
|
3877
|
+
const results = Array.isArray(runResults) ? runResults : [];
|
|
3878
|
+
const matrix = {
|
|
3879
|
+
total: results.length,
|
|
3880
|
+
passOrFailWithActionResults: 0,
|
|
3881
|
+
runWithNoActionResults: 0,
|
|
3882
|
+
notApplicable: 0,
|
|
3883
|
+
noRunHistoryActive: 0,
|
|
3884
|
+
other: 0,
|
|
3885
|
+
};
|
|
3886
|
+
const samples = {
|
|
3887
|
+
passOrFailWithActionResults: [] as number[],
|
|
3888
|
+
runWithNoActionResults: [] as number[],
|
|
3889
|
+
notApplicable: [] as number[],
|
|
3890
|
+
noRunHistoryActive: [] as number[],
|
|
3891
|
+
other: [] as number[],
|
|
3892
|
+
};
|
|
3893
|
+
|
|
3894
|
+
const pushSample = (bucket: keyof typeof samples, id: number) => {
|
|
3895
|
+
if (!Number.isFinite(id) || id <= 0) return;
|
|
3896
|
+
if (samples[bucket].length >= 5) return;
|
|
3897
|
+
samples[bucket].push(id);
|
|
3898
|
+
};
|
|
3899
|
+
|
|
3900
|
+
for (const item of results) {
|
|
3901
|
+
const testCaseId = Number(item?.testCaseId || item?.testCase?.id || 0);
|
|
3902
|
+
const hasRun = Number(item?.lastRunId || 0) > 0 && Number(item?.lastResultId || 0) > 0;
|
|
3903
|
+
const rawOutcome = String(item?._debugTestOutcome || '').trim().toLowerCase();
|
|
3904
|
+
const rawState = String(item?._debugTestCaseState || '').trim().toLowerCase();
|
|
3905
|
+
const originalActionResultsCount = Number(item?._debugOriginalActionResultsCount ?? -1);
|
|
3906
|
+
|
|
3907
|
+
if (rawOutcome === 'notapplicable' || rawOutcome === 'not applicable') {
|
|
3908
|
+
matrix.notApplicable += 1;
|
|
3909
|
+
pushSample('notApplicable', testCaseId);
|
|
3910
|
+
continue;
|
|
3911
|
+
}
|
|
3912
|
+
|
|
3913
|
+
if (hasRun && (rawOutcome === 'passed' || rawOutcome === 'failed') && originalActionResultsCount > 0) {
|
|
3914
|
+
matrix.passOrFailWithActionResults += 1;
|
|
3915
|
+
pushSample('passOrFailWithActionResults', testCaseId);
|
|
3916
|
+
continue;
|
|
3917
|
+
}
|
|
3918
|
+
|
|
3919
|
+
if (hasRun && originalActionResultsCount === 0) {
|
|
3920
|
+
matrix.runWithNoActionResults += 1;
|
|
3921
|
+
pushSample('runWithNoActionResults', testCaseId);
|
|
3922
|
+
continue;
|
|
3923
|
+
}
|
|
3924
|
+
|
|
3925
|
+
if (!hasRun && rawState === 'active') {
|
|
3926
|
+
matrix.noRunHistoryActive += 1;
|
|
3927
|
+
pushSample('noRunHistoryActive', testCaseId);
|
|
3928
|
+
continue;
|
|
3929
|
+
}
|
|
3930
|
+
|
|
3931
|
+
matrix.other += 1;
|
|
3932
|
+
pushSample('other', testCaseId);
|
|
3933
|
+
}
|
|
3934
|
+
|
|
3935
|
+
logger.info(
|
|
3936
|
+
`MEWP run debug matrix (${contextLabel}): total=${matrix.total}; ` +
|
|
3937
|
+
`passOrFailWithActionResults=${matrix.passOrFailWithActionResults}; ` +
|
|
3938
|
+
`runWithNoActionResults=${matrix.runWithNoActionResults}; ` +
|
|
3939
|
+
`notApplicable=${matrix.notApplicable}; ` +
|
|
3940
|
+
`noRunHistoryActive=${matrix.noRunHistoryActive}; other=${matrix.other}; ` +
|
|
3941
|
+
`samplePassFail=${samples.passOrFailWithActionResults.join(',') || '-'}; ` +
|
|
3942
|
+
`sampleNoAction=${samples.runWithNoActionResults.join(',') || '-'}; ` +
|
|
3943
|
+
`sampleNA=${samples.notApplicable.join(',') || '-'}; ` +
|
|
3944
|
+
`sampleNoRunActive=${samples.noRunHistoryActive.join(',') || '-'}; ` +
|
|
3945
|
+
`sampleOther=${samples.other.join(',') || '-'}`
|
|
3946
|
+
);
|
|
3947
|
+
}
|
|
3948
|
+
|
|
3938
3949
|
/**
|
|
3939
3950
|
* Converts a run status string into a human-readable format.
|
|
3940
3951
|
*
|
|
@@ -4210,9 +4221,17 @@ export default class ResultDataProvider {
|
|
|
4210
4221
|
suite.testSuiteId
|
|
4211
4222
|
);
|
|
4212
4223
|
const testCaseIds = testCasesItems.map((testCase: any) => testCase.workItem.id);
|
|
4213
|
-
const
|
|
4214
|
-
? await this.fetchTestPoints(
|
|
4224
|
+
const rawTestPointsItems = !fetchCrossPlans
|
|
4225
|
+
? await this.fetchTestPoints(
|
|
4226
|
+
projectName,
|
|
4227
|
+
testPlanId,
|
|
4228
|
+
suite.testSuiteId
|
|
4229
|
+
)
|
|
4215
4230
|
: await this.fetchCrossTestPoints(projectName, testCaseIds);
|
|
4231
|
+
const testPointsItems = this.attachSuiteTestCaseContextToPoints(
|
|
4232
|
+
testCasesItems,
|
|
4233
|
+
rawTestPointsItems
|
|
4234
|
+
);
|
|
4216
4235
|
|
|
4217
4236
|
return { ...suite, testPointsItems, testCasesItems };
|
|
4218
4237
|
} catch (error: any) {
|
|
@@ -4362,6 +4381,11 @@ export default class ResultDataProvider {
|
|
|
4362
4381
|
resultData.iterationDetails.push(iteration);
|
|
4363
4382
|
}
|
|
4364
4383
|
|
|
4384
|
+
const originalActionResultsCount = Array.isArray(iteration?.actionResults)
|
|
4385
|
+
? iteration.actionResults.length
|
|
4386
|
+
: 0;
|
|
4387
|
+
resultData._debugOriginalActionResultsCount = originalActionResultsCount;
|
|
4388
|
+
|
|
4365
4389
|
if (resultData.stepsResultXml && iteration) {
|
|
4366
4390
|
const actionResults = Array.isArray(iteration.actionResults) ? iteration.actionResults : [];
|
|
4367
4391
|
const actionResultsWithSharedModels = actionResults.filter(
|
|
@@ -5074,6 +5098,7 @@ export default class ResultDataProvider {
|
|
|
5074
5098
|
resultData.iterationDetails?.length > 0
|
|
5075
5099
|
? resultData.iterationDetails[resultData.iterationDetails?.length - 1]
|
|
5076
5100
|
: undefined;
|
|
5101
|
+
const debugOutcome = this.getTestOutcome(resultData);
|
|
5077
5102
|
|
|
5078
5103
|
if (!resultData?.testCase || !resultData?.testSuite) {
|
|
5079
5104
|
logger.debug(
|
|
@@ -5107,6 +5132,9 @@ export default class ResultDataProvider {
|
|
|
5107
5132
|
relatedCRs: resultData.relatedCRs || undefined,
|
|
5108
5133
|
lastRunResult: undefined as any,
|
|
5109
5134
|
customFields: {}, // Create an object to store custom fields
|
|
5135
|
+
_debugTestOutcome: debugOutcome,
|
|
5136
|
+
_debugTestCaseState: String(resultData?.state || ''),
|
|
5137
|
+
_debugOriginalActionResultsCount: Number(resultData?._debugOriginalActionResultsCount ?? -1),
|
|
5110
5138
|
};
|
|
5111
5139
|
|
|
5112
5140
|
// Process all custom fields from resultData.filteredFields
|
|
@@ -5126,15 +5154,14 @@ export default class ResultDataProvider {
|
|
|
5126
5154
|
resultDataResponse.priority = resultData.priority;
|
|
5127
5155
|
break;
|
|
5128
5156
|
case 'testCaseResult':
|
|
5129
|
-
const outcome = this.getTestOutcome(resultData);
|
|
5130
5157
|
if (lastRunId === undefined || lastResultId === undefined) {
|
|
5131
5158
|
resultDataResponse.testCaseResult = {
|
|
5132
|
-
resultMessage: `${this.convertRunStatus(
|
|
5159
|
+
resultMessage: `${this.convertRunStatus(debugOutcome)}`,
|
|
5133
5160
|
url: '',
|
|
5134
5161
|
};
|
|
5135
5162
|
} else {
|
|
5136
5163
|
resultDataResponse.testCaseResult = {
|
|
5137
|
-
resultMessage: `${this.convertRunStatus(
|
|
5164
|
+
resultMessage: `${this.convertRunStatus(debugOutcome)} in Run ${lastRunId}`,
|
|
5138
5165
|
url: `${this.orgUrl}${projectName}/_testManagement/runs?runId=${lastRunId}&_a=resultSummary&resultId=${lastResultId}`,
|
|
5139
5166
|
};
|
|
5140
5167
|
}
|