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

97 statements  

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

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 for item in report.items.values(): 

144 if isinstance(item.location, File_Reference): 

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 

157 

158 

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 ) 

177 

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 

193 

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

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

196 

197 @staticmethod 

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

199 config = load_config(options.config) 

200 lobster_online_report( 

201 config, options.out 

202 ) 

203 

204 

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) 

213 

214 

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

216 return OnlineReportTool().run(args)