Coverage for lobster/location.py: 80%

149 statements  

« prev     ^ index     » next       coverage.py v7.10.2, created at 2025-08-06 09:51 +0000

1#!/usr/bin/env python3 

2# 

3# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report 

4# Copyright (C) 2023, 2025 Bayerische Motoren Werke Aktiengesellschaft (BMW AG) 

5# 

6# This program is free software: you can redistribute it and/or modify 

7# it under the terms of the GNU Affero General Public License as 

8# published by the Free Software Foundation, either version 3 of the 

9# License, or (at your option) any later version. 

10# 

11# This program is distributed in the hope that it will be useful, but 

12# WITHOUT ANY WARRANTY; without even the implied warranty of 

13# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the GNU 

14# Affero General Public License for more details. 

15# 

16# You should have received a copy of the GNU Affero General Public 

17# License along with this program. If not, see 

18# <https://www.gnu.org/licenses/>. 

19 

20from abc import ABCMeta, abstractmethod 

21import html 

22from typing import Any, Dict, Optional, Tuple 

23from lobster.exceptions import LOBSTER_Exception 

24 

25 

26class Location(metaclass=ABCMeta): 

27 @abstractmethod 

28 def sorting_key(self) -> Tuple: 

29 pass 

30 

31 @abstractmethod 

32 def to_string(self) -> str: 

33 pass 

34 

35 @abstractmethod 

36 def to_html(self) -> str: 

37 pass 

38 

39 @abstractmethod 

40 def to_json(self) -> Dict[str, Any]: 

41 pass 

42 

43 @classmethod 

44 def from_json(cls, json): 

45 if not isinstance(json, dict): 45 ↛ 46line 45 didn't jump to line 46 because the condition on line 45 was never true

46 raise LOBSTER_Exception("location data not an object", 

47 json) 

48 if "kind" not in json: 48 ↛ 49line 48 didn't jump to line 49 because the condition on line 48 was never true

49 raise LOBSTER_Exception("location data does not contain 'kind'", 

50 json) 

51 

52 try: 

53 if json["kind"] == "file": 

54 return File_Reference.from_json(json) 

55 elif json["kind"] == "github": 

56 return Github_Reference.from_json(json) 

57 elif json["kind"] == "codebeamer": 

58 return Codebeamer_Reference.from_json(json) 

59 elif json["kind"] == "void": 59 ↛ 62line 59 didn't jump to line 62 because the condition on line 59 was always true

60 return Void_Reference.from_json(json) 

61 else: 

62 raise LOBSTER_Exception("unknown location kind %s" % 

63 json["kind"]) 

64 except KeyError as err: 

65 raise LOBSTER_Exception( 

66 "malformed location data, missing %s" % err.args[0], 

67 json) from err 

68 except AssertionError: 

69 raise LOBSTER_Exception( 

70 "malformed %s location data" % json["kind"], 

71 json) from err 

72 

73 

74class Void_Reference(Location): 

75 def __init__(self): 

76 pass 

77 

78 def sorting_key(self): 

79 return tuple() 

80 

81 def to_string(self): 

82 return "<unknown location>" 

83 

84 def to_html(self): 

85 return html.escape(self.to_string()) 

86 

87 def to_json(self): 

88 return {"kind": "void"} 

89 

90 @classmethod 

91 def from_json(cls, json): 

92 assert isinstance(json, dict) 

93 assert json["kind"] == "void" 

94 return Void_Reference() 

95 

96 

97class File_Reference(Location): 

98 def __init__(self, filename, line=None, column=None): 

99 assert isinstance(filename, str) 

100 assert line is None or (isinstance(line, int) and 

101 line >= 1) 

102 assert column is None or (line is not None and 

103 isinstance(column, int) and 

104 column >= 1) 

105 self.filename = filename 

106 self.line = line 

107 self.column = column 

108 

109 def sorting_key(self): 

110 if self.line is not None: 

111 if self.column is not None: 

112 return (self.filename, self.line, self.column) 

113 else: 

114 return (self.filename, self.line) 

115 else: 

116 return (self.filename,) 

117 

118 def to_string(self): 

119 rv = self.filename 

120 if self.line: 

121 rv += ":%u" % self.line 

122 if self.column: 

123 rv += ":%u" % self.column 

124 return rv 

125 

126 def to_html(self): 

127 return '<a href="%s" target="_blank">%s</a>' % (self.filename, 

128 self.filename) 

129 

130 def to_json(self): 

