/home/noah/src/ruchy/src/bin/handlers/mod.rs
Line | Count | Source |
1 | | use anyhow::{Context, Result}; |
2 | | |
3 | | mod commands; |
4 | | mod handlers_modules; |
5 | | use ruchy::{Parser as RuchyParser, Transpiler}; |
6 | | use ruchy::frontend::ast::Expr; |
7 | | use ruchy::runtime::Repl; |
8 | | use ruchy::runtime::replay_converter::ConversionConfig; |
9 | | // Replay functionality imports removed - not needed in handler, used directly in REPL |
10 | | use std::fs; |
11 | | use std::io::{self, Read}; |
12 | | use std::path::{Path, PathBuf}; |
13 | | |
14 | | /// Handle eval command - evaluate a one-liner expression with -e flag |
15 | | /// |
16 | | /// # Arguments |
17 | | /// * `expr` - The expression to evaluate |
18 | | /// * `verbose` - Enable verbose output |
19 | | /// * `format` - Output format ("json" or default text) |
20 | | /// |
21 | | /// # Examples |
22 | | /// ``` |
23 | | /// // This function is typically called by the CLI with parsed arguments |
24 | | /// // handle_eval_command("2 + 2", false, "text"); |
25 | | /// ``` |
26 | | /// |
27 | | /// # Errors |
28 | | /// Returns error if expression cannot be parsed or evaluated |
29 | 0 | pub fn handle_eval_command(expr: &str, verbose: bool, format: &str) -> Result<()> { |
30 | 0 | if verbose { |
31 | 0 | eprintln!("Parsing expression: {expr}"); |
32 | 0 | } |
33 | | |
34 | 0 | let mut repl = Repl::new()?; |
35 | 0 | match repl.eval(expr) { |
36 | 0 | Ok(result) => { |
37 | 0 | if verbose { |
38 | 0 | eprintln!("Evaluation successful"); |
39 | 0 | } |
40 | | |
41 | 0 | if format == "json" { |
42 | 0 | // Manually construct JSON to ensure field order matches test expectations |
43 | 0 | let result_str = result.replace('"', "\\\""); |
44 | 0 | println!("{{\"success\":true,\"result\":\"{result_str}\"}}"); |
45 | 0 | } else { |
46 | 0 | // Default text output - always show result for one-liner evaluation |
47 | 0 | println!("{result}"); |
48 | 0 | } |
49 | 0 | Ok(()) |
50 | | } |
51 | 0 | Err(e) => { |
52 | 0 | if verbose { |
53 | 0 | eprintln!("Evaluation failed: {e}"); |
54 | 0 | } |
55 | | |
56 | 0 | match format { |
57 | 0 | "json" => { |
58 | 0 | println!( |
59 | 0 | "{}", |
60 | 0 | serde_json::json!({ |
61 | 0 | "success": false, |
62 | 0 | "error": e.to_string() |
63 | 0 | }) |
64 | 0 | ); |
65 | 0 | } |
66 | 0 | _ => { |
67 | 0 | eprintln!("Error: {e}"); |
68 | 0 | } |
69 | | } |
70 | 0 | std::process::exit(1); |
71 | | } |
72 | | } |
73 | 0 | } |
74 | | |
75 | | /// Handle file execution - run a Ruchy script file directly (not via subcommand) |
76 | | /// |
77 | | /// # Arguments |
78 | | /// * `file` - Path to the Ruchy file to execute |
79 | | /// |
80 | | /// # Examples |
81 | | /// ``` |
82 | | /// // This function is typically called by the CLI |
83 | | /// // handle_file_execution(&Path::new("script.ruchy")); |
84 | | /// ``` |
85 | | /// |
86 | | /// # Errors |
87 | | /// Returns error if file cannot be read, parsed, or executed |
88 | 0 | pub fn handle_file_execution(file: &Path) -> Result<()> { |
89 | 0 | let source = fs::read_to_string(file) |
90 | 0 | .with_context(|| format!("Failed to read file: {}", file.display()))?; |
91 | | |
92 | | // Use REPL to evaluate the file |
93 | 0 | let mut repl = Repl::new()?; |
94 | 0 | match repl.eval(&source) { |
95 | 0 | Ok(result) => { |
96 | | // Only print non-unit results |
97 | 0 | if result != "Unit" && result != "()" { |
98 | 0 | println!("{result}"); |
99 | 0 | } |
100 | 0 | Ok(()) |
101 | | } |
102 | 0 | Err(e) => { |
103 | 0 | eprintln!("Error: {e}"); |
104 | 0 | std::process::exit(1); |
105 | | } |
106 | | } |
107 | 0 | } |
108 | | |
109 | | /// Handle stdin/piped input - evaluate input from standard input |
110 | | /// |
111 | | /// # Arguments |
112 | | /// * `input` - The input string to evaluate |
113 | | /// |
114 | | /// # Examples |
115 | | /// ``` |
116 | | /// // This function is typically called when input is piped to the CLI |
117 | | /// // handle_stdin_input("2 + 2"); |
118 | | /// ``` |
119 | | /// |
120 | | /// # Errors |
121 | | /// Returns error if input cannot be parsed or evaluated |
122 | 0 | pub fn handle_stdin_input(input: &str) -> Result<()> { |
123 | 0 | let mut repl = Repl::new()?; |
124 | 0 | match repl.eval(input) { |
125 | 0 | Ok(result) => { |
126 | 0 | println!("{result}"); |
127 | 0 | Ok(()) |
128 | | } |
129 | 0 | Err(e) => { |
130 | 0 | eprintln!("Error: {e}"); |
131 | 0 | std::process::exit(1); |
132 | | } |
133 | | } |
134 | 0 | } |
135 | | |
136 | | /// Handle parse command - show AST for a Ruchy file |
137 | 0 | pub fn handle_parse_command(file: &Path, verbose: bool) -> Result<()> { |
138 | 0 | if verbose { |
139 | 0 | eprintln!("Parsing file: {}", file.display()); |
140 | 0 | } |
141 | | |
142 | 0 | let source = fs::read_to_string(file) |
143 | 0 | .with_context(|| format!("Failed to read file: {}", file.display()))?; |
144 | | |
145 | 0 | let mut parser = RuchyParser::new(&source); |
146 | 0 | match parser.parse() { |
147 | 0 | Ok(ast) => { |
148 | 0 | println!("{ast:#?}"); |
149 | 0 | Ok(()) |
150 | | } |
151 | 0 | Err(e) => { |
152 | 0 | eprintln!("Parse error: {e}"); |
153 | 0 | std::process::exit(1); |
154 | | } |
155 | | } |
156 | 0 | } |
157 | | |
158 | | /// Handle transpile command - convert Ruchy to Rust |
159 | 0 | pub fn handle_transpile_command( |
160 | 0 | file: &Path, |
161 | 0 | output: Option<&Path>, |
162 | 0 | minimal: bool, |
163 | 0 | verbose: bool |
164 | 0 | ) -> Result<()> { |
165 | 0 | log_transpile_start(file, minimal, verbose); |
166 | | |
167 | 0 | let source = read_source_file(file, verbose)?; |
168 | 0 | let ast = parse_source(&source)?; |
169 | 0 | let rust_code = transpile_ast(&ast, minimal)?; |
170 | | |
171 | 0 | write_output(&rust_code, output, verbose)?; |
172 | 0 | Ok(()) |
173 | 0 | } |
174 | | |
175 | | /// Log transpilation start (complexity: 3) |
176 | 0 | fn log_transpile_start(file: &Path, minimal: bool, verbose: bool) { |
177 | 0 | if !verbose { |
178 | 0 | return; |
179 | 0 | } |
180 | 0 | eprintln!("Transpiling file: {}", file.display()); |
181 | 0 | if minimal { |
182 | 0 | eprintln!("Using minimal codegen for self-hosting"); |
183 | 0 | } |
184 | 0 | } |
185 | | |
186 | | /// Read source from file or stdin (complexity: 5) |
187 | 0 | fn read_source_file(file: &Path, verbose: bool) -> Result<String> { |
188 | 0 | if file.as_os_str() == "-" { |
189 | 0 | if verbose { |
190 | 0 | eprintln!("Reading from stdin..."); |
191 | 0 | } |
192 | 0 | let mut input = String::new(); |
193 | 0 | io::stdin().read_to_string(&mut input)?; |
194 | 0 | Ok(input) |
195 | | } else { |
196 | 0 | fs::read_to_string(file) |
197 | 0 | .with_context(|| format!("Failed to read file: {}", file.display())) |
198 | | } |
199 | 0 | } |
200 | | |
201 | | /// Parse source code to AST (complexity: 2) |
202 | 0 | fn parse_source(source: &str) -> Result<Expr> { |
203 | 0 | let mut parser = RuchyParser::new(source); |
204 | 0 | parser.parse() |
205 | 0 | .with_context(|| "Failed to parse input") |
206 | 0 | } |
207 | | |
208 | | /// Transpile AST to Rust code (complexity: 4) |
209 | 0 | fn transpile_ast(ast: &Expr, minimal: bool) -> Result<String> { |
210 | 0 | let mut transpiler = Transpiler::new(); |
211 | 0 | if minimal { |
212 | 0 | transpiler.transpile_minimal(ast) |
213 | 0 | .with_context(|| "Failed to transpile to Rust (minimal)") |
214 | | } else { |
215 | 0 | transpiler.transpile_to_program(ast) |
216 | 0 | .map(|tokens| tokens.to_string()) |
217 | 0 | .with_context(|| "Failed to transpile to Rust") |
218 | | } |
219 | 0 | } |
220 | | |
221 | | /// Write output to file or stdout (complexity: 5) |
222 | 0 | fn write_output(rust_code: &str, output: Option<&Path>, verbose: bool) -> Result<()> { |
223 | 0 | if let Some(output_path) = output { |
224 | 0 | fs::write(output_path, rust_code) |
225 | 0 | .with_context(|| format!("Failed to write output file: {}", output_path.display()))?; |
226 | 0 | if verbose { |
227 | 0 | eprintln!("Output written to: {}", output_path.display()); |
228 | 0 | } |
229 | 0 | } else { |
230 | 0 | print!("{rust_code}"); |
231 | 0 | } |
232 | 0 | Ok(()) |
233 | 0 | } |
234 | | |
235 | | /// Handle run command - compile and execute a Ruchy file |
236 | 0 | pub fn handle_run_command(file: &Path, verbose: bool) -> Result<()> { |
237 | 0 | log_run_start(file, verbose); |
238 | | |
239 | | // Parse and transpile |
240 | 0 | let source = fs::read_to_string(file) |
241 | 0 | .with_context(|| format!("Failed to read file: {}", file.display()))?; |
242 | 0 | let ast = parse_source(&source)?; |
243 | 0 | let rust_code = transpile_for_execution(&ast, file)?; |
244 | | |
245 | | // Compile and execute |
246 | 0 | let (temp_source, binary_path) = prepare_compilation(&rust_code, verbose)?; |
247 | 0 | compile_rust_code(temp_source.path(), &binary_path)?; |
248 | 0 | execute_binary(&binary_path)?; |
249 | | |
250 | 0 | Ok(()) |
251 | 0 | } |
252 | | |
253 | | /// Log run command start (complexity: 2) |
254 | 0 | fn log_run_start(file: &Path, verbose: bool) { |
255 | 0 | if verbose { |
256 | 0 | eprintln!("Running file: {}", file.display()); |
257 | 0 | } |
258 | 0 | } |
259 | | |
260 | | /// Transpile AST for execution with context (complexity: 3) |
261 | 0 | fn transpile_for_execution(ast: &Expr, file: &Path) -> Result<String> { |
262 | 0 | let transpiler = Transpiler::new(); |
263 | 0 | transpiler.transpile_to_program_with_context(ast, Some(file)) |
264 | 0 | .map(|tokens| tokens.to_string()) |
265 | 0 | .with_context(|| "Failed to transpile to Rust") |
266 | 0 | } |
267 | | |
268 | | /// Prepare compilation artifacts (complexity: 4) |
269 | 0 | fn prepare_compilation(rust_code: &str, verbose: bool) -> Result<(tempfile::NamedTempFile, PathBuf)> { |
270 | 0 | let temp_source = tempfile::NamedTempFile::new() |
271 | 0 | .with_context(|| "Failed to create temporary file")?; |
272 | 0 | fs::write(temp_source.path(), rust_code) |
273 | 0 | .with_context(|| "Failed to write temporary file")?; |
274 | | |
275 | 0 | if verbose { |
276 | 0 | eprintln!("Temporary Rust file: {}", temp_source.path().display()); |
277 | 0 | eprintln!("Compiling and running..."); |
278 | 0 | } |
279 | | |
280 | | // Create unique binary path using process ID for temporary compilation output |
281 | 0 | let binary_path = std::env::temp_dir().join(format!("ruchy_temp_bin_{}", std::process::id())); |
282 | | |
283 | 0 | Ok((temp_source, binary_path)) |
284 | 0 | } |
285 | | |
286 | | /// Compile Rust code using rustc (complexity: 5) |
287 | 0 | fn compile_rust_code(source_path: &Path, binary_path: &Path) -> Result<()> { |
288 | 0 | let output = std::process::Command::new("rustc") |
289 | 0 | .arg("--edition=2021") |
290 | 0 | .arg("--crate-name=ruchy_temp") |
291 | 0 | .arg("-o") |
292 | 0 | .arg(binary_path) |
293 | 0 | .arg(source_path) |
294 | 0 | .output() |
295 | 0 | .with_context(|| "Failed to run rustc")?; |
296 | | |
297 | 0 | if !output.status.success() { |
298 | 0 | eprintln!("Compilation failed:"); |
299 | 0 | eprintln!("{}", String::from_utf8_lossy(&output.stderr)); |
300 | 0 | std::process::exit(1); |
301 | 0 | } |
302 | | |
303 | 0 | Ok(()) |
304 | 0 | } |
305 | | |
306 | | /// Execute compiled binary and handle output (complexity: 5) |
307 | 0 | fn execute_binary(binary_path: &Path) -> Result<()> { |
308 | 0 | let run_output = std::process::Command::new(binary_path) |
309 | 0 | .output() |
310 | 0 | .with_context(|| "Failed to run compiled binary")?; |
311 | | |
312 | 0 | print!("{}", String::from_utf8_lossy(&run_output.stdout)); |
313 | 0 | if !run_output.stderr.is_empty() { |
314 | 0 | eprint!("{}", String::from_utf8_lossy(&run_output.stderr)); |
315 | 0 | } |
316 | | |
317 | 0 | if !run_output.status.success() { |
318 | 0 | std::process::exit(run_output.status.code().unwrap_or(1)); |
319 | 0 | } |
320 | | |
321 | 0 | Ok(()) |
322 | 0 | } |
323 | | |
324 | | /// Handle interactive theorem prover (RUCHY-0820) - delegated to refactored module |
325 | 0 | pub fn handle_prove_command( |
326 | 0 | file: Option<&std::path::Path>, |
327 | 0 | backend: &str, |
328 | 0 | ml_suggestions: bool, |
329 | 0 | timeout: u64, |
330 | 0 | script: Option<&std::path::Path>, |
331 | 0 | export: Option<&std::path::Path>, |
332 | 0 | check: bool, |
333 | 0 | counterexample: bool, |
334 | 0 | verbose: bool, |
335 | 0 | format: &str, |
336 | 0 | ) -> anyhow::Result<()> { |
337 | | // Delegate to refactored module with ā¤10 complexity |
338 | 0 | handlers_modules::prove::handle_prove_command( |
339 | 0 | file, |
340 | 0 | backend, |
341 | 0 | ml_suggestions, |
342 | 0 | timeout, |
343 | 0 | script, |
344 | 0 | export, |
345 | 0 | check, |
346 | 0 | counterexample, |
347 | 0 | verbose, |
348 | 0 | format, |
349 | | ) |
350 | 0 | } |
351 | | |
352 | | /// Print prover help - moved to separate function for clarity |
353 | 0 | fn print_prover_help() { |
354 | 0 | println!("\nInteractive Prover Commands:"); |
355 | 0 | println!(" help - Show this help message"); |
356 | 0 | println!(" quit/exit - Exit the prover"); |
357 | 0 | println!(" goals - Show current proof goals"); |
358 | 0 | println!(" tactics - List available tactics"); |
359 | 0 | println!(" goal <stmt> - Add a new proof goal"); |
360 | 0 | println!(" apply <tactic> - Apply a tactic to current goal"); |
361 | 0 | println!("\nTactics:"); |
362 | 0 | println!(" intro - Introduce hypothesis from implication"); |
363 | 0 | println!(" split - Split conjunction into subgoals"); |
364 | 0 | println!(" induction - Proof by induction"); |
365 | 0 | println!(" contradiction - Proof by contradiction"); |
366 | 0 | println!(" reflexivity - Prove equality by reflexivity"); |
367 | 0 | println!(" simplify - Simplify expression"); |
368 | 0 | println!(" assumption - Prove using an assumption"); |
369 | 0 | println!("\nExamples:"); |
370 | 0 | println!(" goal x > 0 -> x + 1 > 1"); |
371 | 0 | println!(" apply intro"); |
372 | 0 | println!(" apply simplify\n"); |
373 | 0 | } |
374 | | |
375 | | /// Handle REPL command - start the interactive Read-Eval-Print Loop |
376 | | /// |
377 | | /// # Examples |
378 | | /// ``` |
379 | | /// // This function is typically called when no command or when repl command is specified |
380 | | /// // handle_repl_command(); |
381 | | /// ``` |
382 | | /// |
383 | | /// # Errors |
384 | | /// Returns error if REPL fails to initialize or run |
385 | 0 | pub fn handle_repl_command(record_file: Option<PathBuf>) -> Result<()> { |
386 | | use colored::Colorize; |
387 | | |
388 | 0 | let version_msg = format!("Welcome to Ruchy REPL v{}", env!("CARGO_PKG_VERSION")); |
389 | 0 | println!("{}", version_msg.bright_cyan().bold()); |
390 | 0 | println!( |
391 | 0 | "Type {} for commands, {} to exit\n", |
392 | 0 | ":help".green(), |
393 | 0 | ":quit".yellow() |
394 | | ); |
395 | | |
396 | 0 | let mut repl = Repl::new()?; |
397 | | |
398 | 0 | if let Some(record_path) = record_file { |
399 | 0 | repl.run_with_recording(&record_path) |
400 | | } else { |
401 | 0 | repl.run() |
402 | | } |
403 | 0 | } |
404 | | |
405 | | /// Handle compile command - compile Ruchy file to native binary |
406 | | /// |
407 | | /// # Arguments |
408 | | /// * `file` - Path to the Ruchy file to compile |
409 | | /// * `output` - Output binary path |
410 | | /// * `opt_level` - Optimization level (0, 1, 2, 3, s, z) |
411 | | /// * `strip` - Strip debug symbols |
412 | | /// * `static_link` - Use static linking |
413 | | /// * `target` - Target triple for cross-compilation |
414 | | /// |
415 | | /// # Examples |
416 | | /// ``` |
417 | | /// // This function is typically called by the CLI compile command |
418 | | /// // handle_compile_command(&Path::new("app.ruchy"), PathBuf::from("app"), "2".to_string(), true, false, None); |
419 | | /// ``` |
420 | | /// |
421 | | /// # Errors |
422 | | /// Returns error if compilation fails or rustc is not available |
423 | 0 | pub fn handle_compile_command( |
424 | 0 | file: &Path, |
425 | 0 | output: PathBuf, |
426 | 0 | opt_level: String, |
427 | 0 | strip: bool, |
428 | 0 | static_link: bool, |
429 | 0 | target: Option<String>, |
430 | 0 | ) -> Result<()> { |
431 | | use ruchy::backend::{CompileOptions, compile_to_binary as backend_compile}; |
432 | | use colored::Colorize; |
433 | | use std::fs; |
434 | | |
435 | | // Check if rustc is available |
436 | 0 | if let Err(e) = ruchy::backend::compiler::check_rustc_available() { |
437 | 0 | eprintln!("{} {}", "Error:".bright_red(), e); |
438 | 0 | eprintln!("Please install Rust toolchain from https://rustup.rs/"); |
439 | 0 | std::process::exit(1); |
440 | 0 | } |
441 | | |
442 | 0 | println!("{} Compiling {}...", "ā".bright_blue(), file.display()); |
443 | | |
444 | 0 | let options = CompileOptions { |
445 | 0 | output, |
446 | 0 | opt_level, |
447 | 0 | strip, |
448 | 0 | static_link, |
449 | 0 | target, |
450 | 0 | rustc_flags: Vec::new(), |
451 | 0 | }; |
452 | | |
453 | 0 | match backend_compile(file, &options) { |
454 | 0 | Ok(binary_path) => { |
455 | 0 | println!( |
456 | 0 | "{} Successfully compiled to: {}", |
457 | 0 | "ā".bright_green(), |
458 | 0 | binary_path.display() |
459 | | ); |
460 | | |
461 | | // Make the binary executable on Unix |
462 | | #[cfg(unix)] |
463 | | { |
464 | | use std::os::unix::fs::PermissionsExt; |
465 | 0 | let mut perms = fs::metadata(&binary_path)?.permissions(); |
466 | 0 | perms.set_mode(0o755); |
467 | 0 | fs::set_permissions(&binary_path, perms)?; |
468 | | } |
469 | | |
470 | 0 | println!( |
471 | 0 | "{} Binary size: {} bytes", |
472 | 0 | "ā¹".bright_blue(), |
473 | 0 | fs::metadata(&binary_path)?.len() |
474 | | ); |
475 | | } |
476 | 0 | Err(e) => { |
477 | 0 | eprintln!("{} Compilation failed: {}", "ā".bright_red(), e); |
478 | 0 | std::process::exit(1); |
479 | | } |
480 | | } |
481 | | |
482 | 0 | Ok(()) |
483 | 0 | } |
484 | | |
485 | | /// Handle check command - check syntax of a Ruchy file |
486 | | /// |
487 | | /// # Arguments |
488 | | /// * `file` - Path to the Ruchy file to check |
489 | | /// * `watch` - Enable file watching mode |
490 | | /// |
491 | | /// # Examples |
492 | | /// ``` |
493 | | /// // This function is typically called by the CLI check command |
494 | | /// // handle_check_command(&Path::new("script.ruchy"), false); |
495 | | /// ``` |
496 | | /// |
497 | | /// # Errors |
498 | | /// Returns error if file cannot be read or has syntax errors |
499 | 0 | pub fn handle_check_command(file: &Path, watch: bool) -> Result<()> { |
500 | 0 | if watch { |
501 | 0 | handle_watch_and_check(file) |
502 | | } else { |
503 | 0 | handle_check_syntax(file) |
504 | | } |
505 | 0 | } |
506 | | |
507 | | /// Check syntax of a single file |
508 | 0 | fn handle_check_syntax(file: &Path) -> Result<()> { |
509 | | use colored::Colorize; |
510 | | |
511 | 0 | let source = fs::read_to_string(file)?; |
512 | 0 | let mut parser = RuchyParser::new(&source); |
513 | 0 | match parser.parse() { |
514 | | Ok(_) => { |
515 | 0 | println!("{}", "ā Syntax is valid".green()); |
516 | 0 | Ok(()) |
517 | | } |
518 | 0 | Err(e) => { |
519 | 0 | eprintln!("{}", format!("ā Syntax error: {e}").red()); |
520 | 0 | std::process::exit(1); |
521 | | } |
522 | | } |
523 | 0 | } |
524 | | |
525 | | /// Watch a file and check syntax on changes |
526 | 0 | fn handle_watch_and_check(file: &Path) -> Result<()> { |
527 | | use colored::Colorize; |
528 | | use std::thread; |
529 | | use std::time::Duration; |
530 | | |
531 | 0 | println!( |
532 | 0 | "{} Watching {} for changes...", |
533 | 0 | "š".bright_cyan(), |
534 | 0 | file.display() |
535 | | ); |
536 | 0 | println!("Press Ctrl+C to stop watching\n"); |
537 | | |
538 | | // Initial check |
539 | 0 | handle_check_syntax(file)?; |
540 | | |
541 | | // Simple file watching using polling |
542 | 0 | let mut last_modified = fs::metadata(file)?.modified()?; |
543 | | |
544 | | loop { |
545 | 0 | thread::sleep(Duration::from_millis(500)); |
546 | | |
547 | 0 | let Ok(metadata) = fs::metadata(file) else { |
548 | 0 | continue; // File might be temporarily unavailable |
549 | | }; |
550 | | |
551 | 0 | let Ok(modified) = metadata.modified() else { |
552 | 0 | continue; |
553 | | }; |
554 | | |
555 | 0 | if modified != last_modified { |
556 | 0 | last_modified = modified; |
557 | 0 | println!("\n{} File changed, checking...", "ā".bright_cyan()); |
558 | 0 | let _ = handle_check_syntax(file); // Don't exit on error, keep watching |
559 | 0 | } |
560 | | } |
561 | 0 | } |
562 | | |
563 | | /// Handle test command - run tests with various options (delegated to refactored module) |
564 | | /// |
565 | | /// # Arguments |
566 | | /// * `path` - Optional path to test directory |
567 | | /// * `watch` - Enable watch mode |
568 | | /// * `verbose` - Enable verbose output |
569 | | /// * `filter` - Optional test filter |
570 | | /// * `coverage` - Enable coverage reporting |
571 | | /// * `coverage_format` - Coverage report format |
572 | | /// * `parallel` - Number of parallel test threads |
573 | | /// * `threshold` - Coverage threshold |
574 | | /// * `format` - Output format |
575 | | /// |
576 | | /// # Examples |
577 | | /// ``` |
578 | | /// // This function is typically called by the CLI test command |
579 | | /// // handle_test_command(None, false, false, None, false, &"text".to_string(), 1, 80.0, &"text".to_string()); |
580 | | /// ``` |
581 | | /// |
582 | | /// # Errors |
583 | | /// Returns error if tests fail to run or coverage threshold is not met |
584 | 0 | pub fn handle_test_command( |
585 | 0 | path: Option<PathBuf>, |
586 | 0 | watch: bool, |
587 | 0 | verbose: bool, |
588 | 0 | filter: Option<&str>, |
589 | 0 | coverage: bool, |
590 | 0 | coverage_format: &str, |
591 | 0 | parallel: usize, |
592 | 0 | threshold: f64, |
593 | 0 | format: &str, |
594 | 0 | ) -> Result<()> { |
595 | | // Delegate to refactored module with ā¤10 complexity |
596 | 0 | handlers_modules::test::handle_test_command( |
597 | 0 | path, |
598 | 0 | watch, |
599 | 0 | verbose, |
600 | 0 | filter, |
601 | 0 | coverage, |
602 | 0 | coverage_format, |
603 | 0 | parallel, |
604 | 0 | threshold, |
605 | 0 | format, |
606 | | ) |
607 | 0 | } |
608 | | |
609 | | /// Handle the coverage command - generate coverage report for Ruchy code |
610 | | /// |
611 | | /// # Arguments |
612 | | /// * `path` - File or directory path to analyze |
613 | | /// * `threshold` - Coverage threshold to check against |
614 | | /// * `format` - Output format (text, html, json) |
615 | | /// * `verbose` - Enable verbose output |
616 | | /// |
617 | | /// # Errors |
618 | | /// Returns error if coverage analysis fails or threshold is not met |
619 | 0 | pub fn handle_coverage_command( |
620 | 0 | path: &Path, |
621 | 0 | threshold: f64, |
622 | 0 | format: &str, |
623 | 0 | verbose: bool, |
624 | 0 | ) -> Result<()> { |
625 | | use ruchy::quality::ruchy_coverage::RuchyCoverageCollector; |
626 | | |
627 | 0 | if verbose { |
628 | 0 | println!("š Analyzing coverage for: {}", path.display()); |
629 | 0 | println!("š Threshold: {:.1}%", threshold); |
630 | 0 | println!("š Format: {}", format); |
631 | 0 | } |
632 | | |
633 | | // Create coverage collector |
634 | 0 | let mut collector = RuchyCoverageCollector::new(); |
635 | | |
636 | | // Execute the file with coverage collection |
637 | 0 | collector.execute_with_coverage(path)?; |
638 | | |
639 | | // Generate the coverage report based on format |
640 | 0 | let report = match format { |
641 | 0 | "html" => collector.generate_html_report(), |
642 | 0 | "json" => collector.generate_json_report(), |
643 | 0 | _ => collector.generate_text_report(), // Default to text |
644 | | }; |
645 | 0 | println!("{}", report); |
646 | | |
647 | | // Check threshold if specified |
648 | 0 | if threshold > 0.0 { |
649 | 0 | if collector.meets_threshold(threshold) { |
650 | 0 | println!("\nā
Coverage meets threshold of {:.1}%", threshold); |
651 | 0 | } else { |
652 | 0 | eprintln!("\nā Coverage below threshold of {:.1}%", threshold); |
653 | 0 | std::process::exit(1); |
654 | | } |
655 | 0 | } |
656 | | |
657 | 0 | Ok(()) |
658 | 0 | } |
659 | | |
660 | | /// Watch and run tests on changes - delegated to refactored module |
661 | 0 | fn handle_watch_and_test(path: &Path, verbose: bool, filter: Option<&str>) -> Result<()> { |
662 | 0 | handlers_modules::test::handle_test_command( |
663 | 0 | Some(path.to_path_buf()), |
664 | | true, // watch mode |
665 | 0 | verbose, |
666 | 0 | filter, |
667 | | false, // coverage |
668 | 0 | "text", |
669 | | 1, |
670 | | 0.0, |
671 | 0 | "text", |
672 | | ) |
673 | 0 | } |
674 | | |
675 | | /// Run enhanced tests - delegated to refactored module |
676 | | #[allow(clippy::unnecessary_wraps)] |
677 | 0 | fn handle_run_enhanced_tests( |
678 | 0 | path: &Path, |
679 | 0 | verbose: bool, |
680 | 0 | filter: Option<&str>, |
681 | 0 | coverage: bool, |
682 | 0 | coverage_format: &str, |
683 | 0 | parallel: usize, |
684 | 0 | threshold: f64, |
685 | 0 | format: &str, |
686 | 0 | ) -> Result<()> { |
687 | 0 | handlers_modules::test::handle_test_command( |
688 | 0 | Some(path.to_path_buf()), |
689 | | false, // not watch mode |
690 | 0 | verbose, |
691 | 0 | filter, |
692 | 0 | coverage, |
693 | 0 | coverage_format, |
694 | 0 | parallel, |
695 | 0 | threshold, |
696 | 0 | format, |
697 | | ) |
698 | 0 | } |
699 | | |
700 | | /// Run a single .ruchy test file - delegated to `test_helpers` module |
701 | 0 | fn run_ruchy_test_file(test_file: &Path, verbose: bool) -> Result<()> { |
702 | 0 | handlers_modules::test_helpers::run_test_file(test_file, verbose) |
703 | 0 | } |
704 | | |
705 | | /// Verify proofs extracted from AST - delegated to `prove_helpers` module |
706 | 0 | fn verify_proofs_from_ast( |
707 | 0 | ast: &ruchy::frontend::ast::Expr, |
708 | 0 | file_path: &std::path::Path, |
709 | 0 | format: &str, |
710 | 0 | counterexample: bool, |
711 | 0 | verbose: bool |
712 | 0 | ) -> Result<()> { |
713 | 0 | handlers_modules::prove_helpers::verify_proofs_from_ast( |
714 | 0 | ast, |
715 | 0 | file_path, |
716 | 0 | format, |
717 | 0 | counterexample, |
718 | 0 | verbose, |
719 | | ) |
720 | 0 | } |
721 | | /// |
722 | | /// # Arguments |
723 | | /// * `command` - The command to execute |
724 | | /// |
725 | | /// # Examples |
726 | | /// ``` |
727 | | /// // This function is typically called by the main dispatcher for complex commands |
728 | | /// ``` |
729 | | /// |
730 | | /// # Errors |
731 | | /// Returns error if command execution fails |
732 | | #[allow(clippy::unnecessary_wraps)] |
733 | 0 | pub fn handle_complex_command(command: crate::Commands) -> Result<()> { |
734 | 0 | match command { |
735 | | crate::Commands::Ast { |
736 | 0 | file, |
737 | 0 | json, |
738 | 0 | graph, |
739 | 0 | metrics, |
740 | 0 | symbols, |
741 | 0 | deps, |
742 | 0 | verbose, |
743 | 0 | output |
744 | | } => { |
745 | 0 | commands::handle_ast_command( |
746 | 0 | &file, |
747 | 0 | json, |
748 | 0 | graph, |
749 | 0 | metrics, |
750 | 0 | symbols, |
751 | 0 | deps, |
752 | 0 | verbose, |
753 | 0 | output.as_deref(), |
754 | | ) |
755 | | } |
756 | | crate::Commands::Provability { |
757 | 0 | file, |
758 | 0 | verify, |
759 | 0 | contracts, |
760 | 0 | invariants, |
761 | 0 | termination, |
762 | 0 | bounds, |
763 | 0 | verbose, |
764 | 0 | output |
765 | | } => { |
766 | 0 | commands::handle_provability_command( |
767 | 0 | &file, |
768 | 0 | verify, |
769 | 0 | contracts, |
770 | 0 | invariants, |
771 | 0 | termination, |
772 | 0 | bounds, |
773 | 0 | verbose, |
774 | 0 | output.as_deref(), |
775 | | ) |
776 | | } |
777 | | crate::Commands::Runtime { |
778 | 0 | file, |
779 | 0 | profile, |
780 | 0 | bigo, |
781 | 0 | bench, |
782 | 0 | compare, |
783 | 0 | memory, |
784 | 0 | verbose, |
785 | 0 | output |
786 | | } => { |
787 | 0 | commands::handle_runtime_command( |
788 | 0 | &file, |
789 | 0 | profile, |
790 | 0 | bigo, |
791 | 0 | bench, |
792 | 0 | compare.as_deref(), |
793 | 0 | memory, |
794 | 0 | verbose, |
795 | 0 | output.as_deref(), |
796 | | ) |
797 | | } |
798 | | crate::Commands::Score { |
799 | 0 | path, |
800 | 0 | depth, |
801 | 0 | fast, |
802 | 0 | deep, |
803 | 0 | watch, |
804 | 0 | explain, |
805 | 0 | baseline, |
806 | 0 | min, |
807 | 0 | config, |
808 | 0 | format, |
809 | 0 | verbose, |
810 | 0 | output, |
811 | | } => { |
812 | 0 | commands::handle_score_command( |
813 | 0 | &path, |
814 | 0 | &depth, |
815 | 0 | fast, |
816 | 0 | deep, |
817 | 0 | watch, |
818 | 0 | explain, |
819 | 0 | baseline.as_deref(), |
820 | 0 | min, |
821 | 0 | config.as_deref(), |
822 | 0 | &format, |
823 | 0 | verbose, |
824 | 0 | output.as_deref(), |
825 | | ) |
826 | | } |
827 | | crate::Commands::QualityGate { |
828 | 0 | path, |
829 | 0 | config, |
830 | | depth: _, |
831 | 0 | fail_fast, |
832 | 0 | format, |
833 | 0 | export, |
834 | | ci: _, |
835 | 0 | verbose, |
836 | | } => { |
837 | | // Simplified quality gate handling |
838 | 0 | commands::handle_quality_gate_command( |
839 | 0 | &path, |
840 | 0 | config.as_deref(), |
841 | 0 | fail_fast, // Use as strict |
842 | 0 | !verbose, // Use as quiet |
843 | 0 | format == "json", |
844 | 0 | verbose, |
845 | 0 | None, // No output field |
846 | 0 | export.as_deref(), |
847 | | ) |
848 | | } |
849 | | crate::Commands::Fmt { |
850 | 0 | file, |
851 | 0 | all, |
852 | 0 | check, |
853 | 0 | stdout, |
854 | 0 | diff, |
855 | 0 | config, |
856 | | line_width: _, |
857 | | indent: _, |
858 | | use_tabs: _, |
859 | | } => { |
860 | | // Simplified fmt handling |
861 | 0 | commands::handle_fmt_command( |
862 | 0 | &file, |
863 | 0 | check, |
864 | 0 | !check && !stdout, // write if not check or stdout |
865 | 0 | config.as_deref(), |
866 | 0 | all, |
867 | 0 | diff, |
868 | 0 | stdout, |
869 | | false, // verbose not available |
870 | | ) |
871 | | } |
872 | | crate::Commands::Lint { |
873 | 0 | file, |
874 | | all: _, |
875 | 0 | fix, |
876 | 0 | strict, |
877 | 0 | verbose, |
878 | 0 | format, |
879 | 0 | rules, |
880 | | deny_warnings: _, |
881 | | max_complexity: _, |
882 | 0 | config, |
883 | 0 | init_config, |
884 | | } => { |
885 | 0 | if init_config { |
886 | | // Create default lint config |
887 | 0 | println!("Creating default lint configuration..."); |
888 | 0 | Ok(()) |
889 | 0 | } else if let Some(file_path) = file { |
890 | 0 | commands::handle_lint_command( |
891 | 0 | &file_path, |
892 | 0 | fix, |
893 | 0 | strict, |
894 | 0 | rules.as_deref(), |
895 | 0 | format == "json", |
896 | 0 | verbose, |
897 | 0 | None, // ignore not available |
898 | 0 | config.as_deref(), |
899 | | ) |
900 | | } else { |
901 | 0 | eprintln!("Error: Either provide a file or use --all flag"); |
902 | 0 | std::process::exit(1); |
903 | | } |
904 | | } |
905 | | crate::Commands::Prove { |
906 | 0 | file, |
907 | 0 | backend, |
908 | 0 | ml_suggestions, |
909 | 0 | timeout, |
910 | 0 | script, |
911 | 0 | export, |
912 | 0 | check, |
913 | 0 | counterexample, |
914 | 0 | verbose, |
915 | 0 | format, |
916 | | } => { |
917 | 0 | handle_prove_command( |
918 | 0 | file.as_deref(), |
919 | 0 | &backend, |
920 | 0 | ml_suggestions, |
921 | 0 | timeout, |
922 | 0 | script.as_deref(), |
923 | 0 | export.as_deref(), |
924 | 0 | check, |
925 | 0 | counterexample, |
926 | 0 | verbose, |
927 | 0 | &format, |
928 | | ) |
929 | | } |
930 | 0 | crate::Commands::Coverage { path, threshold, format, verbose } => { |
931 | 0 | handle_coverage_command(&path, threshold.unwrap_or(80.0), &format, verbose) |
932 | | } |
933 | 0 | crate::Commands::ReplayToTests { input, output, property_tests, benchmarks, timeout } => { |
934 | 0 | handle_replay_to_tests_command(&input, output.as_deref(), property_tests, benchmarks, timeout) |
935 | | } |
936 | | _ => { |
937 | | // Other commands not yet implemented |
938 | 0 | eprintln!("Command not yet implemented"); |
939 | 0 | Ok(()) |
940 | | } |
941 | | } |
942 | | |
943 | | /* |
944 | | // Original complex command handling - commented out until handlers implemented |
945 | | match command { |
946 | | crate::Commands::Ast { |
947 | | file, |
948 | | json, |
949 | | graph, |
950 | | metrics, |
951 | | symbols, |
952 | | deps, |
953 | | verbose, |
954 | | output |
955 | | } => { |
956 | | // AST analysis implementation planned |
957 | | eprintln!("AST analysis not yet implemented"); |
958 | | Ok(()) |
959 | | } |
960 | | crate::Commands::Provability { |
961 | | file, |
962 | | verify, |
963 | | contracts, |
964 | | invariants, |
965 | | termination, |
966 | | bounds, |
967 | | verbose, |
968 | | output |
969 | | } => { |
970 | | // Provability analysis implementation planned |
971 | | eprintln!("Provability analysis not yet implemented"); |
972 | | Ok(()) |
973 | | } |
974 | | crate::Commands::Runtime { |
975 | | file, |
976 | | profile, |
977 | | bigo, |
978 | | bench, |
979 | | compare, |
980 | | memory, |
981 | | verbose, |
982 | | output |
983 | | } => { |
984 | | // Runtime analysis implementation planned |
985 | | eprintln!("Runtime analysis not yet implemented"); |
986 | | Ok(()) |
987 | | } |
988 | | crate::Commands::Score { |
989 | | path, |
990 | | depth, |
991 | | fast, |
992 | | deep, |
993 | | watch, |
994 | | explain, |
995 | | baseline, |
996 | | min, |
997 | | config, |
998 | | format, |
999 | | verbose, |
1000 | | output, |
1001 | | } => { |
1002 | | let baseline_str = baseline.as_deref(); |
1003 | | let config_str = config.as_ref().and_then(|p| p.to_str()); |
1004 | | let output_str = output.as_ref().and_then(|p| p.to_str()); |
1005 | | // Quality score calculation implementation planned |
1006 | | eprintln!("Quality score calculation not yet implemented"); |
1007 | | Ok(()) |
1008 | | } |
1009 | | crate::Commands::QualityGate { |
1010 | | path, |
1011 | | config, |
1012 | | depth, |
1013 | | fail_fast, |
1014 | | format, |
1015 | | export, |
1016 | | ci, |
1017 | | verbose, |
1018 | | } => { |
1019 | | // Quality gates implementation planned |
1020 | | eprintln!("Quality gates enforcement not yet implemented"); |
1021 | | Ok(()) |
1022 | | } |
1023 | | crate::Commands::Fmt { |
1024 | | file: _, |
1025 | | all: _, |
1026 | | check: _, |
1027 | | stdout: _, |
1028 | | diff: _, |
1029 | | config: _, |
1030 | | line_width: _, |
1031 | | indent: _, |
1032 | | use_tabs: _, |
1033 | | } => { |
1034 | | // Code formatting implementation planned |
1035 | | eprintln!("Code formatting not yet implemented"); |
1036 | | Ok(()) |
1037 | | } |
1038 | | crate::Commands::Doc { |
1039 | | path, |
1040 | | output, |
1041 | | format, |
1042 | | private, |
1043 | | open, |
1044 | | all, |
1045 | | verbose, |
1046 | | } => { |
1047 | | // Documentation generation implementation planned |
1048 | | eprintln!("Documentation generation not yet implemented"); |
1049 | | Ok(()) |
1050 | | } |
1051 | | crate::Commands::Bench { |
1052 | | file, |
1053 | | iterations, |
1054 | | warmup, |
1055 | | format, |
1056 | | output, |
1057 | | verbose, |
1058 | | } => { |
1059 | | crate::benchmark_ruchy_code( |
1060 | | &file, |
1061 | | iterations, |
1062 | | warmup, |
1063 | | &format, |
1064 | | output.as_deref(), |
1065 | | verbose, |
1066 | | ) |
1067 | | } |
1068 | | crate::Commands::Lint { |
1069 | | file, |
1070 | | all: _, |
1071 | | fix, |
1072 | | strict, |
1073 | | verbose, |
1074 | | format, |
1075 | | rules, |
1076 | | deny_warnings: _, |
1077 | | max_complexity: _, |
1078 | | config, |
1079 | | init_config, |
1080 | | } => { |
1081 | | if init_config { |
1082 | | crate::generate_default_lint_config() |
1083 | | } else { |
1084 | | // Load custom rules if config provided |
1085 | | let custom_rules = if let Some(config_path) = config { |
1086 | | crate::load_custom_lint_rules(&config_path)? |
1087 | | } else { |
1088 | | // Custom lint rules implementation planned |
1089 | | Default::default() |
1090 | | }; |
1091 | | |
1092 | | if all { |
1093 | | crate::lint_ruchy_code( |
1094 | | &PathBuf::from("."), |
1095 | | all, |
1096 | | fix, |
1097 | | strict, |
1098 | | verbose, |
1099 | | &format, |
1100 | | rules.as_deref(), |
1101 | | deny_warnings, |
1102 | | max_complexity, |
1103 | | &custom_rules, |
1104 | | ) |
1105 | | } else if let Some(file) = file { |
1106 | | crate::lint_ruchy_code( |
1107 | | &file, |
1108 | | false, |
1109 | | fix, |
1110 | | strict, |
1111 | | verbose, |
1112 | | &format, |
1113 | | rules.as_deref(), |
1114 | | deny_warnings, |
1115 | | max_complexity, |
1116 | | &custom_rules, |
1117 | | ) |
1118 | | } else { |
1119 | | eprintln!("Error: Either provide a file or use --all flag"); |
1120 | | std::process::exit(1); |
1121 | | } |
1122 | | } |
1123 | | } |
1124 | | crate::Commands::Add { |
1125 | | package, |
1126 | | version, |
1127 | | dev, |
1128 | | registry, |
1129 | | } => { |
1130 | | crate::add_package(&package, version.as_deref(), dev, ®istry) |
1131 | | } |
1132 | | crate::Commands::Publish { |
1133 | | registry, |
1134 | | version, |
1135 | | dry_run, |
1136 | | allow_dirty, |
1137 | | } => { |
1138 | | crate::publish_package(®istry, version.as_deref(), dry_run, allow_dirty) |
1139 | | } |
1140 | | crate::Commands::Mcp { |
1141 | | name, |
1142 | | streaming, |
1143 | | timeout, |
1144 | | min_score, |
1145 | | max_complexity, |
1146 | | verbose, |
1147 | | config, |
1148 | | } => { |
1149 | | let config_str = config.as_ref().and_then(|p| p.to_str()); |
1150 | | crate::start_mcp_server(&name, streaming, timeout, min_score, max_complexity, verbose, config_str) |
1151 | | } |
1152 | | crate::Commands::Optimize { |
1153 | | file, |
1154 | | hardware, |
1155 | | depth, |
1156 | | cache, |
1157 | | branches, |
1158 | | vectorization, |
1159 | | abstractions, |
1160 | | benchmark, |
1161 | | format, |
1162 | | output, |
1163 | | verbose, |
1164 | | threshold, |
1165 | | } => { |
1166 | | crate::optimize_file( |
1167 | | &file, |
1168 | | &hardware, |
1169 | | &depth, |
1170 | | cache, |
1171 | | branches, |
1172 | | vectorization, |
1173 | | abstractions, |
1174 | | benchmark, |
1175 | | &format, |
1176 | | output.as_deref(), |
1177 | | verbose, |
1178 | | threshold, |
1179 | | ) |
1180 | | } |
1181 | | crate::Commands::ActorObserve { |
1182 | | config, |
1183 | | refresh_interval, |
1184 | | max_traces, |
1185 | | max_actors, |
1186 | | enable_deadlock_detection, |
1187 | | deadlock_interval, |
1188 | | start_mode, |
1189 | | no_color, |
1190 | | format, |
1191 | | export, |
1192 | | duration, |
1193 | | verbose, |
1194 | | filter_actor, |
1195 | | filter_failed, |
1196 | | filter_slow, |
1197 | | } => { |
1198 | | crate::start_actor_observatory( |
1199 | | config.as_ref(), |
1200 | | refresh_interval, |
1201 | | max_traces, |
1202 | | max_actors, |
1203 | | enable_deadlock_detection, |
1204 | | deadlock_interval, |
1205 | | &start_mode, |
1206 | | !no_color, |
1207 | | &format, |
1208 | | export.as_ref(), |
1209 | | duration, |
1210 | | verbose, |
1211 | | filter_actor.as_ref(), |
1212 | | filter_failed, |
1213 | | filter_slow, |
1214 | | ) |
1215 | | } |
1216 | | crate::Commands::DataflowDebug { |
1217 | | config, |
1218 | | max_rows, |
1219 | | auto_materialize, |
1220 | | enable_profiling, |
1221 | | timeout, |
1222 | | track_memory, |
1223 | | compute_diffs, |
1224 | | sample_rate, |
1225 | | refresh_interval, |
1226 | | no_color, |
1227 | | format, |
1228 | | export, |
1229 | | verbose, |
1230 | | breakpoint, |
1231 | | start_mode, |
1232 | | } => { |
1233 | | crate::start_dataflow_debugger( |
1234 | | config.as_ref(), |
1235 | | max_rows, |
1236 | | auto_materialize, |
1237 | | enable_profiling, |
1238 | | timeout, |
1239 | | track_memory, |
1240 | | compute_diffs, |
1241 | | sample_rate, |
1242 | | refresh_interval, |
1243 | | !no_color, |
1244 | | &format, |
1245 | | export.as_ref(), |
1246 | | verbose, |
1247 | | &breakpoint, |
1248 | | &start_mode, |
1249 | | ) |
1250 | | } |
1251 | | crate::Commands::Wasm { |
1252 | | file, |
1253 | | output, |
1254 | | target, |
1255 | | wit, |
1256 | | deploy, |
1257 | | deploy_target, |
1258 | | portability, |
1259 | | opt_level, |
1260 | | debug, |
1261 | | simd, |
1262 | | threads, |
1263 | | component_model, |
1264 | | name, |
1265 | | version, |
1266 | | verbose, |
1267 | | } => { |
1268 | | crate::handle_wasm_command( |
1269 | | &file, |
1270 | | output.as_deref(), |
1271 | | &target, |
1272 | | wit, |
1273 | | deploy, |
1274 | | deploy_target.as_deref(), |
1275 | | portability, |
1276 | | &opt_level, |
1277 | | debug, |
1278 | | simd, |
1279 | | threads, |
1280 | | component_model, |
1281 | | name.as_deref(), |
1282 | | &version, |
1283 | | verbose, |
1284 | | ) |
1285 | | } |
1286 | | _ => { |
1287 | | // This should not be reached since handled commands are processed elsewhere |
1288 | | eprintln!("Error: Command not implemented in complex handler"); |
1289 | | std::process::exit(1); |
1290 | | } |
1291 | | } |
1292 | | */ |
1293 | 0 | } |
1294 | | |
1295 | | /// Handle replay-to-tests command - convert .replay files to regression tests |
1296 | | /// |
1297 | | /// # Arguments |
1298 | | /// * `input` - Input replay file or directory containing .replay files |
1299 | | /// * `output` - Optional output test file path |
1300 | | /// * `property_tests` - Whether to include property tests |
1301 | | /// * `benchmarks` - Whether to include benchmarks |
1302 | | /// * `timeout` - Test timeout in milliseconds |
1303 | | /// |
1304 | | /// # Examples |
1305 | | /// ``` |
1306 | | /// // Convert single replay file |
1307 | | /// handle_replay_to_tests_command(Path::new("demo.replay"), None, true, false, 5000); |
1308 | | /// |
1309 | | /// // Convert directory of replay files |
1310 | | /// handle_replay_to_tests_command(Path::new("demos/"), Some(Path::new("tests/replays.rs")), true, true, 10000); |
1311 | | /// ``` |
1312 | | /// |
1313 | | /// # Errors |
1314 | | /// Returns error if replay files can't be read or test files can't be written |
1315 | | |
1316 | | /// Setup conversion configuration for replay-to-test conversion (complexity: 4) |
1317 | 0 | fn setup_conversion_config(property_tests: bool, benchmarks: bool, timeout: u64) -> ConversionConfig { |
1318 | 0 | ConversionConfig { |
1319 | 0 | test_module_prefix: "replay_generated".to_string(), |
1320 | 0 | include_property_tests: property_tests, |
1321 | 0 | include_benchmarks: benchmarks, |
1322 | 0 | timeout_ms: timeout, |
1323 | 0 | } |
1324 | 0 | } |
1325 | | |
1326 | | /// Determine output path, using default if none provided (complexity: 3) |
1327 | 0 | fn determine_output_path(output: Option<&Path>) -> &Path { |
1328 | 0 | let default_output = Path::new("tests/generated_from_replays.rs"); |
1329 | 0 | output.unwrap_or(default_output) |
1330 | 0 | } |
1331 | | |
1332 | | /// Validate that file has .replay extension (complexity: 3) |
1333 | 0 | fn validate_replay_file(path: &Path) -> Result<()> { |
1334 | 0 | if path.extension().and_then(|s| s.to_str()) == Some("replay") { |
1335 | 0 | Ok(()) |
1336 | | } else { |
1337 | 0 | eprintln!("ā Input file must have .replay extension"); |
1338 | 0 | Err(anyhow::anyhow!("Invalid file extension")) |
1339 | | } |
1340 | 0 | } |
1341 | | |
1342 | | /// Process a single .replay file (complexity: 8) |
1343 | 0 | fn process_single_file( |
1344 | 0 | input: &Path, |
1345 | 0 | converter: &ruchy::runtime::replay_converter::ReplayConverter, |
1346 | 0 | all_tests: &mut Vec<ruchy::runtime::replay_converter::GeneratedTest>, |
1347 | 0 | processed_files: &mut usize |
1348 | 0 | ) -> Result<()> { |
1349 | | |
1350 | | |
1351 | 0 | validate_replay_file(input)?; |
1352 | | |
1353 | 0 | println!("š Processing replay file: {}", input.display()); |
1354 | | |
1355 | 0 | match converter.convert_file(input) { |
1356 | 0 | Ok(tests) => { |
1357 | 0 | println!(" ā
Generated {} tests", tests.len()); |
1358 | 0 | all_tests.extend(tests); |
1359 | 0 | *processed_files += 1; |
1360 | 0 | Ok(()) |
1361 | | } |
1362 | 0 | Err(e) => { |
1363 | 0 | eprintln!(" ā Failed to process {}: {}", input.display(), e); |
1364 | 0 | Err(e) |
1365 | | } |
1366 | | } |
1367 | 0 | } |
1368 | | |
1369 | | /// Process directory containing .replay files (complexity: 10) |
1370 | 0 | fn process_directory( |
1371 | 0 | input: &Path, |
1372 | 0 | converter: &ruchy::runtime::replay_converter::ReplayConverter, |
1373 | 0 | all_tests: &mut Vec<ruchy::runtime::replay_converter::GeneratedTest>, |
1374 | 0 | processed_files: &mut usize |
1375 | 0 | ) -> Result<()> { |
1376 | | |
1377 | | use std::fs; |
1378 | | |
1379 | 0 | println!("š Processing replay directory: {}", input.display()); |
1380 | | |
1381 | | // Find all .replay files in directory |
1382 | 0 | let replay_files: Vec<_> = fs::read_dir(input)? |
1383 | 0 | .filter_map(|entry| { |
1384 | 0 | let entry = entry.ok()?; |
1385 | 0 | let path = entry.path(); |
1386 | 0 | if path.is_file() && path.extension()? == "replay" { |
1387 | 0 | Some(path) |
1388 | | } else { |
1389 | 0 | None |
1390 | | } |
1391 | 0 | }) |
1392 | 0 | .collect(); |
1393 | | |
1394 | 0 | if replay_files.is_empty() { |
1395 | 0 | println!("ā ļø No .replay files found in directory"); |
1396 | 0 | return Ok(()); |
1397 | 0 | } |
1398 | | |
1399 | 0 | println!("š Found {} replay files", replay_files.len()); |
1400 | | |
1401 | 0 | for replay_file in replay_files { |
1402 | 0 | println!("š Processing: {}", replay_file.display()); |
1403 | | |
1404 | 0 | match converter.convert_file(&replay_file) { |
1405 | 0 | Ok(tests) => { |
1406 | 0 | println!(" ā
Generated {} tests", tests.len()); |
1407 | 0 | all_tests.extend(tests); |
1408 | 0 | *processed_files += 1; |
1409 | 0 | } |
1410 | 0 | Err(e) => { |
1411 | 0 | eprintln!(" ā ļø Failed to process {}: {}", replay_file.display(), e); |
1412 | 0 | // Continue with other files instead of failing completely |
1413 | 0 | } |
1414 | | } |
1415 | | } |
1416 | | |
1417 | 0 | Ok(()) |
1418 | 0 | } |
1419 | | |
1420 | | /// Write test output to file, creating directories if needed (complexity: 4) |
1421 | 0 | fn write_test_output( |
1422 | 0 | converter: &ruchy::runtime::replay_converter::ReplayConverter, |
1423 | 0 | all_tests: &[ruchy::runtime::replay_converter::GeneratedTest], |
1424 | 0 | output_path: &Path |
1425 | 0 | ) -> Result<()> { |
1426 | | use std::fs; |
1427 | | use anyhow::Context; |
1428 | | |
1429 | | // Create output directory if needed |
1430 | 0 | if let Some(parent) = output_path.parent() { |
1431 | 0 | fs::create_dir_all(parent)?; |
1432 | 0 | } |
1433 | | |
1434 | 0 | println!("š Writing tests to: {}", output_path.display()); |
1435 | | |
1436 | 0 | converter.write_tests(all_tests, output_path) |
1437 | 0 | .context("Failed to write test file")?; |
1438 | | |
1439 | 0 | Ok(()) |
1440 | 0 | } |
1441 | | |
1442 | | /// Generate comprehensive summary report of conversion results (complexity: 8) |
1443 | 0 | fn generate_summary_report( |
1444 | 0 | all_tests: &[ruchy::runtime::replay_converter::GeneratedTest], |
1445 | 0 | processed_files: usize |
1446 | 0 | ) { |
1447 | | use colored::Colorize; |
1448 | | use std::collections::{HashMap, HashSet}; |
1449 | | |
1450 | 0 | println!("\n{}", "š Conversion Summary".bright_green().bold()); |
1451 | 0 | println!("====================================="); |
1452 | 0 | println!("š Files processed: {}", processed_files); |
1453 | 0 | println!("ā
Tests generated: {}", all_tests.len()); |
1454 | | |
1455 | | // Breakdown by test category |
1456 | 0 | let mut category_counts = HashMap::new(); |
1457 | 0 | let mut coverage_areas = HashSet::new(); |
1458 | | |
1459 | 0 | for test in all_tests { |
1460 | 0 | *category_counts.entry(&test.category).or_insert(0) += 1; |
1461 | 0 | coverage_areas.extend(test.coverage_areas.iter().cloned()); |
1462 | 0 | } |
1463 | | |
1464 | 0 | println!("\nš Test Breakdown:"); |
1465 | 0 | for (category, count) in category_counts { |
1466 | 0 | println!(" {:?}: {}", category, count); |
1467 | 0 | } |
1468 | | |
1469 | 0 | println!("\nšÆ Coverage Areas: {} unique areas", coverage_areas.len()); |
1470 | 0 | if !coverage_areas.is_empty() { |
1471 | 0 | let mut areas: Vec<_> = coverage_areas.into_iter().collect(); |
1472 | 0 | areas.sort(); |
1473 | 0 | for area in areas.iter().take(10) { // Show first 10 |
1474 | 0 | println!(" ⢠{}", area); |
1475 | 0 | } |
1476 | 0 | if areas.len() > 10 { |
1477 | 0 | println!(" ... and {} more", areas.len() - 10); |
1478 | 0 | } |
1479 | 0 | } |
1480 | | |
1481 | 0 | println!("\nš” Next Steps:"); |
1482 | 0 | println!(" 1. Run tests: cargo test"); |
1483 | 0 | println!(" 2. Measure coverage: cargo test -- --test-threads=1"); |
1484 | 0 | println!(" 3. Validate replay determinism"); |
1485 | | |
1486 | 0 | println!("\nš {}", "Replay-to-test conversion complete!".bright_green()); |
1487 | 0 | } |
1488 | | |
1489 | | /// Process input path (file or directory) with replay files (complexity: 5) |
1490 | 0 | fn process_input_path( |
1491 | 0 | input: &Path, |
1492 | 0 | converter: &ruchy::runtime::replay_converter::ReplayConverter, |
1493 | 0 | all_tests: &mut Vec<ruchy::runtime::replay_converter::GeneratedTest>, |
1494 | 0 | processed_files: &mut usize |
1495 | 0 | ) -> Result<()> { |
1496 | 0 | if input.is_file() { |
1497 | 0 | process_single_file(input, converter, all_tests, processed_files) |
1498 | 0 | } else if input.is_dir() { |
1499 | 0 | process_directory(input, converter, all_tests, processed_files) |
1500 | | } else { |
1501 | 0 | eprintln!("ā Input path must be a file or directory"); |
1502 | 0 | Err(anyhow::anyhow!("Invalid input path")) |
1503 | | } |
1504 | 0 | } |
1505 | | |
1506 | | /// Convert REPL replay files to regression tests (complexity: 7) |
1507 | 0 | pub fn handle_replay_to_tests_command( |
1508 | 0 | input: &Path, |
1509 | 0 | output: Option<&Path>, |
1510 | 0 | property_tests: bool, |
1511 | 0 | benchmarks: bool, |
1512 | 0 | timeout: u64, |
1513 | 0 | ) -> Result<()> { |
1514 | | use colored::Colorize; |
1515 | | use ruchy::runtime::replay_converter::ReplayConverter; |
1516 | | |
1517 | 0 | println!("{}", "š Converting REPL replay files to regression tests".bright_cyan().bold()); |
1518 | 0 | println!("Input: {}", input.display()); |
1519 | | |
1520 | 0 | let config = setup_conversion_config(property_tests, benchmarks, timeout); |
1521 | 0 | let converter = ReplayConverter::with_config(config); |
1522 | 0 | let mut all_tests = Vec::new(); |
1523 | 0 | let mut processed_files = 0; |
1524 | | |
1525 | 0 | let output_path = determine_output_path(output); |
1526 | | |
1527 | 0 | process_input_path(input, &converter, &mut all_tests, &mut processed_files)?; |
1528 | | |
1529 | 0 | if all_tests.is_empty() { |
1530 | 0 | println!("ā ļø No tests generated"); |
1531 | 0 | return Ok(()); |
1532 | 0 | } |
1533 | | |
1534 | 0 | write_test_output(&converter, &all_tests, output_path)?; |
1535 | 0 | generate_summary_report(&all_tests, processed_files); |
1536 | | |
1537 | 0 | Ok(()) |
1538 | 0 | } |