@elisra-devops/docgen-data-provider 1.75.0 → 1.77.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 +1156 -119
- 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 +269 -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 +1399 -166
- package/src/tests/modules/ResultDataProvider.test.ts +1471 -42
- package/src/utils/mewpExternalIngestionUtils.ts +349 -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,92 @@ 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
|
+
);
|
|
481
|
+
const hasExternalBugsFile = !!String(
|
|
482
|
+
options?.externalBugsFile?.name ||
|
|
483
|
+
options?.externalBugsFile?.objectName ||
|
|
484
|
+
options?.externalBugsFile?.text ||
|
|
485
|
+
options?.externalBugsFile?.url ||
|
|
486
|
+
''
|
|
487
|
+
).trim();
|
|
488
|
+
const hasExternalL3L4File = !!String(
|
|
489
|
+
options?.externalL3L4File?.name ||
|
|
490
|
+
options?.externalL3L4File?.objectName ||
|
|
491
|
+
options?.externalL3L4File?.text ||
|
|
492
|
+
options?.externalL3L4File?.url ||
|
|
493
|
+
''
|
|
494
|
+
).trim();
|
|
495
|
+
const externalBugLinksCount = [...externalBugsByTestCase.values()].reduce(
|
|
496
|
+
(sum, items) => sum + (Array.isArray(items) ? items.length : 0),
|
|
497
|
+
0
|
|
498
|
+
);
|
|
499
|
+
const externalL3L4LinksCount = [...externalL3L4ByBaseKey.values()].reduce(
|
|
500
|
+
(sum, items) => sum + (Array.isArray(items) ? items.length : 0),
|
|
501
|
+
0
|
|
502
|
+
);
|
|
503
|
+
logger.info(
|
|
504
|
+
`MEWP coverage external ingestion summary: ` +
|
|
505
|
+
`bugsFileProvided=${hasExternalBugsFile} bugsTestCases=${externalBugsByTestCase.size} bugsLinks=${externalBugLinksCount}; ` +
|
|
506
|
+
`l3l4FileProvided=${hasExternalL3L4File} l3l4BaseKeys=${externalL3L4ByBaseKey.size} l3l4Links=${externalL3L4LinksCount}`
|
|
507
|
+
);
|
|
508
|
+
if (hasExternalBugsFile && externalBugLinksCount === 0) {
|
|
509
|
+
logger.warn(
|
|
510
|
+
`MEWP coverage: external bugs file was provided but produced 0 links. ` +
|
|
511
|
+
`Check SR/test-case/state values in ingestion logs.`
|
|
512
|
+
);
|
|
513
|
+
}
|
|
514
|
+
if (hasExternalL3L4File && externalL3L4LinksCount === 0) {
|
|
515
|
+
logger.warn(
|
|
516
|
+
`MEWP coverage: external L3/L4 file was provided but produced 0 links. ` +
|
|
517
|
+
`Check SR/AREA34/state/SAPWBS filters in ingestion logs.`
|
|
518
|
+
);
|
|
519
|
+
}
|
|
410
520
|
if (requirements.length === 0) {
|
|
411
521
|
return {
|
|
412
522
|
...defaultPayload,
|
|
@@ -414,47 +524,40 @@ export default class ResultDataProvider {
|
|
|
414
524
|
};
|
|
415
525
|
}
|
|
416
526
|
|
|
417
|
-
const requirementIndex = new Map
|
|
527
|
+
const requirementIndex: MewpRequirementIndex = new Map();
|
|
418
528
|
const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
|
|
419
529
|
const requirementKeys = new Set<string>();
|
|
420
530
|
requirements.forEach((requirement) => {
|
|
421
|
-
const key =
|
|
531
|
+
const key = String(requirement?.baseKey || '').trim();
|
|
422
532
|
if (!key) return;
|
|
423
533
|
requirementKeys.add(key);
|
|
424
534
|
});
|
|
425
535
|
|
|
426
536
|
const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
|
|
427
537
|
const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
|
|
428
|
-
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
429
|
-
|
|
430
538
|
const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
|
|
431
539
|
for (const runResult of runResults) {
|
|
432
540
|
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
|
|
541
|
+
const rawActionResults = Array.isArray(runResult?.iteration?.actionResults)
|
|
542
|
+
? runResult.iteration.actionResults.filter((item: any) => !item?.isSharedStepTitle)
|
|
441
543
|
: [];
|
|
544
|
+
const actionResults = rawActionResults.sort((a: any, b: any) =>
|
|
545
|
+
this.compareActionResults(
|
|
546
|
+
String(a?.stepPosition || a?.stepIdentifier || ''),
|
|
547
|
+
String(b?.stepPosition || b?.stepIdentifier || '')
|
|
548
|
+
)
|
|
549
|
+
);
|
|
442
550
|
const hasExecutedRun =
|
|
443
551
|
Number(runResult?.lastRunId || 0) > 0 && Number(runResult?.lastResultId || 0) > 0;
|
|
444
552
|
|
|
445
553
|
if (actionResults.length > 0) {
|
|
446
|
-
|
|
447
|
-
|
|
448
|
-
|
|
449
|
-
|
|
450
|
-
|
|
451
|
-
|
|
452
|
-
|
|
453
|
-
requirementKeys,
|
|
454
|
-
requirementIndex,
|
|
455
|
-
observedTestCaseIdsByRequirement
|
|
456
|
-
);
|
|
457
|
-
}
|
|
554
|
+
this.accumulateRequirementCountsFromActionResults(
|
|
555
|
+
actionResults,
|
|
556
|
+
testCaseId,
|
|
557
|
+
requirementKeys,
|
|
558
|
+
requirementIndex,
|
|
559
|
+
observedTestCaseIdsByRequirement
|
|
560
|
+
);
|
|
458
561
|
continue;
|
|
459
562
|
}
|
|
460
563
|
|
|
@@ -475,24 +578,51 @@ export default class ResultDataProvider {
|
|
|
475
578
|
}
|
|
476
579
|
|
|
477
580
|
const definitionSteps = parsedDefinitionStepsByTestCase.get(testCaseId) || [];
|
|
478
|
-
|
|
479
|
-
|
|
480
|
-
|
|
481
|
-
|
|
482
|
-
|
|
483
|
-
|
|
484
|
-
|
|
485
|
-
|
|
486
|
-
|
|
487
|
-
);
|
|
488
|
-
|
|
581
|
+
const fallbackActionResults = definitionSteps
|
|
582
|
+
.filter((step) => !step?.isSharedStepTitle)
|
|
583
|
+
.sort((a, b) =>
|
|
584
|
+
this.compareActionResults(String(a?.stepPosition || ''), String(b?.stepPosition || ''))
|
|
585
|
+
)
|
|
586
|
+
.map((step) => ({
|
|
587
|
+
stepPosition: step?.stepPosition,
|
|
588
|
+
expected: step?.expected,
|
|
589
|
+
outcome: 'Unspecified',
|
|
590
|
+
}));
|
|
591
|
+
|
|
592
|
+
this.accumulateRequirementCountsFromActionResults(
|
|
593
|
+
fallbackActionResults,
|
|
594
|
+
testCaseId,
|
|
595
|
+
requirementKeys,
|
|
596
|
+
requirementIndex,
|
|
597
|
+
observedTestCaseIdsByRequirement
|
|
598
|
+
);
|
|
489
599
|
}
|
|
490
600
|
|
|
491
601
|
const rows = this.buildMewpCoverageRows(
|
|
492
602
|
requirements,
|
|
493
603
|
requirementIndex,
|
|
494
604
|
observedTestCaseIdsByRequirement,
|
|
495
|
-
|
|
605
|
+
linkedRequirementsByTestCase,
|
|
606
|
+
externalL3L4ByBaseKey,
|
|
607
|
+
externalBugsByTestCase
|
|
608
|
+
);
|
|
609
|
+
const coverageRowStats = rows.reduce(
|
|
610
|
+
(acc, row) => {
|
|
611
|
+
const hasBug = Number(row?.['Bug ID'] || 0) > 0;
|
|
612
|
+
const hasL3 = String(row?.['L3 REQ ID'] || '').trim() !== '';
|
|
613
|
+
const hasL4 = String(row?.['L4 REQ ID'] || '').trim() !== '';
|
|
614
|
+
if (hasBug) acc.bugRows += 1;
|
|
615
|
+
if (hasL3) acc.l3Rows += 1;
|
|
616
|
+
if (hasL4) acc.l4Rows += 1;
|
|
617
|
+
if (!hasBug && !hasL3 && !hasL4) acc.baseOnlyRows += 1;
|
|
618
|
+
return acc;
|
|
619
|
+
},
|
|
620
|
+
{ bugRows: 0, l3Rows: 0, l4Rows: 0, baseOnlyRows: 0 }
|
|
621
|
+
);
|
|
622
|
+
logger.info(
|
|
623
|
+
`MEWP coverage output summary: requirements=${requirements.length} rows=${rows.length} ` +
|
|
624
|
+
`bugRows=${coverageRowStats.bugRows} l3Rows=${coverageRowStats.l3Rows} ` +
|
|
625
|
+
`l4Rows=${coverageRowStats.l4Rows} baseOnlyRows=${coverageRowStats.baseOnlyRows}`
|
|
496
626
|
);
|
|
497
627
|
|
|
498
628
|
return {
|
|
@@ -502,10 +632,261 @@ export default class ResultDataProvider {
|
|
|
502
632
|
};
|
|
503
633
|
} catch (error: any) {
|
|
504
634
|
logger.error(`Error during getMewpL2CoverageFlatResults: ${error.message}`);
|
|
635
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
636
|
+
throw error;
|
|
637
|
+
}
|
|
638
|
+
return defaultPayload;
|
|
639
|
+
}
|
|
640
|
+
}
|
|
641
|
+
|
|
642
|
+
public async getMewpInternalValidationFlatResults(
|
|
643
|
+
testPlanId: string,
|
|
644
|
+
projectName: string,
|
|
645
|
+
selectedSuiteIds: number[] | undefined,
|
|
646
|
+
linkedQueryRequest?: any,
|
|
647
|
+
options?: MewpInternalValidationRequestOptions
|
|
648
|
+
): Promise<MewpInternalValidationFlatPayload> {
|
|
649
|
+
const defaultPayload: MewpInternalValidationFlatPayload = {
|
|
650
|
+
sheetName: `MEWP Internal Validation - Plan ${testPlanId}`,
|
|
651
|
+
columnOrder: [...ResultDataProvider.INTERNAL_VALIDATION_COLUMNS],
|
|
652
|
+
rows: [],
|
|
653
|
+
};
|
|
654
|
+
|
|
655
|
+
try {
|
|
656
|
+
const planName = await this.fetchTestPlanName(testPlanId, projectName);
|
|
657
|
+
const testData = await this.fetchMewpScopedTestData(
|
|
658
|
+
testPlanId,
|
|
659
|
+
projectName,
|
|
660
|
+
selectedSuiteIds,
|
|
661
|
+
!!options?.useRelFallback
|
|
662
|
+
);
|
|
663
|
+
const allRequirements = await this.fetchMewpL2Requirements(projectName);
|
|
664
|
+
const linkedRequirementsByTestCase = await this.buildLinkedRequirementsByTestCase(
|
|
665
|
+
allRequirements,
|
|
666
|
+
testData,
|
|
667
|
+
projectName
|
|
668
|
+
);
|
|
669
|
+
const scopedRequirementKeys = await this.resolveMewpRequirementScopeKeysFromQuery(
|
|
670
|
+
linkedQueryRequest,
|
|
671
|
+
allRequirements,
|
|
672
|
+
linkedRequirementsByTestCase
|
|
673
|
+
);
|
|
674
|
+
const requirementFamilies = this.buildRequirementFamilyMap(
|
|
675
|
+
allRequirements,
|
|
676
|
+
scopedRequirementKeys?.size ? scopedRequirementKeys : undefined
|
|
677
|
+
);
|
|
678
|
+
|
|
679
|
+
const rows: MewpInternalValidationRow[] = [];
|
|
680
|
+
const stepsXmlByTestCase = this.buildTestCaseStepsXmlMap(testData);
|
|
681
|
+
const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
|
|
682
|
+
const allTestCaseIds = new Set<number>();
|
|
683
|
+
for (const suite of testData || []) {
|
|
684
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
685
|
+
for (const testCase of testCasesItems) {
|
|
686
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
687
|
+
if (Number.isFinite(id) && id > 0) allTestCaseIds.add(id);
|
|
688
|
+
}
|
|
689
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
690
|
+
for (const testPoint of testPointsItems) {
|
|
691
|
+
const id = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
|
|
692
|
+
if (Number.isFinite(id) && id > 0) allTestCaseIds.add(id);
|
|
693
|
+
}
|
|
694
|
+
}
|
|
695
|
+
|
|
696
|
+
const validL2BaseKeys = new Set<string>([...requirementFamilies.keys()]);
|
|
697
|
+
|
|
698
|
+
for (const testCaseId of [...allTestCaseIds].sort((a, b) => a - b)) {
|
|
699
|
+
const stepsXml = stepsXmlByTestCase.get(testCaseId) || '';
|
|
700
|
+
const parsedSteps =
|
|
701
|
+
stepsXml && String(stepsXml).trim() !== ''
|
|
702
|
+
? await this.testStepParserHelper.parseTestSteps(stepsXml, new Map<number, number>())
|
|
703
|
+
: [];
|
|
704
|
+
const mentionEntries = this.extractRequirementMentionsFromExpectedSteps(parsedSteps, true);
|
|
705
|
+
const mentionedL2Only = new Set<string>();
|
|
706
|
+
const mentionedCodeFirstStep = new Map<string, string>();
|
|
707
|
+
const mentionedBaseFirstStep = new Map<string, string>();
|
|
708
|
+
for (const mentionEntry of mentionEntries) {
|
|
709
|
+
const scopeFilteredCodes =
|
|
710
|
+
scopedRequirementKeys?.size && mentionEntry.codes.size > 0
|
|
711
|
+
? [...mentionEntry.codes].filter((code) => scopedRequirementKeys.has(this.toRequirementKey(code)))
|
|
712
|
+
: [...mentionEntry.codes];
|
|
713
|
+
for (const code of scopeFilteredCodes) {
|
|
714
|
+
const baseKey = this.toRequirementKey(code);
|
|
715
|
+
if (!baseKey) continue;
|
|
716
|
+
if (validL2BaseKeys.has(baseKey)) {
|
|
717
|
+
mentionedL2Only.add(code);
|
|
718
|
+
if (!mentionedCodeFirstStep.has(code)) {
|
|
719
|
+
mentionedCodeFirstStep.set(code, mentionEntry.stepRef);
|
|
720
|
+
}
|
|
721
|
+
if (!mentionedBaseFirstStep.has(baseKey)) {
|
|
722
|
+
mentionedBaseFirstStep.set(baseKey, mentionEntry.stepRef);
|
|
723
|
+
}
|
|
724
|
+
}
|
|
725
|
+
}
|
|
726
|
+
}
|
|
727
|
+
|
|
728
|
+
const mentionedBaseKeys = new Set<string>(
|
|
729
|
+
[...mentionedL2Only].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
730
|
+
);
|
|
731
|
+
|
|
732
|
+
const expectedFamilyCodes = new Set<string>();
|
|
733
|
+
for (const baseKey of mentionedBaseKeys) {
|
|
734
|
+
const familyCodes = requirementFamilies.get(baseKey);
|
|
735
|
+
if (familyCodes?.size) {
|
|
736
|
+
familyCodes.forEach((code) => expectedFamilyCodes.add(code));
|
|
737
|
+
} else {
|
|
738
|
+
for (const code of mentionedL2Only) {
|
|
739
|
+
if (this.toRequirementKey(code) === baseKey) expectedFamilyCodes.add(code);
|
|
740
|
+
}
|
|
741
|
+
}
|
|
742
|
+
}
|
|
743
|
+
|
|
744
|
+
const linkedFullCodesRaw = linkedRequirementsByTestCase.get(testCaseId)?.fullCodes || new Set<string>();
|
|
745
|
+
const linkedFullCodes =
|
|
746
|
+
scopedRequirementKeys?.size && linkedFullCodesRaw.size > 0
|
|
747
|
+
? new Set<string>(
|
|
748
|
+
[...linkedFullCodesRaw].filter((code) =>
|
|
749
|
+
scopedRequirementKeys.has(this.toRequirementKey(code))
|
|
750
|
+
)
|
|
751
|
+
)
|
|
752
|
+
: linkedFullCodesRaw;
|
|
753
|
+
const linkedBaseKeys = new Set<string>(
|
|
754
|
+
[...linkedFullCodes].map((code) => this.toRequirementKey(code)).filter((code) => !!code)
|
|
755
|
+
);
|
|
756
|
+
|
|
757
|
+
const missingMentioned = [...mentionedL2Only].filter((code) => {
|
|
758
|
+
const baseKey = this.toRequirementKey(code);
|
|
759
|
+
if (!baseKey) return false;
|
|
760
|
+
const hasSpecificSuffix = /-\d+$/.test(code);
|
|
761
|
+
if (hasSpecificSuffix) return !linkedFullCodes.has(code);
|
|
762
|
+
return !linkedBaseKeys.has(baseKey);
|
|
763
|
+
});
|
|
764
|
+
const missingFamily = [...expectedFamilyCodes].filter((code) => !linkedFullCodes.has(code));
|
|
765
|
+
const extraLinked = [...linkedFullCodes].filter((code) => !expectedFamilyCodes.has(code));
|
|
766
|
+
const mentionedButNotLinkedByStep = new Map<string, Set<string>>();
|
|
767
|
+
const appendMentionedButNotLinked = (requirementId: string, stepRef: string) => {
|
|
768
|
+
const normalizedRequirementId = this.normalizeMewpRequirementCodeWithSuffix(requirementId);
|
|
769
|
+
if (!normalizedRequirementId) return;
|
|
770
|
+
const normalizedStepRef = String(stepRef || 'Step ?').trim() || 'Step ?';
|
|
771
|
+
if (!mentionedButNotLinkedByStep.has(normalizedStepRef)) {
|
|
772
|
+
mentionedButNotLinkedByStep.set(normalizedStepRef, new Set<string>());
|
|
773
|
+
}
|
|
774
|
+
mentionedButNotLinkedByStep.get(normalizedStepRef)!.add(normalizedRequirementId);
|
|
775
|
+
};
|
|
776
|
+
|
|
777
|
+
const sortedMissingMentioned = [...new Set(missingMentioned)].sort((a, b) => a.localeCompare(b));
|
|
778
|
+
const sortedMissingFamily = [...new Set(missingFamily)].sort((a, b) => a.localeCompare(b));
|
|
779
|
+
for (const code of sortedMissingMentioned) {
|
|
780
|
+
const stepRef = mentionedCodeFirstStep.get(code) || 'Step ?';
|
|
781
|
+
appendMentionedButNotLinked(code, stepRef);
|
|
782
|
+
}
|
|
783
|
+
for (const code of sortedMissingFamily) {
|
|
784
|
+
const baseKey = this.toRequirementKey(code);
|
|
785
|
+
const stepRef = mentionedBaseFirstStep.get(baseKey) || 'Step ?';
|
|
786
|
+
appendMentionedButNotLinked(code, stepRef);
|
|
787
|
+
}
|
|
788
|
+
|
|
789
|
+
const sortedExtraLinked = [...new Set(extraLinked)]
|
|
790
|
+
.map((code) => this.normalizeMewpRequirementCodeWithSuffix(code))
|
|
791
|
+
.filter((code) => !!code)
|
|
792
|
+
.sort((a, b) => a.localeCompare(b));
|
|
793
|
+
|
|
794
|
+
const parseStepOrder = (stepRef: string): number => {
|
|
795
|
+
const match = /step\s+(\d+)/i.exec(String(stepRef || ''));
|
|
796
|
+
const parsed = Number(match?.[1] || Number.POSITIVE_INFINITY);
|
|
797
|
+
return Number.isFinite(parsed) ? parsed : Number.POSITIVE_INFINITY;
|
|
798
|
+
};
|
|
799
|
+
const mentionedButNotLinked = [...mentionedButNotLinkedByStep.entries()]
|
|
800
|
+
.sort((a, b) => {
|
|
801
|
+
const stepOrderA = parseStepOrder(a[0]);
|
|
802
|
+
const stepOrderB = parseStepOrder(b[0]);
|
|
803
|
+
if (stepOrderA !== stepOrderB) return stepOrderA - stepOrderB;
|
|
804
|
+
return String(a[0]).localeCompare(String(b[0]));
|
|
805
|
+
})
|
|
806
|
+
.map(([stepRef, requirementIds]) => {
|
|
807
|
+
const requirementList = [...requirementIds].sort((a, b) => a.localeCompare(b));
|
|
808
|
+
return `${stepRef}: ${requirementList.join(', ')}`;
|
|
809
|
+
})
|
|
810
|
+
.join('; ');
|
|
811
|
+
const linkedButNotMentioned = sortedExtraLinked.join('; ');
|
|
812
|
+
const validationStatus: 'Pass' | 'Fail' =
|
|
813
|
+
mentionedButNotLinked || linkedButNotMentioned ? 'Fail' : 'Pass';
|
|
814
|
+
|
|
815
|
+
rows.push({
|
|
816
|
+
'Test Case ID': testCaseId,
|
|
817
|
+
'Test Case Title': String(testCaseTitleMap.get(testCaseId) || '').trim(),
|
|
818
|
+
'Mentioned but Not Linked': mentionedButNotLinked,
|
|
819
|
+
'Linked but Not Mentioned': linkedButNotMentioned,
|
|
820
|
+
'Validation Status': validationStatus,
|
|
821
|
+
});
|
|
822
|
+
}
|
|
823
|
+
|
|
824
|
+
return {
|
|
825
|
+
sheetName: this.buildInternalValidationSheetName(planName, testPlanId),
|
|
826
|
+
columnOrder: [...ResultDataProvider.INTERNAL_VALIDATION_COLUMNS],
|
|
827
|
+
rows,
|
|
828
|
+
};
|
|
829
|
+
} catch (error: any) {
|
|
830
|
+
logger.error(`Error during getMewpInternalValidationFlatResults: ${error.message}`);
|
|
505
831
|
return defaultPayload;
|
|
506
832
|
}
|
|
507
833
|
}
|
|
508
834
|
|
|
835
|
+
public async validateMewpExternalFiles(options: {
|
|
836
|
+
externalBugsFile?: MewpExternalFileRef | null;
|
|
837
|
+
externalL3L4File?: MewpExternalFileRef | null;
|
|
838
|
+
}): Promise<MewpExternalFilesValidationResponse> {
|
|
839
|
+
const response: MewpExternalFilesValidationResponse = { valid: true };
|
|
840
|
+
const validateOne = async (
|
|
841
|
+
file: MewpExternalFileRef | null | undefined,
|
|
842
|
+
tableType: 'bugs' | 'l3l4'
|
|
843
|
+
): Promise<MewpExternalTableValidationResult | undefined> => {
|
|
844
|
+
const sourceName = String(file?.name || file?.objectName || file?.text || file?.url || '').trim();
|
|
845
|
+
if (!sourceName) return undefined;
|
|
846
|
+
|
|
847
|
+
try {
|
|
848
|
+
const { rows, meta } = await this.mewpExternalTableUtils.loadExternalTableRowsWithMeta(
|
|
849
|
+
file,
|
|
850
|
+
tableType
|
|
851
|
+
);
|
|
852
|
+
return {
|
|
853
|
+
tableType,
|
|
854
|
+
sourceName: meta.sourceName,
|
|
855
|
+
valid: true,
|
|
856
|
+
headerRow: meta.headerRow,
|
|
857
|
+
matchedRequiredColumns: meta.matchedRequiredColumns,
|
|
858
|
+
totalRequiredColumns: meta.totalRequiredColumns,
|
|
859
|
+
missingRequiredColumns: [],
|
|
860
|
+
rowCount: rows.length,
|
|
861
|
+
message: 'File schema is valid',
|
|
862
|
+
};
|
|
863
|
+
} catch (error: any) {
|
|
864
|
+
if (error instanceof MewpExternalFileValidationError) {
|
|
865
|
+
return {
|
|
866
|
+
...error.details,
|
|
867
|
+
valid: false,
|
|
868
|
+
};
|
|
869
|
+
}
|
|
870
|
+
return {
|
|
871
|
+
tableType,
|
|
872
|
+
sourceName: sourceName || tableType,
|
|
873
|
+
valid: false,
|
|
874
|
+
headerRow: '',
|
|
875
|
+
matchedRequiredColumns: 0,
|
|
876
|
+
totalRequiredColumns: this.mewpExternalTableUtils.getRequiredColumnCount(tableType),
|
|
877
|
+
missingRequiredColumns: [],
|
|
878
|
+
rowCount: 0,
|
|
879
|
+
message: String(error?.message || error || 'Unknown validation error'),
|
|
880
|
+
};
|
|
881
|
+
}
|
|
882
|
+
};
|
|
883
|
+
|
|
884
|
+
response.bugs = await validateOne(options?.externalBugsFile, 'bugs');
|
|
885
|
+
response.l3l4 = await validateOne(options?.externalL3L4File, 'l3l4');
|
|
886
|
+
response.valid = [response.bugs, response.l3l4].filter(Boolean).every((item) => !!item?.valid);
|
|
887
|
+
return response;
|
|
888
|
+
}
|
|
889
|
+
|
|
509
890
|
/**
|
|
510
891
|
* Mapping each attachment to a proper URL for downloading it
|
|
511
892
|
* @param runResults Array of run results
|
|
@@ -549,33 +930,63 @@ export default class ResultDataProvider {
|
|
|
549
930
|
return `MEWP L2 Coverage - ${suffix}`;
|
|
550
931
|
}
|
|
551
932
|
|
|
933
|
+
private buildInternalValidationSheetName(planName: string, testPlanId: string): string {
|
|
934
|
+
const suffix = String(planName || '').trim() || `Plan ${testPlanId}`;
|
|
935
|
+
return `MEWP Internal Validation - ${suffix}`;
|
|
936
|
+
}
|
|
937
|
+
|
|
552
938
|
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) : '';
|
|
939
|
+
requirement: Pick<MewpL2RequirementFamily, 'requirementId' | 'title' | 'subSystem' | 'responsibility'>,
|
|
940
|
+
runStatus: MewpRunStatus,
|
|
941
|
+
bug: MewpCoverageBugCell,
|
|
942
|
+
linkedL3L4: MewpCoverageL3L4Cell
|
|
943
|
+
): MewpCoverageRow {
|
|
944
|
+
const l2ReqId = this.formatMewpCustomerId(requirement.requirementId);
|
|
945
|
+
const l2ReqTitle = this.toMewpComparableText(requirement.title);
|
|
946
|
+
const l2SubSystem = this.toMewpComparableText(requirement.subSystem);
|
|
566
947
|
|
|
567
948
|
return {
|
|
568
|
-
'
|
|
569
|
-
'
|
|
570
|
-
'
|
|
571
|
-
'
|
|
572
|
-
'
|
|
573
|
-
'
|
|
574
|
-
'
|
|
575
|
-
'
|
|
949
|
+
'L2 REQ ID': l2ReqId,
|
|
950
|
+
'L2 REQ Title': l2ReqTitle,
|
|
951
|
+
'L2 SubSystem': l2SubSystem,
|
|
952
|
+
'L2 Run Status': runStatus,
|
|
953
|
+
'Bug ID': Number.isFinite(Number(bug?.id)) && Number(bug?.id) > 0 ? Number(bug?.id) : '',
|
|
954
|
+
'Bug Title': String(bug?.title || '').trim(),
|
|
955
|
+
'Bug Responsibility': String(bug?.responsibility || '').trim(),
|
|
956
|
+
'L3 REQ ID': String(linkedL3L4?.l3Id || '').trim(),
|
|
957
|
+
'L3 REQ Title': String(linkedL3L4?.l3Title || '').trim(),
|
|
958
|
+
'L4 REQ ID': String(linkedL3L4?.l4Id || '').trim(),
|
|
959
|
+
'L4 REQ Title': String(linkedL3L4?.l4Title || '').trim(),
|
|
576
960
|
};
|
|
577
961
|
}
|
|
578
962
|
|
|
963
|
+
private createEmptyMewpCoverageBugCell(): MewpCoverageBugCell {
|
|
964
|
+
return { id: '' as '', title: '', responsibility: '' };
|
|
965
|
+
}
|
|
966
|
+
|
|
967
|
+
private createEmptyMewpCoverageL3L4Cell(): MewpCoverageL3L4Cell {
|
|
968
|
+
return { l3Id: '', l3Title: '', l4Id: '', l4Title: '' };
|
|
969
|
+
}
|
|
970
|
+
|
|
971
|
+
private buildMewpCoverageL3L4Rows(links: MewpL3L4Link[]): MewpCoverageL3L4Cell[] {
|
|
972
|
+
const sorted = [...(links || [])].sort((a, b) => {
|
|
973
|
+
if (a.level !== b.level) return a.level === 'L3' ? -1 : 1;
|
|
974
|
+
return String(a.id || '').localeCompare(String(b.id || ''));
|
|
975
|
+
});
|
|
976
|
+
|
|
977
|
+
const rows: MewpCoverageL3L4Cell[] = [];
|
|
978
|
+
for (const item of sorted) {
|
|
979
|
+
const isL3 = item.level === 'L3';
|
|
980
|
+
rows.push({
|
|
981
|
+
l3Id: isL3 ? String(item?.id || '').trim() : '',
|
|
982
|
+
l3Title: isL3 ? String(item?.title || '').trim() : '',
|
|
983
|
+
l4Id: isL3 ? '' : String(item?.id || '').trim(),
|
|
984
|
+
l4Title: isL3 ? '' : String(item?.title || '').trim(),
|
|
985
|
+
});
|
|
986
|
+
}
|
|
987
|
+
return rows;
|
|
988
|
+
}
|
|
989
|
+
|
|
579
990
|
private formatMewpCustomerId(rawValue: string): string {
|
|
580
991
|
const normalized = this.normalizeMewpRequirementCode(this.toMewpComparableText(rawValue));
|
|
581
992
|
if (normalized) return normalized;
|
|
@@ -586,51 +997,109 @@ export default class ResultDataProvider {
|
|
|
586
997
|
}
|
|
587
998
|
|
|
588
999
|
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 }>>,
|
|
1000
|
+
requirements: MewpL2RequirementFamily[],
|
|
1001
|
+
requirementIndex: MewpRequirementIndex,
|
|
596
1002
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>,
|
|
597
|
-
|
|
598
|
-
|
|
599
|
-
|
|
1003
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase,
|
|
1004
|
+
l3l4ByBaseKey: Map<string, MewpL3L4Link[]>,
|
|
1005
|
+
externalBugsByTestCase: Map<number, MewpBugLink[]>
|
|
1006
|
+
): MewpCoverageRow[] {
|
|
1007
|
+
const rows: MewpCoverageRow[] = [];
|
|
1008
|
+
const linkedByRequirement = this.invertBaseRequirementLinks(linkedRequirementsByTestCase);
|
|
600
1009
|
for (const requirement of requirements) {
|
|
601
|
-
const key = this.toRequirementKey(requirement.requirementId);
|
|
1010
|
+
const key = String(requirement?.baseKey || this.toRequirementKey(requirement.requirementId) || '').trim();
|
|
602
1011
|
const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
|
|
603
1012
|
(id) => Number.isFinite(id) && Number(id) > 0
|
|
604
1013
|
);
|
|
1014
|
+
const linkedByTestCase = key ? Array.from(linkedByRequirement.get(key) || []) : [];
|
|
605
1015
|
const observedTestCaseIds = key
|
|
606
1016
|
? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
|
|
607
1017
|
: [];
|
|
608
1018
|
|
|
609
|
-
const testCaseIds = Array.from(
|
|
610
|
-
(
|
|
611
|
-
);
|
|
1019
|
+
const testCaseIds = Array.from(
|
|
1020
|
+
new Set<number>([...linkedTestCaseIds, ...linkedByTestCase, ...observedTestCaseIds])
|
|
1021
|
+
).sort((a, b) => a - b);
|
|
612
1022
|
|
|
613
|
-
|
|
614
|
-
|
|
615
|
-
|
|
616
|
-
|
|
617
|
-
failed: 0,
|
|
618
|
-
notRun: 0,
|
|
619
|
-
})
|
|
620
|
-
);
|
|
621
|
-
continue;
|
|
622
|
-
}
|
|
1023
|
+
let totalPassed = 0;
|
|
1024
|
+
let totalFailed = 0;
|
|
1025
|
+
let totalNotRun = 0;
|
|
1026
|
+
const aggregatedBugs = new Map<number, MewpBugLink>();
|
|
623
1027
|
|
|
624
1028
|
for (const testCaseId of testCaseIds) {
|
|
625
1029
|
const summary = key
|
|
626
1030
|
? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
|
|
627
1031
|
: { passed: 0, failed: 0, notRun: 0 };
|
|
1032
|
+
totalPassed += summary.passed;
|
|
1033
|
+
totalFailed += summary.failed;
|
|
1034
|
+
totalNotRun += summary.notRun;
|
|
1035
|
+
|
|
1036
|
+
if (summary.failed > 0) {
|
|
1037
|
+
const externalBugs = externalBugsByTestCase.get(testCaseId) || [];
|
|
1038
|
+
for (const bug of externalBugs) {
|
|
1039
|
+
const bugBaseKey = String(bug?.requirementBaseKey || '').trim();
|
|
1040
|
+
if (bugBaseKey && bugBaseKey !== key) continue;
|
|
1041
|
+
const bugId = Number(bug?.id || 0);
|
|
1042
|
+
if (!Number.isFinite(bugId) || bugId <= 0) continue;
|
|
1043
|
+
aggregatedBugs.set(bugId, {
|
|
1044
|
+
...bug,
|
|
1045
|
+
responsibility: this.resolveCoverageBugResponsibility(
|
|
1046
|
+
String(bug?.responsibility || ''),
|
|
1047
|
+
requirement
|
|
1048
|
+
),
|
|
1049
|
+
});
|
|
1050
|
+
}
|
|
1051
|
+
}
|
|
1052
|
+
}
|
|
1053
|
+
|
|
1054
|
+
const runStatus = this.resolveMewpL2RunStatus({
|
|
1055
|
+
passed: totalPassed,
|
|
1056
|
+
failed: totalFailed,
|
|
1057
|
+
notRun: totalNotRun,
|
|
1058
|
+
hasAnyTestCase: testCaseIds.length > 0,
|
|
1059
|
+
});
|
|
1060
|
+
|
|
1061
|
+
const bugsForRows =
|
|
1062
|
+
runStatus === 'Fail'
|
|
1063
|
+
? Array.from(aggregatedBugs.values()).sort((a, b) => a.id - b.id)
|
|
1064
|
+
: [];
|
|
1065
|
+
const l3l4ForRows = [...(l3l4ByBaseKey.get(key) || [])];
|
|
1066
|
+
|
|
1067
|
+
const bugRows: MewpCoverageBugCell[] =
|
|
1068
|
+
bugsForRows.length > 0
|
|
1069
|
+
? bugsForRows
|
|
1070
|
+
: [];
|
|
1071
|
+
const l3l4Rows: MewpCoverageL3L4Cell[] = this.buildMewpCoverageL3L4Rows(l3l4ForRows);
|
|
1072
|
+
|
|
1073
|
+
if (bugRows.length === 0 && l3l4Rows.length === 0) {
|
|
628
1074
|
rows.push(
|
|
629
1075
|
this.createMewpCoverageRow(
|
|
630
1076
|
requirement,
|
|
631
|
-
|
|
632
|
-
|
|
633
|
-
|
|
1077
|
+
runStatus,
|
|
1078
|
+
this.createEmptyMewpCoverageBugCell(),
|
|
1079
|
+
this.createEmptyMewpCoverageL3L4Cell()
|
|
1080
|
+
)
|
|
1081
|
+
);
|
|
1082
|
+
continue;
|
|
1083
|
+
}
|
|
1084
|
+
|
|
1085
|
+
for (const bug of bugRows) {
|
|
1086
|
+
rows.push(
|
|
1087
|
+
this.createMewpCoverageRow(
|
|
1088
|
+
requirement,
|
|
1089
|
+
runStatus,
|
|
1090
|
+
bug,
|
|
1091
|
+
this.createEmptyMewpCoverageL3L4Cell()
|
|
1092
|
+
)
|
|
1093
|
+
);
|
|
1094
|
+
}
|
|
1095
|
+
|
|
1096
|
+
for (const linkedL3L4 of l3l4Rows) {
|
|
1097
|
+
rows.push(
|
|
1098
|
+
this.createMewpCoverageRow(
|
|
1099
|
+
requirement,
|
|
1100
|
+
runStatus,
|
|
1101
|
+
this.createEmptyMewpCoverageBugCell(),
|
|
1102
|
+
linkedL3L4
|
|
634
1103
|
)
|
|
635
1104
|
);
|
|
636
1105
|
}
|
|
@@ -639,6 +1108,262 @@ export default class ResultDataProvider {
|
|
|
639
1108
|
return rows;
|
|
640
1109
|
}
|
|
641
1110
|
|
|
1111
|
+
private resolveCoverageBugResponsibility(
|
|
1112
|
+
rawResponsibility: string,
|
|
1113
|
+
requirement: Pick<MewpL2RequirementFamily, 'responsibility'>
|
|
1114
|
+
): string {
|
|
1115
|
+
const direct = String(rawResponsibility || '').trim();
|
|
1116
|
+
if (direct && direct.toLowerCase() !== 'unknown') return direct;
|
|
1117
|
+
|
|
1118
|
+
const requirementResponsibility = String(requirement?.responsibility || '')
|
|
1119
|
+
.trim()
|
|
1120
|
+
.toUpperCase();
|
|
1121
|
+
if (requirementResponsibility === 'ESUK') return 'ESUK';
|
|
1122
|
+
if (requirementResponsibility === 'IL' || requirementResponsibility === 'ELISRA') return 'Elisra';
|
|
1123
|
+
|
|
1124
|
+
return direct || 'Unknown';
|
|
1125
|
+
}
|
|
1126
|
+
|
|
1127
|
+
private resolveMewpL2RunStatus(input: {
|
|
1128
|
+
passed: number;
|
|
1129
|
+
failed: number;
|
|
1130
|
+
notRun: number;
|
|
1131
|
+
hasAnyTestCase: boolean;
|
|
1132
|
+
}): MewpRunStatus {
|
|
1133
|
+
if ((input?.failed || 0) > 0) return 'Fail';
|
|
1134
|
+
if ((input?.notRun || 0) > 0) return 'Not Run';
|
|
1135
|
+
if ((input?.passed || 0) > 0) return 'Pass';
|
|
1136
|
+
return input?.hasAnyTestCase ? 'Not Run' : 'Not Run';
|
|
1137
|
+
}
|
|
1138
|
+
|
|
1139
|
+
private async fetchMewpScopedTestData(
|
|
1140
|
+
testPlanId: string,
|
|
1141
|
+
projectName: string,
|
|
1142
|
+
selectedSuiteIds: number[] | undefined,
|
|
1143
|
+
useRelFallback: boolean
|
|
1144
|
+
): Promise<any[]> {
|
|
1145
|
+
if (!useRelFallback) {
|
|
1146
|
+
const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1147
|
+
return this.fetchTestData(suites, projectName, testPlanId, false);
|
|
1148
|
+
}
|
|
1149
|
+
|
|
1150
|
+
const selectedSuites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
|
|
1151
|
+
const selectedRel = this.resolveMaxRelNumberFromSuites(selectedSuites);
|
|
1152
|
+
if (selectedRel <= 0) {
|
|
1153
|
+
return this.fetchTestData(selectedSuites, projectName, testPlanId, false);
|
|
1154
|
+
}
|
|
1155
|
+
|
|
1156
|
+
const allSuites = await this.fetchTestSuites(testPlanId, projectName, undefined, true);
|
|
1157
|
+
const relScopedSuites = allSuites.filter((suite) => {
|
|
1158
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1159
|
+
return rel > 0 && rel <= selectedRel;
|
|
1160
|
+
});
|
|
1161
|
+
const suitesForFetch = relScopedSuites.length > 0 ? relScopedSuites : selectedSuites;
|
|
1162
|
+
const rawTestData = await this.fetchTestData(suitesForFetch, projectName, testPlanId, false);
|
|
1163
|
+
return this.reduceToLatestRelRunPerTestCase(rawTestData);
|
|
1164
|
+
}
|
|
1165
|
+
|
|
1166
|
+
private extractRelNumberFromSuite(suite: any): number {
|
|
1167
|
+
const candidates = [
|
|
1168
|
+
suite?.suiteName,
|
|
1169
|
+
suite?.parentSuiteName,
|
|
1170
|
+
suite?.suitePath,
|
|
1171
|
+
suite?.testGroupName,
|
|
1172
|
+
];
|
|
1173
|
+
const pattern = /(?:^|[^a-z0-9])rel\s*([0-9]+)/i;
|
|
1174
|
+
for (const item of candidates) {
|
|
1175
|
+
const match = pattern.exec(String(item || ''));
|
|
1176
|
+
if (!match) continue;
|
|
1177
|
+
const parsed = Number(match[1]);
|
|
1178
|
+
if (Number.isFinite(parsed) && parsed > 0) {
|
|
1179
|
+
return parsed;
|
|
1180
|
+
}
|
|
1181
|
+
}
|
|
1182
|
+
return 0;
|
|
1183
|
+
}
|
|
1184
|
+
|
|
1185
|
+
private resolveMaxRelNumberFromSuites(suites: any[]): number {
|
|
1186
|
+
let maxRel = 0;
|
|
1187
|
+
for (const suite of suites || []) {
|
|
1188
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1189
|
+
if (rel > maxRel) maxRel = rel;
|
|
1190
|
+
}
|
|
1191
|
+
return maxRel;
|
|
1192
|
+
}
|
|
1193
|
+
|
|
1194
|
+
private reduceToLatestRelRunPerTestCase(testData: any[]): any[] {
|
|
1195
|
+
type Candidate = {
|
|
1196
|
+
point: any;
|
|
1197
|
+
rel: number;
|
|
1198
|
+
runId: number;
|
|
1199
|
+
resultId: number;
|
|
1200
|
+
hasRun: boolean;
|
|
1201
|
+
};
|
|
1202
|
+
|
|
1203
|
+
const candidatesByTestCase = new Map<number, Candidate[]>();
|
|
1204
|
+
const testCaseDefinitionById = new Map<number, any>();
|
|
1205
|
+
|
|
1206
|
+
for (const suite of testData || []) {
|
|
1207
|
+
const rel = this.extractRelNumberFromSuite(suite);
|
|
1208
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1209
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1210
|
+
|
|
1211
|
+
for (const testCase of testCasesItems) {
|
|
1212
|
+
const testCaseId = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1213
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1214
|
+
if (!testCaseDefinitionById.has(testCaseId)) {
|
|
1215
|
+
testCaseDefinitionById.set(testCaseId, testCase);
|
|
1216
|
+
}
|
|
1217
|
+
}
|
|
1218
|
+
|
|
1219
|
+
for (const point of testPointsItems) {
|
|
1220
|
+
const testCaseId = Number(point?.testCaseId || point?.testCase?.id || 0);
|
|
1221
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1222
|
+
|
|
1223
|
+
const runId = Number(point?.lastRunId || 0);
|
|
1224
|
+
const resultId = Number(point?.lastResultId || 0);
|
|
1225
|
+
const hasRun = runId > 0 && resultId > 0;
|
|
1226
|
+
if (!candidatesByTestCase.has(testCaseId)) {
|
|
1227
|
+
candidatesByTestCase.set(testCaseId, []);
|
|
1228
|
+
}
|
|
1229
|
+
candidatesByTestCase.get(testCaseId)!.push({
|
|
1230
|
+
point,
|
|
1231
|
+
rel,
|
|
1232
|
+
runId,
|
|
1233
|
+
resultId,
|
|
1234
|
+
hasRun,
|
|
1235
|
+
});
|
|
1236
|
+
}
|
|
1237
|
+
}
|
|
1238
|
+
|
|
1239
|
+
const selectedPoints: any[] = [];
|
|
1240
|
+
const selectedTestCaseIds = new Set<number>();
|
|
1241
|
+
for (const [testCaseId, candidates] of candidatesByTestCase.entries()) {
|
|
1242
|
+
const sorted = [...candidates].sort((a, b) => {
|
|
1243
|
+
if (a.hasRun !== b.hasRun) return a.hasRun ? -1 : 1;
|
|
1244
|
+
if (a.rel !== b.rel) return b.rel - a.rel;
|
|
1245
|
+
if (a.runId !== b.runId) return b.runId - a.runId;
|
|
1246
|
+
return b.resultId - a.resultId;
|
|
1247
|
+
});
|
|
1248
|
+
const chosen = sorted[0];
|
|
1249
|
+
if (!chosen?.point) continue;
|
|
1250
|
+
selectedPoints.push(chosen.point);
|
|
1251
|
+
selectedTestCaseIds.add(testCaseId);
|
|
1252
|
+
}
|
|
1253
|
+
|
|
1254
|
+
const selectedTestCases: any[] = [];
|
|
1255
|
+
for (const testCaseId of selectedTestCaseIds) {
|
|
1256
|
+
const definition = testCaseDefinitionById.get(testCaseId);
|
|
1257
|
+
if (definition) {
|
|
1258
|
+
selectedTestCases.push(definition);
|
|
1259
|
+
}
|
|
1260
|
+
}
|
|
1261
|
+
|
|
1262
|
+
return [
|
|
1263
|
+
{
|
|
1264
|
+
testSuiteId: 'MEWP_REL_SCOPED',
|
|
1265
|
+
suiteId: 'MEWP_REL_SCOPED',
|
|
1266
|
+
suiteName: 'MEWP Rel Scoped',
|
|
1267
|
+
parentSuiteId: '',
|
|
1268
|
+
parentSuiteName: '',
|
|
1269
|
+
suitePath: 'MEWP Rel Scoped',
|
|
1270
|
+
testGroupName: 'MEWP Rel Scoped',
|
|
1271
|
+
testPointsItems: selectedPoints,
|
|
1272
|
+
testCasesItems: selectedTestCases,
|
|
1273
|
+
},
|
|
1274
|
+
];
|
|
1275
|
+
}
|
|
1276
|
+
|
|
1277
|
+
private async loadExternalBugsByTestCase(
|
|
1278
|
+
externalBugsFile: MewpExternalFileRef | null | undefined
|
|
1279
|
+
): Promise<Map<number, MewpBugLink[]>> {
|
|
1280
|
+
return this.mewpExternalIngestionUtils.loadExternalBugsByTestCase(externalBugsFile, {
|
|
1281
|
+
toComparableText: (value) => this.toMewpComparableText(value),
|
|
1282
|
+
toRequirementKey: (value) => this.toRequirementKey(value),
|
|
1283
|
+
resolveBugResponsibility: (fields) => this.resolveBugResponsibility(fields),
|
|
1284
|
+
isExternalStateInScope: (value, itemType) => this.isExternalStateInScope(value, itemType),
|
|
1285
|
+
isExcludedL3L4BySapWbs: (value) => this.isExcludedL3L4BySapWbs(value),
|
|
1286
|
+
resolveRequirementSapWbsByBaseKey: () => '',
|
|
1287
|
+
});
|
|
1288
|
+
}
|
|
1289
|
+
|
|
1290
|
+
private async loadExternalL3L4ByBaseKey(
|
|
1291
|
+
externalL3L4File: MewpExternalFileRef | null | undefined,
|
|
1292
|
+
requirementSapWbsByBaseKey: Map<string, string> = new Map<string, string>()
|
|
1293
|
+
): Promise<Map<string, MewpL3L4Link[]>> {
|
|
1294
|
+
return this.mewpExternalIngestionUtils.loadExternalL3L4ByBaseKey(externalL3L4File, {
|
|
1295
|
+
toComparableText: (value) => this.toMewpComparableText(value),
|
|
1296
|
+
toRequirementKey: (value) => this.toRequirementKey(value),
|
|
1297
|
+
resolveBugResponsibility: (fields) => this.resolveBugResponsibility(fields),
|
|
1298
|
+
isExternalStateInScope: (value, itemType) => this.isExternalStateInScope(value, itemType),
|
|
1299
|
+
isExcludedL3L4BySapWbs: (value) => this.isExcludedL3L4BySapWbs(value),
|
|
1300
|
+
resolveRequirementSapWbsByBaseKey: (baseKey) => String(requirementSapWbsByBaseKey.get(baseKey) || ''),
|
|
1301
|
+
});
|
|
1302
|
+
}
|
|
1303
|
+
|
|
1304
|
+
private buildRequirementSapWbsByBaseKey(
|
|
1305
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'baseKey' | 'responsibility'>>
|
|
1306
|
+
): Map<string, string> {
|
|
1307
|
+
const out = new Map<string, string>();
|
|
1308
|
+
for (const requirement of requirements || []) {
|
|
1309
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1310
|
+
if (!baseKey) continue;
|
|
1311
|
+
|
|
1312
|
+
const normalized = this.resolveMewpResponsibility(this.toMewpComparableText(requirement?.responsibility));
|
|
1313
|
+
if (!normalized) continue;
|
|
1314
|
+
|
|
1315
|
+
const existing = out.get(baseKey) || '';
|
|
1316
|
+
// Keep ESUK as dominant if conflicting values are ever present across family items.
|
|
1317
|
+
if (existing === 'ESUK') continue;
|
|
1318
|
+
if (normalized === 'ESUK' || !existing) {
|
|
1319
|
+
out.set(baseKey, normalized);
|
|
1320
|
+
}
|
|
1321
|
+
}
|
|
1322
|
+
return out;
|
|
1323
|
+
}
|
|
1324
|
+
|
|
1325
|
+
private isExternalStateInScope(value: string, itemType: 'bug' | 'requirement'): boolean {
|
|
1326
|
+
const normalized = String(value || '').trim().toLowerCase();
|
|
1327
|
+
if (!normalized) return true;
|
|
1328
|
+
|
|
1329
|
+
// TFS/ADO processes usually don't expose a literal "Open" state.
|
|
1330
|
+
// Keep non-terminal states, exclude terminal states.
|
|
1331
|
+
const terminalStates = new Set<string>([
|
|
1332
|
+
'resolved',
|
|
1333
|
+
'closed',
|
|
1334
|
+
'done',
|
|
1335
|
+
'completed',
|
|
1336
|
+
'complete',
|
|
1337
|
+
'removed',
|
|
1338
|
+
'rejected',
|
|
1339
|
+
'cancelled',
|
|
1340
|
+
'canceled',
|
|
1341
|
+
'obsolete',
|
|
1342
|
+
]);
|
|
1343
|
+
|
|
1344
|
+
if (terminalStates.has(normalized)) return false;
|
|
1345
|
+
|
|
1346
|
+
// Bug-specific terminal variants often used in custom processes.
|
|
1347
|
+
if (itemType === 'bug') {
|
|
1348
|
+
if (normalized === 'fixed') return false;
|
|
1349
|
+
}
|
|
1350
|
+
|
|
1351
|
+
return true;
|
|
1352
|
+
}
|
|
1353
|
+
|
|
1354
|
+
private invertBaseRequirementLinks(
|
|
1355
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase
|
|
1356
|
+
): Map<string, Set<number>> {
|
|
1357
|
+
const out = new Map<string, Set<number>>();
|
|
1358
|
+
for (const [testCaseId, links] of linkedRequirementsByTestCase.entries()) {
|
|
1359
|
+
for (const baseKey of links?.baseKeys || []) {
|
|
1360
|
+
if (!out.has(baseKey)) out.set(baseKey, new Set<number>());
|
|
1361
|
+
out.get(baseKey)!.add(testCaseId);
|
|
1362
|
+
}
|
|
1363
|
+
}
|
|
1364
|
+
return out;
|
|
1365
|
+
}
|
|
1366
|
+
|
|
642
1367
|
private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
|
|
643
1368
|
const map = new Map<number, string>();
|
|
644
1369
|
|
|
@@ -736,56 +1461,197 @@ export default class ResultDataProvider {
|
|
|
736
1461
|
return 'notRun';
|
|
737
1462
|
}
|
|
738
1463
|
|
|
739
|
-
private
|
|
740
|
-
|
|
741
|
-
status: 'passed' | 'failed' | 'notRun',
|
|
1464
|
+
private accumulateRequirementCountsFromActionResults(
|
|
1465
|
+
actionResults: any[],
|
|
742
1466
|
testCaseId: number,
|
|
743
1467
|
requirementKeys: Set<string>,
|
|
744
1468
|
counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
|
|
745
1469
|
observedTestCaseIdsByRequirement: Map<string, Set<number>>
|
|
746
1470
|
) {
|
|
747
1471
|
if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
|
|
1472
|
+
const sortedResults = Array.isArray(actionResults) ? actionResults : [];
|
|
1473
|
+
let previousRequirementStepIndex = -1;
|
|
1474
|
+
|
|
1475
|
+
for (let i = 0; i < sortedResults.length; i++) {
|
|
1476
|
+
const actionResult = sortedResults[i];
|
|
1477
|
+
if (actionResult?.isSharedStepTitle) continue;
|
|
1478
|
+
const requirementCodes = this.extractRequirementCodesFromText(actionResult?.expected || '');
|
|
1479
|
+
if (requirementCodes.size === 0) continue;
|
|
1480
|
+
|
|
1481
|
+
const startIndex = previousRequirementStepIndex + 1;
|
|
1482
|
+
const status = this.resolveRequirementStatusForWindow(sortedResults, startIndex, i);
|
|
1483
|
+
previousRequirementStepIndex = i;
|
|
1484
|
+
|
|
1485
|
+
for (const code of requirementCodes) {
|
|
1486
|
+
if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
|
|
1487
|
+
if (!counters.has(code)) {
|
|
1488
|
+
counters.set(code, new Map<number, { passed: number; failed: number; notRun: number }>());
|
|
1489
|
+
}
|
|
1490
|
+
const perTestCaseCounters = counters.get(code)!;
|
|
1491
|
+
if (!perTestCaseCounters.has(testCaseId)) {
|
|
1492
|
+
perTestCaseCounters.set(testCaseId, { passed: 0, failed: 0, notRun: 0 });
|
|
1493
|
+
}
|
|
748
1494
|
|
|
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
|
-
}
|
|
1495
|
+
if (!observedTestCaseIdsByRequirement.has(code)) {
|
|
1496
|
+
observedTestCaseIdsByRequirement.set(code, new Set<number>());
|
|
1497
|
+
}
|
|
1498
|
+
observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
|
|
759
1499
|
|
|
760
|
-
|
|
761
|
-
|
|
1500
|
+
const counter = perTestCaseCounters.get(testCaseId)!;
|
|
1501
|
+
if (status === 'passed') counter.passed += 1;
|
|
1502
|
+
else if (status === 'failed') counter.failed += 1;
|
|
1503
|
+
else counter.notRun += 1;
|
|
762
1504
|
}
|
|
763
|
-
|
|
1505
|
+
}
|
|
1506
|
+
}
|
|
764
1507
|
|
|
765
|
-
|
|
766
|
-
|
|
767
|
-
|
|
768
|
-
|
|
1508
|
+
private resolveRequirementStatusForWindow(
|
|
1509
|
+
actionResults: any[],
|
|
1510
|
+
startIndex: number,
|
|
1511
|
+
endIndex: number
|
|
1512
|
+
): 'passed' | 'failed' | 'notRun' {
|
|
1513
|
+
let hasNotRun = false;
|
|
1514
|
+
for (let index = startIndex; index <= endIndex; index++) {
|
|
1515
|
+
const status = this.classifyRequirementStepOutcome(actionResults[index]?.outcome);
|
|
1516
|
+
if (status === 'failed') return 'failed';
|
|
1517
|
+
if (status === 'notRun') hasNotRun = true;
|
|
769
1518
|
}
|
|
1519
|
+
return hasNotRun ? 'notRun' : 'passed';
|
|
770
1520
|
}
|
|
771
1521
|
|
|
772
1522
|
private extractRequirementCodesFromText(text: string): Set<string> {
|
|
1523
|
+
return this.extractRequirementCodesFromExpectedText(text, false);
|
|
1524
|
+
}
|
|
1525
|
+
|
|
1526
|
+
private extractRequirementMentionsFromExpectedSteps(
|
|
1527
|
+
steps: TestSteps[],
|
|
1528
|
+
includeSuffix: boolean
|
|
1529
|
+
): Array<{ stepRef: string; codes: Set<string> }> {
|
|
1530
|
+
const out: Array<{ stepRef: string; codes: Set<string> }> = [];
|
|
1531
|
+
const allSteps = Array.isArray(steps) ? steps : [];
|
|
1532
|
+
for (let index = 0; index < allSteps.length; index += 1) {
|
|
1533
|
+
const step = allSteps[index];
|
|
1534
|
+
if (step?.isSharedStepTitle) continue;
|
|
1535
|
+
const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
|
|
1536
|
+
if (codes.size === 0) continue;
|
|
1537
|
+
out.push({
|
|
1538
|
+
stepRef: this.resolveValidationStepReference(step, index),
|
|
1539
|
+
codes,
|
|
1540
|
+
});
|
|
1541
|
+
}
|
|
1542
|
+
return out;
|
|
1543
|
+
}
|
|
1544
|
+
|
|
1545
|
+
private extractRequirementCodesFromExpectedSteps(steps: TestSteps[], includeSuffix: boolean): Set<string> {
|
|
1546
|
+
const out = new Set<string>();
|
|
1547
|
+
for (const step of Array.isArray(steps) ? steps : []) {
|
|
1548
|
+
if (step?.isSharedStepTitle) continue;
|
|
1549
|
+
const codes = this.extractRequirementCodesFromExpectedText(step?.expected || '', includeSuffix);
|
|
1550
|
+
codes.forEach((code) => out.add(code));
|
|
1551
|
+
}
|
|
1552
|
+
return out;
|
|
1553
|
+
}
|
|
1554
|
+
|
|
1555
|
+
private extractRequirementCodesFromExpectedText(text: string, includeSuffix: boolean): Set<string> {
|
|
773
1556
|
const out = new Set<string>();
|
|
774
1557
|
const source = this.normalizeRequirementStepText(text);
|
|
775
|
-
|
|
776
|
-
|
|
777
|
-
const
|
|
778
|
-
|
|
779
|
-
|
|
780
|
-
|
|
781
|
-
|
|
782
|
-
|
|
783
|
-
|
|
1558
|
+
if (!source) return out;
|
|
1559
|
+
|
|
1560
|
+
const tokens = source
|
|
1561
|
+
.split(';')
|
|
1562
|
+
.map((token) => String(token || '').trim())
|
|
1563
|
+
.filter((token) => token !== '');
|
|
1564
|
+
|
|
1565
|
+
for (const token of tokens) {
|
|
1566
|
+
const candidates = this.extractRequirementCandidatesFromToken(token);
|
|
1567
|
+
for (const candidate of candidates) {
|
|
1568
|
+
const expandedTokens = this.expandRequirementTokenByComma(candidate);
|
|
1569
|
+
for (const expandedToken of expandedTokens) {
|
|
1570
|
+
if (!expandedToken || /vvrm/i.test(expandedToken)) continue;
|
|
1571
|
+
const normalized = this.normalizeRequirementCodeToken(expandedToken, includeSuffix);
|
|
1572
|
+
if (normalized) {
|
|
1573
|
+
out.add(normalized);
|
|
1574
|
+
}
|
|
1575
|
+
}
|
|
784
1576
|
}
|
|
785
1577
|
}
|
|
1578
|
+
|
|
786
1579
|
return out;
|
|
787
1580
|
}
|
|
788
1581
|
|
|
1582
|
+
private extractRequirementCandidatesFromToken(token: string): string[] {
|
|
1583
|
+
const source = String(token || '');
|
|
1584
|
+
if (!source) return [];
|
|
1585
|
+
const out = new Set<string>();
|
|
1586
|
+
const collectCandidates = (input: string, rejectTailPattern: RegExp) => {
|
|
1587
|
+
for (const match of input.matchAll(/SR\d{4,}(?:-\d+(?:,\d+)*)?/gi)) {
|
|
1588
|
+
const matchedValue = String(match?.[0] || '')
|
|
1589
|
+
.trim()
|
|
1590
|
+
.toUpperCase();
|
|
1591
|
+
if (!matchedValue) continue;
|
|
1592
|
+
const endIndex = Number(match?.index || 0) + matchedValue.length;
|
|
1593
|
+
const tail = String(input.slice(endIndex) || '');
|
|
1594
|
+
if (rejectTailPattern.test(tail)) continue;
|
|
1595
|
+
out.add(matchedValue);
|
|
1596
|
+
}
|
|
1597
|
+
};
|
|
1598
|
+
|
|
1599
|
+
// Normal scan keeps punctuation context (" SR0817-V3.2 " -> reject via tail).
|
|
1600
|
+
collectCandidates(source, /^\s*(?:V\d|VVRM|-V\d)/i);
|
|
1601
|
+
|
|
1602
|
+
// Compact scan preserves legacy support for spaced SR letters/digits
|
|
1603
|
+
// such as "S R 0 0 0 1" and HTML-fragmented tokens.
|
|
1604
|
+
const compactSource = source.replace(/\s+/g, '');
|
|
1605
|
+
if (compactSource && compactSource !== source) {
|
|
1606
|
+
collectCandidates(compactSource, /^(?:V\d|VVRM|-V\d)/i);
|
|
1607
|
+
}
|
|
1608
|
+
|
|
1609
|
+
return [...out];
|
|
1610
|
+
}
|
|
1611
|
+
|
|
1612
|
+
private expandRequirementTokenByComma(token: string): string[] {
|
|
1613
|
+
const compact = String(token || '').trim().toUpperCase();
|
|
1614
|
+
if (!compact) return [];
|
|
1615
|
+
|
|
1616
|
+
const suffixBatchMatch = /^SR(\d{4,})-(\d+(?:,\d+)+)$/.exec(compact);
|
|
1617
|
+
if (suffixBatchMatch) {
|
|
1618
|
+
const base = String(suffixBatchMatch[1] || '').trim();
|
|
1619
|
+
const suffixes = String(suffixBatchMatch[2] || '')
|
|
1620
|
+
.split(',')
|
|
1621
|
+
.map((item) => String(item || '').trim())
|
|
1622
|
+
.filter((item) => /^\d+$/.test(item));
|
|
1623
|
+
return suffixes.map((suffix) => `SR${base}-${suffix}`);
|
|
1624
|
+
}
|
|
1625
|
+
|
|
1626
|
+
return compact
|
|
1627
|
+
.split(',')
|
|
1628
|
+
.map((part) => String(part || '').trim())
|
|
1629
|
+
.filter((part) => !!part);
|
|
1630
|
+
}
|
|
1631
|
+
|
|
1632
|
+
private normalizeRequirementCodeToken(token: string, includeSuffix: boolean): string {
|
|
1633
|
+
const compact = String(token || '')
|
|
1634
|
+
.trim()
|
|
1635
|
+
.replace(/[\u200B-\u200D\uFEFF]/g, '')
|
|
1636
|
+
.toUpperCase();
|
|
1637
|
+
if (!compact) return '';
|
|
1638
|
+
|
|
1639
|
+
const pattern = includeSuffix ? /^SR(\d{4,})(?:-(\d+))?$/ : /^SR(\d{4,})(?:-\d+)?$/;
|
|
1640
|
+
const match = pattern.exec(compact);
|
|
1641
|
+
if (!match) return '';
|
|
1642
|
+
|
|
1643
|
+
const baseDigits = String(match[1] || '').trim();
|
|
1644
|
+
if (!baseDigits) return '';
|
|
1645
|
+
|
|
1646
|
+
if (includeSuffix && match[2]) {
|
|
1647
|
+
const suffixDigits = String(match[2] || '').trim();
|
|
1648
|
+
if (!suffixDigits) return '';
|
|
1649
|
+
return `SR${baseDigits}-${suffixDigits}`;
|
|
1650
|
+
}
|
|
1651
|
+
|
|
1652
|
+
return `SR${baseDigits}`;
|
|
1653
|
+
}
|
|
1654
|
+
|
|
789
1655
|
private normalizeRequirementStepText(text: string): string {
|
|
790
1656
|
const raw = String(text || '');
|
|
791
1657
|
if (!raw) return '';
|
|
@@ -802,23 +1668,19 @@ export default class ResultDataProvider {
|
|
|
802
1668
|
.replace(/\s+/g, ' ');
|
|
803
1669
|
}
|
|
804
1670
|
|
|
1671
|
+
private resolveValidationStepReference(step: TestSteps, index: number): string {
|
|
1672
|
+
const fromPosition = String(step?.stepPosition || '').trim();
|
|
1673
|
+
if (fromPosition) return `Step ${fromPosition}`;
|
|
1674
|
+
const fromId = String(step?.stepId || '').trim();
|
|
1675
|
+
if (fromId) return `Step ${fromId}`;
|
|
1676
|
+
return `Step ${index + 1}`;
|
|
1677
|
+
}
|
|
1678
|
+
|
|
805
1679
|
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
|
-
> {
|
|
1680
|
+
return this.normalizeMewpRequirementCode(requirementId);
|
|
1681
|
+
}
|
|
1682
|
+
|
|
1683
|
+
private async fetchMewpL2Requirements(projectName: string): Promise<MewpL2RequirementWorkItem[]> {
|
|
822
1684
|
const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
|
|
823
1685
|
if (workItemTypeNames.length === 0) {
|
|
824
1686
|
return [];
|
|
@@ -827,17 +1689,36 @@ export default class ResultDataProvider {
|
|
|
827
1689
|
const quotedTypeNames = workItemTypeNames
|
|
828
1690
|
.map((name) => `'${String(name).replace(/'/g, "''")}'`)
|
|
829
1691
|
.join(', ');
|
|
830
|
-
const
|
|
831
|
-
|
|
832
|
-
|
|
833
|
-
|
|
834
|
-
|
|
835
|
-
|
|
836
|
-
|
|
837
|
-
|
|
838
|
-
|
|
839
|
-
.
|
|
840
|
-
.
|
|
1692
|
+
const queryRequirementIds = async (l2AreaPath: string | null): Promise<number[]> => {
|
|
1693
|
+
const escapedAreaPath = l2AreaPath ? String(l2AreaPath).replace(/'/g, "''") : '';
|
|
1694
|
+
const areaFilter = escapedAreaPath ? `\n AND [System.AreaPath] UNDER '${escapedAreaPath}'` : '';
|
|
1695
|
+
const wiql = `SELECT [System.Id]
|
|
1696
|
+
FROM WorkItems
|
|
1697
|
+
WHERE [System.TeamProject] = @project
|
|
1698
|
+
AND [System.WorkItemType] IN (${quotedTypeNames})${areaFilter}
|
|
1699
|
+
ORDER BY [System.Id]`;
|
|
1700
|
+
const wiqlUrl = `${this.orgUrl}${projectName}/_apis/wit/wiql?api-version=7.1-preview.2`;
|
|
1701
|
+
const wiqlResponse = await TFSServices.postRequest(wiqlUrl, this.token, 'Post', { query: wiql }, null);
|
|
1702
|
+
const workItemRefs = Array.isArray(wiqlResponse?.data?.workItems) ? wiqlResponse.data.workItems : [];
|
|
1703
|
+
return workItemRefs
|
|
1704
|
+
.map((item: any) => Number(item?.id))
|
|
1705
|
+
.filter((id: number) => Number.isFinite(id));
|
|
1706
|
+
};
|
|
1707
|
+
|
|
1708
|
+
const defaultL2AreaPath = `${String(projectName || '').trim()}\\Customer Requirements\\Level 2`;
|
|
1709
|
+
let requirementIds: number[] = [];
|
|
1710
|
+
try {
|
|
1711
|
+
requirementIds = await queryRequirementIds(defaultL2AreaPath);
|
|
1712
|
+
} catch (error: any) {
|
|
1713
|
+
logger.warn(
|
|
1714
|
+
`Could not apply MEWP L2 WIQL area-path optimization. Falling back to full requirement scope: ${
|
|
1715
|
+
error?.message || error
|
|
1716
|
+
}`
|
|
1717
|
+
);
|
|
1718
|
+
}
|
|
1719
|
+
if (requirementIds.length === 0) {
|
|
1720
|
+
requirementIds = await queryRequirementIds(null);
|
|
1721
|
+
}
|
|
841
1722
|
|
|
842
1723
|
if (requirementIds.length === 0) {
|
|
843
1724
|
return [];
|
|
@@ -846,16 +1727,294 @@ ORDER BY [System.Id]`;
|
|
|
846
1727
|
const workItems = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
|
|
847
1728
|
const requirements = workItems.map((wi: any) => {
|
|
848
1729
|
const fields = wi?.fields || {};
|
|
1730
|
+
const requirementId = this.extractMewpRequirementIdentifier(fields, Number(wi?.id || 0));
|
|
1731
|
+
const areaPath = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
849
1732
|
return {
|
|
850
1733
|
workItemId: Number(wi?.id || 0),
|
|
851
|
-
requirementId
|
|
1734
|
+
requirementId,
|
|
1735
|
+
baseKey: this.toRequirementKey(requirementId),
|
|
852
1736
|
title: this.toMewpComparableText(fields?.['System.Title'] || wi?.title),
|
|
1737
|
+
subSystem: this.deriveMewpSubSystem(fields),
|
|
853
1738
|
responsibility: this.deriveMewpResponsibility(fields),
|
|
854
1739
|
linkedTestCaseIds: this.extractLinkedTestCaseIdsFromRequirement(wi?.relations || []),
|
|
1740
|
+
relatedWorkItemIds: this.extractLinkedWorkItemIdsFromRelations(wi?.relations || []),
|
|
1741
|
+
areaPath,
|
|
855
1742
|
};
|
|
856
1743
|
});
|
|
857
1744
|
|
|
858
|
-
return requirements
|
|
1745
|
+
return requirements
|
|
1746
|
+
.filter((item) => {
|
|
1747
|
+
if (!item.baseKey) return false;
|
|
1748
|
+
if (!item.areaPath) return true;
|
|
1749
|
+
return this.isMewpL2AreaPath(item.areaPath);
|
|
1750
|
+
})
|
|
1751
|
+
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
1752
|
+
}
|
|
1753
|
+
|
|
1754
|
+
private isMewpL2AreaPath(areaPath: string): boolean {
|
|
1755
|
+
const normalized = String(areaPath || '')
|
|
1756
|
+
.trim()
|
|
1757
|
+
.toLowerCase()
|
|
1758
|
+
.replace(/\//g, '\\');
|
|
1759
|
+
if (!normalized) return false;
|
|
1760
|
+
return normalized.includes('\\customer requirements\\level 2');
|
|
1761
|
+
}
|
|
1762
|
+
|
|
1763
|
+
private collapseMewpRequirementFamilies(
|
|
1764
|
+
requirements: MewpL2RequirementWorkItem[],
|
|
1765
|
+
scopedRequirementKeys?: Set<string>
|
|
1766
|
+
): MewpL2RequirementFamily[] {
|
|
1767
|
+
const families = new Map<
|
|
1768
|
+
string,
|
|
1769
|
+
{
|
|
1770
|
+
representative: MewpL2RequirementWorkItem;
|
|
1771
|
+
score: number;
|
|
1772
|
+
linkedTestCaseIds: Set<number>;
|
|
1773
|
+
}
|
|
1774
|
+
>();
|
|
1775
|
+
|
|
1776
|
+
const calcScore = (item: MewpL2RequirementWorkItem) => {
|
|
1777
|
+
const requirementId = String(item?.requirementId || '').trim();
|
|
1778
|
+
const areaPath = String(item?.areaPath || '')
|
|
1779
|
+
.trim()
|
|
1780
|
+
.toLowerCase();
|
|
1781
|
+
let score = 0;
|
|
1782
|
+
if (/^SR\d+$/i.test(requirementId)) score += 6;
|
|
1783
|
+
if (areaPath.includes('\\customer requirements\\level 2')) score += 3;
|
|
1784
|
+
if (!areaPath.includes('\\mop')) score += 2;
|
|
1785
|
+
if (String(item?.title || '').trim()) score += 1;
|
|
1786
|
+
if (String(item?.subSystem || '').trim()) score += 1;
|
|
1787
|
+
if (String(item?.responsibility || '').trim()) score += 1;
|
|
1788
|
+
return score;
|
|
1789
|
+
};
|
|
1790
|
+
|
|
1791
|
+
for (const requirement of requirements || []) {
|
|
1792
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1793
|
+
if (!baseKey) continue;
|
|
1794
|
+
if (scopedRequirementKeys?.size && !scopedRequirementKeys.has(baseKey)) continue;
|
|
1795
|
+
|
|
1796
|
+
if (!families.has(baseKey)) {
|
|
1797
|
+
families.set(baseKey, {
|
|
1798
|
+
representative: requirement,
|
|
1799
|
+
score: calcScore(requirement),
|
|
1800
|
+
linkedTestCaseIds: new Set<number>(),
|
|
1801
|
+
});
|
|
1802
|
+
}
|
|
1803
|
+
const family = families.get(baseKey)!;
|
|
1804
|
+
const score = calcScore(requirement);
|
|
1805
|
+
if (score > family.score) {
|
|
1806
|
+
family.representative = requirement;
|
|
1807
|
+
family.score = score;
|
|
1808
|
+
}
|
|
1809
|
+
for (const testCaseId of requirement?.linkedTestCaseIds || []) {
|
|
1810
|
+
if (Number.isFinite(testCaseId) && Number(testCaseId) > 0) {
|
|
1811
|
+
family.linkedTestCaseIds.add(Number(testCaseId));
|
|
1812
|
+
}
|
|
1813
|
+
}
|
|
1814
|
+
}
|
|
1815
|
+
|
|
1816
|
+
return [...families.entries()]
|
|
1817
|
+
.map(([baseKey, family]) => ({
|
|
1818
|
+
requirementId: String(family?.representative?.requirementId || baseKey),
|
|
1819
|
+
baseKey,
|
|
1820
|
+
title: String(family?.representative?.title || ''),
|
|
1821
|
+
subSystem: String(family?.representative?.subSystem || ''),
|
|
1822
|
+
responsibility: String(family?.representative?.responsibility || ''),
|
|
1823
|
+
linkedTestCaseIds: [...family.linkedTestCaseIds].sort((a, b) => a - b),
|
|
1824
|
+
}))
|
|
1825
|
+
.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
|
|
1826
|
+
}
|
|
1827
|
+
|
|
1828
|
+
private buildRequirementFamilyMap(
|
|
1829
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'requirementId' | 'baseKey'>>,
|
|
1830
|
+
scopedRequirementKeys?: Set<string>
|
|
1831
|
+
): Map<string, Set<string>> {
|
|
1832
|
+
const familyMap = new Map<string, Set<string>>();
|
|
1833
|
+
for (const requirement of requirements || []) {
|
|
1834
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1835
|
+
if (!baseKey) continue;
|
|
1836
|
+
if (scopedRequirementKeys?.size && !scopedRequirementKeys.has(baseKey)) continue;
|
|
1837
|
+
const fullCode = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
|
|
1838
|
+
if (!fullCode) continue;
|
|
1839
|
+
if (!familyMap.has(baseKey)) familyMap.set(baseKey, new Set<string>());
|
|
1840
|
+
familyMap.get(baseKey)!.add(fullCode);
|
|
1841
|
+
}
|
|
1842
|
+
return familyMap;
|
|
1843
|
+
}
|
|
1844
|
+
|
|
1845
|
+
private async buildLinkedRequirementsByTestCase(
|
|
1846
|
+
requirements: Array<
|
|
1847
|
+
Pick<MewpL2RequirementWorkItem, 'workItemId' | 'requirementId' | 'baseKey' | 'linkedTestCaseIds'>
|
|
1848
|
+
>,
|
|
1849
|
+
testData: any[],
|
|
1850
|
+
projectName: string
|
|
1851
|
+
): Promise<MewpLinkedRequirementsByTestCase> {
|
|
1852
|
+
const map: MewpLinkedRequirementsByTestCase = new Map();
|
|
1853
|
+
const ensure = (testCaseId: number) => {
|
|
1854
|
+
if (!map.has(testCaseId)) {
|
|
1855
|
+
map.set(testCaseId, {
|
|
1856
|
+
baseKeys: new Set<string>(),
|
|
1857
|
+
fullCodes: new Set<string>(),
|
|
1858
|
+
bugIds: new Set<number>(),
|
|
1859
|
+
});
|
|
1860
|
+
}
|
|
1861
|
+
return map.get(testCaseId)!;
|
|
1862
|
+
};
|
|
1863
|
+
|
|
1864
|
+
const requirementById = new Map<number, { baseKey: string; fullCode: string }>();
|
|
1865
|
+
for (const requirement of requirements || []) {
|
|
1866
|
+
const workItemId = Number(requirement?.workItemId || 0);
|
|
1867
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1868
|
+
const fullCode = this.normalizeMewpRequirementCodeWithSuffix(requirement?.requirementId || '');
|
|
1869
|
+
if (workItemId > 0 && baseKey && fullCode) {
|
|
1870
|
+
requirementById.set(workItemId, { baseKey, fullCode });
|
|
1871
|
+
}
|
|
1872
|
+
|
|
1873
|
+
for (const testCaseIdRaw of requirement?.linkedTestCaseIds || []) {
|
|
1874
|
+
const testCaseId = Number(testCaseIdRaw);
|
|
1875
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0 || !baseKey || !fullCode) continue;
|
|
1876
|
+
const entry = ensure(testCaseId);
|
|
1877
|
+
entry.baseKeys.add(baseKey);
|
|
1878
|
+
entry.fullCodes.add(fullCode);
|
|
1879
|
+
}
|
|
1880
|
+
}
|
|
1881
|
+
|
|
1882
|
+
const testCaseIds = new Set<number>();
|
|
1883
|
+
for (const suite of testData || []) {
|
|
1884
|
+
const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
|
|
1885
|
+
for (const testCase of testCasesItems) {
|
|
1886
|
+
const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id || 0);
|
|
1887
|
+
if (Number.isFinite(id) && id > 0) testCaseIds.add(id);
|
|
1888
|
+
}
|
|
1889
|
+
const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
|
|
1890
|
+
for (const testPoint of testPointsItems) {
|
|
1891
|
+
const id = Number(testPoint?.testCaseId || testPoint?.testCase?.id || 0);
|
|
1892
|
+
if (Number.isFinite(id) && id > 0) testCaseIds.add(id);
|
|
1893
|
+
}
|
|
1894
|
+
}
|
|
1895
|
+
|
|
1896
|
+
const relatedIdsByTestCase = new Map<number, Set<number>>();
|
|
1897
|
+
const allRelatedIds = new Set<number>();
|
|
1898
|
+
if (testCaseIds.size > 0) {
|
|
1899
|
+
const testCaseWorkItems = await this.fetchWorkItemsByIds(projectName, [...testCaseIds], true);
|
|
1900
|
+
for (const workItem of testCaseWorkItems || []) {
|
|
1901
|
+
const testCaseId = Number(workItem?.id || 0);
|
|
1902
|
+
if (!Number.isFinite(testCaseId) || testCaseId <= 0) continue;
|
|
1903
|
+
const relations = Array.isArray(workItem?.relations) ? workItem.relations : [];
|
|
1904
|
+
if (!relatedIdsByTestCase.has(testCaseId)) relatedIdsByTestCase.set(testCaseId, new Set<number>());
|
|
1905
|
+
for (const relation of relations) {
|
|
1906
|
+
const linkedWorkItemId = this.extractLinkedWorkItemIdFromRelation(relation);
|
|
1907
|
+
if (!linkedWorkItemId) continue;
|
|
1908
|
+
relatedIdsByTestCase.get(testCaseId)!.add(linkedWorkItemId);
|
|
1909
|
+
allRelatedIds.add(linkedWorkItemId);
|
|
1910
|
+
|
|
1911
|
+
if (this.isTestCaseToRequirementRelation(relation) && requirementById.has(linkedWorkItemId)) {
|
|
1912
|
+
const linkedRequirement = requirementById.get(linkedWorkItemId)!;
|
|
1913
|
+
const entry = ensure(testCaseId);
|
|
1914
|
+
entry.baseKeys.add(linkedRequirement.baseKey);
|
|
1915
|
+
entry.fullCodes.add(linkedRequirement.fullCode);
|
|
1916
|
+
}
|
|
1917
|
+
}
|
|
1918
|
+
}
|
|
1919
|
+
}
|
|
1920
|
+
|
|
1921
|
+
if (allRelatedIds.size > 0) {
|
|
1922
|
+
const relatedWorkItems = await this.fetchWorkItemsByIds(projectName, [...allRelatedIds], false);
|
|
1923
|
+
const typeById = new Map<number, string>();
|
|
1924
|
+
for (const workItem of relatedWorkItems || []) {
|
|
1925
|
+
const id = Number(workItem?.id || 0);
|
|
1926
|
+
if (!Number.isFinite(id) || id <= 0) continue;
|
|
1927
|
+
const type = String(workItem?.fields?.['System.WorkItemType'] || '')
|
|
1928
|
+
.trim()
|
|
1929
|
+
.toLowerCase();
|
|
1930
|
+
typeById.set(id, type);
|
|
1931
|
+
}
|
|
1932
|
+
|
|
1933
|
+
for (const [testCaseId, ids] of relatedIdsByTestCase.entries()) {
|
|
1934
|
+
const entry = ensure(testCaseId);
|
|
1935
|
+
for (const linkedId of ids) {
|
|
1936
|
+
const linkedType = typeById.get(linkedId) || '';
|
|
1937
|
+
if (linkedType === 'bug') {
|
|
1938
|
+
entry.bugIds.add(linkedId);
|
|
1939
|
+
}
|
|
1940
|
+
}
|
|
1941
|
+
}
|
|
1942
|
+
}
|
|
1943
|
+
|
|
1944
|
+
return map;
|
|
1945
|
+
}
|
|
1946
|
+
|
|
1947
|
+
private async resolveMewpRequirementScopeKeysFromQuery(
|
|
1948
|
+
linkedQueryRequest: any,
|
|
1949
|
+
requirements: Array<Pick<MewpL2RequirementWorkItem, 'workItemId' | 'baseKey'>>,
|
|
1950
|
+
linkedRequirementsByTestCase: MewpLinkedRequirementsByTestCase
|
|
1951
|
+
): Promise<Set<string> | undefined> {
|
|
1952
|
+
const mode = String(linkedQueryRequest?.linkedQueryMode || '')
|
|
1953
|
+
.trim()
|
|
1954
|
+
.toLowerCase();
|
|
1955
|
+
const wiqlHref = String(linkedQueryRequest?.testAssociatedQuery?.wiql?.href || '').trim();
|
|
1956
|
+
if (mode !== 'query' || !wiqlHref) return undefined;
|
|
1957
|
+
|
|
1958
|
+
try {
|
|
1959
|
+
const queryResult = await TFSServices.getItemContent(wiqlHref, this.token);
|
|
1960
|
+
const queryIds = new Set<number>();
|
|
1961
|
+
if (Array.isArray(queryResult?.workItems)) {
|
|
1962
|
+
for (const workItem of queryResult.workItems) {
|
|
1963
|
+
const id = Number(workItem?.id || 0);
|
|
1964
|
+
if (Number.isFinite(id) && id > 0) queryIds.add(id);
|
|
1965
|
+
}
|
|
1966
|
+
}
|
|
1967
|
+
if (Array.isArray(queryResult?.workItemRelations)) {
|
|
1968
|
+
for (const relation of queryResult.workItemRelations) {
|
|
1969
|
+
const sourceId = Number(relation?.source?.id || 0);
|
|
1970
|
+
const targetId = Number(relation?.target?.id || 0);
|
|
1971
|
+
if (Number.isFinite(sourceId) && sourceId > 0) queryIds.add(sourceId);
|
|
1972
|
+
if (Number.isFinite(targetId) && targetId > 0) queryIds.add(targetId);
|
|
1973
|
+
}
|
|
1974
|
+
}
|
|
1975
|
+
|
|
1976
|
+
if (queryIds.size === 0) return undefined;
|
|
1977
|
+
|
|
1978
|
+
const reqIdToBaseKey = new Map<number, string>();
|
|
1979
|
+
for (const requirement of requirements || []) {
|
|
1980
|
+
const id = Number(requirement?.workItemId || 0);
|
|
1981
|
+
const baseKey = String(requirement?.baseKey || '').trim();
|
|
1982
|
+
if (id > 0 && baseKey) reqIdToBaseKey.set(id, baseKey);
|
|
1983
|
+
}
|
|
1984
|
+
|
|
1985
|
+
const scopedKeys = new Set<string>();
|
|
1986
|
+
for (const queryId of queryIds) {
|
|
1987
|
+
if (reqIdToBaseKey.has(queryId)) {
|
|
1988
|
+
scopedKeys.add(reqIdToBaseKey.get(queryId)!);
|
|
1989
|
+
continue;
|
|
1990
|
+
}
|
|
1991
|
+
|
|
1992
|
+
const linked = linkedRequirementsByTestCase.get(queryId);
|
|
1993
|
+
if (!linked?.baseKeys?.size) continue;
|
|
1994
|
+
linked.baseKeys.forEach((baseKey) => scopedKeys.add(baseKey));
|
|
1995
|
+
}
|
|
1996
|
+
|
|
1997
|
+
return scopedKeys.size > 0 ? scopedKeys : undefined;
|
|
1998
|
+
} catch (error: any) {
|
|
1999
|
+
logger.warn(`Could not resolve MEWP query scope: ${error?.message || error}`);
|
|
2000
|
+
return undefined;
|
|
2001
|
+
}
|
|
2002
|
+
}
|
|
2003
|
+
|
|
2004
|
+
private isTestCaseToRequirementRelation(relation: any): boolean {
|
|
2005
|
+
const rel = String(relation?.rel || '')
|
|
2006
|
+
.trim()
|
|
2007
|
+
.toLowerCase();
|
|
2008
|
+
if (!rel) return false;
|
|
2009
|
+
return rel.includes('testedby-reverse') || (rel.includes('tests') && rel.includes('reverse'));
|
|
2010
|
+
}
|
|
2011
|
+
|
|
2012
|
+
private extractLinkedWorkItemIdFromRelation(relation: any): number {
|
|
2013
|
+
const url = String(relation?.url || '');
|
|
2014
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
2015
|
+
if (!match) return 0;
|
|
2016
|
+
const parsed = Number(match[1]);
|
|
2017
|
+
return Number.isFinite(parsed) ? parsed : 0;
|
|
859
2018
|
}
|
|
860
2019
|
|
|
861
2020
|
private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
|
|
@@ -919,6 +2078,18 @@ ORDER BY [System.Id]`;
|
|
|
919
2078
|
return [...out].sort((a, b) => a - b);
|
|
920
2079
|
}
|
|
921
2080
|
|
|
2081
|
+
private extractLinkedWorkItemIdsFromRelations(relations: any[]): number[] {
|
|
2082
|
+
const out = new Set<number>();
|
|
2083
|
+
for (const relation of Array.isArray(relations) ? relations : []) {
|
|
2084
|
+
const url = String(relation?.url || '');
|
|
2085
|
+
const match = /\/workItems\/(\d+)/i.exec(url);
|
|
2086
|
+
if (!match) continue;
|
|
2087
|
+
const id = Number(match[1]);
|
|
2088
|
+
if (Number.isFinite(id) && id > 0) out.add(id);
|
|
2089
|
+
}
|
|
2090
|
+
return [...out].sort((a, b) => a - b);
|
|
2091
|
+
}
|
|
2092
|
+
|
|
922
2093
|
private extractMewpRequirementIdentifier(fields: Record<string, any>, fallbackWorkItemId: number): string {
|
|
923
2094
|
const entries = Object.entries(fields || {});
|
|
924
2095
|
|
|
@@ -938,7 +2109,7 @@ ORDER BY [System.Id]`;
|
|
|
938
2109
|
|
|
939
2110
|
const valueAsString = this.toMewpComparableText(value);
|
|
940
2111
|
if (!valueAsString) continue;
|
|
941
|
-
const normalized = this.
|
|
2112
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
942
2113
|
if (normalized) return normalized;
|
|
943
2114
|
}
|
|
944
2115
|
|
|
@@ -950,13 +2121,13 @@ ORDER BY [System.Id]`;
|
|
|
950
2121
|
|
|
951
2122
|
const valueAsString = this.toMewpComparableText(value);
|
|
952
2123
|
if (!valueAsString) continue;
|
|
953
|
-
const normalized = this.
|
|
2124
|
+
const normalized = this.normalizeMewpRequirementCodeWithSuffix(valueAsString);
|
|
954
2125
|
if (normalized) return normalized;
|
|
955
2126
|
}
|
|
956
2127
|
|
|
957
2128
|
// Optional fallback from title only (avoid scanning all fields and accidental SR matches).
|
|
958
2129
|
const title = this.toMewpComparableText(fields?.['System.Title']);
|
|
959
|
-
const titleCode = this.
|
|
2130
|
+
const titleCode = this.normalizeMewpRequirementCodeWithSuffix(title);
|
|
960
2131
|
if (titleCode) return titleCode;
|
|
961
2132
|
|
|
962
2133
|
return fallbackWorkItemId ? `SR${fallbackWorkItemId}` : '';
|
|
@@ -988,17 +2159,69 @@ ORDER BY [System.Id]`;
|
|
|
988
2159
|
return '';
|
|
989
2160
|
}
|
|
990
2161
|
|
|
2162
|
+
private deriveMewpSubSystem(fields: Record<string, any>): string {
|
|
2163
|
+
const directCandidates = [
|
|
2164
|
+
fields?.['Custom.SubSystem'],
|
|
2165
|
+
fields?.['Custom.Subsystem'],
|
|
2166
|
+
fields?.['SubSystem'],
|
|
2167
|
+
fields?.['Subsystem'],
|
|
2168
|
+
fields?.['subSystem'],
|
|
2169
|
+
];
|
|
2170
|
+
for (const candidate of directCandidates) {
|
|
2171
|
+
const value = this.toMewpComparableText(candidate);
|
|
2172
|
+
if (value) return value;
|
|
2173
|
+
}
|
|
2174
|
+
|
|
2175
|
+
const keyHints = ['subsystem', 'sub system', 'sub_system'];
|
|
2176
|
+
for (const [key, value] of Object.entries(fields || {})) {
|
|
2177
|
+
const normalizedKey = String(key || '').toLowerCase();
|
|
2178
|
+
if (!keyHints.some((hint) => normalizedKey.includes(hint))) continue;
|
|
2179
|
+
const resolved = this.toMewpComparableText(value);
|
|
2180
|
+
if (resolved) return resolved;
|
|
2181
|
+
}
|
|
2182
|
+
|
|
2183
|
+
return '';
|
|
2184
|
+
}
|
|
2185
|
+
|
|
2186
|
+
private resolveBugResponsibility(fields: Record<string, any>): string {
|
|
2187
|
+
const sapWbsRaw = this.toMewpComparableText(fields?.['Custom.SAPWBS'] || fields?.['SAPWBS']);
|
|
2188
|
+
const fromSapWbs = this.resolveMewpResponsibility(sapWbsRaw);
|
|
2189
|
+
if (fromSapWbs === 'ESUK') return 'ESUK';
|
|
2190
|
+
if (fromSapWbs === 'IL') return 'Elisra';
|
|
2191
|
+
|
|
2192
|
+
const areaPathRaw = this.toMewpComparableText(fields?.['System.AreaPath']);
|
|
2193
|
+
const fromAreaPath = this.resolveMewpResponsibility(areaPathRaw);
|
|
2194
|
+
if (fromAreaPath === 'ESUK') return 'ESUK';
|
|
2195
|
+
if (fromAreaPath === 'IL') return 'Elisra';
|
|
2196
|
+
|
|
2197
|
+
return 'Unknown';
|
|
2198
|
+
}
|
|
2199
|
+
|
|
991
2200
|
private resolveMewpResponsibility(value: string): string {
|
|
992
|
-
const
|
|
993
|
-
|
|
994
|
-
|
|
995
|
-
|
|
2201
|
+
const raw = String(value || '').trim();
|
|
2202
|
+
if (!raw) return '';
|
|
2203
|
+
|
|
2204
|
+
const rawUpper = raw.toUpperCase();
|
|
2205
|
+
if (rawUpper === 'ESUK') return 'ESUK';
|
|
2206
|
+
if (rawUpper === 'IL') return 'IL';
|
|
2207
|
+
|
|
2208
|
+
const normalizedPath = raw
|
|
2209
|
+
.toLowerCase()
|
|
2210
|
+
.replace(/\//g, '\\')
|
|
2211
|
+
.replace(/\\+/g, '\\')
|
|
2212
|
+
.trim();
|
|
2213
|
+
|
|
2214
|
+
if (normalizedPath.endsWith('\\atp\\esuk') || normalizedPath === 'atp\\esuk') return 'ESUK';
|
|
2215
|
+
if (normalizedPath.endsWith('\\atp') || normalizedPath === 'atp') return 'IL';
|
|
996
2216
|
|
|
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
2217
|
return '';
|
|
1000
2218
|
}
|
|
1001
2219
|
|
|
2220
|
+
private isExcludedL3L4BySapWbs(value: string): boolean {
|
|
2221
|
+
const responsibility = this.resolveMewpResponsibility(this.toMewpComparableText(value));
|
|
2222
|
+
return responsibility === 'ESUK';
|
|
2223
|
+
}
|
|
2224
|
+
|
|
1002
2225
|
private normalizeMewpRequirementCode(value: string): string {
|
|
1003
2226
|
const text = String(value || '').trim();
|
|
1004
2227
|
if (!text) return '';
|
|
@@ -1007,6 +2230,16 @@ ORDER BY [System.Id]`;
|
|
|
1007
2230
|
return `SR${match[1]}`;
|
|
1008
2231
|
}
|
|
1009
2232
|
|
|
2233
|
+
private normalizeMewpRequirementCodeWithSuffix(value: string): string {
|
|
2234
|
+
const text = String(value || '').trim();
|
|
2235
|
+
if (!text) return '';
|
|
2236
|
+
const compact = text.replace(/\s+/g, '');
|
|
2237
|
+
const match = /^SR(\d+)(?:-(\d+))?$/i.exec(compact);
|
|
2238
|
+
if (!match) return '';
|
|
2239
|
+
if (match[2]) return `SR${match[1]}-${match[2]}`;
|
|
2240
|
+
return `SR${match[1]}`;
|
|
2241
|
+
}
|
|
2242
|
+
|
|
1010
2243
|
private toMewpComparableText(value: any): string {
|
|
1011
2244
|
if (value === null || value === undefined) return '';
|
|
1012
2245
|
if (typeof value === 'string' || typeof value === 'number' || typeof value === 'boolean') {
|