Coverage for lobster/tools/core/online_report/online_report.py: 59%
99 statements
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13:02 +0000
« prev ^ index » next coverage.py v7.10.5, created at 2025-08-27 13:02 +0000
1#!/usr/bin/env python3
2#
3# lobster_online_report - Transform file references to github references
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/>.
20import os
21import sys
22from typing import Optional, Sequence, Union
23from argparse import Namespace
24from pathlib import Path
25from dataclasses import dataclass
26import yaml
27from lobster.common.exceptions import LOBSTER_Exception
28from lobster.common.errors import LOBSTER_Error
29from lobster.common.report import Report
30from lobster.common.location import File_Reference, Github_Reference
31from lobster.common.meta_data_tool_base import MetaDataToolBase
32from lobster.tools.core.online_report.path_to_url_converter import (
33 PathToUrlConverter,
34 NotInsideRepositoryException
35)
37LOBSTER_REPORT = "report"
38COMMIT_ID = "commit_id"
39BASE_URL = "base_url"
40REPO_ROOT = "repo_root"
43@dataclass
44class Config:
45 repo_root: str
46 base_url: str
47 commit_id: str
48 report: str = "report.lobster"
51TOOL_NAME = "lobster-online-report"
54def load_config(file_name: str) -> Config:
55 if not os.path.isfile(file_name):
56 raise FileNotFoundError(f'{file_name} is not an existing file!')
58 with open(file_name, "r", encoding='utf-8') as file:
59 try:
60 config_dict = yaml.safe_load(file)
61 except yaml.scanner.ScannerError as ex:
62 raise LOBSTER_Exception(message="Invalid config file") from ex
63 return parse_config_data(config_dict)
66def parse_config_data(config_dict: dict) -> Config:
67 """Parse a YAML configuration file for the online report tool.
69 This function reads a YAML configuration file and extracts the necessary
70 configuration parameters for transforming file references to GitHub references
71 in a LOBSTER report.
73 Args:
74 config_dict (dict): YAML configuration file converted to dict.
76 Returns:
77 Config: A configuration object containing the following attributes:
78 - report (str): Path to the input LOBSTER report file
79 (defaults to "report.lobster").
80 - base_url (str): Base URL for GitHub references.
81 - commit_id (str): Git commit ID to use for references.
82 - repo_root (str): Path to the root of the Git repository.
84 Raises:
85 FileNotFoundError: If the specified configuration file doesn't exist.
86 LOBSTER_Exception: If the YAML file has syntax errors.
87 ValueError: If required attributes are missing or have incorrect types.
89 """
90 if (not config_dict or
91 COMMIT_ID not in config_dict):
92 raise KeyError(f'Please follow the right config file structure! '
93 f'Missing attribute {COMMIT_ID}')
95 if BASE_URL not in config_dict:
96 raise KeyError(f'Please follow the right config file structure! '
97 f'Missing attribute {BASE_URL}')
99 if REPO_ROOT not in config_dict: 99 ↛ 103line 99 didn't jump to line 103 because the condition on line 99 was always true
100 raise KeyError(f'Please follow the right config file structure! '
101 f'Missing attribute {REPO_ROOT}')
103 base_url = config_dict.get(BASE_URL)
104 repo_root = config_dict.get(REPO_ROOT)
105 commit_id = config_dict.get(COMMIT_ID)
106 report = config_dict.get(LOBSTER_REPORT, "report.lobster")
108 if not isinstance(base_url, str):
109 raise ValueError(f'Please follow the right config file structure! '
110 f'{BASE_URL} must be a string but got '
111 f'{type(base_url).__name__}.')
113 if not isinstance(repo_root, str):
114 raise ValueError(f'Please follow the right config file structure! '
115 f'{REPO_ROOT} must be a string but got '
116 f'{type(repo_root).__name__}.')
118 if not isinstance(commit_id, str):
119 raise ValueError(f'Please follow the right config file structure! '
120 f'{COMMIT_ID} must be a string but got '
121 f'{type(commit_id).__name__}.')
123 if not isinstance(report, str):
124 raise ValueError(f'Please follow the right config file structure! '
125 f'{LOBSTER_REPORT} must be a string but got '
126 f'{type(report).__name__}.')
128 return Config(
129 report=report,
130 repo_root=repo_root,
131 base_url=base_url,
132 commit_id=commit_id
133 )
136def add_github_reference_to_items(
137 repo_root: str,
138 base_url: str,
139 report: Report,
140 commit_id: str
141):
143 repo_root = os.path.abspath(os.path.expanduser(repo_root))
144 path_to_url_converter = PathToUrlConverter(repo_root, base_url)
146 for item in report.items.values():
147 if isinstance(item.location, File_Reference):
148 file_path = Path(item.location.filename).resolve()
149 try:
150 url_parts = path_to_url_converter.path_to_url(file_path, commit_id)
151 item.location = Github_Reference(
152 gh_root=url_parts.url_start,
153 filename=url_parts.path_html,
154 line=item.location.line,
155 commit=url_parts.commit_hash,
156 )
157 except NotInsideRepositoryException as e:
158 print(f"Error converting path to URL for {file_path}: {e}")
159 continue
162class OnlineReportTool(MetaDataToolBase):
163 def __init__(self):
164 super().__init__(
165 name="lobster-online-report",
166 description="Update file locations in LOBSTER report to GitHub references.",
167 official=True,
168 )
169 self._argument_parser.add_argument(
170 "--config",
171 help=("Path to YAML file with arguments, "
172 "by default (online-report-config.yaml)"),
173 default="online-report-config.yaml",
174 )
175 self._argument_parser.add_argument(
176 "--out",
177 help="output file, by default overwrite input",
178 default="online_report.lobster",
179 )
181 def _run_impl(self, options: Namespace) -> int:
182 try:
183 self._execute(options)
184 return 0
185 except FileNotFoundError as file_not_found_error:
186 self._print_error(file_not_found_error)
187 except FileExistsError as file_exists_error:
188 self._print_error(file_exists_error)
189 except ValueError as value_error:
190 self._print_error(value_error)
191 except KeyError as key_error:
192 self._print_error(key_error)
193 except LOBSTER_Error as lobster_error:
194 self._print_error(lobster_error)
195 return 1
197 @staticmethod
198 def _print_error(error: Union[Exception, str]):
199 print(f"{TOOL_NAME}: {error}", file=sys.stderr)
201 @staticmethod
202 def _execute(options: Namespace) -> None:
203 config = load_config(options.config)
204 lobster_online_report(
205 config, options.out
206 )
209def lobster_online_report(config: Config, out_file: str) -> None:
210 # This is an API function for Lobster online report tool.
211 report = Report()
212 report.load_report(config.report)
213 add_github_reference_to_items(
214 config.repo_root, config.base_url, report, config.commit_id
215 )
216 report.write_report(out_file)
219def main(args: Optional[Sequence[str]] = None) -> int:
220 return OnlineReportTool().run(args)