131 return {"kind" : "file", 

132 "file" : self.filename, 

133 "line" : self.line, 

134 "column" : self.column} 

135 

136 @classmethod 

137 def from_json(cls, json): 

138 assert isinstance(json, dict) 

139 assert json["kind"] == "file" 

140 

141 filename = json["file"] 

142 line = json.get("line", None) 

143 if line is not None: 143 ↛ 144line 143 didn't jump to line 144 because the condition on line 143 was never true

144 column = json.get("column", None) 

145 else: 

146 column = None 

147 return File_Reference(filename, line, column) 

148 

149 

150class Github_Reference(Location): 

151 def __init__(self, gh_root, filename, line, commit): 

152 assert isinstance(gh_root, str) 

153 assert gh_root.startswith("http") 

154 assert isinstance(filename, str) 

155 assert line is None or (isinstance(line, int) and 

156 line >= 1) 

157 assert isinstance(commit, str) 

158 

159 self.gh_root = gh_root.rstrip("/") 

160 self.gh_repo = self.gh_root.split("/")[-1] 

161 self.commit = commit 

162 self.filename = filename 

163 self.line = line 

164 

165 def sorting_key(self): 

166 if self.line is not None: 

167 return (self.filename, self.line) 

168 else: 

169 return (self.filename,) 

170 

171 def to_string(self): 

172 if self.line: 172 ↛ 175line 172 didn't jump to line 175 because the condition on line 172 was always true

173 return f"{self.filename}:{self.line}" 

174 else: 

175 return self.filename 

176 

177 def to_html(self): 

178 file_ref = self.filename 

179 if self.line: 179 ↛ 182line 179 didn't jump to line 182 because the condition on line 179 was always true

180 file_ref += "#L%u" % self.line 

181 

182 return f'<a href="{self.gh_root}/blob/{self.commit}/{file_ref}" ' \ 

183 f'target="_blank">{self.to_string()}</a>' 

184 

185 def to_json(self): 

186 return {"kind" : "github", 

187 "gh_root" : self.gh_root, 

188 "commit" : self.commit, 

189 "file" : self.filename, 

190 "line" : self.line 

191 } 

192 

193 @classmethod 

194 def from_json(cls, json): 

195 assert isinstance(json, dict) 

196 assert json["kind"] == "github" 

197 

198 gh_root = json["gh_root"] 

199 filename = json["file"] 

200 line = json.get("line", None) 

201 commit = json.get("commit") 

202 return Github_Reference(gh_root, filename, line, commit) 

203 

204 

205class Codebeamer_Reference(Location): 

206 def __init__(self, cb_root: str, tracker: int, item: int, 

207 version: Optional[int] = None, name: Optional[str] = None): 

208 assert isinstance(cb_root, str) 

209 assert cb_root.startswith("http") 

210 assert isinstance(tracker, int) and tracker >= 1 

211 assert isinstance(item, int) and item >= 1 

212 assert version is None or (isinstance(version, int) and 

213 version >= 1) 

214 assert name is None or isinstance(name, str) 

215 

216 self.cb_root = cb_root 

217 self.tracker = tracker 

218 self.item = item 

219 self.version = version 

220 self.name = name 

221 

222 def sorting_key(self): 

223 return (self.cb_root, self.tracker, self.item) 

224 

225 def to_string(self): 

226 # lobster-trace: Codebeamer_Item_as_String 

227 if self.name: 

228 return "cb item %u '%s'" % (self.item, self.name) 

229 else: 

230 return "cb item %u" % self.item 

231 

232 def to_html(self): 

233 # lobster-trace: Codebeamer_URL 

234 url = self.cb_root 

235 url += "/issue/%u" % self.item 

236 if self.version: 

237 url += "?version=%u" % self.version 

238 return '<a href="%s" target="_blank">%s</a>' % (url, self.to_string()) 

239 

240 def to_json(self): 

241 return {"kind" : "codebeamer", 

242 "cb_root" : self.cb_root, 

243 "tracker" : self.tracker, 

244 "item" : self.item, 

245 "version" : self.version, 

246 "name" : self.name} 

247 

248 @classmethod 

249 def from_json(cls, json): 

250 assert isinstance(json, dict) 

251 assert json["kind"] == "codebeamer" 

252 

253 cb_root = json["cb_root"] 

254 tracker = json["tracker"] 

255 item = json["item"] 

256 version = json.get("version", None) 

257 name = json.get("name", None) 

258 return Codebeamer_Reference(cb_root, tracker, item, version, name)