Coverage for lobster/common/location.py: 81%
150 statements
« prev ^ index » next coverage.py v7.10.7, created at 2026-03-04 12:54 +0000
« 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/>.
20from abc import ABCMeta, abstractmethod
21import html
22from typing import Any, Dict, Optional, Tuple
23from lobster.common.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, source_root="") -> 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, source_root=""):
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, 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)
131 def to_json(self):
132 return {"kind" : "file",
133 "file" : self.filename,
134 "line" : self.line,
135 "column" : self.column}
137 @classmethod
138 def from_json(cls, json):
139 assert isinstance(json, dict)
140 assert json["kind"] == "file"
142 filename = json["file"]
143 line = json.get("line", None)
144 if line is not None:
145 column = json.get("column", None)
146 else:
147 column = None
148 return File_Reference(filename, line, column)
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)
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
166 def sorting_key(self):
167 if self.line is not None:
168 return (self.filename, self.line)
169 else:
170 return (self.filename,)
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
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
183 return f'<a href="{self.gh_root}/blob/{self.commit}/{file_ref}" ' \
184 f'target="_blank">{self.to_string()}</a>'
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 }
194 @classmethod
195 def from_json(cls, json):
196 assert isinstance(json, dict)
197 assert json["kind"] == "github"
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)
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)
217 self.cb_root = cb_root
218 self.tracker = tracker
219 self.item = item
220 self.version = version
221 self.name = name
223 def sorting_key(self):
224 return (self.cb_root, self.tracker, self.item)
226 def to_string(self):
227 # lobster-trace: Codebeamer_Item_as_String
228 if self.name:
229 return "cb item %u '%s'" % (self.item, self.name)
230 else:
231 return "cb item %u" % self.item
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:
238 url += "?version=%u" % self.version
239 return '<a href="%s" target="_blank">%s</a>' % (url, self.to_string())
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}
249 @classmethod
250 def from_json(cls, json):
251 assert isinstance(json, dict)
252 assert json["kind"] == "codebeamer"
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)