Coverage for src/c2puml/main.py: 62%

120 statements  

« prev     ^ index     » next       coverage.py v7.10.4, created at 2025-08-20 03:53 +0000

1#!/usr/bin/env python3 

2""" 

3Main entry point for C to PlantUML converter 

4 

5Processing Flow: 

61. Parse C/C++ files and generate model.json 

72. Transform model based on configuration 

83. Generate PlantUML files from the transformed model 

9""" 

10 

11import argparse 

12import json 

13import logging 

14import os 

15import sys 

16from pathlib import Path 

17 

18from .config import Config 

19from .core.generator import Generator 

20from .core.parser import Parser 

21from .core.transformer import Transformer 

22 

23 

24def setup_logging(verbose: bool = False) -> None: 

25 level = logging.DEBUG if verbose else logging.INFO 

26 logging.basicConfig( 

27 level=level, 

28 format="%(asctime)s - %(name)s - %(levelname)s - %(message)s", 

29 handlers=[logging.StreamHandler(sys.stdout)], 

30 ) 

31 

32 

33def load_config_from_path(config_path: str) -> dict: 

34 path = Path(config_path) 

35 if path.is_file(): 

36 with open(path, "r", encoding="utf-8") as f: 

37 return json.load(f) 

38 elif path.is_dir(): 

39 # Merge all .json files in the folder 

40 config = {} 

41 for file in path.glob("*.json"): 

42 with open(file, "r", encoding="utf-8") as f: 

43 data = json.load(f) 

44 config.update(data) 

45 return config 

46 else: 

47 raise FileNotFoundError(f"Config path not found: {config_path}") 

48 

49 

50def main() -> int: 

51 parser = argparse.ArgumentParser( 

52 description="C to PlantUML Converter (Simplified CLI)", 

53 formatter_class=argparse.RawDescriptionHelpFormatter, 

54 epilog=""" 

55Usage: 

56 %(prog)s --config config.json [parse|transform|generate] 

57 %(prog)s config_folder [parse|transform|generate] 

58 %(prog)s [parse|transform|generate] # Uses current directory as config folder 

59 %(prog)s # Full workflow (parse, transform, generate) 

60 """, 

61 ) 

62 parser.add_argument( 

63 "--config", 

64 "-c", 

65 type=str, 

66 default=None, 

67 help="Path to config.json or config folder (optional, default: current directory)", 

68 ) 

69 parser.add_argument( 

70 "command", 

71 nargs="?", 

72 choices=["parse", "transform", "generate"], 

73 help="Which step to run: parse, transform, or generate. If omitted, runs full workflow.", 

74 ) 

75 parser.add_argument( 

76 "--verbose", "-v", action="store_true", help="Enable verbose output" 

77 ) 

78 args = parser.parse_args() 

79 

80 setup_logging(args.verbose) 

81 

82 # Determine config path 

83 config_path = args.config 

84 if config_path is None: 

85 config_path = os.getcwd() 

86 

87 logging.info("Using config: %s", config_path) 

88 

89 # Load config 

90 try: 

91 config_data = load_config_from_path(config_path) 

92 config = Config(**config_data) 

93 except Exception as e: 

94 logging.error("Failed to load configuration: %s", e) 

95 return 1 

96 

97 # Determine output folder from config, default to ./output 

98 output_folder = getattr(config, "output_dir", None) or os.path.join( 

99 os.getcwd(), "output" 

100 ) 

101 output_folder = os.path.abspath(output_folder) 

102 os.makedirs(output_folder, exist_ok=True) 

103 logging.info("Output folder: %s", output_folder) 

104 

105 model_file = os.path.join(output_folder, "model.json") 

106 transformed_model_file = os.path.join(output_folder, "model_transformed.json") 

107 

108 # Parse command 

109 if args.command == "parse": 

110 try: 

111 parser_obj = Parser() 

112 # Use the parse function with multiple source folders 

113 parser_obj.parse( 

114 source_folders=config.source_folders, 

115 output_file=model_file, 

116 recursive_search=getattr(config, "recursive_search", True), 

117 config=config, 

118 ) 

119 logging.info("Model saved to: %s", model_file) 

120 return 0 

121 except (OSError, ValueError, RuntimeError) as e: 

122 logging.error("Error during parsing: %s", e) 

123 # Provide additional context for common issues 

124 if "Source folder not found" in str(e): 

125 logging.error("Please check that the source_folders in your configuration exist and are accessible.") 

