Coverage for lobster/tools/core/online_report/online_report.py: 60%
97 statements
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 11:07 +0000
« prev ^ index » next coverage.py v7.10.6, created at 2025-09-18 11:07 +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"
51def load_config(file_name: str) -> Config:
52 if not os.path.isfile(file_name): 52 ↛ 53line 52 didn't jump to line 53 because the condition on line 52 was never true
53 raise FileNotFoundError(f'{file_name} is not an existing file!')
55 with open(file_name, "r", encoding='utf-8') as file:
56 try:
57 config_dict = yaml.safe_load(file)
58 except yaml.scanner.ScannerError as ex:
59 raise LOBSTER_Exception(message="Invalid config file") from ex
60 return parse_config_data(config_dict)
63def parse_config_data(config_dict: dict) -> Config:
64 """Parse a YAML configuration file for the online report tool.
66 This function reads a YAML configuration file and extracts the necessary
67 configuration parameters for transforming file references to GitHub references
68 in a LOBSTER report.
70 Args:
71 config_dict (dict): YAML configuration file converted to dict.
73 Returns:
74 Config: A configuration object containing the following attributes:
75 - report (str): Path to the input LOBSTER report file
76 (defaults to "report.lobster").
77 - base_url (str): Base URL for GitHub references.
78 - commit_id (str): Git commit ID to use for references.
79 - repo_root (str): Path to the root of the Git repository.
81 Raises:
82 FileNotFoundError: If the specified configuration file doesn't exist.
83 LOBSTER_Exception: If the YAML file has syntax errors.
84 ValueError: If required attributes are missing or have incorrect types.
86 """
87 if (not config_dict or 87 ↛ 89line 87 didn't jump to line 89 because the condition on line 87 was never true
88 COMMIT_ID not in config_dict):
89 raise KeyError(f'Please follow the right config file structure! '
90 f'Missing attribute {COMMIT_ID}')
92 if BASE_URL not in config_dict: 92 ↛ 93line 92 didn't jump to line 93 because the condition on line 92 was never true
93 raise KeyError(f'Please follow the right config file structure! '
94 f'Missing attribute {BASE_URL}')
96 if REPO_ROOT not in config_dict: 96 ↛ 97line 96 didn't jump to line 97 because the condition on line 96 was never true
97 raise KeyError(f'Please follow the right config file structure! '
98 f'Missing attribute {REPO_ROOT}')
100 base_url = config_dict.get(BASE_URL)
101 repo_root = config_dict.get(REPO_ROOT)
102 commit_id = config_dict.get(COMMIT_ID)
103 report = config_dict.get(LOBSTER_REPORT, "report.lobster")
105 if not isinstance(base_url, str): 105 ↛ 106line 105 didn't jump to line 106 because the condition on line 105 was never true
106 raise ValueError(f'Please follow the right config file structure! '
107 f'{BASE_URL} must be a string but got '
108 f'{type(base_url).__name__}.')
110 if not isinstance(repo_root, str): 110 ↛ 111line 110 didn't jump to line 111 because the condition on line 110 was never true
111 raise ValueError(f'Please follow the right config file structure! '
112 f'{REPO_ROOT} must be a string but got '
113 f'{type(repo_root).__name__}.')
115 if not isinstance(commit_id, str): 115 ↛ 116line 115 didn't jump to line 116 because the condition on line 115 was never true
116 raise ValueError(f'Please follow the right config file structure! '
117 f'{COMMIT_ID} must be a string but got '
118 f'{type(commit_id).__name__}.')
120 if not isinstance(report, str): 120 ↛ 121line 120 didn't jump to line 121 because the condition on line 120 was never true
121 raise ValueError(f'Please follow the right config file structure! '
122 f'{LOBSTER_REPORT} must be a string but got '
123 f'{type(report).__name__}.')
125 return Config(
126 report=report,
127 repo_root=repo_root,
128 base_url=base_url,
129 commit_id=commit_id
130 )
133def add_github_reference_to_items(
134 repo_root: str,
135 base_url: str,
136 report: Report,
137 commit_id: str
138):
140 repo_root = os.path.abspath(os.path.expanduser(repo_root))
141 path_to_url_converter = PathToUrlConverter(repo_root, base_url)
143 for item in report.items.values():
144 if isinstance(item.location, File_Reference): 144 ↛ 143line 144 didn't jump to line 143 because the condition on line 144 was always true
145 file_path = Path(item.location.filename).resolve()
146 try:
147 url_parts = path_to_url_converter.path_to_url(file_path, commit_id)
148 item.location = Github_Reference(
149 gh_root=url_parts.url_start,
150 filename=url_parts.path_html,
151 line=item.location.line,
152 commit=url_parts.commit_hash,
153 )
154 except NotInsideRepositoryException as e:
155 print(f"Error converting path to URL for {file_path}: {e}")
156 continue
159class OnlineReportTool(MetaDataToolBase):
160 def __init__(self):
161 super().__init__(
162 name="online-report",
163 description="Update file locations in LOBSTER report to GitHub references.",
164 official=True,
165 )
166 self._argument_parser.add_argument(
167 "--config",
168 help=("Path to YAML file with arguments, "
169 "by default (online-report-config.yaml)"),
170 default="online-report-config.yaml",
171 )
172 self._argument_parser.add_argument(
173 "--out",
174 help="output file, by default overwrite input",
175 default="online_report.lobster",
176 )
178 def _run_impl(self, options: Namespace) -> int:
179 try:
180 self._execute(options)
181 return 0
182 except FileNotFoundError as file_not_found_error:
183 self._print_error(file_not_found_error)
184 except FileExistsError as file_exists_error:
185 self._print_error(file_exists_error)
186 except ValueError as value_error:
187 self._print_error(value_error)
188 except KeyError as key_error:
189 self._print_error(key_error)
190 except LOBSTER_Error as lobster_error:
191 self._print_error(lobster_error)
192 return 1
194 def _print_error(self, error: Union[Exception, str]):
195 print(f"{self.name}: {error}", file=sys.stderr)
197 @staticmethod
198 def _execute(options: Namespace) -> None:
199 config = load_config(options.config)
200 lobster_online_report(
201 config, options.out
202 )
205def lobster_online_report(config: Config, out_file: str) -> None:
206 # This is an API function for Lobster online report tool.
207 report = Report()
208 report.load_report(config.report)
209 add_github_reference_to_items(
210 config.repo_root, config.base_url, report, config.commit_id
211 )
212 report.write_report(out_file)
215def main(args: Optional[Sequence[str]] = None) -> int:
216 return OnlineReportTool().run(args)