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

1"""Document specific types: Leaves, Publisher, and Tracking.""" 

2 

3import logging 

4import operator 

5from typing import Any, Union 

6 

7import lxml.objectify # nosec B410 

8 

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 

13 

14from muuntaa import APP_ALIAS, ConfigType, NOW_CODE, VERSION, VERSION_PATTERN, cleanse_id, integer_tuple 

15 

16RootType = lxml.objectify.ObjectifiedElement 

17RevHistType = list[dict[str, Union[str, None, tuple[int, ...]]]] 

18 

19 

20class Leafs(Subtree): 

21 """Represent leaf element content below CSAF path. 

22 

23 ( 

24 /document, 

25 ) 

26 """ 

27 

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') 

34 

35 def always(self, root: RootType) -> None: 

36 self.hook['category'] = root.DocumentType.text 

37 self.hook['title'] = root.DocumentTitle.text 

38 

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 

42 

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 

47 

48 

49class Publisher(Subtree): 

50 """Represents the Publisher type: 

51 

52 ( 

53 /cvrf:cvrfdoc/cvrf:DocumentPublisher, 

54 ) 

55 """ 

56 

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'] 

67 

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 

71 

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 

77 

78 

79class Tracking(Subtree): 

80 """Represents the Tracking type. 

81 ( 

82 /cvrf:cvrfdoc/cvrf:DocumentTracking, 

83 ) 

84 """ 

85 

86 fix_insert_current_version_into_revision_history: bool = False 

87 

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'] 

111 

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 

127 

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] 

131 

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

136 

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. 

139 

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 """ 

143 

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 ) 

156 

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 ) 

162 

163 revision_history_sorted = sorted(revision_history, key=operator.itemgetter('version_as_int_tuple')) 

164 

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'] 

170 

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

173 

174 return revision_history_sorted, version # type: ignore 

175 

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 

188 

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) 

208 

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) 

218 

219 for revision in revision_history: # remove temporary fields 

220 revision.pop('number_cvrf') 

221 revision.pop('version_as_int_tuple') 

222 

223 return revision_history, version