@elisra-devops/docgen-data-provider 1.75.0 → 1.76.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 +120 -0
- package/bin/models/mewp-reporting.js +3 -0
- package/bin/models/mewp-reporting.js.map +1 -0
- package/bin/modules/ResultDataProvider.d.ts +46 -6
- package/bin/modules/ResultDataProvider.js +1115 -118
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +1285 -33
- package/bin/tests/modules/ResultDataProvider.test.js.map +1 -1
- package/bin/utils/mewpExternalIngestionUtils.d.ts +17 -0
- package/bin/utils/mewpExternalIngestionUtils.js +197 -0
- package/bin/utils/mewpExternalIngestionUtils.js.map +1 -0
- package/bin/utils/mewpExternalTableUtils.d.ts +36 -0
- package/bin/utils/mewpExternalTableUtils.js +320 -0
- package/bin/utils/mewpExternalTableUtils.js.map +1 -0
- package/package.json +10 -1
- package/src/models/mewp-reporting.ts +138 -0
- package/src/modules/ResultDataProvider.ts +1342 -166
- package/src/tests/modules/ResultDataProvider.test.ts +1471 -42
- package/src/utils/mewpExternalIngestionUtils.ts +270 -0
- package/src/utils/mewpExternalTableUtils.ts +461 -0
|
@@ -2,7 +2,31 @@ import DataProviderUtils from '../utils/DataProviderUtils';
|
|
|
2
2
|
import { TFSServices } from '../helpers/tfs';
|
|
3
3
|
import { OpenPcrRequest, PlainTestResult, TestSteps } from '../models/tfs-data';
|
|
4
4
|
import { AdoWorkItemComment, AdoWorkItemCommentsResponse } from '../models/ado-comments';
|
|
5
|
+
import type {
|
|
6
|
+
MewpBugLink,
|
|
7
|
+
MewpCoverageBugCell,
|
|
8
|
+
MewpCoverageFlatPayload,
|
|
9
|
+
MewpExternalFilesValidationResponse,
|
|
10
|
+
MewpCoverageL3L4Cell,
|
|
11
|
+
MewpCoverageRequestOptions,
|
|
12
|
+
MewpExternalFileRef,
|
|
13
|
+
MewpExternalTableValidationResult,
|
|
14
|
+
MewpCoverageRow,
|
|
15
|
+
MewpInternalValidationFlatPayload,
|
|
16
|
+
MewpInternalValidationRequestOptions,
|
|
17
|
+
MewpInternalValidationRow,
|
|
18
|
+
MewpL2RequirementFamily,
|
|
19
|
+
MewpL2RequirementWorkItem,
|
|
20
|
+
MewpL3L4Link,
|
|
21
|
+
MewpLinkedRequirementsByTestCase,
|
|
22
|
+
MewpRequirementIndex,
|
|
23
|
+
MewpRunStatus,
|
|
24
|
+
} from '../models/mewp-reporting';
|
|
5
25
|
import logger from '../utils/logger';
|
|
26
|
+
import MewpExternalIngestionUtils from '../utils/mewpExternalIngestionUtils';
|
|
27
|
+
import MewpExternalTableUtils, {
|
|
28
|
+
MewpExternalFileValidationError,
|
|
29
|
+
} from '../utils/mewpExternalTableUtils';
|
|
6
30
|
import Utils from '../utils/testStepParserHelper';
|
|
7
31
|
import TicketsDataProvider from './TicketsDataProvider';
|
|
8
32
|
const pLimit = require('p-limit');
|
|
@@ -30,14 +54,24 @@ const pLimit = require('p-limit');
|
|
|
30
54
|
*/
|
|
31
55
|
export default class ResultDataProvider {
|
|
32
56
|
private static readonly MEWP_L2_COVERAGE_COLUMNS = [
|
|
33
|
-
'
|
|
34
|
-
'
|
|
35
|
-
'
|
|
36
|
-
'
|
|
37
|
-
'
|
|
38
|
-
'
|
|
39
|
-
'
|
|
40
|
-
'
|
|
57
|
+
'L2 REQ ID',
|
|
58
|
+
'L2 REQ Title',
|
|
59
|
+
'L2 SubSystem',
|
|
60
|
+
'L2 Run Status',
|
|
61
|
+
'Bug ID',
|
|
62
|
+
'Bug Title',
|
|
63
|
+
'Bug Responsibility',
|
|
64
|
+
'L3 REQ ID',
|
|
65
|
+
'L3 REQ Title',
|
|
66
|
+
'L4 REQ ID',
|
|
67
|
+
'L4 REQ Title',
|
|
68
|
+
];
|
|
69
|
+
private static readonly INTERNAL_VALIDATION_COLUMNS = [
|
|
70
|
+
'Test Case ID',
|
|
71
|
+
'Test Case Title',
|
|
72
|
+
'Mentioned but Not Linked',
|
|
73
|
+
'Linked but Not Mentioned',
|
|
74
|
+
'Validation Status',
|
|
41
75
|
];
|
|
42
76
|
|
|
43
77
|
orgUrl: string = '';
|
|
@@ -47,6 +81,8 @@ export default class ResultDataProvider {
|
|
|
47
81
|
private testToAssociatedItemMap: Map<number, Set<any>>;
|
|
48
82
|
private querySelectedColumns: any[];
|
|
49
83
|
private workItemDiscussionCache: Map<number, any[]>;
|
|
84
|
+
private mewpExternalTableUtils: MewpExternalTableUtils;
|
|
85
|
+
private mewpExternalIngestionUtils: MewpExternalIngestionUtils;
|
|
50
86
|
constructor(orgUrl: string, token: string) {
|
|
51
87
|
this.orgUrl = orgUrl;
|
|
52
88
|
this.token = token;
|
|
@@ -54,6 +90,8 @@ export default class ResultDataProvider {
|
|
|
54
90
|
this.testToAssociatedItemMap = new Map<number, Set<any>>();
|
|
55
91
|
this.querySelectedColumns = [];
|
|
56
92
|
this.workItemDiscussionCache = new Map<number, any[]>();
|
|
93
|
+
this.mewpExternalTableUtils = new MewpExternalTableUtils();
|
|
94
|
+
this.mewpExternalIngestionUtils = new MewpExternalIngestionUtils(this.mewpExternalTableUtils);
|
|
57
95
|
}
|
|
58
96
|
|
|
59
97
|
/**
|
|
@@ -393,20 +431,53 @@ export default class ResultDataProvider {
|
|
|
393
431
|
public async getMewpL2CoverageFlatResults(
|
|
394
432
|
testPlanId: string,
|
|
395
433
|
projectName: string,
|
|
396
|
-
selectedSuiteIds: number[] | undefined
|
|
397
|
-
|
|
398
|
-
|
|
434
|
+
selectedSuiteIds: number[] | undefined,
|
|
435
|
+
linkedQueryRequest?: any,
|
|
436
|
+
options?: MewpCoverageRequestOptions
|
|
437
|
+
): Promise<MewpCoverageFlatPayload> {
|
|
438
|
+
const defaultPayload: MewpCoverageFlatPayload = {
|
|
399
439
|
sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
|
|
400
440
|
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
401
|
-
rows: []
|
|
441
|
+
rows: [],
|
|
402
442
|
};
|
|
403
443
|
|
|
404
444
|
try {
|
|
405
445
|
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
406
|
-
const
|
|
407
|
-
|
|
446
|
+
const testData = await this.fetchMewpScopedTestData(
|
|
447
|
+
testPlanId,
|
|
448
|
+
projectName,
|
|
449
|
+
selectedSuiteIds,
|
|
450
|
+
!!options?.useRelFallback
|
|
451
|
+
);
|
|
452
|
+
|
|
453
|
+
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
454
|
+
if (allRequirements.length === 0) {
|
|
455
|
+
return {
|
|
456
|
+
...defaultPayload,
|
|
457
|
+
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
408
460
|
|
|
409
|
-
const
|
|
461
|
+
const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
|
|
462
|
+
allRequirements,
|
|
463
|
+
testData,
|
|
464
|
+
projectName
|
|
465
|
+
);
|
|
466
|
+
const scopedRequirementKeys = await this.resolveMewpRequirementScopeKeysFromQuery(
|
|
467
|
+
linkedQueryRequest,
|
|
468
|
+
allRequirements,
|
|
469
|
+
linkedRequirementsByTestCase
|
|
470
|
+
);
|
|
471
|
+
const requirements = this.collapseMewpRequirementFamilies(
|
|
472
|
+
allRequirements,
|
|
473
|
+
scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
|
|
474
|
+
);
|
|
475
|
+
const requirementSapWbsByBaseKey = this.buildRequirementSapWbsByBaseKey(allRequirements);
|
|
476
|
+
const externalBugsByTestCase = await this.loadExternalBugsByTestCase(options?.externalBugsFile);
|
|
477
|
+
const externalL3L4ByBaseKey = await this.loadExternalL3L4ByBaseKey(
|
|
478
|
+
options?.externalL3L4File,
|
|
479
|
+
requirementSapWbsByBaseKey
|
|
480
|
+
);
|
|
410
481
|
if (requirements.length === 0) {
|
|
411
482
|
return {
|
|
412
483
|
...defaultPayload,
|
|
@@ -414,47 +485,40 @@ export default class ResultDataProvider {
|
|
|
414
485
|
};
|
|
415
486
|
}
|
|
416
487
|
|
|
417
|
-
const requirementIndex = new Map
|
|
488
|
+
const requirementIndex: MewpRequirementIndex = new Map();
|
|
418
489
|
const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
|
|
419
490
|
const requirementKeys = new Set<string>();
|
|
420
491
|
requirements.forEach((requirement) => {
|
|
421
|
-
const key =
|
|
492
|
+
const key = String(requirement?.baseKey || '').trim();
|
|
422
493
|
if (!key) return;
|
|
423
494
|
requirementKeys.add(key);
|
|
424
495
|
});
|
|
425
496
|
|
|
426
497
|
const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
|
|
427
498
|
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
428
|
-
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
429
|
-
|
|
430
499
|
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
431
500
|
for (const runResult of runResults) {
|
|
432
501
|
const testCaseId = this.extractMewpTestCaseId(runResult);
|
|
433
|
-
const
|
|
434
|
-
runResult
|
|
435
|
-
);
|
|
436
|
-
if (Number.isFinite(testCaseId) && testCaseId > 0 && runTestCaseTitle && !testCaseTitleMap.has(testCaseId)) {
|
|
437
|
-
testCaseTitleMap.set(testCaseId, runTestCaseTitle);
|
|
438
|
-
}
|
|
439
|
-
const actionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
440
|
-
? runResult.iteration.actionResults
|
|
502
|
+
const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
503
|
+
? runResult.iteration.actionResults.filter((item: any) => !item?.isSharedStepTitle)
|
|
441
504
|
: [];
|
|
505
|
+
const actionResults = rawActionResults.sort((a: any, b: any) =>
|
|
506
|
+
this.compareActionResults(
|
|
507
|
+
String(a?.stepPosition || a?.stepIdentifier || ''),
|
|
508
|
+
String(b?.stepPosition || b?.stepIdentifier || '')
|
|
509
|
+
)
|
|
510
|
+
);
|
|
442
511
|
const hasExecutedRun =
|
|
443
512
|
Number(runResult?.lastRunId || 0) > 0 && Number(runResult?.lastResultId || 0) > 0;
|
|
444
513
|
|
|
445
514
|
if (actionResults.length > 0) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
requirementKeys,
|
|
454
|
-
requirementIndex,
|
|
455
|
-
observedTestCaseIdsByRequirement
|
|
456
|
-
);
|
|
457
|
-
}
|
|
515
|
+
this.accumulateRequirementCountsFromActionResults(
|
|
516
|
+
actionResults,
|
|
517
|
+
testCaseId,
|
|
518
|
+
requirementKeys,
|
|
519
|
+
requirementIndex,
|
|
520
|
+
observedTestCaseIdsByRequirement
|
|
521
|
+
);
|
|
458
522
|
continue;
|
|
459
523
|
}
|
|
460
524
|
|
|
@@ -475,24 +539,33 @@ export default class ResultDataProvider {
|
|
|
475
539
|
}
|
|
476
540
|
|
|
477
541
|
const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
);
|
|
488
|
-
|
|
542
|
+
const fallbackActionResults = definitionSteps
|
|
543
|
+
.filter((step) => !step?.isSharedStepTitle)
|
|
544
|
+
.sort((a, b) =>
|
|
545
|
+
this.compareActionResults(String(a?.stepPosition || ''), String(b?.stepPosition || ''))
|
|
546
|
+
)
|
|
547
|
+
.map((step) => ({
|
|
548
|
+
stepPosition: step?.stepPosition,
|
|
549
|
+
expected: step?.expected,
|
|
550
|
+
outcome: 'Unspecified',
|
|
551
|
+
}));
|
|
552
|
+
|
|
553
|
+
this.accumulateRequirementCountsFromActionResults(
|
|
554
|
+
fallbackActionResults,
|
|
555
|
+
testCaseId,
|
|
556
|
+
requirementKeys,
|
|
557
|
+
requirementIndex,
|
|
558
|
+
observedTestCaseIdsByRequirement
|
|
559
|
+
);
|
|
489
560
|
}
|
|
490
561
|
|
|
491
562
|
const rows = this.buildMewpCoverageRows(
|
|
492
563
|
requirements,
|
|
493
564
|
requirementIndex,
|
|
494
565
|
observedTestCaseIdsByRequirement,
|
|
495
|
-
|
|
566
|
+
linkedRequirementsByTestCase,
|
|
567
|
+
externalL3L4ByBaseKey,
|
|
568
|
+
externalBugsByTestCase
|
|
496
569
|
);
|
|
497
570
|
|
|
498
571
|
return {
|
|
@@ -502,10 +575,261 @@ export default class ResultDataProvider {
|
|
|
502
575
|
};
|
|
503
576
|
} catch (error: any) {
|
|
504
577
|
logger.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
|
|
578
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
581
|
+
return defaultPayload;
|
|
582
|
+
}
|
|
583
|
+
}
|
|
584
|
+
|
|
585
|
+
public async getMewpInternalValidationFlatResults(
|
|
586
|
+
testPlanId: string,
|
|
587
|
+
projectName: string,
|
|
588
|
+
selectedSuiteIds: number[] | undefined,
|
|
589
|
+
linkedQueryRequest?: any,
|
|
590
|
+
options?: MewpInternalValidationRequestOptions
|
|
591
|
+
): Promise<MewpInternalValidationFlatPayload> {
|
|
592
|
+
const defaultPayload: MewpInternalValidationFlatPayload = {
|
|
593
|
+
sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
|
|
594
|
+
columnOrder: [...ResultDataProvider.INTERNAL_VALIDATION_COLUMNS],
|
|
595
|
+
rows: [],
|
|
596
|
+
};
|
|
597
|
+
|
|
598
|
+
try {
|
|
599
|
+
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
600
|
+
const testData = await this.fetchMewpScopedTestData(
|
|
601
|
+
testPlanId,
|
|
602
|
+
projectName,
|
|
603
|
+
selectedSuiteIds,
|
|
604
|
+
!!options?.useRelFallback
|
|
605
|
+
);
|
|
606
|
+
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
607
|
+
const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
|
|
608
|
+
allRequirements,
|
|
609
|
+
testData,
|
|
610
|
+
projectName
|
|
611
|
+
);
|
|
612
|
+
const scopedRequirementKeys = await this.resolveMewpRequirementScopeKeysFromQuery(
|
|
613
|
+
linkedQueryRequest,
|
|
614
|
+
allRequirements,
|
|
615
|
+
linkedRequirementsByTestCase
|
|
616
|
+
);
|
|
617
|
+
const requirementFamilies = this.buildRequirementFamilyMap(
|
|
618
|
+
allRequirements,
|
|
619
|
+
scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
|
|
620
|
+
);
|
|
621
|
+
|
|
622
|
+
const rows: MewpInternalValidationRow[] = [];
|
|
623
|
+
const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
|
|
624
|
+
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
625
|
+
const allTestCaseIds = new Set<number>();
|
|
626
|
+
for (const suite of testData || []) {
|
|
627
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
628
|
+
for (const testCase of testCasesItems) {
|
|
629
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
630
|
+
if (Number.isFinite(id) && id > 0) allTestCaseIds.add(id);
|
|
631
|
+
}
|
|
632
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
633
|
+
for (const testPoint of testPointsItems) {
|
|
634
|
+
const id = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
|
|
635
|
+
if (Number.isFinite(id) && id > 0) allTestCaseIds.add(id);
|
|
636
|
+
}
|
|
637
|
+
}
|
|
638
|
+
|
|
639
|
+
const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
|
|
640
|
+
|
|
641
|
+
for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
|
|
642
|
+
const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
|
|
643
|
+
const parsedSteps =
|
|
644
|
+
stepsXml && String(stepsXml).trim() !== ''
|
|
645
|
+
? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
|
|
646
|
+
: [];
|
|
647
|
+
const mentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
|
|
648
|
+
const mentionedL2Only = new Set<string>();
|
|
649
|
+
const mentionedCodeFirstStep = new Map<string, string>();
|
|
650
|
+
const mentionedBaseFirstStep = new Map<string, string>();
|
|
651
|
+
for (const mentionEntry of mentionEntries) {
|
|
652
|
+
const scopeFilteredCodes =
|
|
653
|
+
scopedRequirementKeys?.size && mentionEntry.codes.size > 0
|
|
654
|
+
? [...mentionEntry.codes].filter((code) => scopedRequirementKeys.has(this.toRequirementKey(code)))
|
|
655
|
+
: [...mentionEntry.codes];
|
|
656
|
+
for (const code of scopeFilteredCodes) {
|
|
657
|
+
const baseKey = this.toRequirementKey(code);
|
|
658
|
+
if (!baseKey) continue;
|
|
659
|
+
if (validL2BaseKeys.has(baseKey)) {
|
|
660
|
+
mentionedL2Only.add(code);
|
|
661
|
+
if (!mentionedCodeFirstStep.has(code)) {
|
|
662
|
+
mentionedCodeFirstStep.set(code, mentionEntry.stepRef);
|
|
663
|
+
}
|
|
664
|
+
if (!mentionedBaseFirstStep.has(baseKey)) {
|
|
665
|
+
mentionedBaseFirstStep.set(baseKey, mentionEntry.stepRef);
|
|
666
|
+
}
|
|
667
|
+
}
|
|
668
|
+
}
|
|
669
|
+
}
|
|
670
|
+
|
|
671
|
+
const mentionedBaseKeys = new Set<string>(
|
|
672
|
+
[...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
673
|
+
);
|
|
674
|
+
|
|
675
|
+
const expectedFamilyCodes = new Set<string>();
|
|
676
|
+
for (const baseKey of mentionedBaseKeys) {
|
|
677
|
+
const familyCodes = requirementFamilies.get(baseKey);
|
|
678
|
+
if (familyCodes?.size) {
|
|
679
|
+
familyCodes.forEach((code) => expectedFamilyCodes.add(code));
|
|
680
|
+
} else {
|
|
681
|
+
for (const code of mentionedL2Only) {
|
|
682
|
+
if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
|
|
683
|
+
}
|
|
684
|
+
}
|
|
685
|
+
}
|
|
686
|
+
|
|
687
|
+
const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
|
|
688
|
+
const linkedFullCodes =
|
|
689
|
+
scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
|
|
690
|
+
? new Set<string>(
|
|
691
|
+
[...linkedFullCodesRaw].filter((code) =>
|
|
692
|
+
scopedRequirementKeys.has(this.toRequirementKey(code))
|
|
693
|
+
)
|
|
694
|
+
)
|
|
695
|
+
: linkedFullCodesRaw;
|
|
696
|
+
const linkedBaseKeys = new Set<string>(
|
|
697
|
+
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
698
|
+
);
|
|
699
|
+
|
|
700
|
+
const missingMentioned = [...mentionedL2Only].filter((code) => {
|
|
701
|
+
const baseKey = this.toRequirementKey(code);
|
|
702
|
+
if (!baseKey) return false;
|
|
703
|
+
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
704
|
+
if (hasSpecificSuffix) return !linkedFullCodes.has(code);
|
|
705
|
+
return !linkedBaseKeys.has(baseKey);
|
|
706
|
+
});
|
|
707
|
+
const missingFamily = [...expectedFamilyCodes].filter((code) => !linkedFullCodes.has(code));
|
|
708
|
+
const extraLinked = [...linkedFullCodes].filter((code) => !expectedFamilyCodes.has(code));
|
|
709
|
+
const mentionedButNotLinkedByStep = new Map<string, Set<string>>();
|
|
710
|
+
const appendMentionedButNotLinked = (requirementId: string, stepRef: string) => {
|
|
711
|
+
const normalizedRequirementId = this.normalizeMewpRequirementCodeWithSuffix(requirementId);
|
|
712
|
+
if (!normalizedRequirementId) return;
|
|
713
|
+
const normalizedStepRef = String(stepRef || 'Step ?').trim() || 'Step ?';
|
|
714
|
+
if (!mentionedButNotLinkedByStep.has(normalizedStepRef)) {
|
|
715
|
+
mentionedButNotLinkedByStep.set(normalizedStepRef, new Set<string>());
|
|
716
|
+
}
|
|
717
|
+
mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
|
|
718
|
+
};
|
|
719
|
+
|
|
720
|
+
const sortedMissingMentioned = [...new Set(missingMentioned)].sort((a, b) => a.localeCompare(b));
|
|
721
|
+
const sortedMissingFamily = [...new Set(missingFamily)].sort((a, b) => a.localeCompare(b));
|
|
722
|
+
for (const code of sortedMissingMentioned) {
|
|
723
|
+
const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
|
|
724
|
+
appendMentionedButNotLinked(code, stepRef);
|
|
725
|
+
}
|
|
726
|
+
for (const code of sortedMissingFamily) {
|
|
727
|
+
const baseKey = this.toRequirementKey(code);
|
|
728
|
+
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
729
|
+
appendMentionedButNotLinked(code, stepRef);
|
|
730
|
+
}
|
|
731
|
+
|
|
732
|
+
const sortedExtraLinked = [...new Set(extraLinked)]
|
|
733
|
+
.map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
|
|
734
|
+
.filter((code) => !!code)
|
|
735
|
+
.sort((a, b) => a.localeCompare(b));
|
|
736
|
+
|
|
737
|
+
const parseStepOrder = (stepRef: string): number => {
|
|
738
|
+
const match = /step\s+(\d+)/i.exec(String(stepRef || ''));
|
|
739
|
+
const parsed = Number(match?.[1] || Number.POSITIVE_INFINITY);
|
|
740
|
+
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
741
|
+
};
|
|
742
|
+
const mentionedButNotLinked = [...mentionedButNotLinkedByStep.entries()]
|
|
743
|
+
.sort((a, b) => {
|
|
744
|
+
const stepOrderA = parseStepOrder(a[0]);
|
|
745
|
+
const stepOrderB = parseStepOrder(b[0]);
|
|
746
|
+
if (stepOrderA !== stepOrderB) return stepOrderA - stepOrderB;
|
|
747
|
+
return String(a[0]).localeCompare(String(b[0]));
|
|
748
|
+
})
|
|
749
|
+
.map(([stepRef, requirementIds]) => {
|
|
750
|
+
const requirementList = [...requirementIds].sort((a, b) => a.localeCompare(b));
|
|
751
|
+
return `${stepRef}: ${requirementList.join(', ')}`;
|
|
752
|
+
})
|
|
753
|
+
.join('; ');
|
|
754
|
+
const linkedButNotMentioned = sortedExtraLinked.join('; ');
|
|
755
|
+
const validationStatus: 'Pass' | 'Fail' =
|
|
756
|
+
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
757
|
+
|
|
758
|
+
rows.push({
|
|
759
|
+
'Test Case ID': testCaseId,
|
|
760
|
+
'Test Case Title': String(testCaseTitleMap.get(testCaseId) || '').trim(),
|
|
761
|
+
'Mentioned but Not Linked': mentionedButNotLinked,
|
|
762
|
+
'Linked but Not Mentioned': linkedButNotMentioned,
|
|
763
|
+
'Validation Status': validationStatus,
|
|
764
|
+
});
|
|
765
|
+
}
|
|
766
|
+
|
|
767
|
+
return {
|
|
768
|
+
sheetName: this.buildInternalValidationSheetName(planName, testPlanId),
|
|
769
|
+
columnOrder: [...ResultDataProvider.INTERNAL_VALIDATION_COLUMNS],
|
|
770
|
+
rows,
|
|
771
|
+
};
|
|
772
|
+
} catch (error: any) {
|
|
773
|
+
logger.error(`Error during getMewpInternalValidationFlatResults: ${error.message}`);
|
|
505
774
|
return defaultPayload;
|
|
506
775
|
}
|
|
507
776
|
}
|
|
508
777
|
|
|
778
|
+
public async validateMewpExternalFiles(options: {
|
|
779
|
+
externalBugsFile?: MewpExternalFileRef | null;
|
|
780
|
+
externalL3L4File?: MewpExternalFileRef | null;
|
|
781
|
+
}): Promise<MewpExternalFilesValidationResponse> {
|
|
782
|
+
const response: MewpExternalFilesValidationResponse = { valid: true };
|
|
783
|
+
const validateOne = async (
|
|
784
|
+
file: MewpExternalFileRef | null | undefined,
|
|
785
|
+
tableType: 'bugs' | 'l3l4'
|
|
786
|
+
): Promise<MewpExternalTableValidationResult | undefined> => {
|
|
787
|
+
const sourceName = String(file?.name || file?.objectName || file?.text || file?.url || '').trim();
|
|
788
|
+
if (!sourceName) return undefined;
|
|
789
|
+
|
|
790
|
+
try {
|
|
791
|
+
const { rows, meta } = await this.mewpExternalTableUtils.loadExternalTableRowsWithMeta(
|
|
792
|
+
file,
|
|
793
|
+
tableType
|
|
794
|
+
);
|
|
795
|
+
return {
|
|
796
|
+
tableType,
|
|
797
|
+
sourceName: meta.sourceName,
|
|
798
|
+
valid: true,
|
|
799
|
+
headerRow: meta.headerRow,
|
|
800
|
+
matchedRequiredColumns: meta.matchedRequiredColumns,
|
|
801
|
+
totalRequiredColumns: meta.totalRequiredColumns,
|
|
802
|
+
missingRequiredColumns: [],
|
|
803
|
+
rowCount: rows.length,
|
|
804
|
+
message: 'File schema is valid',
|
|
805
|
+
};
|
|
806
|
+
} catch (error: any) {
|
|
807
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
808
|
+
return {
|
|
809
|
+
...error.details,
|
|
810
|
+
valid: false,
|
|
811
|
+
};
|
|
812
|
+
}
|
|
813
|
+
return {
|
|
814
|
+
tableType,
|
|
815
|
+
sourceName: sourceName || tableType,
|
|
816
|
+
valid: false,
|
|
817
|
+
headerRow: '',
|
|
818
|
+
matchedRequiredColumns: 0,
|
|
819
|
+
totalRequiredColumns: this.mewpExternalTableUtils.getRequiredColumnCount(tableType),
|
|
820
|
+
missingRequiredColumns: [],
|
|
821
|
+
rowCount: 0,
|
|
822
|
+
message: String(error?.message || error || 'Unknown validation error'),
|
|
823
|
+
};
|
|
824
|
+
}
|
|
825
|
+
};
|
|
826
|
+
|
|
827
|
+
response.bugs = await validateOne(options?.externalBugsFile, 'bugs');
|
|
828
|
+
response.l3l4 = await validateOne(options?.externalL3L4File, 'l3l4');
|
|
829
|
+
response.valid = [response.bugs, response.l3l4].filter(Boolean).every((item) => !!item?.valid);
|
|
830
|
+
return response;
|
|
831
|
+
}
|
|
832
|
+
|
|
509
833
|
/**
|
|
510
834
|
* Mapping each attachment to a proper URL for downloading it
|
|
511
835
|
* @param runResults Array of run results
|
|
@@ -549,33 +873,63 @@ export default class ResultDataProvider {
|
|
|
549
873
|
return `MEWP L2 Coverage - ${suffix}`;
|
|
550
874
|
}
|
|
551
875
|
|
|
876
|
+
private buildInternalValidationSheetName(planName: string, testPlanId: string): string {
|
|
877
|
+
const suffix = String(planName || '').trim() || `Plan ${testPlanId}`;
|
|
878
|
+
return `MEWP Internal Validation - ${suffix}`;
|
|
879
|
+
}
|
|
880
|
+
|
|
552
881
|
private createMewpCoverageRow(
|
|
553
|
-
requirement:
|
|
554
|
-
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
) {
|
|
562
|
-
const customerId = this.formatMewpCustomerId(requirement.requirementId);
|
|
563
|
-
const customerTitle = this.toMewpComparableText(requirement.title);
|
|
564
|
-
const responsibility = this.toMewpComparableText(requirement.responsibility);
|
|
565
|
-
const safeTestCaseId = Number.isFinite(testCaseId) && Number(testCaseId) > 0 ? Number(testCaseId) : '';
|
|
882
|
+
requirement: Pick<MewpL2RequirementFamily, 'requirementId' | 'title' | 'subSystem' | 'responsibility'>,
|
|
883
|
+
runStatus: MewpRunStatus,
|
|
884
|
+
bug: MewpCoverageBugCell,
|
|
885
|
+
linkedL3L4: MewpCoverageL3L4Cell
|
|
886
|
+
): MewpCoverageRow {
|
|
887
|
+
const l2ReqId = this.formatMewpCustomerId(requirement.requirementId);
|
|
888
|
+
const l2ReqTitle = this.toMewpComparableText(requirement.title);
|
|
889
|
+
const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
|
|
566
890
|
|
|
567
891
|
return {
|
|
568
|
-
'
|
|
569
|
-
'
|
|
570
|
-
'
|
|
571
|
-
'
|
|
572
|
-
'
|
|
573
|
-
'
|
|
574
|
-
'
|
|
575
|
-
'
|
|
892
|
+
'L2 REQ ID': l2ReqId,
|
|
893
|
+
'L2 REQ Title': l2ReqTitle,
|
|
894
|
+
'L2 SubSystem': l2SubSystem,
|
|
895
|
+
'L2 Run Status': runStatus,
|
|
896
|
+
'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
|
|
897
|
+
'Bug Title': String(bug?.title || '').trim(),
|
|
898
|
+
'Bug Responsibility': String(bug?.responsibility || '').trim(),
|
|
899
|
+
'L3 REQ ID': String(linkedL3L4?.l3Id || '').trim(),
|
|
900
|
+
'L3 REQ Title': String(linkedL3L4?.l3Title || '').trim(),
|
|
901
|
+
'L4 REQ ID': String(linkedL3L4?.l4Id || '').trim(),
|
|
902
|
+
'L4 REQ Title': String(linkedL3L4?.l4Title || '').trim(),
|
|
576
903
|
};
|
|
577
904
|
}
|
|
578
905
|
|
|
906
|
+
private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
|
|
907
|
+
return { id: '' as '', title: '', responsibility: '' };
|
|
908
|
+
}
|
|
909
|
+
|
|
910
|
+
private createEmptyMewpCoverageL3L4Cell(): MewpCoverageL3L4Cell {
|
|
911
|
+
return { l3Id: '', l3Title: '', l4Id: '', l4Title: '' };
|
|
912
|
+
}
|
|
913
|
+
|
|
914
|
+
private buildMewpCoverageL3L4Rows(links: MewpL3L4Link[]): MewpCoverageL3L4Cell[] {
|
|
915
|
+
const sorted = [...(links || [])].sort((a, b) => {
|
|
916
|
+
if (a.level !== b.level) return a.level === 'L3' ? -1 : 1;
|
|
917
|
+
return String(a.id || '').localeCompare(String(b.id || ''));
|
|
918
|
+
});
|
|
919
|
+
|
|
920
|
+
const rows: MewpCoverageL3L4Cell[] = [];
|
|
921
|
+
for (const item of sorted) {
|
|
922
|
+
const isL3 = item.level === 'L3';
|
|
923
|
+
rows.push({
|
|
924
|
+
l3Id: isL3 ? String(item?.id || '').trim() : '',
|
|
925
|
+
l3Title: isL3 ? String(item?.title || '').trim() : '',
|
|
926
|
+
l4Id: isL3 ? '' : String(item?.id || '').trim(),
|
|
927
|
+
l4Title: isL3 ? '' : String(item?.title || '').trim(),
|
|
928
|
+
});
|
|
929
|
+
}
|
|
930
|
+
return rows;
|
|
931
|
+
}
|
|
932
|
+
|
|
579
933
|
private formatMewpCustomerId(rawValue: string): string {
|
|
580
934
|
const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
|
|
581
935
|
if (normalized) return normalized;
|
|
@@ -586,51 +940,109 @@ export default class ResultDataProvider {
|
|
|
586
940
|
}
|
|
587
941
|
|
|
588
942
|
private buildMewpCoverageRows(
|
|
589
|
-
requirements:
|
|
590
|
-
|
|
591
|
-
title: string;
|
|
592
|
-
responsibility: string;
|
|
593
|
-
linkedTestCaseIds: number[];
|
|
594
|
-
}>,
|
|
595
|
-
requirementIndex: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
943
|
+
requirements: MewpL2RequirementFamily[],
|
|
944
|
+
requirementIndex: MewpRequirementIndex,
|
|
596
945
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
946
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
|
|
947
|
+
l3l4ByBaseKey: Map<string, MewpL3L4Link[]>,
|
|
948
|
+
externalBugsByTestCase: Map<number, MewpBugLink[]>
|
|
949
|
+
): MewpCoverageRow[] {
|
|
950
|
+
const rows: MewpCoverageRow[] = [];
|
|
951
|
+
const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
|
|
600
952
|
for (const requirement of requirements) {
|
|
601
|
-
const key = this.toRequirementKey(requirement.requirementId);
|
|
953
|
+
const key = String(requirement?.baseKey || this.toRequirementKey(requirement.requirementId) || '').trim();
|
|
602
954
|
const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
|
|
603
955
|
(id) => Number.isFinite(id) && Number(id) > 0
|
|
604
956
|
);
|
|
957
|
+
const linkedByTestCase = key ? Array.from(linkedByRequirement.get(key) || []) : [];
|
|
605
958
|
const observedTestCaseIds = key
|
|
606
959
|
? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
|
|
607
960
|
: [];
|
|
608
961
|
|
|
609
|
-
const testCaseIds = Array.from(
|
|
610
|
-
(
|
|
611
|
-
);
|
|
962
|
+
const testCaseIds = Array.from(
|
|
963
|
+
new Set<number>([...linkedTestCaseIds, ...linkedByTestCase, ...observedTestCaseIds])
|
|
964
|
+
).sort((a, b) => a - b);
|
|
612
965
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
failed: 0,
|
|
618
|
-
notRun: 0,
|
|
619
|
-
})
|
|
620
|
-
);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
966
|
+
let totalPassed = 0;
|
|
967
|
+
let totalFailed = 0;
|
|
968
|
+
let totalNotRun = 0;
|
|
969
|
+
const aggregatedBugs = new Map<number, MewpBugLink>();
|
|
623
970
|
|
|
624
971
|
for (const testCaseId of testCaseIds) {
|
|
625
972
|
const summary = key
|
|
626
973
|
? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
|
|
627
974
|
: { passed: 0, failed: 0, notRun: 0 };
|
|
975
|
+
totalPassed += summary.passed;
|
|
976
|
+
totalFailed += summary.failed;
|
|
977
|
+
totalNotRun += summary.notRun;
|
|
978
|
+
|
|
979
|
+
if (summary.failed > 0) {
|
|
980
|
+
const externalBugs = externalBugsByTestCase.get(testCaseId) || [];
|
|
981
|
+
for (const bug of externalBugs) {
|
|
982
|
+
const bugBaseKey = String(bug?.requirementBaseKey || '').trim();
|
|
983
|
+
if (bugBaseKey && bugBaseKey !== key) continue;
|
|
984
|
+
const bugId = Number(bug?.id || 0);
|
|
985
|
+
if (!Number.isFinite(bugId) || bugId <= 0) continue;
|
|
986
|
+
aggregatedBugs.set(bugId, {
|
|
987
|
+
...bug,
|
|
988
|
+
responsibility: this.resolveCoverageBugResponsibility(
|
|
989
|
+
String(bug?.responsibility || ''),
|
|
990
|
+
requirement
|
|
991
|
+
),
|
|
992
|
+
});
|
|
993
|
+
}
|
|
994
|
+
}
|
|
995
|
+
}
|
|
996
|
+
|
|
997
|
+
const runStatus = this.resolveMewpL2RunStatus({
|
|
998
|
+
passed: totalPassed,
|
|
999
|
+
failed: totalFailed,
|
|
1000
|
+
notRun: totalNotRun,
|
|
1001
|
+
hasAnyTestCase: testCaseIds.length > 0,
|
|
1002
|
+
});
|
|
1003
|
+
|
|
1004
|
+
const bugsForRows =
|
|
1005
|
+
runStatus === 'Fail'
|
|
1006
|
+
? Array.from(aggregatedBugs.values()).sort((a, b) => a.id - b.id)
|
|
1007
|
+
: [];
|
|
1008
|
+
const l3l4ForRows = [...(l3l4ByBaseKey.get(key) || [])];
|
|
1009
|
+
|
|
1010
|
+
const bugRows: MewpCoverageBugCell[] =
|
|
1011
|
+
bugsForRows.length > 0
|
|
1012
|
+
? bugsForRows
|
|
1013
|
+
: [];
|
|
1014
|
+
const l3l4Rows: MewpCoverageL3L4Cell[] = this.buildMewpCoverageL3L4Rows(l3l4ForRows);
|
|
1015
|
+
|
|
1016
|
+
if (bugRows.length === 0 && l3l4Rows.length === 0) {
|
|
628
1017
|
rows.push(
|
|
629
1018
|
this.createMewpCoverageRow(
|
|
630
1019
|
requirement,
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1020
|
+
runStatus,
|
|
1021
|
+
this.createEmptyMewpCoverageBugCell(),
|
|
1022
|
+
this.createEmptyMewpCoverageL3L4Cell()
|
|
1023
|
+
)
|
|
1024
|
+
);
|
|
1025
|
+
continue;
|
|
1026
|
+
}
|
|
1027
|
+
|
|
1028
|
+
for (const bug of bugRows) {
|
|
1029
|
+
rows.push(
|
|
1030
|
+
this.createMewpCoverageRow(
|
|
1031
|
+
requirement,
|
|
1032
|
+
runStatus,
|
|
1033
|
+
bug,
|
|
1034
|
+
this.createEmptyMewpCoverageL3L4Cell()
|
|
1035
|
+
)
|
|
1036
|
+
);
|
|
1037
|
+
}
|
|
1038
|
+
|
|
1039
|
+
for (const linkedL3L4 of l3l4Rows) {
|
|
1040
|
+
rows.push(
|
|
1041
|
+
this.createMewpCoverageRow(
|
|
1042
|
+
requirement,
|
|
1043
|
+
runStatus,
|
|
1044
|
+
this.createEmptyMewpCoverageBugCell(),
|
|
1045
|
+
linkedL3L4
|
|
634
1046
|
)
|
|
635
1047
|
);
|
|
636
1048
|
}
|
|
@@ -639,6 +1051,262 @@ export default class ResultDataProvider {
|
|
|
639
1051
|
return rows;
|
|
640
1052
|
}
|
|
641
1053
|
|
|
1054
|
+
private resolveCoverageBugResponsibility(
|
|
1055
|
+
rawResponsibility: string,
|
|
1056
|
+
requirement: Pick<MewpL2RequirementFamily, 'responsibility'>
|
|
1057
|
+
): string {
|
|
1058
|
+
const direct = String(rawResponsibility || '').trim();
|
|
1059
|
+
if (direct && direct.toLowerCase() !== 'unknown') return direct;
|
|
1060
|
+
|
|
1061
|
+
const requirementResponsibility = String(requirement?.responsibility || '')
|
|
1062
|
+
.trim()
|
|
1063
|
+
.toUpperCase();
|
|
1064
|
+
if (requirementResponsibility === 'ESUK') return 'ESUK';
|
|
1065
|
+
if (requirementResponsibility === 'IL' || requirementResponsibility === 'ELISRA') return 'Elisra';
|
|
1066
|
+
|
|
1067
|
+
return direct || 'Unknown';
|
|
1068
|
+
}
|
|
1069
|
+
|
|
1070
|
+
private resolveMewpL2RunStatus(input: {
|
|
1071
|
+
passed: number;
|
|
1072
|
+
failed: number;
|
|
1073
|
+
notRun: number;
|
|
1074
|
+
hasAnyTestCase: boolean;
|
|
1075
|
+
}): MewpRunStatus {
|
|
1076
|
+
if ((input?.failed || 0) > 0) return 'Fail';
|
|
1077
|
+
if ((input?.notRun || 0) > 0) return 'Not Run';
|
|
1078
|
+
if ((input?.passed || 0) > 0) return 'Pass';
|
|
1079
|
+
return input?.hasAnyTestCase ? 'Not Run' : 'Not Run';
|
|
1080
|
+
}
|
|
1081
|
+
|
|
1082
|
+
private async fetchMewpScopedTestData(
|
|
1083
|
+
testPlanId: string,
|
|
1084
|
+
projectName: string,
|
|
1085
|
+
selectedSuiteIds: number[] | undefined,
|
|
1086
|
+
useRelFallback: boolean
|
|
1087
|
+
): Promise<any[]> {
|
|
1088
|
+
if (!useRelFallback) {
|
|
1089
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1090
|
+
return this.fetchTestData(suites, projectName, testPlanId, false);
|
|
1091
|
+
}
|
|
1092
|
+
|
|
1093
|
+
const selectedSuites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1094
|
+
const selectedRel = this.resolveMaxRelNumberFromSuites(selectedSuites);
|
|
1095
|
+
if (selectedRel <= 0) {
|
|
1096
|
+
return this.fetchTestData(selectedSuites, projectName, testPlanId, false);
|
|
1097
|
+
}
|
|
1098
|
+
|
|
1099
|
+
const allSuites = await this.fetchTestSuites(testPlanId, projectName, undefined, true);
|
|
1100
|
+
const relScopedSuites = allSuites.filter((suite) => {
|
|
1101
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1102
|
+
return rel > 0 && rel <= selectedRel;
|
|
1103
|
+
});
|
|
1104
|
+
const suitesForFetch = relScopedSuites.length > 0 ? relScopedSuites : selectedSuites;
|
|
1105
|
+
const rawTestData = await this.fetchTestData(suitesForFetch, projectName, testPlanId, false);
|
|
1106
|
+
return this.reduceToLatestRelRunPerTestCase(rawTestData);
|
|
1107
|
+
}
|
|
1108
|
+
|
|
1109
|
+
private extractRelNumberFromSuite(suite: any): number {
|
|
1110
|
+
const candidates = [
|
|
1111
|
+
suite?.suiteName,
|
|
1112
|
+
suite?.parentSuiteName,
|
|
1113
|
+
suite?.suitePath,
|
|
1114
|
+
suite?.testGroupName,
|
|
1115
|
+
];
|
|
1116
|
+
const pattern = /(?:^|[^a-z0-9])rel\s*([0-9]+)/i;
|
|
1117
|
+
for (const item of candidates) {
|
|
1118
|
+
const match = pattern.exec(String(item || ''));
|
|
1119
|
+
if (!match) continue;
|
|
1120
|
+
const parsed = Number(match[1]);
|
|
1121
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1122
|
+
return parsed;
|
|
1123
|
+
}
|
|
1124
|
+
}
|
|
1125
|
+
return 0;
|
|
1126
|
+
}
|
|
1127
|
+
|
|
1128
|
+
private resolveMaxRelNumberFromSuites(suites: any[]): number {
|
|
1129
|
+
let maxRel = 0;
|
|
1130
|
+
for (const suite of suites || []) {
|
|
1131
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1132
|
+
if (rel > maxRel) maxRel = rel;
|
|
1133
|
+
}
|
|
1134
|
+
return maxRel;
|
|
1135
|
+
}
|
|
1136
|
+
|
|
1137
|
+
private reduceToLatestRelRunPerTestCase(testData: any[]): any[] {
|
|
1138
|
+
type Candidate = {
|
|
1139
|
+
point: any;
|
|
1140
|
+
rel: number;
|
|
1141
|
+
runId: number;
|
|
1142
|
+
resultId: number;
|
|
1143
|
+
hasRun: boolean;
|
|
1144
|
+
};
|
|
1145
|
+
|
|
1146
|
+
const candidatesByTestCase = new Map<number, Candidate[]>();
|
|
1147
|
+
const testCaseDefinitionById = new Map<number, any>();
|
|
1148
|
+
|
|
1149
|
+
for (const suite of testData || []) {
|
|
1150
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1151
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1152
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1153
|
+
|
|
1154
|
+
for (const testCase of testCasesItems) {
|
|
1155
|
+
const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1156
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1157
|
+
if (!testCaseDefinitionById.has(testCaseId)) {
|
|
1158
|
+
testCaseDefinitionById.set(testCaseId, testCase);
|
|
1159
|
+
}
|
|
1160
|
+
}
|
|
1161
|
+
|
|
1162
|
+
for (const point of testPointsItems) {
|
|
1163
|
+
const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
|
|
1164
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1165
|
+
|
|
1166
|
+
const runId = Number(point?.lastRunId || 0);
|
|
1167
|
+
const resultId = Number(point?.lastResultId || 0);
|
|
1168
|
+
const hasRun = runId > 0 && resultId > 0;
|
|
1169
|
+
if (!candidatesByTestCase.has(testCaseId)) {
|
|
1170
|
+
candidatesByTestCase.set(testCaseId, []);
|
|
1171
|
+
}
|
|
1172
|
+
candidatesByTestCase.get(testCaseId)!.push({
|
|
1173
|
+
point,
|
|
1174
|
+
rel,
|
|
1175
|
+
runId,
|
|
1176
|
+
resultId,
|
|
1177
|
+
hasRun,
|
|
1178
|
+
});
|
|
1179
|
+
}
|
|
1180
|
+
}
|
|
1181
|
+
|
|
1182
|
+
const selectedPoints: any[] = [];
|
|
1183
|
+
const selectedTestCaseIds = new Set<number>();
|
|
1184
|
+
for (const [testCaseId, candidates] of candidatesByTestCase.entries()) {
|
|
1185
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
1186
|
+
if (a.hasRun !== b.hasRun) return a.hasRun ? -1 : 1;
|
|
1187
|
+
if (a.rel !== b.rel) return b.rel - a.rel;
|
|
1188
|
+
if (a.runId !== b.runId) return b.runId - a.runId;
|
|
1189
|
+
return b.resultId - a.resultId;
|
|
1190
|
+
});
|
|
1191
|
+
const chosen = sorted[0];
|
|
1192
|
+
if (!chosen?.point) continue;
|
|
1193
|
+
selectedPoints.push(chosen.point);
|
|
1194
|
+
selectedTestCaseIds.add(testCaseId);
|
|
1195
|
+
}
|
|
1196
|
+
|
|
1197
|
+
const selectedTestCases: any[] = [];
|
|
1198
|
+
for (const testCaseId of selectedTestCaseIds) {
|
|
1199
|
+
const definition = testCaseDefinitionById.get(testCaseId);
|
|
1200
|
+
if (definition) {
|
|
1201
|
+
selectedTestCases.push(definition);
|
|
1202
|
+
}
|
|
1203
|
+
}
|
|
1204
|
+
|
|
1205
|
+
return [
|
|
1206
|
+
{
|
|
1207
|
+
testSuiteId: 'MEWP_REL_SCOPED',
|
|
1208
|
+
suiteId: 'MEWP_REL_SCOPED',
|
|
1209
|
+
suiteName: 'MEWP Rel Scoped',
|
|
1210
|
+
parentSuiteId: '',
|
|
1211
|
+
parentSuiteName: '',
|
|
1212
|
+
suitePath: 'MEWP Rel Scoped',
|
|
1213
|
+
testGroupName: 'MEWP Rel Scoped',
|
|
1214
|
+
testPointsItems: selectedPoints,
|
|
1215
|
+
testCasesItems: selectedTestCases,
|
|
1216
|
+
},
|
|
1217
|
+
];
|
|
1218
|
+
}
|
|
1219
|
+
|
|
1220
|
+
private async loadExternalBugsByTestCase(
|
|
1221
|
+
externalBugsFile: MewpExternalFileRef | null | undefined
|
|
1222
|
+
): Promise<Map<number, MewpBugLink[]>> {
|
|
1223
|
+
return this.mewpExternalIngestionUtils.loadExternalBugsByTestCase(externalBugsFile, {
|
|
1224
|
+
toComparableText: (value) => this.toMewpComparableText(value),
|
|
1225
|
+
toRequirementKey: (value) => this.toRequirementKey(value),
|
|
1226
|
+
resolveBugResponsibility: (fields) => this.resolveBugResponsibility(fields),
|
|
1227
|
+
isExternalStateInScope: (value, itemType) => this.isExternalStateInScope(value, itemType),
|
|
1228
|
+
isExcludedL3L4BySapWbs: (value) => this.isExcludedL3L4BySapWbs(value),
|
|
1229
|
+
resolveRequirementSapWbsByBaseKey: () => '',
|
|
1230
|
+
});
|
|
1231
|
+
}
|
|
1232
|
+
|
|
1233
|
+
private async loadExternalL3L4ByBaseKey(
|
|
1234
|
+
externalL3L4File: MewpExternalFileRef | null | undefined,
|
|
1235
|
+
requirementSapWbsByBaseKey: Map<string, string> = new Map<string, string>()
|
|
1236
|
+
): Promise<Map<string, MewpL3L4Link[]>> {
|
|
1237
|
+
return this.mewpExternalIngestionUtils.loadExternalL3L4ByBaseKey(externalL3L4File, {
|
|
1238
|
+
toComparableText: (value) => this.toMewpComparableText(value),
|
|
1239
|
+
toRequirementKey: (value) => this.toRequirementKey(value),
|
|
1240
|
+
resolveBugResponsibility: (fields) => this.resolveBugResponsibility(fields),
|
|
1241
|
+
isExternalStateInScope: (value, itemType) => this.isExternalStateInScope(value, itemType),
|
|
1242
|
+
isExcludedL3L4BySapWbs: (value) => this.isExcludedL3L4BySapWbs(value),
|
|
1243
|
+
resolveRequirementSapWbsByBaseKey: (baseKey) => String(requirementSapWbsByBaseKey.get(baseKey) || ''),
|
|
1244
|
+
});
|
|
1245
|
+
}
|
|
1246
|
+
|
|
1247
|
+
private buildRequirementSapWbsByBaseKey(
|
|
1248
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
|
|
1249
|
+
): Map<string, string> {
|
|
1250
|
+
const out = new Map<string, string>();
|
|
1251
|
+
for (const requirement of requirements || []) {
|
|
1252
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1253
|
+
if (!baseKey) continue;
|
|
1254
|
+
|
|
1255
|
+
const normalized = this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
|
|
1256
|
+
if (!normalized) continue;
|
|
1257
|
+
|
|
1258
|
+
const existing = out.get(baseKey) || '';
|
|
1259
|
+
// Keep ESUK as dominant if conflicting values are ever present across family items.
|
|
1260
|
+
if (existing === 'ESUK') continue;
|
|
1261
|
+
if (normalized === 'ESUK' || !existing) {
|
|
1262
|
+
out.set(baseKey, normalized);
|
|
1263
|
+
}
|
|
1264
|
+
}
|
|
1265
|
+
return out;
|
|
1266
|
+
}
|
|
1267
|
+
|
|
1268
|
+
private isExternalStateInScope(value: string, itemType: 'bug' | 'requirement'): boolean {
|
|
1269
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
1270
|
+
if (!normalized) return true;
|
|
1271
|
+
|
|
1272
|
+
// TFS/ADO processes usually don't expose a literal "Open" state.
|
|
1273
|
+
// Keep non-terminal states, exclude terminal states.
|
|
1274
|
+
const terminalStates = new Set<string>([
|
|
1275
|
+
'resolved',
|
|
1276
|
+
'closed',
|
|
1277
|
+
'done',
|
|
1278
|
+
'completed',
|
|
1279
|
+
'complete',
|
|
1280
|
+
'removed',
|
|
1281
|
+
'rejected',
|
|
1282
|
+
'cancelled',
|
|
1283
|
+
'canceled',
|
|
1284
|
+
'obsolete',
|
|
1285
|
+
]);
|
|
1286
|
+
|
|
1287
|
+
if (terminalStates.has(normalized)) return false;
|
|
1288
|
+
|
|
1289
|
+
// Bug-specific terminal variants often used in custom processes.
|
|
1290
|
+
if (itemType === 'bug') {
|
|
1291
|
+
if (normalized === 'fixed') return false;
|
|
1292
|
+
}
|
|
1293
|
+
|
|
1294
|
+
return true;
|
|
1295
|
+
}
|
|
1296
|
+
|
|
1297
|
+
private invertBaseRequirementLinks(
|
|
1298
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase
|
|
1299
|
+
): Map<string, Set<number>> {
|
|
1300
|
+
const out = new Map<string, Set<number>>();
|
|
1301
|
+
for (const [testCaseId, links] of linkedRequirementsByTestCase.entries()) {
|
|
1302
|
+
for (const baseKey of links?.baseKeys || []) {
|
|
1303
|
+
if (!out.has(baseKey)) out.set(baseKey, new Set<number>());
|
|
1304
|
+
out.get(baseKey)!.add(testCaseId);
|
|
1305
|
+
}
|
|
1306
|
+
}
|
|
1307
|
+
return out;
|
|
1308
|
+
}
|
|
1309
|
+
|
|
642
1310
|
private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
|
|
643
1311
|
const map = new Map<number, string>();
|
|
644
1312
|
|
|
@@ -736,56 +1404,197 @@ export default class ResultDataProvider {
|
|
|
736
1404
|
return 'notRun';
|
|
737
1405
|
}
|
|
738
1406
|
|
|
739
|
-
private
|
|
740
|
-
|
|
741
|
-
status: 'passed' | 'failed' | 'notRun',
|
|
1407
|
+
private accumulateRequirementCountsFromActionResults(
|
|
1408
|
+
actionResults: any[],
|
|
742
1409
|
testCaseId: number,
|
|
743
1410
|
requirementKeys: Set<string>,
|
|
744
1411
|
counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
745
1412
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>
|
|
746
1413
|
) {
|
|
747
1414
|
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
|
|
1415
|
+
const sortedResults = Array.isArray(actionResults) ? actionResults : [];
|
|
1416
|
+
let previousRequirementStepIndex = -1;
|
|
1417
|
+
|
|
1418
|
+
for (let i = 0; i < sortedResults.length; i++) {
|
|
1419
|
+
const actionResult = sortedResults[i];
|
|
1420
|
+
if (actionResult?.isSharedStepTitle) continue;
|
|
1421
|
+
const requirementCodes = this.extractRequirementCodesFromText(actionResult?.expected || '');
|
|
1422
|
+
if (requirementCodes.size === 0) continue;
|
|
1423
|
+
|
|
1424
|
+
const startIndex = previousRequirementStepIndex + 1;
|
|
1425
|
+
const status = this.resolveRequirementStatusForWindow(sortedResults, startIndex, i);
|
|
1426
|
+
previousRequirementStepIndex = i;
|
|
1427
|
+
|
|
1428
|
+
for (const code of requirementCodes) {
|
|
1429
|
+
if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
|
|
1430
|
+
if (!counters.has(code)) {
|
|
1431
|
+
counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
|
|
1432
|
+
}
|
|
1433
|
+
const perTestCaseCounters = counters.get(code)!;
|
|
1434
|
+
if (!perTestCaseCounters.has(testCaseId)) {
|
|
1435
|
+
perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
|
|
1436
|
+
}
|
|
748
1437
|
|
|
749
|
-
|
|
750
|
-
|
|
751
|
-
|
|
752
|
-
|
|
753
|
-
counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
|
|
754
|
-
}
|
|
755
|
-
const perTestCaseCounters = counters.get(code)!;
|
|
756
|
-
if (!perTestCaseCounters.has(testCaseId)) {
|
|
757
|
-
perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
|
|
758
|
-
}
|
|
1438
|
+
if (!observedTestCaseIdsByRequirement.has(code)) {
|
|
1439
|
+
observedTestCaseIdsByRequirement.set(code, new Set<number>());
|
|
1440
|
+
}
|
|
1441
|
+
observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
|
|
759
1442
|
|
|
760
|
-
|
|
761
|
-
|
|
1443
|
+
const counter = perTestCaseCounters.get(testCaseId)!;
|
|
1444
|
+
if (status === 'passed') counter.passed += 1;
|
|
1445
|
+
else if (status === 'failed') counter.failed += 1;
|
|
1446
|
+
else counter.notRun += 1;
|
|
762
1447
|
}
|
|
763
|
-
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
764
1450
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1451
|
+
private resolveRequirementStatusForWindow(
|
|
1452
|
+
actionResults: any[],
|
|
1453
|
+
startIndex: number,
|
|
1454
|
+
endIndex: number
|
|
1455
|
+
): 'passed' | 'failed' | 'notRun' {
|
|
1456
|
+
let hasNotRun = false;
|
|
1457
|
+
for (let index = startIndex; index <= endIndex; index++) {
|
|
1458
|
+
const status = this.classifyRequirementStepOutcome(actionResults[index]?.outcome);
|
|
1459
|
+
if (status === 'failed') return 'failed';
|
|
1460
|
+
if (status === 'notRun') hasNotRun = true;
|
|
769
1461
|
}
|
|
1462
|
+
return hasNotRun ? 'notRun' : 'passed';
|
|
770
1463
|
}
|
|
771
1464
|
|
|
772
1465
|
private extractRequirementCodesFromText(text: string): Set<string> {
|
|
1466
|
+
return this.extractRequirementCodesFromExpectedText(text, false);
|
|
1467
|
+
}
|
|
1468
|
+
|
|
1469
|
+
private extractRequirementMentionsFromExpectedSteps(
|
|
1470
|
+
steps: TestSteps[],
|
|
1471
|
+
includeSuffix: boolean
|
|
1472
|
+
): Array<{ stepRef: string; codes: Set<string> }> {
|
|
1473
|
+
const out: Array<{ stepRef: string; codes: Set<string> }> = [];
|
|
1474
|
+
const allSteps = Array.isArray(steps) ? steps : [];
|
|
1475
|
+
for (let index = 0; index < allSteps.length; index += 1) {
|
|
1476
|
+
const step = allSteps[index];
|
|
1477
|
+
if (step?.isSharedStepTitle) continue;
|
|
1478
|
+
const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
|
|
1479
|
+
if (codes.size === 0) continue;
|
|
1480
|
+
out.push({
|
|
1481
|
+
stepRef: this.resolveValidationStepReference(step, index),
|
|
1482
|
+
codes,
|
|
1483
|
+
});
|
|
1484
|
+
}
|
|
1485
|
+
return out;
|
|
1486
|
+
}
|
|
1487
|
+
|
|
1488
|
+
private extractRequirementCodesFromExpectedSteps(steps: TestSteps[], includeSuffix: boolean): Set<string> {
|
|
1489
|
+
const out = new Set<string>();
|
|
1490
|
+
for (const step of Array.isArray(steps) ? steps : []) {
|
|
1491
|
+
if (step?.isSharedStepTitle) continue;
|
|
1492
|
+
const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
|
|
1493
|
+
codes.forEach((code) => out.add(code));
|
|
1494
|
+
}
|
|
1495
|
+
return out;
|
|
1496
|
+
}
|
|
1497
|
+
|
|
1498
|
+
private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
|
|
773
1499
|
const out = new Set<string>();
|
|
774
1500
|
const source = this.normalizeRequirementStepText(text);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1501
|
+
if (!source) return out;
|
|
1502
|
+
|
|
1503
|
+
const tokens = source
|
|
1504
|
+
.split(';')
|
|
1505
|
+
.map((token) => String(token || '').trim())
|
|
1506
|
+
.filter((token) => token !== '');
|
|
1507
|
+
|
|
1508
|
+
for (const token of tokens) {
|
|
1509
|
+
const candidates = this.extractRequirementCandidatesFromToken(token);
|
|
1510
|
+
for (const candidate of candidates) {
|
|
1511
|
+
const expandedTokens = this.expandRequirementTokenByComma(candidate);
|
|
1512
|
+
for (const expandedToken of expandedTokens) {
|
|
1513
|
+
if (!expandedToken || /vvrm/i.test(expandedToken)) continue;
|
|
1514
|
+
const normalized = this.normalizeRequirementCodeToken(expandedToken, includeSuffix);
|
|
1515
|
+
if (normalized) {
|
|
1516
|
+
out.add(normalized);
|
|
1517
|
+
}
|
|
1518
|
+
}
|
|
784
1519
|
}
|
|
785
1520
|
}
|
|
1521
|
+
|
|
786
1522
|
return out;
|
|
787
1523
|
}
|
|
788
1524
|
|
|
1525
|
+
private extractRequirementCandidatesFromToken(token: string): string[] {
|
|
1526
|
+
const source = String(token || '');
|
|
1527
|
+
if (!source) return [];
|
|
1528
|
+
const out = new Set<string>();
|
|
1529
|
+
const collectCandidates = (input: string, rejectTailPattern: RegExp) => {
|
|
1530
|
+
for (const match of input.matchAll(/SR\d{4,}(?:-\d+(?:,\d+)*)?/gi)) {
|
|
1531
|
+
const matchedValue = String(match?.[0] || '')
|
|
1532
|
+
.trim()
|
|
1533
|
+
.toUpperCase();
|
|
1534
|
+
if (!matchedValue) continue;
|
|
1535
|
+
const endIndex = Number(match?.index || 0) + matchedValue.length;
|
|
1536
|
+
const tail = String(input.slice(endIndex) || '');
|
|
1537
|
+
if (rejectTailPattern.test(tail)) continue;
|
|
1538
|
+
out.add(matchedValue);
|
|
1539
|
+
}
|
|
1540
|
+
};
|
|
1541
|
+
|
|
1542
|
+
// Normal scan keeps punctuation context (" SR0817-V3.2 " -> reject via tail).
|
|
1543
|
+
collectCandidates(source, /^\s*(?:V\d|VVRM|-V\d)/i);
|
|
1544
|
+
|
|
1545
|
+
// Compact scan preserves legacy support for spaced SR letters/digits
|
|
1546
|
+
// such as "S R 0 0 0 1" and HTML-fragmented tokens.
|
|
1547
|
+
const compactSource = source.replace(/\s+/g, '');
|
|
1548
|
+
if (compactSource && compactSource !== source) {
|
|
1549
|
+
collectCandidates(compactSource, /^(?:V\d|VVRM|-V\d)/i);
|
|
1550
|
+
}
|
|
1551
|
+
|
|
1552
|
+
return [...out];
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
private expandRequirementTokenByComma(token: string): string[] {
|
|
1556
|
+
const compact = String(token || '').trim().toUpperCase();
|
|
1557
|
+
if (!compact) return [];
|
|
1558
|
+
|
|
1559
|
+
const suffixBatchMatch = /^SR(\d{4,})-(\d+(?:,\d+)+)$/.exec(compact);
|
|
1560
|
+
if (suffixBatchMatch) {
|
|
1561
|
+
const base = String(suffixBatchMatch[1] || '').trim();
|
|
1562
|
+
const suffixes = String(suffixBatchMatch[2] || '')
|
|
1563
|
+
.split(',')
|
|
1564
|
+
.map((item) => String(item || '').trim())
|
|
1565
|
+
.filter((item) => /^\d+$/.test(item));
|
|
1566
|
+
return suffixes.map((suffix) => `SR${base}-${suffix}`);
|
|
1567
|
+
}
|
|
1568
|
+
|
|
1569
|
+
return compact
|
|
1570
|
+
.split(',')
|
|
1571
|
+
.map((part) => String(part || '').trim())
|
|
1572
|
+
.filter((part) => !!part);
|
|
1573
|
+
}
|
|
1574
|
+
|
|
1575
|
+
private normalizeRequirementCodeToken(token: string, includeSuffix: boolean): string {
|
|
1576
|
+
const compact = String(token || '')
|
|
1577
|
+
.trim()
|
|
1578
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
1579
|
+
.toUpperCase();
|
|
1580
|
+
if (!compact) return '';
|
|
1581
|
+
|
|
1582
|
+
const pattern = includeSuffix ? /^SR(\d{4,})(?:-(\d+))?$/ : /^SR(\d{4,})(?:-\d+)?$/;
|
|
1583
|
+
const match = pattern.exec(compact);
|
|
1584
|
+
if (!match) return '';
|
|
1585
|
+
|
|
1586
|
+
const baseDigits = String(match[1] || '').trim();
|
|
1587
|
+
if (!baseDigits) return '';
|
|
1588
|
+
|
|
1589
|
+
if (includeSuffix && match[2]) {
|
|
1590
|
+
const suffixDigits = String(match[2] || '').trim();
|
|
1591
|
+
if (!suffixDigits) return '';
|
|
1592
|
+
return `SR${baseDigits}-${suffixDigits}`;
|
|
1593
|
+
}
|
|
1594
|
+
|
|
1595
|
+
return `SR${baseDigits}`;
|
|
1596
|
+
}
|
|
1597
|
+
|
|
789
1598
|
private normalizeRequirementStepText(text: string): string {
|
|
790
1599
|
const raw = String(text || '');
|
|
791
1600
|
if (!raw) return '';
|
|
@@ -802,23 +1611,19 @@ export default class ResultDataProvider {
|
|
|
802
1611
|
.replace(/\s+/g, ' ');
|
|
803
1612
|
}
|
|
804
1613
|
|
|
1614
|
+
private resolveValidationStepReference(step: TestSteps, index: number): string {
|
|
1615
|
+
const fromPosition = String(step?.stepPosition || '').trim();
|
|
1616
|
+
if (fromPosition) return `Step ${fromPosition}`;
|
|
1617
|
+
const fromId = String(step?.stepId || '').trim();
|
|
1618
|
+
if (fromId) return `Step ${fromId}`;
|
|
1619
|
+
return `Step ${index + 1}`;
|
|
1620
|
+
}
|
|
1621
|
+
|
|
805
1622
|
private toRequirementKey(requirementId: string): string {
|
|
806
|
-
|
|
807
|
-
|
|
808
|
-
|
|
809
|
-
|
|
810
|
-
return `SR${digits}`;
|
|
811
|
-
}
|
|
812
|
-
|
|
813
|
-
private async fetchMewpL2Requirements(projectName: string): Promise<
|
|
814
|
-
Array<{
|
|
815
|
-
workItemId: number;
|
|
816
|
-
requirementId: string;
|
|
817
|
-
title: string;
|
|
818
|
-
responsibility: string;
|
|
819
|
-
linkedTestCaseIds: number[];
|
|
820
|
-
}>
|
|
821
|
-
> {
|
|
1623
|
+
return this.normalizeMewpRequirementCode(requirementId);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
private async fetchMewpL2Requirements(projectName: string): Promise<MewpL2RequirementWorkItem[]> {
|
|
822
1627
|
const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
823
1628
|
if (workItemTypeNames.length === 0) {
|
|
824
1629
|
return [];
|
|
@@ -827,17 +1632,36 @@ export default class ResultDataProvider {
|
|
|
827
1632
|
const quotedTypeNames = workItemTypeNames
|
|
828
1633
|
.map((name) => `'${String(name).replace(/'/g, "''")}'`)
|
|
829
1634
|
.join(', ');
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
.
|
|
840
|
-
.
|
|
1635
|
+
const queryRequirementIds = async (l2AreaPath: string | null): Promise<number[]> => {
|
|
1636
|
+
const escapedAreaPath = l2AreaPath ? String(l2AreaPath).replace(/'/g, "''") : '';
|
|
1637
|
+
const areaFilter = escapedAreaPath ? `\n AND [System.AreaPath] UNDER '${escapedAreaPath}'` : '';
|
|
1638
|
+
const wiql = `SELECT [System.Id]
|
|
1639
|
+
FROM WorkItems
|
|
1640
|
+
WHERE [System.TeamProject] = @project
|
|
1641
|
+
AND [System.WorkItemType] IN (${quotedTypeNames})${areaFilter}
|
|
1642
|
+
ORDER BY [System.Id]`;
|
|
1643
|
+
const wiqlUrl = `${this.orgUrl}${projectName}/_apis/wit/wiql?api-version=7.1-preview.2`;
|
|
1644
|
+
const wiqlResponse = await TFSServices.postRequest(wiqlUrl, this.token, 'Post', { query: wiql }, null);
|
|
1645
|
+
const workItemRefs = Array.isArray(wiqlResponse?.data?.workItems) ? wiqlResponse.data.workItems : [];
|
|
1646
|
+
return workItemRefs
|
|
1647
|
+
.map((item: any) => Number(item?.id))
|
|
1648
|
+
.filter((id: number) => Number.isFinite(id));
|
|
1649
|
+
};
|
|
1650
|
+
|
|
1651
|
+
const defaultL2AreaPath = `${String(projectName || '').trim()}\\Customer Requirements\\Level 2`;
|
|
1652
|
+
let requirementIds: number[] = [];
|
|
1653
|
+
try {
|
|
1654
|
+
requirementIds = await queryRequirementIds(defaultL2AreaPath);
|
|
1655
|
+
} catch (error: any) {
|
|
1656
|
+
logger.warn(
|
|
1657
|
+
`Could not apply MEWP L2 WIQL area-path optimization. Falling back to full requirement scope: ${
|
|
1658
|
+
error?.message || error
|
|
1659
|
+
}`
|
|
1660
|
+
);
|
|
1661
|
+
}
|
|
1662
|
+
if (requirementIds.length === 0) {
|
|
1663
|
+
requirementIds = await queryRequirementIds(null);
|
|
1664
|
+
}
|
|
841
1665
|
|
|
842
1666
|
if (requirementIds.length === 0) {
|
|
843
1667
|
return [];
|
|
@@ -846,16 +1670,294 @@ ORDER BY [System.Id]`;
|
|
|
846
1670
|
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
847
1671
|
const requirements = workItems.map((wi: any) => {
|
|
848
1672
|
const fields = wi?.fields || {};
|
|
1673
|
+
const requirementId = this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0));
|
|
1674
|
+
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
849
1675
|
return {
|
|
850
1676
|
workItemId: Number(wi?.id || 0),
|
|
851
|
-
requirementId
|
|
1677
|
+
requirementId,
|
|
1678
|
+
baseKey: this.toRequirementKey(requirementId),
|
|
852
1679
|
title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
|
|
1680
|
+
subSystem: this.deriveMewpSubSystem(fields),
|
|
853
1681
|
responsibility: this.deriveMewpResponsibility(fields),
|
|
854
1682
|
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
|
|
1683
|
+
relatedWorkItemIds: this.extractLinkedWorkItemIdsFromRelations(wi?.relations || []),
|
|
1684
|
+
areaPath,
|
|
855
1685
|
};
|
|
856
1686
|
});
|
|
857
1687
|
|
|
858
|
-
return requirements
|
|
1688
|
+
return requirements
|
|
1689
|
+
.filter((item) => {
|
|
1690
|
+
if (!item.baseKey) return false;
|
|
1691
|
+
if (!item.areaPath) return true;
|
|
1692
|
+
return this.isMewpL2AreaPath(item.areaPath);
|
|
1693
|
+
})
|
|
1694
|
+
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
1695
|
+
}
|
|
1696
|
+
|
|
1697
|
+
private isMewpL2AreaPath(areaPath: string): boolean {
|
|
1698
|
+
const normalized = String(areaPath || '')
|
|
1699
|
+
.trim()
|
|
1700
|
+
.toLowerCase()
|
|
1701
|
+
.replace(/\//g, '\\');
|
|
1702
|
+
if (!normalized) return false;
|
|
1703
|
+
return normalized.includes('\\customer requirements\\level 2');
|
|
1704
|
+
}
|
|
1705
|
+
|
|
1706
|
+
private collapseMewpRequirementFamilies(
|
|
1707
|
+
requirements: MewpL2RequirementWorkItem[],
|
|
1708
|
+
scopedRequirementKeys?: Set<string>
|
|
1709
|
+
): MewpL2RequirementFamily[] {
|
|
1710
|
+
const families = new Map<
|
|
1711
|
+
string,
|
|
1712
|
+
{
|
|
1713
|
+
representative: MewpL2RequirementWorkItem;
|
|
1714
|
+
score: number;
|
|
1715
|
+
linkedTestCaseIds: Set<number>;
|
|
1716
|
+
}
|
|
1717
|
+
>();
|
|
1718
|
+
|
|
1719
|
+
const calcScore = (item: MewpL2RequirementWorkItem) => {
|
|
1720
|
+
const requirementId = String(item?.requirementId || '').trim();
|
|
1721
|
+
const areaPath = String(item?.areaPath || '')
|
|
1722
|
+
.trim()
|
|
1723
|
+
.toLowerCase();
|
|
1724
|
+
let score = 0;
|
|
1725
|
+
if (/^SR\d+$/i.test(requirementId)) score += 6;
|
|
1726
|
+
if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
|
|
1727
|
+
if (!areaPath.includes('\\mop')) score += 2;
|
|
1728
|
+
if (String(item?.title || '').trim()) score += 1;
|
|
1729
|
+
if (String(item?.subSystem || '').trim()) score += 1;
|
|
1730
|
+
if (String(item?.responsibility || '').trim()) score += 1;
|
|
1731
|
+
return score;
|
|
1732
|
+
};
|
|
1733
|
+
|
|
1734
|
+
for (const requirement of requirements || []) {
|
|
1735
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1736
|
+
if (!baseKey) continue;
|
|
1737
|
+
if (scopedRequirementKeys?.size && !scopedRequirementKeys.has(baseKey)) continue;
|
|
1738
|
+
|
|
1739
|
+
if (!families.has(baseKey)) {
|
|
1740
|
+
families.set(baseKey, {
|
|
1741
|
+
representative: requirement,
|
|
1742
|
+
score: calcScore(requirement),
|
|
1743
|
+
linkedTestCaseIds: new Set<number>(),
|
|
1744
|
+
});
|
|
1745
|
+
}
|
|
1746
|
+
const family = families.get(baseKey)!;
|
|
1747
|
+
const score = calcScore(requirement);
|
|
1748
|
+
if (score > family.score) {
|
|
1749
|
+
family.representative = requirement;
|
|
1750
|
+
family.score = score;
|
|
1751
|
+
}
|
|
1752
|
+
for (const testCaseId of requirement?.linkedTestCaseIds || []) {
|
|
1753
|
+
if (Number.isFinite(testCaseId) && Number(testCaseId) > 0) {
|
|
1754
|
+
family.linkedTestCaseIds.add(Number(testCaseId));
|
|
1755
|
+
}
|
|
1756
|
+
}
|
|
1757
|
+
}
|
|
1758
|
+
|
|
1759
|
+
return [...families.entries()]
|
|
1760
|
+
.map(([baseKey, family]) => ({
|
|
1761
|
+
requirementId: String(family?.representative?.requirementId || baseKey),
|
|
1762
|
+
baseKey,
|
|
1763
|
+
title: String(family?.representative?.title || ''),
|
|
1764
|
+
subSystem: String(family?.representative?.subSystem || ''),
|
|
1765
|
+
responsibility: String(family?.representative?.responsibility || ''),
|
|
1766
|
+
linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
|
|
1767
|
+
}))
|
|
1768
|
+
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
1769
|
+
}
|
|
1770
|
+
|
|
1771
|
+
private buildRequirementFamilyMap(
|
|
1772
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'requirementId' | 'baseKey'>>,
|
|
1773
|
+
scopedRequirementKeys?: Set<string>
|
|
1774
|
+
): Map<string, Set<string>> {
|
|
1775
|
+
const familyMap = new Map<string, Set<string>>();
|
|
1776
|
+
for (const requirement of requirements || []) {
|
|
1777
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1778
|
+
if (!baseKey) continue;
|
|
1779
|
+
if (scopedRequirementKeys?.size && !scopedRequirementKeys.has(baseKey)) continue;
|
|
1780
|
+
const fullCode = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
|
|
1781
|
+
if (!fullCode) continue;
|
|
1782
|
+
if (!familyMap.has(baseKey)) familyMap.set(baseKey, new Set<string>());
|
|
1783
|
+
familyMap.get(baseKey)!.add(fullCode);
|
|
1784
|
+
}
|
|
1785
|
+
return familyMap;
|
|
1786
|
+
}
|
|
1787
|
+
|
|
1788
|
+
private async buildLinkedRequirementsByTestCase(
|
|
1789
|
+
requirements: Array<
|
|
1790
|
+
Pick<MewpL2RequirementWorkItem, 'workItemId' | 'requirementId' | 'baseKey' | 'linkedTestCaseIds'>
|
|
1791
|
+
>,
|
|
1792
|
+
testData: any[],
|
|
1793
|
+
projectName: string
|
|
1794
|
+
): Promise<MewpLinkedRequirementsByTestCase> {
|
|
1795
|
+
const map: MewpLinkedRequirementsByTestCase = new Map();
|
|
1796
|
+
const ensure = (testCaseId: number) => {
|
|
1797
|
+
if (!map.has(testCaseId)) {
|
|
1798
|
+
map.set(testCaseId, {
|
|
1799
|
+
baseKeys: new Set<string>(),
|
|
1800
|
+
fullCodes: new Set<string>(),
|
|
1801
|
+
bugIds: new Set<number>(),
|
|
1802
|
+
});
|
|
1803
|
+
}
|
|
1804
|
+
return map.get(testCaseId)!;
|
|
1805
|
+
};
|
|
1806
|
+
|
|
1807
|
+
const requirementById = new Map<number, { baseKey: string; fullCode: string }>();
|
|
1808
|
+
for (const requirement of requirements || []) {
|
|
1809
|
+
const workItemId = Number(requirement?.workItemId || 0);
|
|
1810
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1811
|
+
const fullCode = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
|
|
1812
|
+
if (workItemId > 0 && baseKey && fullCode) {
|
|
1813
|
+
requirementById.set(workItemId, { baseKey, fullCode });
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
for (const testCaseIdRaw of requirement?.linkedTestCaseIds || []) {
|
|
1817
|
+
const testCaseId = Number(testCaseIdRaw);
|
|
1818
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || !baseKey || !fullCode) continue;
|
|
1819
|
+
const entry = ensure(testCaseId);
|
|
1820
|
+
entry.baseKeys.add(baseKey);
|
|
1821
|
+
entry.fullCodes.add(fullCode);
|
|
1822
|
+
}
|
|
1823
|
+
}
|
|
1824
|
+
|
|
1825
|
+
const testCaseIds = new Set<number>();
|
|
1826
|
+
for (const suite of testData || []) {
|
|
1827
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1828
|
+
for (const testCase of testCasesItems) {
|
|
1829
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1830
|
+
if (Number.isFinite(id) && id > 0) testCaseIds.add(id);
|
|
1831
|
+
}
|
|
1832
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1833
|
+
for (const testPoint of testPointsItems) {
|
|
1834
|
+
const id = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
|
|
1835
|
+
if (Number.isFinite(id) && id > 0) testCaseIds.add(id);
|
|
1836
|
+
}
|
|
1837
|
+
}
|
|
1838
|
+
|
|
1839
|
+
const relatedIdsByTestCase = new Map<number, Set<number>>();
|
|
1840
|
+
const allRelatedIds = new Set<number>();
|
|
1841
|
+
if (testCaseIds.size > 0) {
|
|
1842
|
+
const testCaseWorkItems = await this.fetchWorkItemsByIds(projectName, [...testCaseIds], true);
|
|
1843
|
+
for (const workItem of testCaseWorkItems || []) {
|
|
1844
|
+
const testCaseId = Number(workItem?.id || 0);
|
|
1845
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1846
|
+
const relations = Array.isArray(workItem?.relations) ? workItem.relations : [];
|
|
1847
|
+
if (!relatedIdsByTestCase.has(testCaseId)) relatedIdsByTestCase.set(testCaseId, new Set<number>());
|
|
1848
|
+
for (const relation of relations) {
|
|
1849
|
+
const linkedWorkItemId = this.extractLinkedWorkItemIdFromRelation(relation);
|
|
1850
|
+
if (!linkedWorkItemId) continue;
|
|
1851
|
+
relatedIdsByTestCase.get(testCaseId)!.add(linkedWorkItemId);
|
|
1852
|
+
allRelatedIds.add(linkedWorkItemId);
|
|
1853
|
+
|
|
1854
|
+
if (this.isTestCaseToRequirementRelation(relation) && requirementById.has(linkedWorkItemId)) {
|
|
1855
|
+
const linkedRequirement = requirementById.get(linkedWorkItemId)!;
|
|
1856
|
+
const entry = ensure(testCaseId);
|
|
1857
|
+
entry.baseKeys.add(linkedRequirement.baseKey);
|
|
1858
|
+
entry.fullCodes.add(linkedRequirement.fullCode);
|
|
1859
|
+
}
|
|
1860
|
+
}
|
|
1861
|
+
}
|
|
1862
|
+
}
|
|
1863
|
+
|
|
1864
|
+
if (allRelatedIds.size > 0) {
|
|
1865
|
+
const relatedWorkItems = await this.fetchWorkItemsByIds(projectName, [...allRelatedIds], false);
|
|
1866
|
+
const typeById = new Map<number, string>();
|
|
1867
|
+
for (const workItem of relatedWorkItems || []) {
|
|
1868
|
+
const id = Number(workItem?.id || 0);
|
|
1869
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1870
|
+
const type = String(workItem?.fields?.['System.WorkItemType'] || '')
|
|
1871
|
+
.trim()
|
|
1872
|
+
.toLowerCase();
|
|
1873
|
+
typeById.set(id, type);
|
|
1874
|
+
}
|
|
1875
|
+
|
|
1876
|
+
for (const [testCaseId, ids] of relatedIdsByTestCase.entries()) {
|
|
1877
|
+
const entry = ensure(testCaseId);
|
|
1878
|
+
for (const linkedId of ids) {
|
|
1879
|
+
const linkedType = typeById.get(linkedId) || '';
|
|
1880
|
+
if (linkedType === 'bug') {
|
|
1881
|
+
entry.bugIds.add(linkedId);
|
|
1882
|
+
}
|
|
1883
|
+
}
|
|
1884
|
+
}
|
|
1885
|
+
}
|
|
1886
|
+
|
|
1887
|
+
return map;
|
|
1888
|
+
}
|
|
1889
|
+
|
|
1890
|
+
private async resolveMewpRequirementScopeKeysFromQuery(
|
|
1891
|
+
linkedQueryRequest: any,
|
|
1892
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'workItemId' | 'baseKey'>>,
|
|
1893
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase
|
|
1894
|
+
): Promise<Set<string> | undefined> {
|
|
1895
|
+
const mode = String(linkedQueryRequest?.linkedQueryMode || '')
|
|
1896
|
+
.trim()
|
|
1897
|
+
.toLowerCase();
|
|
1898
|
+
const wiqlHref = String(linkedQueryRequest?.testAssociatedQuery?.wiql?.href || '').trim();
|
|
1899
|
+
if (mode !== 'query' || !wiqlHref) return undefined;
|
|
1900
|
+
|
|
1901
|
+
try {
|
|
1902
|
+
const queryResult = await TFSServices.getItemContent(wiqlHref, this.token);
|
|
1903
|
+
const queryIds = new Set<number>();
|
|
1904
|
+
if (Array.isArray(queryResult?.workItems)) {
|
|
1905
|
+
for (const workItem of queryResult.workItems) {
|
|
1906
|
+
const id = Number(workItem?.id || 0);
|
|
1907
|
+
if (Number.isFinite(id) && id > 0) queryIds.add(id);
|
|
1908
|
+
}
|
|
1909
|
+
}
|
|
1910
|
+
if (Array.isArray(queryResult?.workItemRelations)) {
|
|
1911
|
+
for (const relation of queryResult.workItemRelations) {
|
|
1912
|
+
const sourceId = Number(relation?.source?.id || 0);
|
|
1913
|
+
const targetId = Number(relation?.target?.id || 0);
|
|
1914
|
+
if (Number.isFinite(sourceId) && sourceId > 0) queryIds.add(sourceId);
|
|
1915
|
+
if (Number.isFinite(targetId) && targetId > 0) queryIds.add(targetId);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
|
|
1919
|
+
if (queryIds.size === 0) return undefined;
|
|
1920
|
+
|
|
1921
|
+
const reqIdToBaseKey = new Map<number, string>();
|
|
1922
|
+
for (const requirement of requirements || []) {
|
|
1923
|
+
const id = Number(requirement?.workItemId || 0);
|
|
1924
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1925
|
+
if (id > 0 && baseKey) reqIdToBaseKey.set(id, baseKey);
|
|
1926
|
+
}
|
|
1927
|
+
|
|
1928
|
+
const scopedKeys = new Set<string>();
|
|
1929
|
+
for (const queryId of queryIds) {
|
|
1930
|
+
if (reqIdToBaseKey.has(queryId)) {
|
|
1931
|
+
scopedKeys.add(reqIdToBaseKey.get(queryId)!);
|
|
1932
|
+
continue;
|
|
1933
|
+
}
|
|
1934
|
+
|
|
1935
|
+
const linked = linkedRequirementsByTestCase.get(queryId);
|
|
1936
|
+
if (!linked?.baseKeys?.size) continue;
|
|
1937
|
+
linked.baseKeys.forEach((baseKey) => scopedKeys.add(baseKey));
|
|
1938
|
+
}
|
|
1939
|
+
|
|
1940
|
+
return scopedKeys.size > 0 ? scopedKeys : undefined;
|
|
1941
|
+
} catch (error: any) {
|
|
1942
|
+
logger.warn(`Could not resolve MEWP query scope: ${error?.message || error}`);
|
|
1943
|
+
return undefined;
|
|
1944
|
+
}
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
private isTestCaseToRequirementRelation(relation: any): boolean {
|
|
1948
|
+
const rel = String(relation?.rel || '')
|
|
1949
|
+
.trim()
|
|
1950
|
+
.toLowerCase();
|
|
1951
|
+
if (!rel) return false;
|
|
1952
|
+
return rel.includes('testedby-reverse') || (rel.includes('tests') && rel.includes('reverse'));
|
|
1953
|
+
}
|
|
1954
|
+
|
|
1955
|
+
private extractLinkedWorkItemIdFromRelation(relation: any): number {
|
|
1956
|
+
const url = String(relation?.url || '');
|
|
1957
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
1958
|
+
if (!match) return 0;
|
|
1959
|
+
const parsed = Number(match[1]);
|
|
1960
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
859
1961
|
}
|
|
860
1962
|
|
|
861
1963
|
private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
|
|
@@ -919,6 +2021,18 @@ ORDER BY [System.Id]`;
|
|
|
919
2021
|
return [...out].sort((a, b) => a - b);
|
|
920
2022
|
}
|
|
921
2023
|
|
|
2024
|
+
private extractLinkedWorkItemIdsFromRelations(relations: any[]): number[] {
|
|
2025
|
+
const out = new Set<number>();
|
|
2026
|
+
for (const relation of Array.isArray(relations) ? relations : []) {
|
|
2027
|
+
const url = String(relation?.url || '');
|
|
2028
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
2029
|
+
if (!match) continue;
|
|
2030
|
+
const id = Number(match[1]);
|
|
2031
|
+
if (Number.isFinite(id) && id > 0) out.add(id);
|
|
2032
|
+
}
|
|
2033
|
+
return [...out].sort((a, b) => a - b);
|
|
2034
|
+
}
|
|
2035
|
+
|
|
922
2036
|
private extractMewpRequirementIdentifier(fields: Record<string, any>, fallbackWorkItemId: number): string {
|
|
923
2037
|
const entries = Object.entries(fields || {});
|
|
924
2038
|
|
|
@@ -938,7 +2052,7 @@ ORDER BY [System.Id]`;
|
|
|
938
2052
|
|
|
939
2053
|
const valueAsString = this.toMewpComparableText(value);
|
|
940
2054
|
if (!valueAsString) continue;
|
|
941
|
-
const normalized = this.
|
|
2055
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
942
2056
|
if (normalized) return normalized;
|
|
943
2057
|
}
|
|
944
2058
|
|
|
@@ -950,13 +2064,13 @@ ORDER BY [System.Id]`;
|
|
|
950
2064
|
|
|
951
2065
|
const valueAsString = this.toMewpComparableText(value);
|
|
952
2066
|
if (!valueAsString) continue;
|
|
953
|
-
const normalized = this.
|
|
2067
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
954
2068
|
if (normalized) return normalized;
|
|
955
2069
|
}
|
|
956
2070
|
|
|
957
2071
|
// Optional fallback from title only (avoid scanning all fields and accidental SR matches).
|
|
958
2072
|
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
959
|
-
const titleCode = this.
|
|
2073
|
+
const titleCode = this.normalizeMewpRequirementCodeWithSuffix(title);
|
|
960
2074
|
if (titleCode) return titleCode;
|
|
961
2075
|
|
|
962
2076
|
return fallbackWorkItemId ? `SR${fallbackWorkItemId}` : '';
|
|
@@ -988,17 +2102,69 @@ ORDER BY [System.Id]`;
|
|
|
988
2102
|
return '';
|
|
989
2103
|
}
|
|
990
2104
|
|
|
2105
|
+
private deriveMewpSubSystem(fields: Record<string, any>): string {
|
|
2106
|
+
const directCandidates = [
|
|
2107
|
+
fields?.['Custom.SubSystem'],
|
|
2108
|
+
fields?.['Custom.Subsystem'],
|
|
2109
|
+
fields?.['SubSystem'],
|
|
2110
|
+
fields?.['Subsystem'],
|
|
2111
|
+
fields?.['subSystem'],
|
|
2112
|
+
];
|
|
2113
|
+
for (const candidate of directCandidates) {
|
|
2114
|
+
const value = this.toMewpComparableText(candidate);
|
|
2115
|
+
if (value) return value;
|
|
2116
|
+
}
|
|
2117
|
+
|
|
2118
|
+
const keyHints = ['subsystem', 'sub system', 'sub_system'];
|
|
2119
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
2120
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
2121
|
+
if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
2122
|
+
const resolved = this.toMewpComparableText(value);
|
|
2123
|
+
if (resolved) return resolved;
|
|
2124
|
+
}
|
|
2125
|
+
|
|
2126
|
+
return '';
|
|
2127
|
+
}
|
|
2128
|
+
|
|
2129
|
+
private resolveBugResponsibility(fields: Record<string, any>): string {
|
|
2130
|
+
const sapWbsRaw = this.toMewpComparableText(fields?.['Custom.SAPWBS'] || fields?.['SAPWBS']);
|
|
2131
|
+
const fromSapWbs = this.resolveMewpResponsibility(sapWbsRaw);
|
|
2132
|
+
if (fromSapWbs === 'ESUK') return 'ESUK';
|
|
2133
|
+
if (fromSapWbs === 'IL') return 'Elisra';
|
|
2134
|
+
|
|
2135
|
+
const areaPathRaw = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
2136
|
+
const fromAreaPath = this.resolveMewpResponsibility(areaPathRaw);
|
|
2137
|
+
if (fromAreaPath === 'ESUK') return 'ESUK';
|
|
2138
|
+
if (fromAreaPath === 'IL') return 'Elisra';
|
|
2139
|
+
|
|
2140
|
+
return 'Unknown';
|
|
2141
|
+
}
|
|
2142
|
+
|
|
991
2143
|
private resolveMewpResponsibility(value: string): string {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
2144
|
+
const raw = String(value || '').trim();
|
|
2145
|
+
if (!raw) return '';
|
|
2146
|
+
|
|
2147
|
+
const rawUpper = raw.toUpperCase();
|
|
2148
|
+
if (rawUpper === 'ESUK') return 'ESUK';
|
|
2149
|
+
if (rawUpper === 'IL') return 'IL';
|
|
2150
|
+
|
|
2151
|
+
const normalizedPath = raw
|
|
2152
|
+
.toLowerCase()
|
|
2153
|
+
.replace(/\//g, '\\')
|
|
2154
|
+
.replace(/\\+/g, '\\')
|
|
2155
|
+
.trim();
|
|
2156
|
+
|
|
2157
|
+
if (normalizedPath.endsWith('\\atp\\esuk') || normalizedPath === 'atp\\esuk') return 'ESUK';
|
|
2158
|
+
if (normalizedPath.endsWith('\\atp') || normalizedPath === 'atp') return 'IL';
|
|
996
2159
|
|
|
997
|
-
if (/(^|[^a-z0-9])esuk([^a-z0-9]|$)/i.test(text)) return 'ESUK';
|
|
998
|
-
if (/(^|[^a-z0-9])il([^a-z0-9]|$)/i.test(text)) return 'IL';
|
|
999
2160
|
return '';
|
|
1000
2161
|
}
|
|
1001
2162
|
|
|
2163
|
+
private isExcludedL3L4BySapWbs(value: string): boolean {
|
|
2164
|
+
const responsibility = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
2165
|
+
return responsibility === 'ESUK';
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1002
2168
|
private normalizeMewpRequirementCode(value: string): string {
|
|
1003
2169
|
const text = String(value || '').trim();
|
|
1004
2170
|
if (!text) return '';
|
|
@@ -1007,6 +2173,16 @@ ORDER BY [System.Id]`;
|
|
|
1007
2173
|
return `SR${match[1]}`;
|
|
1008
2174
|
}
|
|
1009
2175
|
|
|
2176
|
+
private normalizeMewpRequirementCodeWithSuffix(value: string): string {
|
|
2177
|
+
const text = String(value || '').trim();
|
|
2178
|
+
if (!text) return '';
|
|
2179
|
+
const compact = text.replace(/\s+/g, '');
|
|
2180
|
+
const match = /^SR(\d+)(?:-(\d+))?$/i.exec(compact);
|
|
2181
|
+
if (!match) return '';
|
|
2182
|
+
if (match[2]) return `SR${match[1]}-${match[2]}`;
|
|
2183
|
+
return `SR${match[1]}`;
|
|
2184
|
+
}
|
|
2185
|
+
|
|
1010
2186
|
private toMewpComparableText(value: any): string {
|
|
1011
2187
|
if (value === null || value === undefined) return '';
|
|
1012
2188
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|