@elisra-devops/docgen-data-provider 1.73.0 → 1.74.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.
@@ -30,14 +30,14 @@ const pLimit = require('p-limit');
30
30
  */
31
31
  export default class ResultDataProvider {
32
32
  private static readonly MEWP_L2_COVERAGE_COLUMNS = [
33
- 'Customer Requirement',
34
- 'Requirement ID',
35
- 'Requirement Title',
36
- 'Responsibility',
37
- 'SAPWBS / Responsibility',
33
+ 'Customer ID',
34
+ 'Title (Customer name)',
35
+ 'Responsibility - SAPWBS (ESUK/IL)',
36
+ 'Test case id',
37
+ 'Test case title',
38
38
  'Number of passed steps',
39
39
  'Number of failed steps',
40
- 'Number of steps not run',
40
+ 'Number of not run tests',
41
41
  ];
42
42
 
43
43
  orgUrl: string = '';
@@ -393,7 +393,8 @@ export default class ResultDataProvider {
393
393
  public async getMewpL2CoverageFlatResults(
394
394
  testPlanId: string,
395
395
  projectName: string,
396
- selectedSuiteIds: number[] | undefined
396
+ selectedSuiteIds: number[] | undefined,
397
+ linkedQueryRequest?: any
397
398
  ) {
398
399
  const defaultPayload = {
399
400
  sheetName: `MEWP L2 Coverage - Plan ${testPlanId}`,
@@ -406,7 +407,7 @@ export default class ResultDataProvider {
406
407
  const suites = await this.fetchTestSuites(testPlanId, projectName, selectedSuiteIds, true);
407
408
  const testData = await this.fetchTestData(suites, projectName, testPlanId, false);
408
409
 
409
- const requirements = await this.fetchMewpL2Requirements(projectName);
410
+ const requirements = await this.fetchMewpL2Requirements(projectName, linkedQueryRequest);
410
411
  if (requirements.length === 0) {
411
412
  return {
412
413
  ...defaultPayload,
@@ -414,26 +415,28 @@ export default class ResultDataProvider {
414
415
  };
415
416
  }
416
417
 
417
- const requirementIndex = new Map<
418
- string,
419
- { passed: number; failed: number; notRun: number }
420
- >();
418
+ const requirementIndex = new Map<string, Map<number, { passed: number; failed: number; notRun: number }>>();
419
+ const observedTestCaseIdsByRequirement = new Map<string, Set<number>>();
421
420
  const requirementKeys = new Set<string>();
422
421
  requirements.forEach((requirement) => {
423
422
  const key = this.toRequirementKey(requirement.requirementId);
424
423
  if (!key) return;
425
424
  requirementKeys.add(key);
426
- if (!requirementIndex.has(key)) {
427
- requirementIndex.set(key, { passed: 0, failed: 0, notRun: 0 });
428
- }
429
425
  });
430
426
 
431
427
  const parsedDefinitionStepsByTestCase = new Map<number, TestSteps[]>();
432
428
  const testCaseStepsXmlMap = this.buildTestCaseStepsXmlMap(testData);
429
+ const testCaseTitleMap = this.buildMewpTestCaseTitleMap(testData);
433
430
 
434
431
  const runResults = await this.fetchAllResultDataTestReporter(testData, projectName, [], false, false);
435
432
  for (const runResult of runResults) {
436
- const testCaseId = Number(runResult?.testCaseId);
433
+ const testCaseId = this.extractMewpTestCaseId(runResult);
434
+ const runTestCaseTitle = this.toMewpComparableText(
435
+ runResult?.testCase?.name || runResult?.testCaseName || runResult?.testCaseTitle
436
+ );
437
+ if (Number.isFinite(testCaseId) && testCaseId > 0 && runTestCaseTitle && !testCaseTitleMap.has(testCaseId)) {
438
+ testCaseTitleMap.set(testCaseId, runTestCaseTitle);
439
+ }
437
440
  const actionResults = Array.isArray(runResult?.iteration?.actionResults)
438
441
  ? runResult.iteration.actionResults
439
442
  : [];
@@ -447,8 +450,10 @@ export default class ResultDataProvider {
447
450
  this.accumulateRequirementCountsFromStepText(
448
451
  `${String(actionResult?.action || '')} ${String(actionResult?.expected || '')}`,
449
452
  stepStatus,
453
+ testCaseId,
450
454
  requirementKeys,
451
- requirementIndex
455
+ requirementIndex,
456
+ observedTestCaseIdsByRequirement
452
457
  );
453
458
  }
454
459
  continue;
@@ -476,19 +481,20 @@ export default class ResultDataProvider {
476
481
  this.accumulateRequirementCountsFromStepText(
477
482
  `${String(step?.action || '')} ${String(step?.expected || '')}`,
478
483
  'notRun',
484
+ testCaseId,
479
485
  requirementKeys,
480
- requirementIndex
486
+ requirementIndex,
487
+ observedTestCaseIdsByRequirement
481
488
  );
482
489
  }
483
490
  }
484
491
 
485
- const rows: any[] = requirements.map((requirement) => {
486
- const key = this.toRequirementKey(requirement.requirementId);
487
- const summary = key && requirementIndex.has(key)
488
- ? requirementIndex.get(key)!
489
- : { passed: 0, failed: 0, notRun: 0 };
490
- return this.createMewpCoverageRow(requirement, summary);
491
- });
492
+ const rows = this.buildMewpCoverageRows(
493
+ requirements,
494
+ requirementIndex,
495
+ observedTestCaseIdsByRequirement,
496
+ testCaseTitleMap
497
+ );
492
498
 
493
499
  return {
494
500
  sheetName: this.buildMewpCoverageSheetName(planName, testPlanId),
@@ -550,23 +556,131 @@ export default class ResultDataProvider {
550
556
  title: string;
551
557
  responsibility: string;
552
558
  },
559
+ testCaseId: number | undefined,
560
+ testCaseTitle: string,
553
561
  stepSummary: { passed: number; failed: number; notRun: number }
554
562
  ) {
555
- const requirementId = String(requirement.requirementId || '').trim();
556
- const requirementTitle = String(requirement.title || '').trim();
557
- const customerRequirement = [requirementId, requirementTitle].filter(Boolean).join(' - ');
563
+ const customerId = String(requirement.requirementId || '').trim();
564
+ const customerTitle = String(requirement.title || '').trim();
558
565
  const responsibility = String(requirement.responsibility || '').trim();
566
+ const safeTestCaseId = Number.isFinite(testCaseId) && Number(testCaseId) > 0 ? Number(testCaseId) : '';
559
567
 
560
568
  return {
561
- 'Customer Requirement': customerRequirement || requirementId || requirementTitle,
562
- 'Requirement ID': requirementId,
563
- 'Requirement Title': requirementTitle,
564
- Responsibility: responsibility,
565
- 'SAPWBS / Responsibility': responsibility,
569
+ 'Customer ID': customerId,
570
+ 'Title (Customer name)': customerTitle,
571
+ 'Responsibility - SAPWBS (ESUK/IL)': responsibility,
572
+ 'Test case id': safeTestCaseId,
573
+ 'Test case title': String(testCaseTitle || '').trim(),
566
574
  'Number of passed steps': Number.isFinite(stepSummary?.passed) ? stepSummary.passed : 0,
567
575
  'Number of failed steps': Number.isFinite(stepSummary?.failed) ? stepSummary.failed : 0,
568
- 'Number of steps not run': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
576
+ 'Number of not run tests': Number.isFinite(stepSummary?.notRun) ? stepSummary.notRun : 0,
577
+ };
578
+ }
579
+
580
+ private buildMewpCoverageRows(
581
+ requirements: Array<{
582
+ requirementId: string;
583
+ title: string;
584
+ responsibility: string;
585
+ linkedTestCaseIds: number[];
586
+ }>,
587
+ requirementIndex: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
588
+ observedTestCaseIdsByRequirement: Map<string, Set<number>>,
589
+ testCaseTitleMap: Map<number, string>
590
+ ): any[] {
591
+ const rows: any[] = [];
592
+ for (const requirement of requirements) {
593
+ const key = this.toRequirementKey(requirement.requirementId);
594
+ const linkedTestCaseIds = (requirement?.linkedTestCaseIds || []).filter(
595
+ (id) => Number.isFinite(id) && Number(id) > 0
596
+ );
597
+ const observedTestCaseIds = key
598
+ ? Array.from(observedTestCaseIdsByRequirement.get(key) || [])
599
+ : [];
600
+
601
+ const testCaseIds = Array.from(new Set<number>([...linkedTestCaseIds, ...observedTestCaseIds])).sort(
602
+ (a, b) => a - b
603
+ );
604
+
605
+ if (testCaseIds.length === 0) {
606
+ rows.push(
607
+ this.createMewpCoverageRow(requirement, undefined, '', {
608
+ passed: 0,
609
+ failed: 0,
610
+ notRun: 0,
611
+ })
612
+ );
613
+ continue;
614
+ }
615
+
616
+ for (const testCaseId of testCaseIds) {
617
+ const summary = key
618
+ ? requirementIndex.get(key)?.get(testCaseId) || { passed: 0, failed: 0, notRun: 0 }
619
+ : { passed: 0, failed: 0, notRun: 0 };
620
+ rows.push(
621
+ this.createMewpCoverageRow(
622
+ requirement,
623
+ testCaseId,
624
+ String(testCaseTitleMap.get(testCaseId) || ''),
625
+ summary
626
+ )
627
+ );
628
+ }
629
+ }
630
+
631
+ return rows;
632
+ }
633
+
634
+ private buildMewpTestCaseTitleMap(testData: any[]): Map<number, string> {
635
+ const map = new Map<number, string>();
636
+
637
+ const readTitleFromWorkItemFields = (workItemFields: any): string => {
638
+ if (!Array.isArray(workItemFields)) return '';
639
+ for (const field of workItemFields) {
640
+ const keyCandidates = [field?.key, field?.name, field?.referenceName, field?.id]
641
+ .map((item) => String(item || '').toLowerCase().trim());
642
+ const isTitleField =
643
+ keyCandidates.includes('system.title') || keyCandidates.includes('title');
644
+ if (!isTitleField) continue;
645
+ const value = this.toMewpComparableText(field?.value);
646
+ if (value) return value;
647
+ }
648
+ return '';
569
649
  };
650
+
651
+ for (const suite of testData || []) {
652
+ const testPointsItems = Array.isArray(suite?.testPointsItems) ? suite.testPointsItems : [];
653
+ for (const point of testPointsItems) {
654
+ const pointTestCaseId = Number(point?.testCaseId || point?.testCase?.id);
655
+ if (!Number.isFinite(pointTestCaseId) || pointTestCaseId <= 0 || map.has(pointTestCaseId)) continue;
656
+ const pointTitle = this.toMewpComparableText(point?.testCaseName || point?.testCase?.name);
657
+ if (pointTitle) map.set(pointTestCaseId, pointTitle);
658
+ }
659
+
660
+ const testCasesItems = Array.isArray(suite?.testCasesItems) ? suite.testCasesItems : [];
661
+ for (const testCase of testCasesItems) {
662
+ const id = Number(testCase?.workItem?.id || testCase?.testCaseId || testCase?.id);
663
+ if (!Number.isFinite(id) || id <= 0 || map.has(id)) continue;
664
+ const fromDirectFields = this.toMewpComparableText(
665
+ testCase?.testCaseName || testCase?.name || testCase?.workItem?.name
666
+ );
667
+ if (fromDirectFields) {
668
+ map.set(id, fromDirectFields);
669
+ continue;
670
+ }
671
+ const fromWorkItemField = readTitleFromWorkItemFields(testCase?.workItem?.workItemFields);
672
+ if (fromWorkItemField) {
673
+ map.set(id, fromWorkItemField);
674
+ }
675
+ }
676
+ }
677
+
678
+ return map;
679
+ }
680
+
681
+ private extractMewpTestCaseId(runResult: any): number {
682
+ const testCaseId = Number(runResult?.testCaseId || runResult?.testCase?.id || 0);
683
+ return Number.isFinite(testCaseId) ? testCaseId : 0;
570
684
  }
571
685
 
572
686
  private buildTestCaseStepsXmlMap(testData: any[]): Map<number, string> {
@@ -617,16 +731,30 @@ export default class ResultDataProvider {
617
731
  private accumulateRequirementCountsFromStepText(
618
732
  stepText: string,
619
733
  status: 'passed' | 'failed' | 'notRun',
734
+ testCaseId: number,
620
735
  requirementKeys: Set<string>,
621
- counters: Map<string, { passed: number; failed: number; notRun: number }>
736
+ counters: Map<string, Map<number, { passed: number; failed: number; notRun: number }>>,
737
+ observedTestCaseIdsByRequirement: Map<string, Set<number>>
622
738
  ) {
739
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
740
+
623
741
  const codes = this.extractRequirementCodesFromText(stepText);
624
742
  for (const code of codes) {
625
743
  if (requirementKeys.size > 0 && !requirementKeys.has(code)) continue;
626
744
  if (!counters.has(code)) {
627
- counters.set(code, { passed: 0, failed: 0, notRun: 0 });
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 });
628
750
  }
629
- const counter = counters.get(code)!;
751
+
752
+ if (!observedTestCaseIdsByRequirement.has(code)) {
753
+ observedTestCaseIdsByRequirement.set(code, new Set<number>());
754
+ }
755
+ observedTestCaseIdsByRequirement.get(code)!.add(testCaseId);
756
+
757
+ const counter = perTestCaseCounters.get(testCaseId)!;
630
758
  if (status === 'passed') counter.passed += 1;
631
759
  else if (status === 'failed') counter.failed += 1;
632
760
  else counter.notRun += 1;
@@ -674,7 +802,7 @@ export default class ResultDataProvider {
674
802
  return `SR${digits}`;
675
803
  }
676
804
 
677
- private async fetchMewpL2Requirements(projectName: string): Promise<
805
+ private async fetchMewpL2Requirements(projectName: string, linkedQueryRequest?: any): Promise<
678
806
  Array<{
679
807
  workItemId: number;
680
808
  requirementId: string;
@@ -683,6 +811,11 @@ export default class ResultDataProvider {
683
811
  linkedTestCaseIds: number[];
684
812
  }>
685
813
  > {
814
+ const queryHref = this.extractMewpQueryHref(linkedQueryRequest);
815
+ if (queryHref) {
816
+ return this.fetchMewpL2RequirementsFromQuery(projectName, queryHref);
817
+ }
818
+
686
819
  const workItemTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
687
820
  if (workItemTypeNames.length === 0) {
688
821
  return [];
@@ -722,6 +855,225 @@ ORDER BY [System.Id]`;
722
855
  return requirements.sort((a, b) => String(a.requirementId).localeCompare(String(b.requirementId)));
723
856
  }
724
857
 
858
+ private extractMewpQueryHref(linkedQueryRequest?: any): string {
859
+ const mode = String(linkedQueryRequest?.linkedQueryMode || '')
860
+ .trim()
861
+ .toLowerCase();
862
+ if (mode !== 'query') return '';
863
+
864
+ return String(linkedQueryRequest?.testAssociatedQuery?.wiql?.href || '').trim();
865
+ }
866
+
867
+ private async fetchMewpL2RequirementsFromQuery(
868
+ projectName: string,
869
+ queryHref: string
870
+ ): Promise<
871
+ Array<{
872
+ workItemId: number;
873
+ requirementId: string;
874
+ title: string;
875
+ responsibility: string;
876
+ linkedTestCaseIds: number[];
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
+ );
886
+
887
+ const requirementTypeNames = await this.fetchMewpRequirementTypeNames(projectName);
888
+ const requirementTypeSet = new Set(
889
+ requirementTypeNames.map((name) => String(name || '').trim().toLowerCase())
890
+ );
891
+
892
+ const requirementsById = new Map<
893
+ number,
894
+ {
895
+ workItemId: number;
896
+ requirementId: string;
897
+ title: string;
898
+ responsibility: string;
899
+ linkedTestCaseIds: Set<number>;
900
+ }
901
+ >();
902
+
903
+ const upsertRequirement = (workItem: any) => {
904
+ this.upsertMewpRequirement(requirementsById, workItem, requirementTypeSet);
905
+ };
906
+
907
+ const linkRequirementToTestCase = (requirementWorkItem: any, testCaseWorkItem: any) => {
908
+ const requirementId = Number(requirementWorkItem?.id || 0);
909
+ const testCaseId = Number(testCaseWorkItem?.id || 0);
910
+ if (!Number.isFinite(requirementId) || requirementId <= 0) return;
911
+ if (!Number.isFinite(testCaseId) || testCaseId <= 0) return;
912
+
913
+ upsertRequirement(requirementWorkItem);
914
+ const requirement = requirementsById.get(requirementId);
915
+ if (!requirement) return;
916
+ requirement.linkedTestCaseIds.add(testCaseId);
917
+ };
918
+
919
+ if (Array.isArray(queryResult?.fetchedWorkItems)) {
920
+ for (const workItem of queryResult.fetchedWorkItems) {
921
+ upsertRequirement(workItem);
922
+ }
923
+ }
924
+
925
+ if (queryResult?.sourceTargetsMap && typeof queryResult.sourceTargetsMap.entries === 'function') {
926
+ for (const [sourceItem, targets] of queryResult.sourceTargetsMap.entries()) {
927
+ const sourceType = this.getMewpWorkItemType(sourceItem);
928
+ const sourceIsRequirement = this.isMewpRequirementType(sourceType, requirementTypeSet);
929
+ const sourceIsTestCase = this.isMewpTestCaseType(sourceType);
930
+
931
+ if (sourceIsRequirement) {
932
+ upsertRequirement(sourceItem);
933
+ }
934
+
935
+ const relatedItems = Array.isArray(targets) ? targets : [];
936
+ for (const targetItem of relatedItems) {
937
+ const targetType = this.getMewpWorkItemType(targetItem);
938
+ const targetIsRequirement = this.isMewpRequirementType(targetType, requirementTypeSet);
939
+ const targetIsTestCase = this.isMewpTestCaseType(targetType);
940
+
941
+ if (targetIsRequirement) {
942
+ upsertRequirement(targetItem);
943
+ }
944
+
945
+ if (sourceIsRequirement && targetIsTestCase) {
946
+ linkRequirementToTestCase(sourceItem, targetItem);
947
+ } else if (sourceIsTestCase && targetIsRequirement) {
948
+ linkRequirementToTestCase(targetItem, sourceItem);
949
+ }
950
+ }
951
+ }
952
+ }
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
+ }
969
+ }
970
+
971
+ private upsertMewpRequirement(
972
+ requirementsById: Map<
973
+ number,
974
+ {
975
+ workItemId: number;
976
+ requirementId: string;
977
+ title: string;
978
+ responsibility: string;
979
+ linkedTestCaseIds: Set<number>;
980
+ }
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
+
1004
+ existing.requirementId = extractedRequirementId || existing.requirementId || String(workItemId);
1005
+ if (extractedTitle) {
1006
+ existing.title = extractedTitle;
1007
+ }
1008
+ if (extractedResponsibility) {
1009
+ existing.responsibility = extractedResponsibility;
1010
+ }
1011
+
1012
+ requirementsById.set(workItemId, existing);
1013
+ }
1014
+
1015
+ private async hydrateMewpRequirementsFromWorkItems(
1016
+ projectName: string,
1017
+ requirementsById: Map<
1018
+ number,
1019
+ {
1020
+ workItemId: number;
1021
+ requirementId: string;
1022
+ title: string;
1023
+ responsibility: string;
1024
+ linkedTestCaseIds: Set<number>;
1025
+ }
1026
+ >
1027
+ ) {
1028
+ const requirementIds = [...requirementsById.keys()];
1029
+ if (requirementIds.length === 0) return;
1030
+
1031
+ const fetchedRequirements = await this.fetchWorkItemsByIds(projectName, requirementIds, true);
1032
+ for (const requirementWorkItem of fetchedRequirements) {
1033
+ const workItemId = Number(requirementWorkItem?.id || 0);
1034
+ if (!Number.isFinite(workItemId) || workItemId <= 0) continue;
1035
+ const current = requirementsById.get(workItemId);
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
+ );
1045
+
1046
+ current.requirementId = requirementId || current.requirementId || String(workItemId);
1047
+ if (title) {
1048
+ current.title = title;
1049
+ }
1050
+ if (responsibility) {
1051
+ current.responsibility = responsibility;
1052
+ }
1053
+ linkedTestCaseIds.forEach((testCaseId) => current.linkedTestCaseIds.add(testCaseId));
1054
+ }
1055
+ }
1056
+
1057
+ private getMewpWorkItemType(workItem: any): string {
1058
+ return this.toMewpComparableText(workItem?.fields?.['System.WorkItemType']);
1059
+ }
1060
+
1061
+ private isMewpRequirementType(workItemType: string, requirementTypeSet: Set<string>): boolean {
1062
+ const normalized = String(workItemType || '')
1063
+ .trim()
1064
+ .toLowerCase();
1065
+ if (!normalized) return false;
1066
+ if (requirementTypeSet.has(normalized)) return true;
1067
+ return normalized.includes('requirement') || normalized === 'epic';
1068
+ }
1069
+
1070
+ private isMewpTestCaseType(workItemType: string): boolean {
1071
+ const normalized = String(workItemType || '')
1072
+ .trim()
1073
+ .toLowerCase();
1074
+ return normalized === 'test case' || normalized === 'testcase';
1075
+ }
1076
+
725
1077
  private async fetchMewpRequirementTypeNames(projectName: string): Promise<string[]> {
726
1078
  try {
727
1079
  const url = `${this.orgUrl}${projectName}/_apis/wit/workitemtypes?api-version=7.1-preview.2`;