Coverage for lobster/location.py: 80%
149 statements
« prev ^ index » next coverage.py v7.10.2, created at 2025-08-06 09:51 +0000
« 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/>.
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":
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
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:
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:
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 ↛ 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)
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:
167 return (self.filename, self.line)
168 else:
169 return (self.filename,)
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
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
182 return f'<a href="{self.gh_root}/blob/{self.commit}/{file_ref}" ' \
183 f'target="_blank">{self.to_string()}</a>'
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 }
193 @classmethod
194 def from_json(cls, json):
195 assert isinstance(json, dict)
196 assert json["kind"] == "github"
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)
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)
216 self.cb_root = cb_root
217 self.tracker = tracker
218 self.item = item
219 self.version = version
220 self.name = name
222 def sorting_key(self):
223 return (self.cb_root, self.tracker, self.item)
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
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())
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}
248 @classmethod
249 def from_json(cls, json):
250 assert isinstance(json, dict)
251 assert json["kind"] == "codebeamer"
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)