Coverage for lobster/common/location.py: 82%

150 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-03-04 12:54 +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.common.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, source_root="") -> 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: 

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": 57 ↛ 59line 57 didn't jump to line 59 because the condition on line 57 was always true

58 return Codebeamer_Reference.from_json(json) 

59 elif json["kind"] == "void": 

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, source_root=""): 

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: 110 ↛ 116line 110 didn't jump to line 116 because the condition on line 110 was always true

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, source_root=""): 

127 href = source_root + self.filename if source_root else self.filename 

128 return '<a href="%s" target="_blank">%s</a>' % (href, 

129 self.filename) 

130 

131 def to_json(self): 

132 return {"kind" : "file", 

133 "file" : self.filename, 

134 "line" : self.line, 

135 "column" : self.column} 

136 

137 @classmethod 

138 def from_json(cls, json): 

139 assert isinstance(json, dict) 

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

141 

142 filename = json["file"] 

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

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

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

146 else: 

147 column = None 

148 return File_Reference(filename, line, column) 

149 

150 

151class Github_Reference(Location): 

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

153 assert isinstance(gh_root, str) 

154 assert gh_root.startswith("http") 

155 assert isinstance(filename, str) 

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

157 line >= 1) 

158 assert isinstance(commit, str) 

159 

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

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

162 self.commit = commit 

163 self.filename = filename 

164 self.line = line 

165 

166 def sorting_key(self): 

167 if self.line is not None: 167 ↛ 170line 167 didn't jump to line 170 because the condition on line 167 was always true

168 return (self.filename, self.line) 

169 else: 

170 return (self.filename,) 

171 

172 def to_string(self): 

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

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

175 else: 

176 return self.filename 

177 

178 def to_html(self, source_root=""): 

179 file_ref = self.filename 

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

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

182 

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

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

185 

186 def to_json(self): 

187 return {"kind" : "github", 

188 "gh_root" : self.gh_root, 

189 "commit" : self.commit, 

190 "file" : self.filename, 

191 "line" : self.line 

192 } 

193 

194 @classmethod 

195 def from_json(cls, json): 

196 assert isinstance(json, dict) 

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

198 

199 gh_root = json["gh_root"] 

200 filename = json["file"] 

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

202 commit = json.get("commit") 

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

204 

205 

206class Codebeamer_Reference(Location): 

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

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

209 assert isinstance(cb_root, str) 

210 assert cb_root.startswith("http") 

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

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

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

214 version >= 1) 

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

216 

217 self.cb_root = cb_root 

218 self.tracker = tracker 

219 self.item = item 

220 self.version = version 

221 self.name = name 

222 

223 def sorting_key(self): 

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

225 

226 def to_string(self): 

227 # lobster-trace: Codebeamer_Item_as_String 

228 if self.name: 228 ↛ 231line 228 didn't jump to line 231 because the condition on line 228 was always true

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

230 else: 

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

232 

233 def to_html(self, source_root=""): 

234 # lobster-trace: Codebeamer_URL 

235 url = self.cb_root 

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

237 if self.version: 237 ↛ 239line 237 didn't jump to line 239 because the condition on line 237 was always true

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

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

240 

241 def to_json(self): 

242 return {"kind" : "codebeamer", 

243 "cb_root" : self.cb_root, 

244 "tracker" : self.tracker, 

245 "item" : self.item, 

246 "version" : self.version, 

247 "name" : self.name} 

248 

249 @classmethod 

250 def from_json(cls, json): 

251 assert isinstance(json, dict) 

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

253 

254 cb_root = json["cb_root"] 

255 tracker = json["tracker"] 

256 item = json["item"] 

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

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

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