Coverage for lobster/location.py: 77%

149 statements  

« prev     ^ index     » next       coverage.py v7.9.1, created at 2025-06-26 14:55 +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: 

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: 

173 return "%s:%u" % (self.filename, 

174 self.line) 

175 else: 

176 return self.filename 

177 

178 def to_html(self): 

179 file_ref = self.filename 

180 if self.line: 

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

182 

183 return '<a href="%s/blob/%s/%s" target="_blank">%s</a>' % ( 

184 self.gh_root, 

185 self.commit, 

186 file_ref, 

187 self.to_string()) 

188 

189 def to_json(self): 

190 return {"kind" : "github", 

191 "gh_root" : self.gh_root, 

192 "commit" : self.commit, 

193 "file" : self.filename, 

194 "line" : self.line 

195 } 

196 

197 @classmethod 

198 def from_json(cls, json): 

199 assert isinstance(json, dict) 

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

201 

202 gh_root = json["gh_root"] 

203 filename = json["file"] 

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

205 commit = json.get("commit") 

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

207 

208 

209class Codebeamer_Reference(Location): 

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

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

212 assert isinstance(cb_root, str) 

213 assert cb_root.startswith("http") 

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

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

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

217 version >= 1) 

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

219 

220 self.cb_root = cb_root 

221 self.tracker = tracker 

222 self.item = item 

223 self.version = version 

224 self.name = name 

225 

226 def sorting_key(self): 

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

228 

229 def to_string(self): 

230 # lobster-trace: Codebeamer_Item_as_String 

231 if self.name: 

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

233 else: 

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

235 

236 def to_html(self): 

237 # lobster-trace: Codebeamer_URL 

238 url = self.cb_root 

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

240 if self.version: 

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

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

243 

244 def to_json(self): 

245 return {"kind" : "codebeamer", 

246 "cb_root" : self.cb_root, 

247 "tracker" : self.tracker, 

248 "item" : self.item, 

249 "version" : self.version, 

250 "name" : self.name} 

251 

252 @classmethod 

253 def from_json(cls, json): 

254 assert isinstance(json, dict) 

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

256 

257 cb_root = json["cb_root"] 

258 tracker = json["tracker"] 

259 item = json["item"] 

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

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

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