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

149 statements  

« prev     ^ index     » next       coverage.py v7.10.7, created at 2026-04-16 05:31 +0000

1#!/usr/bin/env python3 

2# 

3# LOBSTER - Lightweight Open BMW Software Traceability Evidence Report 

4# Copyright (C) 2023, 2025-2026 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 if json["kind"] == "github": 

56 return Github_Reference.from_json(json) 

57 if 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 if json["kind"] == "void": 

60 return Void_Reference.from_json(json) 

61 raise LOBSTER_Exception(f"unknown location kind {json['kind']}") 

62 except KeyError as err: 

63 raise LOBSTER_Exception( 

64 f"malformed location data, missing {err.args[0]}", 

65 json) from err 

66 except AssertionError as err: 

67 raise LOBSTER_Exception( 

68 f"malformed {json['kind']} location data", 

69 json) from err 

70 

71 

72class Void_Reference(Location): 

73 def __init__(self): 

74 pass 

75 

76 def sorting_key(self): 

77 return tuple() 

78 

79 def to_string(self): 

80 return "<unknown location>" 

81 

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

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

84 

85 def to_json(self): 

86 return {"kind": "void"} 

87 

88 @classmethod 

89 def from_json(cls, json): 

90 assert isinstance(json, dict) 

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

92 return Void_Reference() 

93 

94 

95class File_Reference(Location): 

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

97 assert isinstance(filename, str) 

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

99 line >= 1) 

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

101 isinstance(column, int) and 

102 column >= 1) 

103 self.filename = filename 

104 self.line = line 

105 self.column = column 

106 

107 def sorting_key(self): 

108 values = (self.filename, self.line, self.column) 

109 if None in values: 

110 return values[:values.index(None)] 

111 return values 

112 

113 def to_string(self): 

114 rv = self.filename 

115 if self.line: 

116 rv += f":{self.line}" 

117 if self.column: 

118 rv += f":{self.column}" 

119 return rv 

120 

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

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

123 return f'<a href="{href}" target="_blank">{self.filename}</a>' 

124 

125 def to_json(self): 

126 return {"kind" : "file", 

127 "file" : self.filename, 

128 "line" : self.line, 

129 "column" : self.column} 

130 

131 @classmethod 

132 def from_json(cls, json): 

133 assert isinstance(json, dict) 

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

135 

136 filename = json["file"] 

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

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

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

140 else: 

141 column = None 

142 return File_Reference(filename, line, column) 

143 

144 

145class Github_Reference(Location): 

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

147 assert isinstance(gh_root, str) 

148 assert gh_root.startswith("http") 

149 assert isinstance(filename, str) 

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

151 line >= 1) 

152 assert isinstance(commit, str) 

153 

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

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

156 self.commit = commit 

157 self.filename = filename 

158 self.line = line 

159 

160 def sorting_key(self): 

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

162 return (self.filename, self.line) 

163 return (self.filename,) 

164 

165 def to_string(self): 

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

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

168 return self.filename 

169 

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

171 file_ref = self.filename 

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

173 file_ref += f"#L{self.line}" 

174 

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

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

177 

178 def to_json(self): 

179 return {"kind" : "github", 

180 "gh_root" : self.gh_root, 

181 "commit" : self.commit, 

182 "file" : self.filename, 

183 "line" : self.line 

184 } 

185 

186 @classmethod 

187 def from_json(cls, json): 

188 assert isinstance(json, dict) 

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

190 

191 gh_root = json["gh_root"] 

192 filename = json["file"] 

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

194 commit = json.get("commit") 

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

196 

197 

198class Codebeamer_Reference(Location): 

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

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

201 assert isinstance(cb_root, str) 

202 assert cb_root.startswith("http") 

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

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

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

206 version >= 1) 

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

208 

209 self.cb_root = cb_root 

210 self.tracker = tracker 

211 self.item = item 

212 self.version = version 

213 self.name = name 

214 

215 def sorting_key(self): 

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

217 

218 def to_string(self): 

219 # lobster-trace: Codebeamer_Item_as_String 

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

221 return f"cb item {self.item} '{self.name}'" 

222 return f"cb item {self.item}" 

223 

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

225 # lobster-trace: Codebeamer_URL 

226 url = self.cb_root 

227 url += f"/issue/{self.item}" 

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

229 url += f"?version={self.version}" 

230 return f'<a href="{url}" target="_blank">{self.to_string()}</a>' 

231 

232 def to_json(self): 

233 return {"kind" : "codebeamer", 

234 "cb_root" : self.cb_root, 

235 "tracker" : self.tracker, 

236 "item" : self.item, 

237 "version" : self.version, 

238 "name" : self.name} 

239 

240 @classmethod 

241 def from_json(cls, json): 

242 assert isinstance(json, dict) 

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

244 

245 cb_root = json["cb_root"] 

246 tracker = json["tracker"] 

247 item = json["item"] 

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

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

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