Coverage for muuntaa/document.py: 70.06%
117 statements
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
« prev ^ index » next coverage.py v7.6.0, created at 2024-07-21 12:16:47 +00:00
1"""Document specific types: Leaves, Publisher, and Tracking."""
3import logging
4import operator
5from typing import Any, Union
7import lxml.objectify # nosec B410
9from muuntaa.config import boolify
10from muuntaa.dialect import PUBLISHER_TYPE_CATEGORY, TRACKING_STATUS
11from muuntaa.strftime import get_utc_timestamp
12from muuntaa.subtree import Subtree
14from muuntaa import APP_ALIAS, ConfigType, NOW_CODE, VERSION, VERSION_PATTERN, cleanse_id, integer_tuple
16RootType = lxml.objectify.ObjectifiedElement
17RevHistType = list[dict[str, Union[str, None, tuple[int, ...]]]]
20class Leafs(Subtree):
21 """Represent leaf element content below CSAF path.
23 (
24 /document,
25 )
26 """
28 def __init__(self, config: ConfigType) -> None:
29 super().__init__()
30 if self.tree.get('document') is None: 30 ↛ 32line 30 didn't jump to line 32 because the condition on line 30 was always true
31 self.tree['document'] = {}
32 self.hook = self.tree['document']
33 self.hook['csaf_version'] = config.get('csaf_version')
35 def always(self, root: RootType) -> None:
36 self.hook['category'] = root.DocumentType.text
37 self.hook['title'] = root.DocumentTitle.text
39 def sometimes(self, root: RootType) -> None:
40 if doc_dist := root.DocumentDistribution is not None: 40 ↛ 41, 40 ↛ 432 missed branches: 1) line 40 didn't jump to line 41 because the condition on line 40 was never true, 2) line 40 didn't jump to line 43 because the condition on line 40 was always true
41 self.hook['distribution'] = {'text': doc_dist.text} # type: ignore
43 if agg_sev := root.AggregateSeverity is not None:
44 self.hook['aggregate_severity'] = {'text': agg_sev.text} # type: ignore
45 if agg_sev_ns := root.AggregateSeverity.attrib.get('Namespace') is not None:
46 self.hook['aggregate_severity']['namespace'] = agg_sev_ns
49class Publisher(Subtree):
50 """Represents the Publisher type:
52 (
53 /cvrf:cvrfdoc/cvrf:DocumentPublisher,
54 )
55 """
57 def __init__(self, config: ConfigType):
58 super().__init__()
59 if self.tree.get('document') is None: 59 ↛ 61line 59 didn't jump to line 61 because the condition on line 59 was always true
60 self.tree['document'] = {}
61 if self.tree['document'].get('publisher') is None: 61 ↛ 66line 61 didn't jump to line 66 because the condition on line 61 was always true
62 self.tree['document']['publisher'] = {
63 'name': config.get('publisher_name'),
64 'namespace': config.get('publisher_namespace'),
65 }
66 self.hook = self.tree['document']['publisher']
68 def always(self, root: RootType) -> None:
69 category = PUBLISHER_TYPE_CATEGORY.get(root.attrib.get('Type', '')) # TODO consistent key error handling?
70 self.hook['category'] = category
72 def sometimes(self, root: RootType) -> None:
73 if contact_details := root.ContactDetails: 73 ↛ 75line 73 didn't jump to line 75 because the condition on line 73 was always true
74 self.hook['contact_details'] = contact_details.text
75 if issuing_authority := root.IssuingAuthority: 75 ↛ exitline 75 didn't return from function 'sometimes' because the condition on line 75 was always true
76 self.hook['issuing_authority'] = issuing_authority.text
79class Tracking(Subtree):
80 """Represents the Tracking type.
81 (
82 /cvrf:cvrfdoc/cvrf:DocumentTracking,
83 )
84 """
86 fix_insert_current_version_into_revision_history: bool = False
88 def __init__(self, config: ConfigType):
89 super().__init__()
90 boolify(config)
91 self.fix_insert_current_version_into_revision_history = config.get( # type: ignore
92 'fix_insert_current_version_into_revision_history', False
93 )
94 print(f'{self.fix_insert_current_version_into_revision_history=}')
95 processing_ts, problems = get_utc_timestamp(ts_text=NOW_CODE)
96 for level, problem in problems: 96 ↛ 97line 96 didn't jump to line 97 because the loop on line 96 never started
97 logging.log(level, problem)
98 if self.tree.get('document') is None: 98 ↛ 100line 98 didn't jump to line 100 because the condition on line 98 was always true
99 self.tree['document'] = {}
100 if self.tree['document'].get('tracking') is None: 100 ↛ 110line 100 didn't jump to line 110 because the condition on line 100 was always true
101 self.tree['document']['tracking'] = {
102 'generator': {
103 'date': processing_ts,
104 'engine': {
105 'name': APP_ALIAS,
106 'version': VERSION,
107 },
108 },
109 }
110 self.hook = self.tree['document']['tracking']
112 def always(self, root: RootType) -> None:
113 current_release_date, problems = get_utc_timestamp(root.CurrentReleaseDate.text or '')
114 for level, problem in problems: 114 ↛ 115line 114 didn't jump to line 115 because the loop on line 114 never started
115 logging.log(level, problem)
116 initial_release_date, problems = get_utc_timestamp(root.InitialReleaseDate.text or '')
117 for level, problem in problems: 117 ↛ 118line 117 didn't jump to line 118 because the loop on line 117 never started
118 logging.log(level, problem)
119 revision_history, version = self._handle_revision_history_and_version(root)
120 status = TRACKING_STATUS.get(root.Status.text, '') # type: ignore
121 self.hook['current_release_date'] = current_release_date
122 self.hook['id'] = cleanse_id(root.Identification.ID.text or '')
123 self.hook['initial_release_date'] = initial_release_date
124 self.hook['revision_history'] = revision_history
125 self.hook['status'] = status
126 self.hook['version'] = version
128 def sometimes(self, root: RootType) -> None:
129 if aliases := root.Identification.Alias: 129 ↛ 130line 129 didn't jump to line 130 because the condition on line 129 was never true
130 self.hook['aliases'] = [alias.text for alias in aliases]
132 @staticmethod
133 def only_version_t(revision_history: RevHistType) -> bool:
134 """Verifies whether all version numbers in /document/tracking/revision_history comply."""
135 return all(VERSION_PATTERN.match(revision['number']) for revision in revision_history) # type: ignore 135 ↛ exitline 135 didn't finish the generator expression on line 135
137 def _add_current_revision_to_history(self, root: RootType, revision_history: RevHistType) -> None:
138 """Adds the current version to history, if former is missing in latter and fix is requested.
140 The user can request the fix per --fix-insert-current-version-into-revision-history option
141 or per setting the respective configuration key to true.
142 """
144 entry_date, problems = get_utc_timestamp(root.CurrentReleaseDate.text or '')
145 for level, problem in problems:
146 logging.log(level, problem)
147 revision_history.append(
148 {
149 'date': entry_date,
150 'number': root.Version.text,
151 'summary': f'Added by {APP_ALIAS} as the value was missing in the original CVRF.',
152 'number_cvrf': root.Version.text, # Helper field
153 'version_as_int_tuple': integer_tuple(root.Version.text or ''), # Helper field
154 }
155 )
157 @staticmethod
158 def _reindex_versions_to_integers(root: RootType, revision_history: RevHistType) -> tuple[RevHistType, str]:
159 logging.warning(
160 'Some version numbers in revision_history do not match semantic versioning. Reindexing to integers.'
161 )
163 revision_history_sorted = sorted(revision_history, key=operator.itemgetter('version_as_int_tuple'))
165 for rev_number, revision in enumerate(revision_history_sorted, start=1):
166 revision['number'] = str(rev_number)
167 # add property legacy_version with the original version number
168 # for each reindexed version
169 revision['legacy_version'] = revision['number_cvrf']
171 # after reindexing, match document version to corresponding one in revision history
172 version = next(rev for rev in revision_history_sorted if rev['number_cvrf'] == root.Version.text)['number'] 172 ↛ exitline 172 didn't finish the generator expression on line 172
174 return revision_history_sorted, version # type: ignore
176 def _handle_revision_history_and_version(self, root: RootType) -> tuple[list[dict[str, Any]], str | None]:
177 revision_history = [
178 {
179 'date': get_utc_timestamp(revision.Date.text or ''), # type: ignore
180 'number': revision.Number.text, # type: ignore # may be patched later (in case of mismatches)
181 'summary': revision.Description.text, # type: ignore
182 'number_cvrf': revision.Number.text, # type: ignore # keep track of original value (later matching)
183 'version_as_int_tuple': integer_tuple(revision.Number.text or ''), # type: ignore # temporary
184 }
185 for revision in root.RevisionHistory.Revision
186 ]
187 version = root.Version.text
189 missing_latest_version_in_history = False
190 if not [rev for rev in revision_history if rev['number'] == version]: # Current version not in rev. history? 190 ↛ 191line 190 didn't jump to line 191 because the condition on line 190 was never true
191 if self.fix_insert_current_version_into_revision_history:
192 self._add_current_revision_to_history(root, revision_history)
193 level = logging.WARNING
194 message = (
195 'Trying to fix the revision history by adding the current version.'
196 ' This may lead to inconsistent history.'
197 ' This happens because --fix-insert-current-version-into-revision-history is used.'
198 )
199 else:
200 missing_latest_version_in_history = True
201 self.error_occurred = True
202 level = logging.ERROR
203 message = (
204 'Current version is missing in revision history.'
205 ' This can be fixed by using --fix-insert-current-version-into-revision-history.'
206 )
207 logging.log(level, message)
209 if not self.only_version_t(revision_history): # one or more versions do not comply 209 ↛ 219line 209 didn't jump to line 219 because the condition on line 209 was always true
210 if missing_latest_version_in_history: 210 ↛ 211line 210 didn't jump to line 211 because the condition on line 210 was never true
211 self.error_occurred = True
212 logging.error(
213 'Can not reindex revision history to integers because of missing the current version.'
214 ' This can be fixed with --fix-insert-current-version-into-revision-history'
215 )
216 else: # sort and replace version values with rank as per conformance rule
217 revision_history, version = self._reindex_versions_to_integers(root, revision_history)
219 for revision in revision_history: # remove temporary fields
220 revision.pop('number_cvrf')
221 revision.pop('version_as_int_tuple')
223 return revision_history, version