Coverage for lobster/tools/core/online_report/online_report.py: 86%

99 statements  

« prev     ^ index     » next       coverage.py v7.16.2, created at 2026-10-19 07:49 +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/>. 

19 

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) 

36 

37LOBSTER_REPORT = "report" 

38COMMIT_ID = "commit_id" 

39BASE_URL = "base_url" 

40REPO_ROOT = "repo_root" 

41 

42 

43@dataclass 

44class Config: 

45 repo_root: str 

46 base_url: str 

47 commit_id: str 

48 report: str = "report.lobster" 

49 

50 

51def load_config(file_name: str) -> Config: 

52 if not os.path.isfile(file_name): 

53 raise FileNotFoundError(f'{file_name} is not an existing file!') 

54 

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) 

61 

62 

63def parse_config_data(config_dict: dict) -> Config: 

64 """Parse a YAML configuration file for the online report tool. 

65 

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. 

69 

70 Args: 

71 config_dict (dict): YAML configuration file converted to dict. 

72 

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. 

80 

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. 

85 

86 """ 

87 if (not config_dict or 

88 COMMIT_ID not in config_dict): 

89 raise KeyError(f'Please follow the right config file structure! ' 

90 f'Missing attribute {COMMIT_ID}') 

91 

92 if BASE_URL not in config_dict: 

93 raise KeyError(f'Please follow the right config file structure! ' 

94 f'Missing attribute {BASE_URL}') 

95 

96 if REPO_ROOT not in config_dict: 

97 raise KeyError(f'Please follow the right config file structure! ' 

98 f'Missing attribute {REPO_ROOT}') 

99 

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") 

104 

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__}.') 

109 

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__}.') 

114 

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__}.') 

119 

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__}.') 

124 

125 return Config( 

126 report=report, 

127 repo_root=repo_root, 

128 base_url=base_url, 

129 commit_id=commit_id 

130 ) 

131 

132 

133def add_github_reference_to_items( 

134 repo_root: str, 

135 base_url: str, 

136 report: Report, 

137 commit_id: str 

138): 

139 

140 repo_root = os.path.abspath(os.path.expanduser(repo_root)) 

141 path_to_url_converter = PathToUrlConverter(repo_root, base_url) 

142 

143 try: 

144 for item in report.items.values(): 

145 if isinstance(item.location, File_Reference): 

146 file_path = Path(item.location.filename).resolve() 

147 try: 

148 url_parts = path_to_url_converter.path_to_url(file_path, 

149 commit_id) 

150 item.location = Github_Reference( 

151 gh_root=url_parts.url_start, 

152 filename=url_parts.path_html, 

153 line=item.location.line, 

154 commit=url_parts.commit_hash, 

155 ) 

156 except NotInsideRepositoryException as e: 

157 print(f"Error converting path to URL for {file_path}: {e}") 

158 continue 

159 finally: 

160 path_to_url_converter.close() 

161 

162 

163class OnlineReportTool(MetaDataToolBase): 

164 def __init__(self): 

165 super().__init__( 

166 name="online-report", 

167 description="Update file locations in LOBSTER report to GitHub references.", 

168 official=True, 

169 ) 

170 self._argument_parser.add_argument( 

171 "--config", 

172 help=("Path to YAML file with arguments, " 

173 "by default (online-report-config.yaml)"), 

174 default="online-report-config.yaml", 

175 ) 

176 self._argument_parser.add_argument( 

177 "--out", 

178 help="output file, by default overwrite input", 

179 default="online_report.lobster", 

180 ) 

181 

182 def _run_impl(self, options: Namespace) -> int: 

183 try: 

184 self._execute(options) 

185 return 0 

186 except FileNotFoundError as file_not_found_error: 

187 self._print_error(file_not_found_error) 

188 except FileExistsError as file_exists_error: 

189 self._print_error(file_exists_error) 

190 except ValueError as value_error: 

191 self._print_error(value_error) 

192 except KeyError as key_error: 

193 self._print_error(key_error) 

194 except LOBSTER_Error as lobster_error: 

195 self._print_error(lobster_error) 

196 return 1 

197 

198 def _print_error(self, error: Union[Exception, str]): 

199 print(f"{self.name}: {error}", file=sys.stderr) 

200 

201 @staticmethod 

202 def _execute(options: Namespace) -> None: 

203 config = load_config(options.config) 

204 lobster_online_report( 

205 config, options.out 

206 ) 

207 

208 

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) 

217 

218 

219def main(args: Optional[Sequence[str]] = None) -> int: 

220 return OnlineReportTool().run(args)