@elisra-devops/docgen-data-provider 1.74.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 +47 -13
- package/bin/modules/ResultDataProvider.js +1122 -257
- package/bin/modules/ResultDataProvider.js.map +1 -1
- package/bin/tests/modules/ResultDataProvider.test.js +1293 -113
- 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 +1325 -353
- package/src/tests/modules/ResultDataProvider.test.ts +1481 -138
- 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
|
/**
|
|
@@ -394,20 +432,52 @@ export default class ResultDataProvider {
|
|
|
394
432
|
testPlanId: string,
|
|
395
433
|
projectName: string,
|
|
396
434
|
selectedSuiteIds: number[] | undefined,
|
|
397
|
-
linkedQueryRequest?: any
|
|
398
|
-
|
|
399
|
-
|
|
435
|
+
linkedQueryRequest?: any,
|
|
436
|
+
options?: MewpCoverageRequestOptions
|
|
437
|
+
): Promise<MewpCoverageFlatPayload> {
|
|
438
|
+
const defaultPayload: MewpCoverageFlatPayload = {
|
|
400
439
|
sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
|
|
401
440
|
columnOrder: [...ResultDataProvider.MEWP_L2_COVERAGE_COLUMNS],
|
|
402
|
-
rows: []
|
|
441
|
+
rows: [],
|
|
403
442
|
};
|
|
404
443
|
|
|
405
444
|
try {
|
|
406
445
|
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
407
|
-
const
|
|
408
|
-
|
|
446
|
+
const testData = await this.fetchMewpScopedTestData(
|
|
447
|
+
testPlanId,
|
|
448
|
+
projectName,
|
|
449
|
+
selectedSuiteIds,
|
|
450
|
+
!!options?.useRelFallback
|
|
451
|
+
);
|
|
409
452
|
|
|
410
|
-
const
|
|
453
|
+
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
454
|
+
if (allRequirements.length === 0) {
|
|
455
|
+
return {
|
|
456
|
+
...defaultPayload,
|
|
457
|
+
sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
|
|
458
|
+
};
|
|
459
|
+
}
|
|
460
|
+
|
|
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
|
+
);
|
|
411
481
|
if (requirements.length === 0) {
|
|
412
482
|
return {
|
|
413
483
|
...defaultPayload,
|
|
@@ -415,47 +485,40 @@ export default class ResultDataProvider {
|
|
|
415
485
|
};
|
|
416
486
|
}
|
|
417
487
|
|
|
418
|
-
const requirementIndex = new Map
|
|
488
|
+
const requirementIndex: MewpRequirementIndex = new Map();
|
|
419
489
|
const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
|
|
420
490
|
const requirementKeys = new Set<string>();
|
|
421
491
|
requirements.forEach((requirement) => {
|
|
422
|
-
const key =
|
|
492
|
+
const key = String(requirement?.baseKey || '').trim();
|
|
423
493
|
if (!key) return;
|
|
424
494
|
requirementKeys.add(key);
|
|
425
495
|
});
|
|
426
496
|
|
|
427
497
|
const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
|
|
428
498
|
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
429
|
-
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
430
|
-
|
|
431
499
|
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
432
500
|
for (const runResult of runResults) {
|
|
433
501
|
const testCaseId = this.extractMewpTestCaseId(runResult);
|
|
434
|
-
const
|
|
435
|
-
runResult
|
|
436
|
-
);
|
|
437
|
-
if (Number.isFinite(testCaseId) && testCaseId > 0 && runTestCaseTitle && !testCaseTitleMap.has(testCaseId)) {
|
|
438
|
-
testCaseTitleMap.set(testCaseId, runTestCaseTitle);
|
|
439
|
-
}
|
|
440
|
-
const actionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
441
|
-
? runResult.iteration.actionResults
|
|
502
|
+
const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
503
|
+
? runResult.iteration.actionResults.filter((item: any) => !item?.isSharedStepTitle)
|
|
442
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
|
+
);
|
|
443
511
|
const hasExecutedRun =
|
|
444
512
|
Number(runResult?.lastRunId || 0) > 0 && Number(runResult?.lastResultId || 0) > 0;
|
|
445
513
|
|
|
446
514
|
if (actionResults.length > 0) {
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
|
|
454
|
-
requirementKeys,
|
|
455
|
-
requirementIndex,
|
|
456
|
-
observedTestCaseIdsByRequirement
|
|
457
|
-
);
|
|
458
|
-
}
|
|
515
|
+
this.accumulateRequirementCountsFromActionResults(
|
|
516
|
+
actionResults,
|
|
517
|
+
testCaseId,
|
|
518
|
+
requirementKeys,
|
|
519
|
+
requirementIndex,
|
|
520
|
+
observedTestCaseIdsByRequirement
|
|
521
|
+
);
|
|
459
522
|
continue;
|
|
460
523
|
}
|
|
461
524
|
|
|
@@ -476,24 +539,33 @@ export default class ResultDataProvider {
|
|
|
476
539
|
}
|
|
477
540
|
|
|
478
541
|
const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
|
|
488
|
-
);
|
|
489
|
-
|
|
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
|
+
);
|
|
490
560
|
}
|
|
491
561
|
|
|
492
562
|
const rows = this.buildMewpCoverageRows(
|
|
493
563
|
requirements,
|
|
494
564
|
requirementIndex,
|
|
495
565
|
observedTestCaseIdsByRequirement,
|
|
496
|
-
|
|
566
|
+
linkedRequirementsByTestCase,
|
|
567
|
+
externalL3L4ByBaseKey,
|
|
568
|
+
externalBugsByTestCase
|
|
497
569
|
);
|
|
498
570
|
|
|
499
571
|
return {
|
|
@@ -503,10 +575,261 @@ export default class ResultDataProvider {
|
|
|
503
575
|
};
|
|
504
576
|
} catch (error: any) {
|
|
505
577
|
logger.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
|
|
578
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
579
|
+
throw error;
|
|
580
|
+
}
|
|
506
581
|
return defaultPayload;
|
|
507
582
|
}
|
|
508
583
|
}
|
|
509
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}`);
|
|
774
|
+
return defaultPayload;
|
|
775
|
+
}
|
|
776
|
+
}
|
|
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
|
+
|
|
510
833
|
/**
|
|
511
834
|
* Mapping each attachment to a proper URL for downloading it
|
|
512
835
|
* @param runResults Array of run results
|
|
@@ -550,79 +873,176 @@ export default class ResultDataProvider {
|
|
|
550
873
|
return `MEWP L2 Coverage - ${suffix}`;
|
|
551
874
|
}
|
|
552
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
|
+
|
|
553
881
|
private createMewpCoverageRow(
|
|
554
|
-
requirement:
|
|
555
|
-
|
|
556
|
-
|
|
557
|
-
|
|
558
|
-
|
|
559
|
-
|
|
560
|
-
|
|
561
|
-
|
|
562
|
-
) {
|
|
563
|
-
const customerId = String(requirement.requirementId || '').trim();
|
|
564
|
-
const customerTitle = String(requirement.title || '').trim();
|
|
565
|
-
const responsibility = String(requirement.responsibility || '').trim();
|
|
566
|
-
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);
|
|
567
890
|
|
|
568
891
|
return {
|
|
569
|
-
'
|
|
570
|
-
'
|
|
571
|
-
'
|
|
572
|
-
'
|
|
573
|
-
'
|
|
574
|
-
'
|
|
575
|
-
'
|
|
576
|
-
'
|
|
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(),
|
|
577
903
|
};
|
|
578
904
|
}
|
|
579
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
|
+
|
|
933
|
+
private formatMewpCustomerId(rawValue: string): string {
|
|
934
|
+
const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
|
|
935
|
+
if (normalized) return normalized;
|
|
936
|
+
|
|
937
|
+
const onlyDigits = String(rawValue || '').replace(/\D/g, '');
|
|
938
|
+
if (onlyDigits) return `SR${onlyDigits}`;
|
|
939
|
+
return '';
|
|
940
|
+
}
|
|
941
|
+
|
|
580
942
|
private buildMewpCoverageRows(
|
|
581
|
-
requirements:
|
|
582
|
-
|
|
583
|
-
title: string;
|
|
584
|
-
responsibility: string;
|
|
585
|
-
linkedTestCaseIds: number[];
|
|
586
|
-
}>,
|
|
587
|
-
requirementIndex: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
943
|
+
requirements: MewpL2RequirementFamily[],
|
|
944
|
+
requirementIndex: MewpRequirementIndex,
|
|
588
945
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>,
|
|
589
|
-
|
|
590
|
-
|
|
591
|
-
|
|
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);
|
|
592
952
|
for (const requirement of requirements) {
|
|
593
|
-
const key = this.toRequirementKey(requirement.requirementId);
|
|
953
|
+
const key = String(requirement?.baseKey || this.toRequirementKey(requirement.requirementId) || '').trim();
|
|
594
954
|
const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
|
|
595
955
|
(id) => Number.isFinite(id) && Number(id) > 0
|
|
596
956
|
);
|
|
957
|
+
const linkedByTestCase = key ? Array.from(linkedByRequirement.get(key) || []) : [];
|
|
597
958
|
const observedTestCaseIds = key
|
|
598
959
|
? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
|
|
599
960
|
: [];
|
|
600
961
|
|
|
601
|
-
const testCaseIds = Array.from(
|
|
602
|
-
(
|
|
603
|
-
);
|
|
962
|
+
const testCaseIds = Array.from(
|
|
963
|
+
new Set<number>([...linkedTestCaseIds, ...linkedByTestCase, ...observedTestCaseIds])
|
|
964
|
+
).sort((a, b) => a - b);
|
|
604
965
|
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
|
|
608
|
-
|
|
609
|
-
failed: 0,
|
|
610
|
-
notRun: 0,
|
|
611
|
-
})
|
|
612
|
-
);
|
|
613
|
-
continue;
|
|
614
|
-
}
|
|
966
|
+
let totalPassed = 0;
|
|
967
|
+
let totalFailed = 0;
|
|
968
|
+
let totalNotRun = 0;
|
|
969
|
+
const aggregatedBugs = new Map<number, MewpBugLink>();
|
|
615
970
|
|
|
616
971
|
for (const testCaseId of testCaseIds) {
|
|
617
972
|
const summary = key
|
|
618
973
|
? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
|
|
619
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) {
|
|
620
1017
|
rows.push(
|
|
621
1018
|
this.createMewpCoverageRow(
|
|
622
1019
|
requirement,
|
|
623
|
-
|
|
624
|
-
|
|
625
|
-
|
|
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
|
|
626
1046
|
)
|
|
627
1047
|
);
|
|
628
1048
|
}
|
|
@@ -631,6 +1051,262 @@ export default class ResultDataProvider {
|
|
|
631
1051
|
return rows;
|
|
632
1052
|
}
|
|
633
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
|
+
|
|
634
1310
|
private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
|
|
635
1311
|
const map = new Map<number, string>();
|
|
636
1312
|
|
|
@@ -728,56 +1404,197 @@ export default class ResultDataProvider {
|
|
|
728
1404
|
return 'notRun';
|
|
729
1405
|
}
|
|
730
1406
|
|
|
731
|
-
private
|
|
732
|
-
|
|
733
|
-
status: 'passed' | 'failed' | 'notRun',
|
|
1407
|
+
private accumulateRequirementCountsFromActionResults(
|
|
1408
|
+
actionResults: any[],
|
|
734
1409
|
testCaseId: number,
|
|
735
1410
|
requirementKeys: Set<string>,
|
|
736
1411
|
counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
737
1412
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>
|
|
738
1413
|
) {
|
|
739
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
|
+
}
|
|
740
1437
|
|
|
741
|
-
|
|
742
|
-
|
|
743
|
-
|
|
744
|
-
|
|
745
|
-
counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
|
|
746
|
-
}
|
|
747
|
-
const perTestCaseCounters = counters.get(code)!;
|
|
748
|
-
if (!perTestCaseCounters.has(testCaseId)) {
|
|
749
|
-
perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
|
|
750
|
-
}
|
|
1438
|
+
if (!observedTestCaseIdsByRequirement.has(code)) {
|
|
1439
|
+
observedTestCaseIdsByRequirement.set(code, new Set<number>());
|
|
1440
|
+
}
|
|
1441
|
+
observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
|
|
751
1442
|
|
|
752
|
-
|
|
753
|
-
|
|
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;
|
|
754
1447
|
}
|
|
755
|
-
|
|
1448
|
+
}
|
|
1449
|
+
}
|
|
756
1450
|
|
|
757
|
-
|
|
758
|
-
|
|
759
|
-
|
|
760
|
-
|
|
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;
|
|
761
1461
|
}
|
|
1462
|
+
return hasNotRun ? 'notRun' : 'passed';
|
|
762
1463
|
}
|
|
763
1464
|
|
|
764
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> {
|
|
765
1499
|
const out = new Set<string>();
|
|
766
1500
|
const source = this.normalizeRequirementStepText(text);
|
|
767
|
-
|
|
768
|
-
|
|
769
|
-
const
|
|
770
|
-
|
|
771
|
-
|
|
772
|
-
|
|
773
|
-
|
|
774
|
-
|
|
775
|
-
|
|
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
|
+
}
|
|
776
1519
|
}
|
|
777
1520
|
}
|
|
1521
|
+
|
|
778
1522
|
return out;
|
|
779
1523
|
}
|
|
780
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
|
+
|
|
781
1598
|
private normalizeRequirementStepText(text: string): string {
|
|
782
1599
|
const raw = String(text || '');
|
|
783
1600
|
if (!raw) return '';
|
|
@@ -794,28 +1611,19 @@ export default class ResultDataProvider {
|
|
|
794
1611
|
.replace(/\s+/g, ' ');
|
|
795
1612
|
}
|
|
796
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
|
+
|
|
797
1622
|
private toRequirementKey(requirementId: string): string {
|
|
798
|
-
|
|
799
|
-
|
|
800
|
-
const digits = Number.parseInt(normalized.replace(/^SR/i, ''), 10);
|
|
801
|
-
if (!Number.isFinite(digits)) return '';
|
|
802
|
-
return `SR${digits}`;
|
|
803
|
-
}
|
|
804
|
-
|
|
805
|
-
private async fetchMewpL2Requirements(projectName: string, linkedQueryRequest?: any): Promise<
|
|
806
|
-
Array<{
|
|
807
|
-
workItemId: number;
|
|
808
|
-
requirementId: string;
|
|
809
|
-
title: string;
|
|
810
|
-
responsibility: string;
|
|
811
|
-
linkedTestCaseIds: number[];
|
|
812
|
-
}>
|
|
813
|
-
> {
|
|
814
|
-
const queryHref = this.extractMewpQueryHref(linkedQueryRequest);
|
|
815
|
-
if (queryHref) {
|
|
816
|
-
return this.fetchMewpL2RequirementsFromQuery(projectName, queryHref);
|
|
817
|
-
}
|
|
1623
|
+
return this.normalizeMewpRequirementCode(requirementId);
|
|
1624
|
+
}
|
|
818
1625
|
|
|
1626
|
+
private async fetchMewpL2Requirements(projectName: string): Promise<MewpL2RequirementWorkItem[]> {
|
|
819
1627
|
const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
820
1628
|
if (workItemTypeNames.length === 0) {
|
|
821
1629
|
return [];
|
|
@@ -824,17 +1632,36 @@ export default class ResultDataProvider {
|
|
|
824
1632
|
const quotedTypeNames = workItemTypeNames
|
|
825
1633
|
.map((name) => `'${String(name).replace(/'/g, "''")}'`)
|
|
826
1634
|
.join(', ');
|
|
827
|
-
const
|
|
828
|
-
|
|
829
|
-
|
|
830
|
-
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
.
|
|
837
|
-
.
|
|
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
|
+
}
|
|
838
1665
|
|
|
839
1666
|
if (requirementIds.length === 0) {
|
|
840
1667
|
return [];
|
|
@@ -843,235 +1670,294 @@ ORDER BY [System.Id]`;
|
|
|
843
1670
|
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
844
1671
|
const requirements = workItems.map((wi: any) => {
|
|
845
1672
|
const fields = wi?.fields || {};
|
|
1673
|
+
const requirementId = this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0));
|
|
1674
|
+
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
846
1675
|
return {
|
|
847
1676
|
workItemId: Number(wi?.id || 0),
|
|
848
|
-
requirementId
|
|
849
|
-
|
|
1677
|
+
requirementId,
|
|
1678
|
+
baseKey: this.toRequirementKey(requirementId),
|
|
1679
|
+
title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
|
|
1680
|
+
subSystem: this.deriveMewpSubSystem(fields),
|
|
850
1681
|
responsibility: this.deriveMewpResponsibility(fields),
|
|
851
1682
|
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
|
|
1683
|
+
relatedWorkItemIds: this.extractLinkedWorkItemIdsFromRelations(wi?.relations || []),
|
|
1684
|
+
areaPath,
|
|
852
1685
|
};
|
|
853
1686
|
});
|
|
854
1687
|
|
|
855
|
-
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)));
|
|
856
1695
|
}
|
|
857
1696
|
|
|
858
|
-
private
|
|
859
|
-
const
|
|
1697
|
+
private isMewpL2AreaPath(areaPath: string): boolean {
|
|
1698
|
+
const normalized = String(areaPath || '')
|
|
860
1699
|
.trim()
|
|
861
|
-
.toLowerCase()
|
|
862
|
-
|
|
863
|
-
|
|
864
|
-
return
|
|
1700
|
+
.toLowerCase()
|
|
1701
|
+
.replace(/\//g, '\\');
|
|
1702
|
+
if (!normalized) return false;
|
|
1703
|
+
return normalized.includes('\\customer requirements\\level 2');
|
|
865
1704
|
}
|
|
866
1705
|
|
|
867
|
-
private
|
|
868
|
-
|
|
869
|
-
|
|
870
|
-
):
|
|
871
|
-
|
|
872
|
-
|
|
873
|
-
|
|
874
|
-
|
|
875
|
-
|
|
876
|
-
|
|
877
|
-
|
|
878
|
-
|
|
879
|
-
try {
|
|
880
|
-
const ticketsDataProvider = new TicketsDataProvider(this.orgUrl, this.token);
|
|
881
|
-
const queryResult = await ticketsDataProvider.GetQueryResultsFromWiql(
|
|
882
|
-
queryHref,
|
|
883
|
-
true,
|
|
884
|
-
new Map<number, Set<any>>()
|
|
885
|
-
);
|
|
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
|
+
>();
|
|
886
1718
|
|
|
887
|
-
|
|
888
|
-
const
|
|
889
|
-
|
|
890
|
-
|
|
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
|
+
};
|
|
891
1733
|
|
|
892
|
-
|
|
893
|
-
|
|
894
|
-
|
|
895
|
-
|
|
896
|
-
requirementId: string;
|
|
897
|
-
title: string;
|
|
898
|
-
responsibility: string;
|
|
899
|
-
linkedTestCaseIds: Set<number>;
|
|
900
|
-
}
|
|
901
|
-
>();
|
|
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;
|
|
902
1738
|
|
|
903
|
-
|
|
904
|
-
|
|
905
|
-
|
|
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
|
+
}
|
|
906
1758
|
|
|
907
|
-
|
|
908
|
-
|
|
909
|
-
|
|
910
|
-
|
|
911
|
-
|
|
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
|
+
}
|
|
912
1770
|
|
|
913
|
-
|
|
914
|
-
|
|
915
|
-
|
|
916
|
-
|
|
917
|
-
|
|
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
|
+
}
|
|
918
1787
|
|
|
919
|
-
|
|
920
|
-
|
|
921
|
-
|
|
922
|
-
|
|
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
|
+
});
|
|
923
1803
|
}
|
|
1804
|
+
return map.get(testCaseId)!;
|
|
1805
|
+
};
|
|
924
1806
|
|
|
925
|
-
|
|
926
|
-
|
|
927
|
-
|
|
928
|
-
|
|
929
|
-
|
|
930
|
-
|
|
931
|
-
|
|
932
|
-
|
|
933
|
-
}
|
|
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
|
+
}
|
|
934
1815
|
|
|
935
|
-
|
|
936
|
-
|
|
937
|
-
|
|
938
|
-
|
|
939
|
-
|
|
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
|
+
}
|
|
940
1824
|
|
|
941
|
-
|
|
942
|
-
|
|
943
|
-
|
|
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
|
+
}
|
|
944
1838
|
|
|
945
|
-
|
|
946
|
-
|
|
947
|
-
|
|
948
|
-
|
|
949
|
-
|
|
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);
|
|
950
1859
|
}
|
|
951
1860
|
}
|
|
952
1861
|
}
|
|
953
|
-
|
|
954
|
-
await this.hydrateMewpRequirementsFromWorkItems(projectName, requirementsById);
|
|
955
|
-
|
|
956
|
-
return [...requirementsById.values()]
|
|
957
|
-
.map((requirement) => ({
|
|
958
|
-
workItemId: requirement.workItemId,
|
|
959
|
-
requirementId: requirement.requirementId,
|
|
960
|
-
title: requirement.title,
|
|
961
|
-
responsibility: requirement.responsibility,
|
|
962
|
-
linkedTestCaseIds: [...requirement.linkedTestCaseIds].sort((a, b) => a - b),
|
|
963
|
-
}))
|
|
964
|
-
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
965
|
-
} catch (error: any) {
|
|
966
|
-
logger.error(`Could not fetch MEWP requirements from query: ${error?.message || error}`);
|
|
967
|
-
return [];
|
|
968
1862
|
}
|
|
969
|
-
}
|
|
970
1863
|
|
|
971
|
-
|
|
972
|
-
|
|
973
|
-
number,
|
|
974
|
-
{
|
|
975
|
-
|
|
976
|
-
|
|
977
|
-
|
|
978
|
-
|
|
979
|
-
|
|
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);
|
|
980
1874
|
}
|
|
981
|
-
>,
|
|
982
|
-
workItem: any,
|
|
983
|
-
requirementTypeSet: Set<string>
|
|
984
|
-
) {
|
|
985
|
-
const workItemId = Number(workItem?.id || 0);
|
|
986
|
-
if (!Number.isFinite(workItemId) || workItemId <= 0) return;
|
|
987
|
-
|
|
988
|
-
const fields = workItem?.fields || {};
|
|
989
|
-
const workItemType = this.getMewpWorkItemType(workItem);
|
|
990
|
-
if (!this.isMewpRequirementType(workItemType, requirementTypeSet)) return;
|
|
991
|
-
|
|
992
|
-
const existing = requirementsById.get(workItemId) || {
|
|
993
|
-
workItemId,
|
|
994
|
-
requirementId: String(workItemId),
|
|
995
|
-
title: '',
|
|
996
|
-
responsibility: '',
|
|
997
|
-
linkedTestCaseIds: new Set<number>(),
|
|
998
|
-
};
|
|
999
|
-
|
|
1000
|
-
const extractedRequirementId = this.extractMewpRequirementIdentifier(fields, workItemId);
|
|
1001
|
-
const extractedTitle = this.toMewpComparableText(fields?.['System.Title']);
|
|
1002
|
-
const extractedResponsibility = this.deriveMewpResponsibility(fields);
|
|
1003
1875
|
|
|
1004
|
-
|
|
1005
|
-
|
|
1006
|
-
|
|
1007
|
-
|
|
1008
|
-
|
|
1009
|
-
|
|
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
|
+
}
|
|
1010
1885
|
}
|
|
1011
1886
|
|
|
1012
|
-
|
|
1887
|
+
return map;
|
|
1013
1888
|
}
|
|
1014
1889
|
|
|
1015
|
-
private async
|
|
1016
|
-
|
|
1017
|
-
|
|
1018
|
-
|
|
1019
|
-
|
|
1020
|
-
|
|
1021
|
-
|
|
1022
|
-
|
|
1023
|
-
|
|
1024
|
-
|
|
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
|
+
}
|
|
1025
1909
|
}
|
|
1026
|
-
|
|
1027
|
-
|
|
1028
|
-
|
|
1029
|
-
|
|
1030
|
-
|
|
1031
|
-
|
|
1032
|
-
|
|
1033
|
-
|
|
1034
|
-
|
|
1035
|
-
|
|
1036
|
-
if (!current) continue;
|
|
1037
|
-
|
|
1038
|
-
const fields = requirementWorkItem?.fields || {};
|
|
1039
|
-
const requirementId = this.extractMewpRequirementIdentifier(fields, workItemId);
|
|
1040
|
-
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
1041
|
-
const responsibility = this.deriveMewpResponsibility(fields);
|
|
1042
|
-
const linkedTestCaseIds = this.extractLinkedTestCaseIdsFromRequirement(
|
|
1043
|
-
requirementWorkItem?.relations || []
|
|
1044
|
-
);
|
|
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;
|
|
1045
1920
|
|
|
1046
|
-
|
|
1047
|
-
|
|
1048
|
-
|
|
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);
|
|
1049
1926
|
}
|
|
1050
|
-
|
|
1051
|
-
|
|
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));
|
|
1052
1938
|
}
|
|
1053
|
-
linkedTestCaseIds.forEach((testCaseId) => current.linkedTestCaseIds.add(testCaseId));
|
|
1054
|
-
}
|
|
1055
|
-
}
|
|
1056
1939
|
|
|
1057
|
-
|
|
1058
|
-
|
|
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
|
+
}
|
|
1059
1945
|
}
|
|
1060
1946
|
|
|
1061
|
-
private
|
|
1062
|
-
const
|
|
1947
|
+
private isTestCaseToRequirementRelation(relation: any): boolean {
|
|
1948
|
+
const rel = String(relation?.rel || '')
|
|
1063
1949
|
.trim()
|
|
1064
1950
|
.toLowerCase();
|
|
1065
|
-
if (!
|
|
1066
|
-
|
|
1067
|
-
return normalized.includes('requirement') || normalized === 'epic';
|
|
1951
|
+
if (!rel) return false;
|
|
1952
|
+
return rel.includes('testedby-reverse') || (rel.includes('tests') && rel.includes('reverse'));
|
|
1068
1953
|
}
|
|
1069
1954
|
|
|
1070
|
-
private
|
|
1071
|
-
const
|
|
1072
|
-
|
|
1073
|
-
|
|
1074
|
-
|
|
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;
|
|
1075
1961
|
}
|
|
1076
1962
|
|
|
1077
1963
|
private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
|
|
@@ -1135,6 +2021,18 @@ ORDER BY [System.Id]`;
|
|
|
1135
2021
|
return [...out].sort((a, b) => a - b);
|
|
1136
2022
|
}
|
|
1137
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
|
+
|
|
1138
2036
|
private extractMewpRequirementIdentifier(fields: Record<string, any>, fallbackWorkItemId: number): string {
|
|
1139
2037
|
const entries = Object.entries(fields || {});
|
|
1140
2038
|
|
|
@@ -1154,7 +2052,7 @@ ORDER BY [System.Id]`;
|
|
|
1154
2052
|
|
|
1155
2053
|
const valueAsString = this.toMewpComparableText(value);
|
|
1156
2054
|
if (!valueAsString) continue;
|
|
1157
|
-
const normalized = this.
|
|
2055
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
1158
2056
|
if (normalized) return normalized;
|
|
1159
2057
|
}
|
|
1160
2058
|
|
|
@@ -1166,19 +2064,29 @@ ORDER BY [System.Id]`;
|
|
|
1166
2064
|
|
|
1167
2065
|
const valueAsString = this.toMewpComparableText(value);
|
|
1168
2066
|
if (!valueAsString) continue;
|
|
1169
|
-
const normalized = this.
|
|
2067
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
1170
2068
|
if (normalized) return normalized;
|
|
1171
2069
|
}
|
|
1172
2070
|
|
|
1173
2071
|
// Optional fallback from title only (avoid scanning all fields and accidental SR matches).
|
|
1174
2072
|
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
1175
|
-
const titleCode = this.
|
|
2073
|
+
const titleCode = this.normalizeMewpRequirementCodeWithSuffix(title);
|
|
1176
2074
|
if (titleCode) return titleCode;
|
|
1177
2075
|
|
|
1178
|
-
return
|
|
2076
|
+
return fallbackWorkItemId ? `SR${fallbackWorkItemId}` : '';
|
|
1179
2077
|
}
|
|
1180
2078
|
|
|
1181
2079
|
private deriveMewpResponsibility(fields: Record<string, any>): string {
|
|
2080
|
+
const explicitSapWbs = this.toMewpComparableText(fields?.['Custom.SAPWBS']);
|
|
2081
|
+
const fromExplicitSapWbs = this.resolveMewpResponsibility(explicitSapWbs);
|
|
2082
|
+
if (fromExplicitSapWbs) return fromExplicitSapWbs;
|
|
2083
|
+
if (explicitSapWbs) return explicitSapWbs;
|
|
2084
|
+
|
|
2085
|
+
const explicitSapWbsByLabel = this.toMewpComparableText(fields?.['SAPWBS']);
|
|
2086
|
+
const fromExplicitLabel = this.resolveMewpResponsibility(explicitSapWbsByLabel);
|
|
2087
|
+
if (fromExplicitLabel) return fromExplicitLabel;
|
|
2088
|
+
if (explicitSapWbsByLabel) return explicitSapWbsByLabel;
|
|
2089
|
+
|
|
1182
2090
|
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
1183
2091
|
const fromAreaPath = this.resolveMewpResponsibility(areaPath);
|
|
1184
2092
|
if (fromAreaPath) return fromAreaPath;
|
|
@@ -1194,17 +2102,69 @@ ORDER BY [System.Id]`;
|
|
|
1194
2102
|
return '';
|
|
1195
2103
|
}
|
|
1196
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
|
+
|
|
1197
2143
|
private resolveMewpResponsibility(value: string): string {
|
|
1198
|
-
const
|
|
1199
|
-
|
|
1200
|
-
|
|
1201
|
-
|
|
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';
|
|
1202
2159
|
|
|
1203
|
-
if (/(^|[^a-z0-9])esuk([^a-z0-9]|$)/i.test(text)) return 'ESUK';
|
|
1204
|
-
if (/(^|[^a-z0-9])il([^a-z0-9]|$)/i.test(text)) return 'IL';
|
|
1205
2160
|
return '';
|
|
1206
2161
|
}
|
|
1207
2162
|
|
|
2163
|
+
private isExcludedL3L4BySapWbs(value: string): boolean {
|
|
2164
|
+
const responsibility = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
2165
|
+
return responsibility === 'ESUK';
|
|
2166
|
+
}
|
|
2167
|
+
|
|
1208
2168
|
private normalizeMewpRequirementCode(value: string): string {
|
|
1209
2169
|
const text = String(value || '').trim();
|
|
1210
2170
|
if (!text) return '';
|
|
@@ -1213,6 +2173,16 @@ ORDER BY [System.Id]`;
|
|
|
1213
2173
|
return `SR${match[1]}`;
|
|
1214
2174
|
}
|
|
1215
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
|
+
|
|
1216
2186
|
private toMewpComparableText(value: any): string {
|
|
1217
2187
|
if (value === null || value === undefined) return '';
|
|
1218
2188
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|
|
@@ -1225,6 +2195,8 @@ ORDER BY [System.Id]`;
|
|
|
1225
2195
|
if (name) return String(name).trim();
|
|
1226
2196
|
const uniqueName = (value as any).uniqueName;
|
|
1227
2197
|
if (uniqueName) return String(uniqueName).trim();
|
|
2198
|
+
const objectValue = (value as any).value;
|
|
2199
|
+
if (objectValue !== undefined && objectValue !== null) return String(objectValue).trim();
|
|
1228
2200
|
}
|
|
1229
2201
|
return String(value).trim();
|
|
1230
2202
|
}
|