126 logging.error("You can use absolute paths or relative paths from the current working directory.") 

127 elif "Permission denied" in str(e): 

128 logging.error("Please check file permissions for the source folders.") 

129 elif "Invalid JSON" in str(e): 

130 logging.error("Please check that your configuration file contains valid JSON.") 

131 return 1 

132 

133 # Transform command 

134 if args.command == "transform": 

135 try: 

136 transformer = Transformer() 

137 transformer.transform( 

138 model_file=model_file, 

139 config_file=( 

140 config_path 

141 if Path(config_path).is_file() 

142 else str(list(Path(config_path).glob("*.json"))[0]) 

143 ), 

144 output_file=transformed_model_file, 

145 ) 

146 logging.info("Transformed model saved to: %s", transformed_model_file) 

147 return 0 

148 except (OSError, ValueError, RuntimeError) as e: 

149 logging.error("Error during transformation: %s", e) 

150 return 1 

151 

152 # Generate command 

153 if args.command == "generate": 

154 try: 

155 generator = Generator() 

156 # Apply config for signature truncation and macro display 

157 Generator.max_function_signature_chars = getattr(config, "max_function_signature_chars", 0) 

158 Generator.hide_macro_values = getattr(config, "hide_macro_values", False) 

159 Generator.convert_empty_class_to_artifact = getattr(config, "convert_empty_class_to_artifact", False) 

160 # Prefer transformed model, else fallback to model.json 

161 if os.path.exists(transformed_model_file): 

162 model_to_use = transformed_model_file 

163 elif os.path.exists(model_file): 

164 model_to_use = model_file 

165 else: 

166 logging.error("No model file found for generation.") 

167 logging.error("Please run the parse step first to generate a model file.") 

168 return 1 

169 generator.generate( 

170 model_file=model_to_use, 

171 output_dir=output_folder, 

172 ) 

173 logging.info("PlantUML generation complete! Output in: %s", output_folder) 

174 return 0 

175 except (OSError, ValueError, RuntimeError) as e: 

176 logging.error("Error generating PlantUML: %s", e) 

177 return 1 

178 

179 # Default: full workflow 

180 try: 

181 # Step 1: Parse 

182 parser_obj = Parser() 

183 # Use the parse function with multiple source folders 

184 parser_obj.parse( 

185 source_folders=config.source_folders, 

186 output_file=model_file, 

187 recursive_search=getattr(config, "recursive_search", True), 

188 config=config, 

189 ) 

190 logging.info("Model saved to: %s", model_file) 

191 # Step 2: Transform 

192 transformer = Transformer() 

193 transformer.transform( 

194 model_file=model_file, 

195 config_file=( 

196 config_path 

197 if Path(config_path).is_file() 

198 else str(list(Path(config_path).glob("*.json"))[0]) 

199 ), 

200 output_file=transformed_model_file, 

201 ) 

202 logging.info("Transformed model saved to: %s", transformed_model_file) 

203 # Step 3: Generate 

204 generator = Generator() 

205 # Apply config for signature truncation and macro display 

206 Generator.max_function_signature_chars = getattr(config, "max_function_signature_chars", 0) 

207 Generator.hide_macro_values = getattr(config, "hide_macro_values", False) 

208 Generator.convert_empty_class_to_artifact = getattr(config, "convert_empty_class_to_artifact", False) 

209 generator.generate( 

210 model_file=transformed_model_file, 

211 output_dir=output_folder, 

212 ) 

213 logging.info("PlantUML generation complete! Output in: %s", output_folder) 

214 logging.info("Complete workflow finished successfully!") 

215 return 0 

216 except (OSError, ValueError, RuntimeError) as e: 

217 logging.error("Error in workflow: %s", e) 

218 # Provide additional context for common issues 

219 if "Source folder not found" in str(e): 

220 logging.error("Please check that the source_folders in your configuration exist and are accessible.") 

221 logging.error("You can use absolute paths or relative paths from the current working directory.") 

222 elif "Permission denied" in str(e): 

223 logging.error("Please check file permissions for the source folders.") 

224 elif "Invalid JSON" in str(e): 

225 logging.error("Please check that your configuration file contains valid JSON.") 

226 elif "Configuration must contain" in str(e): 

227 logging.error("Please check that your configuration file has the required 'source_folders' field.") 

228 return 1 

229 

230 

231if __name__ == "__main__": 

232 sys.exit(main())