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

99 statements  

« 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/>. 

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 

51TOOL_NAME = "lobster-online-report" 

52 

53 

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

55 if not os.path.isfile(file_name): 55 ↛ 56line 55 didn't jump to line 56 because the condition on line 55 was never true

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

57 

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) 

64 

65 

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

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

68 

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. 

72 

73 Args: 

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

75 

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. 

83 

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. 

88 

89 """ 

90 if (not config_dict or 90 ↛ 92line 90 didn't jump to line 92 because the condition on line 90 was never true

91 COMMIT_ID not in config_dict): 

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

93 f'Missing attribute {COMMIT_ID}') 

94 

95 if BASE_URL not in config_dict: 95 ↛ 96line 95 didn't jump to line 96 because the condition on line 95 was never true

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

97 f'Missing attribute {BASE_URL}') 

98 

99 if REPO_ROOT not in config_dict: 99 ↛ 100line 99 didn't jump to line 100 because the condition on line 99 was never true

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

101 f'Missing attribute {REPO_ROOT}') 

102 

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

107 

108 if not isinstance(base_url, str): 108 ↛ 109line 108 didn't jump to line 109 because the condition on line 108 was never true

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

112 

113 if not isinstance(repo_root, str): 113 ↛ 114line 113 didn't jump to line 114 because the condition on line 113 was never true

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

117 

118 if not isinstance(commit_id, str): 118 ↛ 119line 118 didn't jump to line 119 because the condition on line 118 was never true

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

122 

123 if not isinstance(report, str): 123 ↛ 124line 123 didn't jump to line 124 because the condition on line 123 was never true

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

127 

128 return Config( 

129 report=report, 

130 repo_root=repo_root, 

131 base_url=base_url, 

132 commit_id=commit_id 

133 ) 

134 

135 

136def add_github_reference_to_items( 

137 repo_root: str, 

138 base_url: str, 

139 report: Report, 

140 commit_id: str 

141): 

142 

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

144 path_to_url_converter = PathToUrlConverter(repo_root, base_url) 

145 

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

147 if isinstance(item.location, File_Reference): 147 ↛ 146line 147 didn't jump to line 146 because the condition on line 147 was always true

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 

160 

161 

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 ) 

180 

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 

196 

197 @staticmethod 

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

199 print(f"{TOOL_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)