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
« 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/>.
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:
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 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
72class Void_Reference(Location):
73 def __init__(self):
74 pass
76 def sorting_key(self):
77 return tuple()
79 def to_string(self):
80 return "<unknown location>"
82 def to_html(self, source_root=""):
83 return html.escape(self.to_string())
85 def to_json(self):
86 return {"kind": "void"}
88 @classmethod
89 def from_json(cls, json):
90 assert isinstance(json, dict)
91 assert json["kind"] == "void"
92 return Void_Reference()
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
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
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
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>'
125 def to_json(self):
126 return {"kind" : "file",
127 "file" : self.filename,
128 "line" : self.line,
129 "column" : self.column}
131 @classmethod
132 def from_json(cls, json):
133 assert isinstance(json, dict)
134 assert json["kind"] == "file"
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)
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)
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
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,)
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
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}"
175 return f'<a href="{self.gh_root}/blob/{self.commit}/{file_ref}" ' \
176 f'target="_blank">{self.to_string()}</a>'
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 }
186 @classmethod
187 def from_json(cls, json):
188 assert isinstance(json, dict)
189 assert json["kind"] == "github"
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)
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)
209 self.cb_root = cb_root
210 self.tracker = tracker
211 self.item = item
212 self.version = version
213 self.name = name
215 def sorting_key(self):
216 return (self.cb_root, self.tracker, self.item)
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}"
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>'
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}
240 @classmethod
241 def from_json(cls, json):
242 assert isinstance(json, dict)
243 assert json["kind"] == "codebeamer"
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)