Coverage for lobster/location.py: 80%
149 statements
« prev ^ index » next coverage.py v7.9.1, created at 2025-06-26 14:55 +0000
« 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/>.
20from abc import ABCMeta, abstractmethod
21import html
22from typing import Any, Dict, Optional, Tuple
23from lobster.exceptions import LOBSTER_Exception
26class Location(metaclass=ABCMeta):
27 @abstractmethod
28 def sorting_key(self) -> Tuple:
29 pass
31 @abstractmethod
32 def to_string(self) -> str:
33 pass
35 @abstractmethod
36 def to_html(self) -> str:
37 pass
39 @abstractmethod
40 def to_json(self) -> Dict[str, Any]:
41 pass
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)
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
74class Void_Reference(Location):
75 def __init__(self):
76 pass
78 def sorting_key(self):
79 return tuple()
81 def to_string(self):
82 return "<unknown location>"
84 def to_html(self):
85 return html.escape(self.to_string())
87 def to_json(self):
88 return {"kind": "void"}
90 @classmethod
91 def from_json(cls, json):
92 assert isinstance(json, dict)
93 assert json["kind"] == "void"
94 return Void_Reference()
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
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,)
118 def to_string(self):
119 rv = self.filename
120 if self.line:
121 rv += ":%u" % self.line
122 if self.column: 122 ↛ 123line 122 didn't jump to line 123 because the condition on line 122 was never true
123 rv += ":%u" % self.column
124 return rv
126 def to_html(self):
127 return '<a href="%s" target="_blank">%s</a>' % (self.filename,
128 self.filename)
130 def to_json(self):
131 return {"kind" : "file",
132 "file" : self.filename,
133 "line" : self.line,
134 "column" : self.column}
136 @classmethod
137 def from_json(cls, json):
138 assert isinstance(json, dict)
139 assert json["kind"] == "file"
141 filename = json["file"]
142 line = json.get("line", None)
143 if line is not None: 143 ↛ 146line 143 didn't jump to line 146 because the condition on line 143 was always true
144 column = json.get("column", None)
145 else:
146 column = None
147 return File_Reference(filename, line, column)
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)
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
165 def sorting_key(self):
166 if self.line is not None: 166 ↛ 169line 166 didn't jump to line 169 because the condition on line 166 was always true
167 return (self.filename, self.line)
168 else:
169 return (self.filename,)
171 def to_string(self):
172 if self.line: 172 ↛ 176line 172 didn't jump to line 176 because the condition on line 172 was always true
173 return "%s:%u" % (self.filename,
174 self.line)
175 else:
176 return self.filename
178 def to_html(self):
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
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())
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 }
197 @classmethod
198 def from_json(cls, json):
199 assert isinstance(json, dict)
200 assert json["kind"] == "github"
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)
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)
220 self.cb_root = cb_root
221 self.tracker = tracker
222 self.item = item
223 self.version = version
224 self.name = name
226 def sorting_key(self):
227 return (self.cb_root, self.tracker, self.item)
229 def to_string(self):
230 # lobster-trace: Codebeamer_Item_as_String
231 if self.name: 231 ↛ 234line 231 didn't jump to line 234 because the condition on line 231 was always true
232 return "cb item %u '%s'" % (self.item, self.name)
233 else:
234 return "cb item %u" % self.item
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: 240 ↛ 242line 240 didn't jump to line 242 because the condition on line 240 was always true
241 url += "?version=%u" % self.version
242 return '<a href="%s" target="_blank">%s</a>' % (url, self.to_string())
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}
252 @classmethod
253 def from_json(cls, json):
254 assert isinstance(json, dict)
255 assert json["kind"] == "codebeamer"
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)