@elisra-devops/docgen-data-provider 1.79.0 → 1.80.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/package.json CHANGED
@@ -1,6 +1,6 @@
1
1
  {
2
2
  "name": "@elisra-devops/docgen-data-provider",
3
- "version": "1.79.0",
3
+ "version": "1.80.0",
4
4
  "description": "A document generator data provider, aimed to retrive data from azure devops",
5
5
  "repository": {
6
6
  "type": "git",
@@ -472,6 +472,11 @@ export default class ResultDataProvider {
472
472
  allRequirements,
473
473
  scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
474
474
  );
475
+ const l2ToLinkedL1BaseKeys = await this.buildMewpL2ToLinkedL1BaseKeys(
476
+ allRequirements,
477
+ projectName,
478
+ testData
479
+ );
475
480
  const requirementSapWbsByBaseKey = this.buildRequirementSapWbsByBaseKey(allRequirements);
476
481
  const externalBugsByTestCase = await this.loadExternalBugsByTestCase(options?.externalBugsFile);
477
482
  const externalL3L4ByBaseKey = await this.loadExternalL3L4ByBaseKey(
@@ -523,6 +528,10 @@ export default class ResultDataProvider {
523
528
  sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
524
529
  };
525
530
  }
531
+ const externalJoinKeysByL2 = this.buildMewpExternalJoinKeysByL2Requirement(
532
+ requirements,
533
+ l2ToLinkedL1BaseKeys
534
+ );
526
535
 
527
536
  const requirementIndex: MewpRequirementIndex = new Map();
528
537
  const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
@@ -601,8 +610,16 @@ export default class ResultDataProvider {
601
610
  const requirementBaseKeys = new Set<string>(
602
611
  requirements.map((item) => String(item?.baseKey || '').trim()).filter((item) => !!item)
603
612
  );
613
+ const externalJoinKeyUniverse = new Set<string>();
614
+ for (const keySet of externalJoinKeysByL2.values()) {
615
+ for (const key of keySet) {
616
+ if (key) externalJoinKeyUniverse.add(key);
617
+ }
618
+ }
604
619
  const externalL3L4BaseKeys = new Set<string>([...externalL3L4ByBaseKey.keys()]);
605
- const externalL3L4OverlapKeys = [...externalL3L4BaseKeys].filter((key) => requirementBaseKeys.has(key));
620
+ const externalL3L4OverlapKeys = [...externalL3L4BaseKeys].filter((key) =>
621
+ externalJoinKeyUniverse.has(key)
622
+ );
606
623
  const failedRequirementBaseKeys = new Set<string>();
607
624
  const failedTestCaseIds = new Set<number>();
608
625
  for (const [requirementBaseKey, byTestCase] of requirementIndex.entries()) {
@@ -625,13 +642,14 @@ export default class ResultDataProvider {
625
642
  }
626
643
  }
627
644
  const externalBugRequirementOverlap = [...externalBugBaseKeys].filter((key) =>
628
- requirementBaseKeys.has(key)
645
+ externalJoinKeyUniverse.has(key)
629
646
  );
630
647
  const externalBugFailedRequirementOverlap = [...externalBugBaseKeys].filter((key) =>
631
- failedRequirementBaseKeys.has(key)
648
+ externalJoinKeyUniverse.has(key)
632
649
  );
633
650
  logger.info(
634
651
  `MEWP coverage join diagnostics: requirementBaseKeys=${requirementBaseKeys.size} ` +
652
+ `externalJoinKeys=${externalJoinKeyUniverse.size} ` +
635
653
  `failedRequirementBaseKeys=${failedRequirementBaseKeys.size} failedTestCases=${failedTestCaseIds.size}; ` +
636
654
  `externalL3L4BaseKeys=${externalL3L4BaseKeys.size} externalL3L4Overlap=${externalL3L4OverlapKeys.length}; ` +
637
655
  `externalBugTestCases=${externalBugTestCaseIds.size} externalBugFailedTestCaseOverlap=${externalBugFailedTestCaseOverlap.length}; ` +
@@ -640,18 +658,22 @@ export default class ResultDataProvider {
640
658
  );
641
659
  if (externalL3L4BaseKeys.size > 0 && externalL3L4OverlapKeys.length === 0) {
642
660
  const sampleReq = [...requirementBaseKeys].slice(0, 5);
661
+ const sampleJoin = [...externalJoinKeyUniverse].slice(0, 5);
643
662
  const sampleExt = [...externalL3L4BaseKeys].slice(0, 5);
644
663
  logger.warn(
645
664
  `MEWP coverage join diagnostics: no L3/L4 key overlap found. ` +
646
- `sampleRequirementKeys=${sampleReq.join(', ')} sampleExternalL3L4Keys=${sampleExt.join(', ')}`
665
+ `sampleRequirementKeys=${sampleReq.join(', ')} sampleJoinKeys=${sampleJoin.join(', ')} ` +
666
+ `sampleExternalL3L4Keys=${sampleExt.join(', ')}`
647
667
  );
648
668
  }
649
669
  if (externalBugBaseKeys.size > 0 && externalBugRequirementOverlap.length === 0) {
650
670
  const sampleReq = [...requirementBaseKeys].slice(0, 5);
671
+ const sampleJoin = [...externalJoinKeyUniverse].slice(0, 5);
651
672
  const sampleExt = [...externalBugBaseKeys].slice(0, 5);
652
673
  logger.warn(
653
674
  `MEWP coverage join diagnostics: no bug requirement-key overlap found. ` +
654
- `sampleRequirementKeys=${sampleReq.join(', ')} sampleExternalBugKeys=${sampleExt.join(', ')}`
675
+ `sampleRequirementKeys=${sampleReq.join(', ')} sampleJoinKeys=${sampleJoin.join(', ')} ` +
676
+ `sampleExternalBugKeys=${sampleExt.join(', ')}`
655
677
  );
656
678
  }
657
679
  if (externalBugTestCaseIds.size > 0 && externalBugFailedTestCaseOverlap.length === 0) {
@@ -667,7 +689,8 @@ export default class ResultDataProvider {
667
689
  observedTestCaseIdsByRequirement,
668
690
  linkedRequirementsByTestCase,
669
691
  externalL3L4ByBaseKey,
670
- externalBugsByTestCase
692
+ externalBugsByTestCase,
693
+ externalJoinKeysByL2
671
694
  );
672
695
  const coverageRowStats = rows.reduce(
673
696
  (acc, row) => {
@@ -757,14 +780,26 @@ export default class ResultDataProvider {
757
780
  }
758
781
 
759
782
  const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
783
+ const diagnostics = {
784
+ totalTestCases: 0,
785
+ totalParsedSteps: 0,
786
+ totalStepsWithMentions: 0,
787
+ totalMentionedCustomerIds: 0,
788
+ testCasesWithoutMentionedCustomerIds: 0,
789
+ failingRows: 0,
790
+ };
760
791
 
761
792
  for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
793
+ diagnostics.totalTestCases += 1;
762
794
  const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
763
795
  const parsedSteps =
764
796
  stepsXml && String(stepsXml).trim() !== ''
765
797
  ? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
766
798
  : [];
799
+ const executableSteps = parsedSteps.filter((step) => !step?.isSharedStepTitle);
800
+ diagnostics.totalParsedSteps += executableSteps.length;
767
801
  const mentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
802
+ diagnostics.totalStepsWithMentions += mentionEntries.length;
768
803
  const mentionedL2Only = new Set<string>();
769
804
  const mentionedCodeFirstStep = new Map<string, string>();
770
805
  const mentionedBaseFirstStep = new Map<string, string>();
@@ -787,6 +822,10 @@ export default class ResultDataProvider {
787
822
  }
788
823
  }
789
824
  }
825
+ diagnostics.totalMentionedCustomerIds += mentionedL2Only.size;
826
+ if (mentionedL2Only.size === 0) {
827
+ diagnostics.testCasesWithoutMentionedCustomerIds += 1;
828
+ }
790
829
 
791
830
  const mentionedBaseKeys = new Set<string>(
792
831
  [...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
@@ -874,6 +913,15 @@ export default class ResultDataProvider {
874
913
  const linkedButNotMentioned = sortedExtraLinked.join('; ');
875
914
  const validationStatus: 'Pass' | 'Fail' =
876
915
  mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
916
+ if (validationStatus === 'Fail') diagnostics.failingRows += 1;
917
+ logger.debug(
918
+ `MEWP internal validation parse diagnostics: ` +
919
+ `testCaseId=${testCaseId} parsedSteps=${executableSteps.length} ` +
920
+ `stepsWithMentions=${mentionEntries.length} customerIdsFound=${mentionedL2Only.size} ` +
921
+ `linkedRequirements=${linkedFullCodes.size} mentionedButNotLinked=${sortedMissingMentioned.length + sortedMissingFamily.length} ` +
922
+ `linkedButNotMentioned=${sortedExtraLinked.length} status=${validationStatus} ` +
923
+ `customerIdSample='${[...mentionedL2Only].slice(0, 5).join(', ')}'`
924
+ );
877
925
 
878
926
  rows.push({
879
927
  'Test Case ID': testCaseId,
@@ -883,6 +931,13 @@ export default class ResultDataProvider {
883
931
  'Validation Status': validationStatus,
884
932
  });
885
933
  }
934
+ logger.info(
935
+ `MEWP internal validation summary: testCases=${diagnostics.totalTestCases} ` +
936
+ `parsedSteps=${diagnostics.totalParsedSteps} stepsWithMentions=${diagnostics.totalStepsWithMentions} ` +
937
+ `totalCustomerIdsFound=${diagnostics.totalMentionedCustomerIds} ` +
938
+ `testCasesWithoutCustomerIds=${diagnostics.testCasesWithoutMentionedCustomerIds} ` +
939
+ `failingRows=${diagnostics.failingRows}`
940
+ );
886
941
 
887
942
  return {
888
943
  sheetName: this.buildInternalValidationSheetName(planName, testPlanId),
@@ -1036,7 +1091,21 @@ export default class ResultDataProvider {
1036
1091
  }
1037
1092
 
1038
1093
  private buildMewpCoverageL3L4Rows(links: MewpL3L4Link[]): MewpCoverageL3L4Cell[] {
1039
- const sorted = [...(links || [])].sort((a, b) => {
1094
+ const deduped = new Map<string, MewpL3L4Link>();
1095
+ for (const item of links || []) {
1096
+ const level = item?.level === 'L4' ? 'L4' : 'L3';
1097
+ const id = String(item?.id || '').trim();
1098
+ if (!id) continue;
1099
+ const key = `${level}:${id}`;
1100
+ if (!deduped.has(key)) {
1101
+ deduped.set(key, {
1102
+ id,
1103
+ level,
1104
+ title: String(item?.title || '').trim(),
1105
+ });
1106
+ }
1107
+ }
1108
+ const sorted = [...deduped.values()].sort((a, b) => {
1040
1109
  if (a.level !== b.level) return a.level === 'L3' ? -1 : 1;
1041
1110
  return String(a.id || '').localeCompare(String(b.id || ''));
1042
1111
  });
@@ -1060,12 +1129,15 @@ export default class ResultDataProvider {
1060
1129
  observedTestCaseIdsByRequirement: Map<string, Set<number>>,
1061
1130
  linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
1062
1131
  l3l4ByBaseKey: Map<string, MewpL3L4Link[]>,
1063
- externalBugsByTestCase: Map<number, MewpBugLink[]>
1132
+ externalBugsByTestCase: Map<number, MewpBugLink[]>,
1133
+ externalJoinKeysByL2?: Map<string, Set<string>>
1064
1134
  ): MewpCoverageRow[] {
1065
1135
  const rows: MewpCoverageRow[] = [];
1066
1136
  const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
1137
+ const joinKeysByRequirement = externalJoinKeysByL2 || new Map<string, Set<string>>();
1067
1138
  for (const requirement of requirements) {
1068
1139
  const key = String(requirement?.baseKey || this.toRequirementKey(requirement.requirementId) || '').trim();
1140
+ const externalJoinKeys = joinKeysByRequirement.get(key) || new Set<string>([key]);
1069
1141
  const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
1070
1142
  (id) => Number.isFinite(id) && Number(id) > 0
1071
1143
  );
@@ -1095,7 +1167,7 @@ export default class ResultDataProvider {
1095
1167
  const externalBugs = externalBugsByTestCase.get(testCaseId) || [];
1096
1168
  for (const bug of externalBugs) {
1097
1169
  const bugBaseKey = String(bug?.requirementBaseKey || '').trim();
1098
- if (bugBaseKey && bugBaseKey !== key) continue;
1170
+ if (bugBaseKey && !externalJoinKeys.has(bugBaseKey)) continue;
1099
1171
  const bugId = Number(bug?.id || 0);
1100
1172
  if (!Number.isFinite(bugId) || bugId <= 0) continue;
1101
1173
  aggregatedBugs.set(bugId, {
@@ -1120,7 +1192,7 @@ export default class ResultDataProvider {
1120
1192
  runStatus === 'Fail'
1121
1193
  ? Array.from(aggregatedBugs.values()).sort((a, b) => a.id - b.id)
1122
1194
  : [];
1123
- const l3l4ForRows = [...(l3l4ByBaseKey.get(key) || [])];
1195
+ const l3l4ForRows = [...externalJoinKeys].flatMap((joinKey) => l3l4ByBaseKey.get(joinKey) || []);
1124
1196
 
1125
1197
  const bugRows: MewpCoverageBugCell[] =
1126
1198
  bugsForRows.length > 0
@@ -1884,6 +1956,123 @@ export default class ResultDataProvider {
1884
1956
  .sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
1885
1957
  }
1886
1958
 
1959
+ private async buildMewpL2ToLinkedL1BaseKeys(
1960
+ requirements: MewpL2RequirementWorkItem[],
1961
+ projectName: string,
1962
+ testData: any[]
1963
+ ): Promise<Map<string, Set<string>>> {
1964
+ const out = new Map<string, Set<string>>();
1965
+ const relatedIds = new Set<number>();
1966
+ const linkedTestCaseIdsByL2 = new Map<string, Set<number>>();
1967
+ const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
1968
+
1969
+ for (const requirement of requirements || []) {
1970
+ const l2BaseKey = String(requirement?.baseKey || '').trim();
1971
+ if (!l2BaseKey) continue;
1972
+ if (!out.has(l2BaseKey)) out.set(l2BaseKey, new Set<string>());
1973
+ if (!linkedTestCaseIdsByL2.has(l2BaseKey)) linkedTestCaseIdsByL2.set(l2BaseKey, new Set<number>());
1974
+ for (const testCaseId of requirement?.linkedTestCaseIds || []) {
1975
+ const numeric = Number(testCaseId);
1976
+ if (Number.isFinite(numeric) && numeric > 0) {
1977
+ linkedTestCaseIdsByL2.get(l2BaseKey)!.add(numeric);
1978
+ }
1979
+ }
1980
+ for (const relatedId of requirement?.relatedWorkItemIds || []) {
1981
+ const id = Number(relatedId);
1982
+ if (Number.isFinite(id) && id > 0) relatedIds.add(id);
1983
+ }
1984
+ }
1985
+
1986
+ let titleDerivedCount = 0;
1987
+ for (const [l2BaseKey, testCaseIds] of linkedTestCaseIdsByL2.entries()) {
1988
+ const targetSet = out.get(l2BaseKey) || new Set<string>();
1989
+ for (const testCaseId of testCaseIds) {
1990
+ const title = String(testCaseTitleMap.get(testCaseId) || '').trim();
1991
+ if (!title) continue;
1992
+ const fromTitleCodes = this.extractRequirementCodesFromExpectedText(title, false);
1993
+ if (fromTitleCodes.size > 0) {
1994
+ for (const code of fromTitleCodes) {
1995
+ const normalized = this.toRequirementKey(code);
1996
+ if (normalized) targetSet.add(normalized);
1997
+ }
1998
+ } else {
1999
+ const normalized = this.toRequirementKey(title);
2000
+ if (normalized) targetSet.add(normalized);
2001
+ }
2002
+ }
2003
+ if (targetSet.size > 0) {
2004
+ titleDerivedCount += 1;
2005
+ }
2006
+ out.set(l2BaseKey, targetSet);
2007
+ }
2008
+
2009
+ if (relatedIds.size === 0) {
2010
+ const linkedL1Count = [...out.values()].reduce((sum, set) => sum + set.size, 0);
2011
+ logger.info(
2012
+ `MEWP L2->L1 mapping summary: l2Families=${out.size} ` +
2013
+ `fromTitle=${titleDerivedCount} fallbackFromLinkedL1=0 linkedL1Keys=${linkedL1Count}`
2014
+ );
2015
+ return out;
2016
+ }
2017
+
2018
+ const relatedWorkItems = await this.fetchWorkItemsByIds(projectName, [...relatedIds], false);
2019
+ const l1BaseByWorkItemId = new Map<number, string>();
2020
+ for (const workItem of relatedWorkItems || []) {
2021
+ const workItemId = Number(workItem?.id || 0);
2022
+ if (!Number.isFinite(workItemId) || workItemId <= 0) continue;
2023
+ const fields = workItem?.fields || {};
2024
+ const customerId = this.extractMewpRequirementIdentifier(fields);
2025
+ const baseKey = this.toRequirementKey(customerId);
2026
+ if (!baseKey) continue;
2027
+ l1BaseByWorkItemId.set(workItemId, baseKey);
2028
+ }
2029
+
2030
+ let linkedFallbackCount = 0;
2031
+ for (const requirement of requirements || []) {
2032
+ const l2BaseKey = String(requirement?.baseKey || '').trim();
2033
+ if (!l2BaseKey) continue;
2034
+ if (!out.has(l2BaseKey)) out.set(l2BaseKey, new Set<string>());
2035
+ const targetSet = out.get(l2BaseKey)!;
2036
+ if (targetSet.size > 0) {
2037
+ continue;
2038
+ }
2039
+ for (const relatedId of requirement?.relatedWorkItemIds || []) {
2040
+ const baseKey = l1BaseByWorkItemId.get(Number(relatedId));
2041
+ if (baseKey) targetSet.add(baseKey);
2042
+ }
2043
+ if (targetSet.size > 0) {
2044
+ linkedFallbackCount += 1;
2045
+ }
2046
+ }
2047
+
2048
+ const l2Families = out.size;
2049
+ const withLinkedL1 = [...out.values()].filter((set) => set.size > 0).length;
2050
+ const linkedL1Count = [...out.values()].reduce((sum, set) => sum + set.size, 0);
2051
+ logger.info(
2052
+ `MEWP L2->L1 mapping summary: l2Families=${l2Families} withLinkedL1=${withLinkedL1} ` +
2053
+ `fromTitle=${titleDerivedCount} fallbackFromLinkedL1=${linkedFallbackCount} linkedL1Keys=${linkedL1Count}`
2054
+ );
2055
+ return out;
2056
+ }
2057
+
2058
+ private buildMewpExternalJoinKeysByL2Requirement(
2059
+ requirements: MewpL2RequirementFamily[],
2060
+ l2ToLinkedL1BaseKeys: Map<string, Set<string>>
2061
+ ): Map<string, Set<string>> {
2062
+ const out = new Map<string, Set<string>>();
2063
+ for (const requirement of requirements || []) {
2064
+ const l2BaseKey = String(requirement?.baseKey || '').trim();
2065
+ if (!l2BaseKey) continue;
2066
+ const joinKeys = new Set<string>([l2BaseKey]);
2067
+ for (const l1Key of l2ToLinkedL1BaseKeys.get(l2BaseKey) || []) {
2068
+ const normalized = String(l1Key || '').trim();
2069
+ if (normalized) joinKeys.add(normalized);
2070
+ }
2071
+ out.set(l2BaseKey, joinKeys);
2072
+ }
2073
+ return out;
2074
+ }
2075
+
1887
2076
  private buildRequirementFamilyMap(
1888
2077
  requirements: Array<Pick<MewpL2RequirementWorkItem, 'requirementId' | 'baseKey'>>,
1889
2078
  scopedRequirementKeys?: Set<string>