diff --git a/.gitignore b/.gitignore index 2a0038a..a525525 100644 --- a/.gitignore +++ b/.gitignore @@ -1,2 +1,3 @@ /target -.idea \ No newline at end of file +.idea +.kiro \ No newline at end of file diff --git a/Cargo.lock b/Cargo.lock index 488482e..ad22b2b 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -17,6 +17,30 @@ version = "1.4.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "ace50bade8e6234aa140d9a2f552bbee1db4d353f69b8217bc503490fc1a9f26" +[[package]] +name = "bitflags" +version = "2.9.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1b8e56985ec62d17e9c1001dc89c88ecd7dc08e47eba5ec7c29c7b5eeecde967" + +[[package]] +name = "cfg-if" +version = "1.0.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "9555578bc9e57714c812a1f84e4fc5b4d21fcb063490c624de019f7464c91268" + +[[package]] +name = "cfg_aliases" +version = "0.2.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "613afe47fcd5fac7ccf1db93babcb082c5994d996f20b8b159f2ad1658eb5724" + +[[package]] +name = "libc" +version = "0.2.174" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "1171693293099992e19cddea4e8b849964e9846f4acee11b3948bcc337be8776" + [[package]] name = "memchr" version = "2.7.4" @@ -29,6 +53,18 @@ version = "0.2.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "68354c5c6bd36d73ff3feceb05efa59b6acb7626617f4962be322a825e61f79a" +[[package]] +name = "nix" +version = "0.29.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "71e2746dc3a24dd78b3cfcb7be93368c6de9963d30f43a6a73998a9cf4b17b46" +dependencies = [ + "bitflags", + "cfg-if", + "cfg_aliases", + "libc", +] + [[package]] name = "nom" version = "7.1.3" @@ -59,6 +95,7 @@ name = "r-python" version = "0.1.0" dependencies = [ "approx", + "nix", "nom", "once_cell", ] diff --git a/Cargo.toml b/Cargo.toml index 9ebcc09..e919db5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -7,3 +7,5 @@ edition = "2021" nom = "7.0" approx = "0.5.1" once_cell = "1.10" +[target.'cfg(not(windows))'.dependencies] +nix = { version = "0.29", features = ["signal"] } diff --git a/example.py b/example.py new file mode 100755 index 0000000..9c491bb --- /dev/null +++ b/example.py @@ -0,0 +1,8 @@ +#!/usr/bin/env python3 + +print("Hello from Python!") +print("This is a test script.") +print("Current working directory:", end=" ") + +import os +print(os.getcwd()) \ No newline at end of file diff --git a/src/interpreter/builtins.rs b/src/interpreter/builtins.rs new file mode 100644 index 0000000..e316375 --- /dev/null +++ b/src/interpreter/builtins.rs @@ -0,0 +1,388 @@ +use crate::environment::environment::Environment; +use crate::ir::ast::{Expression, Name}; +use crate::stdlib::{run_command, run_shell_command, RunOptions}; +use super::expression_eval::ExpressionResult; + +/// Represents a built-in function that can be called from RPython +pub type BuiltinFunction = fn(Vec, &Environment) -> Result; + +/// Registry of built-in functions +pub struct BuiltinRegistry { + functions: std::collections::HashMap, +} + +impl BuiltinRegistry { + /// Create a new empty builtin registry + pub fn new() -> Self { + BuiltinRegistry { + functions: std::collections::HashMap::new(), + } + } + + /// Register a built-in function + pub fn register(&mut self, name: Name, func: BuiltinFunction) { + self.functions.insert(name, func); + } + + /// Look up a built-in function by name + pub fn lookup(&self, name: &Name) -> Option<&BuiltinFunction> { + self.functions.get(name) + } +} + +/// Global builtin registry instance using std::sync::OnceLock for thread-safe initialization +static BUILTIN_REGISTRY: std::sync::OnceLock = std::sync::OnceLock::new(); + +/// Get the global builtin registry (thread-safe initialization) +fn get_builtin_registry() -> &'static BuiltinRegistry { + BUILTIN_REGISTRY.get_or_init(|| { + let mut registry = BuiltinRegistry::new(); + register_subprocess_run(&mut registry); + registry + }) +} + +/// Register all built-in functions with the environment +pub fn register_builtins(_env: &mut Environment) { + // Built-in functions are handled through the global registry + // No need to populate the environment directly since we check + // the registry in eval_builtin_function +} + +/// Evaluate a built-in function call +pub fn eval_builtin_function( + name: &Name, + args: Vec, + env: &Environment, +) -> Result, String> { + let registry = get_builtin_registry(); + + if let Some(builtin_func) = registry.lookup(name) { + Ok(Some(builtin_func(args, env)?)) + } else { + Ok(None) + } +} + +/// Register the subprocess.run built-in function +fn register_subprocess_run(registry: &mut BuiltinRegistry) { + registry.register("subprocess.run".to_string(), subprocess_run_builtin); +} + +/// Implementation of subprocess.run built-in function +fn subprocess_run_builtin( + args: Vec, + env: &Environment, +) -> Result { + // Validate argument count (1-3 arguments expected) + if args.is_empty() || args.len() > 3 { + return Err("subprocess.run() takes 1 to 3 arguments".to_string()); + } + + // Evaluate all arguments first + let mut evaluated_args = Vec::new(); + for arg in args { + match super::expression_eval::eval(arg, env)? { + ExpressionResult::Value(expr) => evaluated_args.push(expr), + ExpressionResult::Propagate(expr) => return Ok(ExpressionResult::Propagate(expr)), + } + } + + // Parse the command argument (first argument) + let command = match &evaluated_args[0] { + Expression::ListValue(list) => { + // Command as list of strings + let mut cmd_vec = Vec::new(); + for item in list { + match item { + Expression::CString(s) => cmd_vec.push(s.clone()), + _ => return Err("subprocess.run() command list must contain only strings".to_string()), + } + } + if cmd_vec.is_empty() { + return Err("subprocess.run() command list cannot be empty".to_string()); + } + cmd_vec + } + Expression::CString(s) => { + // Single string command (will be used with shell=True) + vec![s.clone()] + } + _ => return Err("subprocess.run() first argument must be a list of strings or a string".to_string()), + }; + + // Parse optional arguments (shell and capture_output) + let mut options = RunOptions::default(); + + // Second argument: shell (optional, default False) + if evaluated_args.len() > 1 { + match &evaluated_args[1] { + Expression::CTrue => options.shell = true, + Expression::CFalse => options.shell = false, + _ => return Err("subprocess.run() shell argument must be a boolean".to_string()), + } + } + + // Third argument: capture_output (optional, default False) + if evaluated_args.len() > 2 { + match &evaluated_args[2] { + Expression::CTrue => options.capture_output = true, + Expression::CFalse => options.capture_output = false, + _ => return Err("subprocess.run() capture_output argument must be a boolean".to_string()), + } + } + + // Execute the command based on shell option + let result = if options.shell && command.len() == 1 { + // Shell mode with single string command + run_shell_command(command[0].clone(), options) + } else if !options.shell { + // Direct command execution + run_command(command, options) + } else { + // Shell mode with command list - use first element as shell command + run_shell_command(command[0].clone(), options) + }; + + // Convert result to RPython Expression + match result { + Ok(completed_process) => { + Ok(ExpressionResult::Value(Expression::CompletedProcess { + returncode: completed_process.returncode, + stdout: completed_process.stdout, + stderr: completed_process.stderr, + })) + } + Err(subprocess_error) => { + // Convert SubprocessError to String using the From implementation + let error_msg: String = subprocess_error.into(); + // Return error as a Result type (CErr) + Ok(ExpressionResult::Value(Expression::CErr(Box::new( + Expression::CString(error_msg) + )))) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + use crate::environment::environment::Environment; + + fn create_test_env() -> Environment { + let mut env = Environment::new(); + register_builtins(&mut env); + env + } + + #[test] + fn test_builtin_registry_creation() { + let registry = get_builtin_registry(); + assert!(registry.lookup(&"subprocess.run".to_string()).is_some()); + } + + #[test] + fn test_eval_builtin_function_exists() { + let env = create_test_env(); + let args = vec![ + Expression::ListValue(vec![ + Expression::CString("echo".to_string()), + Expression::CString("test".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CTrue, // capture_output=True + ]; + + let result = eval_builtin_function(&"subprocess.run".to_string(), args, &env); + assert!(result.is_ok()); + assert!(result.unwrap().is_some()); + } + + #[test] + fn test_eval_builtin_function_not_exists() { + let env = create_test_env(); + let args = vec![]; + + let result = eval_builtin_function(&"nonexistent.function".to_string(), args, &env); + assert!(result.is_ok()); + assert!(result.unwrap().is_none()); + } + + #[test] + fn test_subprocess_run_basic_command() { + let env = create_test_env(); + let args = vec![ + Expression::ListValue(vec![ + Expression::CString("echo".to_string()), + Expression::CString("hello".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CTrue, // capture_output=True + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_ok()); + + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + assert!(stdout.unwrap().contains("hello")); + } + _ => panic!("Expected CompletedProcess result"), + } + } + + #[test] + fn test_subprocess_run_shell_command() { + let env = create_test_env(); + let args = vec![ + Expression::CString("echo shell_test".to_string()), + Expression::CTrue, // shell=True + Expression::CTrue, // capture_output=True + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_ok()); + + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + assert!(stdout.unwrap().contains("shell_test")); + } + _ => panic!("Expected CompletedProcess result"), + } + } + + #[test] + fn test_subprocess_run_invalid_arguments() { + let env = create_test_env(); + + // Test with no arguments + let result = subprocess_run_builtin(vec![], &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("takes 1 to 3 arguments")); + + // Test with too many arguments + let result = subprocess_run_builtin(vec![ + Expression::CString("echo".to_string()), + Expression::CFalse, + Expression::CFalse, + Expression::CFalse, // 4th argument - too many + ], &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("takes 1 to 3 arguments")); + } + + #[test] + fn test_subprocess_run_invalid_command_type() { + let env = create_test_env(); + let args = vec![ + Expression::CInt(42), // Invalid command type + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("must be a list of strings or a string")); + } + + #[test] + fn test_subprocess_run_invalid_shell_argument() { + let env = create_test_env(); + let args = vec![ + Expression::CString("echo test".to_string()), + Expression::CInt(1), // Invalid shell argument type + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("shell argument must be a boolean")); + } + + #[test] + fn test_subprocess_run_invalid_capture_output_argument() { + let env = create_test_env(); + let args = vec![ + Expression::CString("echo test".to_string()), + Expression::CFalse, + Expression::CString("invalid".to_string()), // Invalid capture_output argument type + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("capture_output argument must be a boolean")); + } + + #[test] + fn test_subprocess_run_command_not_found() { + let env = create_test_env(); + let args = vec![ + Expression::ListValue(vec![ + Expression::CString("nonexistent_command_12345".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CFalse, // capture_output=False + ]; + + let result = subprocess_run_builtin(args, &env); + assert!(result.is_ok()); + + // Should return an error wrapped in CErr + match result.unwrap() { + ExpressionResult::Value(Expression::CErr(error)) => { + match *error { + Expression::CString(msg) => { + assert!(msg.contains("Command not found")); + assert!(msg.contains("nonexistent_command_12345")); + } + _ => panic!("Expected string error message"), + } + } + _ => panic!("Expected CErr result for command not found"), + } + } + + #[test] + fn test_subprocess_error_integration_with_rpython_result_system() { + let env = create_test_env(); + + // Test InvalidArguments error + let result = subprocess_run_builtin(vec![], &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("takes 1 to 3 arguments")); + + // Test CommandNotFound error converted to CErr + let args = vec![ + Expression::ListValue(vec![ + Expression::CString("nonexistent_cmd_xyz".to_string()), + ]), + ]; + let result = subprocess_run_builtin(args, &env); + assert!(result.is_ok()); + match result.unwrap() { + ExpressionResult::Value(Expression::CErr(error)) => { + match *error { + Expression::CString(msg) => { + assert!(msg.starts_with("Command not found:")); + assert!(msg.contains("nonexistent_cmd_xyz")); + } + _ => panic!("Expected string error message"), + } + } + _ => panic!("Expected CErr result"), + } + + // Test InvalidArguments error for empty command list + let args = vec![ + Expression::ListValue(vec![]), // Empty command list + ]; + let result = subprocess_run_builtin(args, &env); + // This should return an error at the builtin level because empty command list + // is caught during argument validation + assert!(result.is_err()); + assert!(result.unwrap_err().contains("command list cannot be empty")); + } +} \ No newline at end of file diff --git a/src/interpreter/expression_eval.rs b/src/interpreter/expression_eval.rs index add00f7..83f128e 100644 --- a/src/interpreter/expression_eval.rs +++ b/src/interpreter/expression_eval.rs @@ -33,6 +33,7 @@ pub fn eval(exp: Expression, env: &Environment) -> Result eval_isnothing_expression(*e, env), Expression::FuncCall(name, args) => eval_function_call(name, args, env), Expression::ListValue(values) => eval_list_value(values, env), + Expression::CompletedProcess { .. } => Ok(ExpressionResult::Value(exp)), _ if is_constant(exp.clone()) => Ok(ExpressionResult::Value(exp)), _ => Err(String::from("Not implemented yet.")), } @@ -387,6 +388,12 @@ pub fn eval_function_call( args: Vec, env: &Environment, ) -> Result { + // Check for built-in functions first + if let Some(result) = super::builtins::eval_builtin_function(&name, args.clone(), env)? { + return Ok(result); + } + + // If not a built-in function, look for user-defined functions match env.lookup_function(&name) { Some(function_definition) => { let mut new_env = Environment::new(); diff --git a/src/interpreter/integration_test.rs b/src/interpreter/integration_test.rs new file mode 100644 index 0000000..581992b --- /dev/null +++ b/src/interpreter/integration_test.rs @@ -0,0 +1,340 @@ +#[cfg(test)] +mod integration_tests { + use crate::environment::environment::Environment; + use crate::ir::ast::Expression; + use crate::interpreter::expression_eval::{eval, ExpressionResult}; + use crate::interpreter::builtins::register_builtins; + + #[test] + fn test_subprocess_run_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Create a function call expression for subprocess.run + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::ListValue(vec![ + Expression::CString("echo".to_string()), + Expression::CString("integration_test".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CTrue, // capture_output=True + ], + ); + + // Evaluate the function call + let result = eval(function_call, &env); + assert!(result.is_ok()); + + // Check that we get a CompletedProcess result + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + assert!(stdout.unwrap().contains("integration_test")); + } + _ => panic!("Expected CompletedProcess result from subprocess.run"), + } + } + + #[test] + fn test_subprocess_run_shell_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Create a shell command function call + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CString("echo 'shell integration test'".to_string()), + Expression::CTrue, // shell=True + Expression::CTrue, // capture_output=True + ], + ); + + // Evaluate the function call + let result = eval(function_call, &env); + assert!(result.is_ok()); + + // Check that we get a CompletedProcess result + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + assert!(stdout.unwrap().contains("shell integration test")); + } + _ => panic!("Expected CompletedProcess result from subprocess.run shell command"), + } + } + + #[test] + fn test_subprocess_run_error_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Create a function call with a non-existent command + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::ListValue(vec![ + Expression::CString("nonexistent_command_xyz123".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CFalse, // capture_output=False + ], + ); + + // Evaluate the function call + let result = eval(function_call, &env); + assert!(result.is_ok()); + + // Check that we get an error result (CErr) + match result.unwrap() { + ExpressionResult::Value(Expression::CErr(error)) => { + match *error { + Expression::CString(msg) => { + assert!(msg.contains("Command not found")); + } + _ => panic!("Expected string error message"), + } + } + _ => panic!("Expected CErr result for non-existent command"), + } + } + + #[test] + fn test_regular_function_call_still_works() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test that regular function calls still work (should fail with function not found) + let function_call = Expression::FuncCall( + "regular_function".to_string(), + vec![Expression::CInt(42)], + ); + + let result = eval(function_call, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("Function regular_function not found")); + } + + #[test] + fn test_subprocess_run_ls_command_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test ls command through RPython interpreter (Requirements 1.1, 1.2, 4.1, 4.2) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::ListValue(vec![ + Expression::CString("ls".to_string()), + Expression::CString("-la".to_string()), + Expression::CString(".".to_string()), + ]), + Expression::CFalse, // shell=False + Expression::CTrue, // capture_output=True + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + let stdout_content = stdout.unwrap(); + assert!(!stdout_content.is_empty()); + // Should contain directory listing information + assert!(stdout_content.contains(".") || stdout_content.contains("total")); + } + _ => panic!("Expected CompletedProcess result from ls command"), + } + } + + #[test] + fn test_subprocess_run_with_different_argument_combinations() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test with only command argument (Requirements 1.1, 2.4) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::ListValue(vec![ + Expression::CString("echo".to_string()), + Expression::CString("minimal_args".to_string()), + ]), + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_none()); // capture_output defaults to False + assert!(stderr.is_none()); + } + _ => panic!("Expected CompletedProcess result"), + } + + // Test with command and shell arguments (Requirements 2.1, 2.4) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CString("echo 'shell_only'".to_string()), + Expression::CTrue, // shell=True + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_none()); // capture_output defaults to False + assert!(stderr.is_none()); + } + _ => panic!("Expected CompletedProcess result"), + } + } + + #[test] + fn test_subprocess_run_comprehensive_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test comprehensive scenario with shell, pipes, and output capture + // (Requirements 2.1, 2.2, 3.1, 3.2, 4.1, 4.2) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CString("echo 'test line 1' && echo 'test line 2'".to_string()), + Expression::CTrue, // shell=True + Expression::CTrue, // capture_output=True + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + let stdout_content = stdout.unwrap(); + assert!(stdout_content.contains("test line 1")); + assert!(stdout_content.contains("test line 2")); + } + _ => panic!("Expected CompletedProcess result from comprehensive test"), + } + } + + #[test] + fn test_subprocess_run_python_script_example() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test running a Python script through shell mode + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CString("python3 example.py".to_string()), + Expression::CTrue, // shell=True (needed for python3 command) + Expression::CTrue, // capture_output=True + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + + match result.unwrap() { + ExpressionResult::Value(Expression::CompletedProcess { returncode, stdout, stderr }) => { + assert_eq!(returncode, 0); + assert!(stdout.is_some()); + assert!(stderr.is_some()); + let stdout_content = stdout.unwrap(); + + // Print what we captured to show the test is working + println!("=== Python Script Output Captured ==="); + println!("Return code: {}", returncode); + println!("Stdout content:\n{}", stdout_content); + println!("Stderr content: {:?}", stderr); + println!("====================================="); + + // Verify the expected content + assert!(stdout_content.contains("Hello from Python!")); + assert!(stdout_content.contains("This is a test script.")); + assert!(stdout_content.contains("Current working directory:")); + } + _ => panic!("Expected CompletedProcess result from Python script execution"), + } + } + + #[test] + fn test_subprocess_run_error_handling_integration() { + let mut env = Environment::new(); + register_builtins(&mut env); + + // Test various error scenarios through RPython interpreter + + // Test invalid argument types (Requirements 4.3, 5.4) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CInt(42), // Invalid command type + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("must be a list of strings or a string")); + + // Test invalid shell argument type + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::CString("echo test".to_string()), + Expression::CString("invalid".to_string()), // Invalid shell type + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_err()); + assert!(result.unwrap_err().contains("shell argument must be a boolean")); + + // Test command not found error propagation (Requirements 4.3, 4.4) + let function_call = Expression::FuncCall( + "subprocess.run".to_string(), + vec![ + Expression::ListValue(vec![ + Expression::CString("definitely_nonexistent_command_xyz".to_string()), + ]), + Expression::CFalse, + Expression::CFalse, + ], + ); + + let result = eval(function_call, &env); + assert!(result.is_ok()); + match result.unwrap() { + ExpressionResult::Value(Expression::CErr(error)) => { + match *error { + Expression::CString(msg) => { + assert!(msg.contains("Command not found")); + assert!(msg.contains("definitely_nonexistent_command_xyz")); + } + _ => panic!("Expected string error message"), + } + } + _ => panic!("Expected CErr result for command not found"), + } + } +} \ No newline at end of file diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index c4f1928..76ee10a 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,5 +1,9 @@ pub mod expression_eval; pub mod statement_execute; +pub mod builtins; +pub mod integration_test; +pub mod subprocess_errors; pub use expression_eval::eval; pub use statement_execute::{execute, run}; +pub use builtins::{register_builtins, eval_builtin_function}; diff --git a/src/interpreter/subprocess_errors.rs b/src/interpreter/subprocess_errors.rs new file mode 100644 index 0000000..17d2647 --- /dev/null +++ b/src/interpreter/subprocess_errors.rs @@ -0,0 +1,134 @@ +use std::io; // Importa std::io::Error para facilitar a conversão de erros de I/O. + +/// Enum para representar os diferentes tipos de erros que podem ocorrer +/// durante a execução de subprocessos. +#[derive(Debug, PartialEq)] // Adicione PartialEq para poder comparar em testes +pub enum SubprocessError { + CommandNotFound(String), + ExecutionFailed { // O comando executado retornou um código de saída diferente de zero + command_name: String, + exit_code: Option, + stdout: Option, + stderr: Option, + }, + IoError(String), // Outros erros de I/O genéricos + InvalidArguments(String), // Erros de validação de argumentos para subprocess.run + PermissionDenied(String), // Erro de permissão negada + OutputCaptureError(String), // Erro na captura da saída + Other(String), // Para erros genéricos ou não mapeados. +} + +// Implementa a conversão de SubprocessError para String. +impl From for String { + fn from(err: SubprocessError) -> Self { + match err { + SubprocessError::CommandNotFound(cmd_info) => format!("Command not found: {}", cmd_info), + SubprocessError::ExecutionFailed { command_name, exit_code, stdout, stderr } => { + format!( + "Command '{}' failed with exit code {:?}. Stdout: {:?}, Stderr: {:?}", + command_name, exit_code, stdout, stderr + ) + }, + SubprocessError::IoError(msg) => format!("I/O Error: {}", msg), + SubprocessError::InvalidArguments(msg) => format!("Invalid arguments: {}", msg), + SubprocessError::PermissionDenied(msg) => format!("Permission denied: {}", msg), + SubprocessError::OutputCaptureError(msg) => format!("Output capture error: {}", msg), + SubprocessError::Other(msg) => format!("Subprocess Error: {}", msg), + } + } +} + +impl SubprocessError { + /// Converte um std::io::Error para um SubprocessError mais específico. + pub fn from_io_error(err: io::Error, command_name: &str) -> Self { + match err.kind() { + io::ErrorKind::NotFound => SubprocessError::CommandNotFound(command_name.to_string()), + io::ErrorKind::PermissionDenied => SubprocessError::PermissionDenied(command_name.to_string()), + _ => SubprocessError::IoError(format!("{}: {}", command_name, err)), // Usar IoError para outros erros + } + } +} + + +#[cfg(test)] +mod tests { + use super::*; + use std::io::{Error, ErrorKind}; + + #[test] + fn test_subprocess_error_from_io_error_not_found() { + let io_err = Error::new(ErrorKind::NotFound, "No such file or directory (os error 2)"); + let sub_err = SubprocessError::from_io_error(io_err, "nonexistent_cmd"); + match sub_err { + SubprocessError::CommandNotFound(cmd) => { + assert_eq!(cmd, "nonexistent_cmd"); + }, + _ => panic!("Expected CommandNotFound error"), + } + } + + #[test] + fn test_subprocess_error_from_io_error_permission_denied() { + let io_err = Error::new(ErrorKind::PermissionDenied, "Permission denied (os error 13)"); + let sub_err = SubprocessError::from_io_error(io_err, "restricted_cmd"); + match sub_err { + SubprocessError::PermissionDenied(cmd) => { + assert_eq!(cmd, "restricted_cmd"); + }, + _ => panic!("Expected PermissionDenied error"), + } + } + + #[test] + fn test_subprocess_error_from_io_error_other() { + let io_err = Error::new(ErrorKind::Other, "Some generic IO error"); + let sub_err = SubprocessError::from_io_error(io_err, "some_cmd"); + match sub_err { + SubprocessError::IoError(msg) => { + assert!(msg.contains("some_cmd")); + assert!(msg.contains("Some generic IO error")); + }, + _ => panic!("Expected IoError"), + } + } + + #[test] + fn test_subprocess_error_to_string_command_not_found() { + let err = SubprocessError::CommandNotFound("nonexistent_cmd".to_string()); + let msg: String = err.into(); + assert_eq!(msg, "Command not found: nonexistent_cmd"); + } + + #[test] + fn test_subprocess_error_to_string_execution_failed() { + let err = SubprocessError::ExecutionFailed { + command_name: "ls".to_string(), + exit_code: Some(1), + stdout: Some("".to_string()), + stderr: Some("ls: cannot access 'nonexistent': No such file or directory\n".to_string()), + }; + let msg: String = err.into(); + assert_eq!(msg, "Command 'ls' failed with exit code Some(1). Stdout: Some(\"\"), Stderr: Some(\"ls: cannot access 'nonexistent': No such file or directory\\n\")"); + } + + #[test] + fn test_subprocess_error_to_string_invalid_arguments() { + let err = SubprocessError::InvalidArguments("Empty command list".to_string()); + let msg: String = err.into(); + assert_eq!(msg, "Invalid arguments: Empty command list"); + } + + #[test] + fn test_subprocess_error_to_string_permission_denied() { + let err = SubprocessError::PermissionDenied("restricted_file".to_string()); + let msg: String = err.into(); + assert_eq!(msg, "Permission denied: restricted_file"); + } + + #[test] + fn test_subprocess_error_to_string_output_capture_error() { + let err = SubprocessError::OutputCaptureError("Failed to read output".to_string()); + let msg: String = err.into(); + assert_eq!(msg, "Output capture error: Failed to read output"); + } +} \ No newline at end of file diff --git a/src/ir/ast.rs b/src/ir/ast.rs index 1172bb8..cbdbf70 100644 --- a/src/ir/ast.rs +++ b/src/ir/ast.rs @@ -54,6 +54,7 @@ pub enum Type { TResult(Box, Box), // Ok, Error TAny, TAlgebraicData(Name, Vec), + TCompletedProcess, // Type for subprocess result objects } // Represents a value constructor for an algebraic data type @@ -121,6 +122,13 @@ pub enum Expression { // Constructor Constructor(Name, Vec>), + + // Subprocess result object + CompletedProcess { + returncode: i32, + stdout: Option, + stderr: Option, + }, } // Represents statements in the AST diff --git a/src/lib.rs b/src/lib.rs index 54163be..72fbb8e 100644 --- a/src/lib.rs +++ b/src/lib.rs @@ -1,2 +1,6 @@ pub mod ir; pub mod parser; +pub mod stdlib; +pub mod interpreter; +pub mod environment; +pub mod type_checker; diff --git a/src/main.rs b/src/main.rs index b038c00..8bed04b 100644 --- a/src/main.rs +++ b/src/main.rs @@ -14,6 +14,7 @@ pub mod environment; pub mod interpreter; pub mod ir; pub mod parser; +pub mod stdlib; pub mod type_checker; fn main() { diff --git a/src/stdlib/mod.rs b/src/stdlib/mod.rs new file mode 100644 index 0000000..8d0ff54 --- /dev/null +++ b/src/stdlib/mod.rs @@ -0,0 +1,3 @@ +pub mod subprocess; + +pub use subprocess::*; \ No newline at end of file diff --git a/src/stdlib/subprocess/mod.rs b/src/stdlib/subprocess/mod.rs new file mode 100644 index 0000000..8fc8185 --- /dev/null +++ b/src/stdlib/subprocess/mod.rs @@ -0,0 +1,5 @@ +pub mod types; +pub mod process; + +pub use types::*; +pub use process::*; \ No newline at end of file diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs new file mode 100644 index 0000000..7f71257 --- /dev/null +++ b/src/stdlib/subprocess/process.rs @@ -0,0 +1,749 @@ +use std::process::{Command, Stdio}; +use super::types::{CompletedProcess, RunOptions, SubprocessError}; + +/// Convert bytes to string, handling both text and binary output appropriately +fn bytes_to_string(bytes: &[u8]) -> String { + // Handle empty output + if bytes.is_empty() { + return String::new(); + } + + // Try UTF-8 first, fall back to lossy conversion for binary data + match std::str::from_utf8(bytes) { + Ok(s) => s.to_string(), + Err(_) => { + // For binary data, use lossy conversion which replaces invalid UTF-8 sequences + String::from_utf8_lossy(bytes).to_string() + } + } +} + +/// Execute a command directly without shell interpretation +pub fn run_command( + command: Vec, + options: RunOptions +) -> Result { + if command.is_empty() { + return Err(SubprocessError::InvalidArguments("Command cannot be empty".to_string())); + } + + let program = &command[0]; + let args = &command[1..]; + + let mut cmd = Command::new(program); + cmd.args(args); + + // Configure stdio based on capture_output option + if options.capture_output { + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + } + + // Execute the command + match cmd.output() { + Ok(output) => { + // Handle output capture based on options + let stdout = if options.capture_output { + Some(bytes_to_string(&output.stdout)) + } else { + None + }; + + let stderr = if options.capture_output { + Some(bytes_to_string(&output.stderr)) + } else { + None + }; + + let returncode = output.status.code().unwrap_or(-1); + + Ok(CompletedProcess { + returncode, + stdout, + stderr, + }) + } + Err(e) => { + Err(SubprocessError::from_io_error(e, program)) + } + } +} + +/// Execute a command through the system shell +pub fn run_shell_command( + command: String, + options: RunOptions +) -> Result { + if command.trim().is_empty() { + return Err(SubprocessError::InvalidArguments("Shell command cannot be empty".to_string())); + } + + // Determine the shell command based on the operating system + let (shell_program, shell_arg) = if cfg!(target_os = "windows") { + ("cmd", "/C") + } else { + ("sh", "-c") + }; + + let mut cmd = Command::new(shell_program); + cmd.arg(shell_arg); + cmd.arg(&command); + + // Configure stdio based on capture_output option + if options.capture_output { + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + } + + // Execute the command + match cmd.output() { + Ok(output) => { + // Handle output capture based on options + let stdout = if options.capture_output { + Some(bytes_to_string(&output.stdout)) + } else { + None + }; + + let stderr = if options.capture_output { + Some(bytes_to_string(&output.stderr)) + } else { + None + }; + + let returncode = output.status.code().unwrap_or(-1); + + Ok(CompletedProcess { + returncode, + stdout, + stderr, + }) + } + Err(e) => { + Err(SubprocessError::from_io_error(e, shell_program)) + } + } +} + +use std::process::{Child, ExitStatus}; +use std::io; +#[cfg(not(windows))] +use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, +}; + +/// Struct representing a running process. +/// It wraps a `std::process::Child` and provides a wait method. +pub struct Processo { + pub processo: Child, +} + +impl Processo { + /// Waits for the process to finish and returns its exit code. + /// Returns -1 if the exit code cannot be determined. + pub fn wait(&mut self) -> io::Result { + let status: ExitStatus = self.processo.wait()?; + Ok(status.code().unwrap_or(-1)) + } + + #[cfg(windows)] + pub fn terminate(&mut self) -> io::Result<()> { + self.processo.kill() + } + + #[cfg(not(windows))] + pub fn terminate(&mut self) -> io::Result<()> { + let pid = Pid::from_raw(self.processo.id() as i32); + match signal::kill(pid, Signal::SIGTERM) { + Ok(_) => Ok(()), + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), + } + } + + pub fn kill(&mut self) -> io::Result<()> { + self.processo.kill() + } + + +} + +use std::process::{ChildStdin, ChildStdout, ChildStderr}; + +/// Representa um processo em execução com acesso a stdin, stdout e stderr. +pub struct PopenProcess { + pub child: Child, + pub stdin: Option, + pub stdout: Option, + pub stderr: Option, +} + +/// Executa um comando e retorna um processo com streams abertos (estilo popen) +pub fn popen_command( + command: Vec, + options: RunOptions, +) -> Result { + if command.is_empty() { + return Err(SubprocessError::InvalidArguments("Command cannot be empty".to_string())); + } + + let program = &command[0]; + let args = &command[1..]; + + let mut cmd = Command::new(program); + cmd.args(args); + cmd.stdin(Stdio::piped()); + + // Redireciona stdout/stderr para pipes conforme solicitado + if options.capture_output { + cmd.stdout(Stdio::piped()); + cmd.stderr(Stdio::piped()); + } + + match cmd.spawn() { + Ok(mut child) => { + let stdin = child.stdin.take(); + let stdout = if options.capture_output { child.stdout.take() } else { None }; + let stderr = if options.capture_output { child.stderr.take() } else { None }; + + Ok(PopenProcess { + child, + stdin, + stdout, + stderr, + }) + } + Err(e) => Err(SubprocessError::from_io_error(e, program)), + } +} + + + +#[cfg(test)] +mod tests { + use super::*; + use std::process::Command; + use std::thread; + use std::time::Duration; + + #[test] + fn test_basic_command_execution() { + let result = run_command( + vec!["echo".to_string(), "hello".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stdout.unwrap().contains("hello")); + } + + #[test] + fn test_shell_command_execution() { + let result = run_shell_command( + "echo hello".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stdout.unwrap().contains("hello")); + } + + #[test] + fn test_command_not_found() { + let result = run_command( + vec!["nonexistent_command_12345".to_string()], + RunOptions { shell: false, capture_output: false } + ); + assert!(result.is_err()); + match result.unwrap_err() { + SubprocessError::CommandNotFound(cmd) => { + assert_eq!(cmd, "nonexistent_command_12345"); + } + _ => panic!("Expected CommandNotFound error"), + } + } + + #[test] + fn test_empty_command() { + let result = run_command( + vec![], + RunOptions { shell: false, capture_output: false } + ); + assert!(result.is_err()); + match result.unwrap_err() { + SubprocessError::InvalidArguments(msg) => { + assert!(msg.contains("Command cannot be empty")); + } + _ => panic!("Expected InvalidArguments error"), + } + } + + #[test] + fn test_empty_shell_command() { + let result = run_shell_command( + "".to_string(), + RunOptions { shell: true, capture_output: false } + ); + assert!(result.is_err()); + match result.unwrap_err() { + SubprocessError::InvalidArguments(msg) => { + assert!(msg.contains("Shell command cannot be empty")); + } + _ => panic!("Expected InvalidArguments error"), + } + } + + // Test output capture functionality (Requirements 3.1, 3.2, 3.3) + #[test] + fn test_stdout_capture() { + let result = run_command( + vec!["echo".to_string(), "test output".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + assert!(process.stdout.unwrap().contains("test output")); + } + + #[test] + fn test_stderr_capture() { + // Use a command that writes to stderr - ls with invalid directory + let result = run_command( + vec!["ls".to_string(), "/nonexistent_directory_12345".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_ne!(process.returncode, 0); // Should fail + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + // stderr should contain error message + let stderr = process.stderr.unwrap(); + assert!(!stderr.is_empty()); + } + + #[test] + fn test_no_capture_output() { + // When capture_output=false, stdout and stderr should be None + let result = run_command( + vec!["echo".to_string(), "not captured".to_string()], + RunOptions { shell: false, capture_output: false } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_none()); + assert!(process.stderr.is_none()); + } + + #[test] + fn test_empty_output_capture() { + // Test command that produces no output + let result = run_command( + vec!["true".to_string()], // 'true' command produces no output + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + assert_eq!(process.stdout.unwrap(), ""); + assert_eq!(process.stderr.unwrap(), ""); + } + + #[test] + fn test_shell_output_capture() { + // Test shell command with output capture + let result = run_shell_command( + "echo 'shell output'".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + assert!(process.stdout.unwrap().contains("shell output")); + } + + #[test] + fn test_bytes_to_string_helper() { + // Test the bytes_to_string helper function directly + assert_eq!(bytes_to_string(b""), ""); + assert_eq!(bytes_to_string(b"hello"), "hello"); + assert_eq!(bytes_to_string(b"hello\n"), "hello\n"); + + // Test with valid UTF-8 + let utf8_bytes = "Hello, 世界!".as_bytes(); + assert_eq!(bytes_to_string(utf8_bytes), "Hello, 世界!"); + + // Test with invalid UTF-8 (should use lossy conversion) + let invalid_utf8 = vec![0xFF, 0xFE, 0x48, 0x65, 0x6C, 0x6C, 0x6F]; + let result = bytes_to_string(&invalid_utf8); + assert!(!result.is_empty()); // Should produce some output, even if lossy + } + + #[test] + fn test_multiline_output_capture() { + // Test capturing multiline output + let result = run_shell_command( + "printf 'line1\\nline2\\nline3'".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + let stdout = process.stdout.unwrap(); + assert!(stdout.contains("line1")); + assert!(stdout.contains("line2")); + assert!(stdout.contains("line3")); + } + + #[test] + fn test_subprocess_error_display() { + // Test that SubprocessError Display implementation works correctly + let error1 = SubprocessError::InvalidArguments("test message".to_string()); + assert_eq!(error1.to_string(), "Invalid arguments: test message"); + + let error2 = SubprocessError::CommandNotFound("test_cmd".to_string()); + assert_eq!(error2.to_string(), "Command not found: test_cmd"); + + let error3 = SubprocessError::PermissionDenied("test_cmd".to_string()); + assert_eq!(error3.to_string(), "Permission denied: test_cmd"); + + let error4 = SubprocessError::ExecutionFailed("test failure".to_string()); + assert_eq!(error4.to_string(), "Execution failed: test failure"); + + let error5 = SubprocessError::OutputCaptureError("capture failed".to_string()); + assert_eq!(error5.to_string(), "Output capture error: capture failed"); + } + + #[test] + fn test_subprocess_error_from_string() { + // Test that SubprocessError can be converted to String + let error = SubprocessError::CommandNotFound("test_cmd".to_string()); + let error_string: String = error.into(); + assert_eq!(error_string, "Command not found: test_cmd"); + } + + #[test] + fn test_subprocess_error_from_io_error() { + // Test the from_io_error helper function + let not_found_error = std::io::Error::new(std::io::ErrorKind::NotFound, "file not found"); + let subprocess_error = SubprocessError::from_io_error(not_found_error, "test_command"); + match subprocess_error { + SubprocessError::CommandNotFound(cmd) => assert_eq!(cmd, "test_command"), + _ => panic!("Expected CommandNotFound error"), + } + + let permission_error = std::io::Error::new(std::io::ErrorKind::PermissionDenied, "permission denied"); + let subprocess_error = SubprocessError::from_io_error(permission_error, "test_command"); + match subprocess_error { + SubprocessError::PermissionDenied(cmd) => assert_eq!(cmd, "test_command"), + _ => panic!("Expected PermissionDenied error"), + } + + let other_error = std::io::Error::new(std::io::ErrorKind::Other, "other error"); + let subprocess_error = SubprocessError::from_io_error(other_error, "test_command"); + match subprocess_error { + SubprocessError::ExecutionFailed(msg) => { + assert!(msg.contains("test_command")); + assert!(msg.contains("other error")); + } + _ => panic!("Expected ExecutionFailed error"), + } + } + + // Additional tests to ensure complete coverage of requirements + #[test] + fn test_ls_command_execution() { + // Test ls command execution (Requirement 1.1, 1.2) + let result = run_command( + vec!["ls".to_string(), "-la".to_string(), ".".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + // Should contain current directory listing + let stdout = process.stdout.unwrap(); + assert!(!stdout.is_empty()); + } + + #[test] + fn test_echo_command_variations() { + // Test various echo command patterns (Requirements 1.1, 1.4, 3.1, 3.2) + + // Test echo with multiple arguments + let result = run_command( + vec!["echo".to_string(), "hello".to_string(), "world".to_string(), "test".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + let stdout = process.stdout.unwrap(); + assert!(stdout.contains("hello")); + assert!(stdout.contains("world")); + assert!(stdout.contains("test")); + + // Test echo with special characters + let result = run_command( + vec!["echo".to_string(), "test@#$%^&*()".to_string()], + RunOptions { shell: false, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.unwrap().contains("test@#$%^&*()")); + } + + #[test] + fn test_shell_command_with_pipes() { + // Test shell command with pipes (Requirements 2.1, 2.2, 3.1) + let result = run_shell_command( + "echo 'hello world' | wc -w".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + // Should output "2" (word count) + let stdout_content = process.stdout.unwrap(); + let stdout = stdout_content.trim(); + assert!(stdout.contains("2")); + } + + #[test] + fn test_shell_command_with_environment_variables() { + // Test shell command with environment variable expansion (Requirement 2.2) + let result = run_shell_command( + "echo $HOME".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + // Should contain some path (HOME environment variable) + let stdout = process.stdout.unwrap(); + assert!(!stdout.trim().is_empty()); + // On most systems, HOME should contain a path separator + assert!(stdout.contains("/") || stdout.contains("\\")); + } + + #[test] + fn test_command_return_codes() { + // Test that return codes are properly captured (Requirements 1.4, 4.2, 4.4) + + // Test successful command (return code 0) + let result = run_command( + vec!["true".to_string()], + RunOptions { shell: false, capture_output: false } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + + // Test failing command (non-zero return code) + let result = run_command( + vec!["false".to_string()], + RunOptions { shell: false, capture_output: false } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_ne!(process.returncode, 0); + assert_eq!(process.returncode, 1); + } + + #[test] + fn test_comprehensive_output_capture_scenarios() { + // Test comprehensive output capture scenarios (Requirements 3.1, 3.2, 3.3, 3.4, 3.5) + + // Test command that outputs to both stdout and stderr + let result = run_shell_command( + "echo 'stdout message' && echo 'stderr message' >&2".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + assert!(process.stderr.is_some()); + assert!(process.stdout.unwrap().contains("stdout message")); + assert!(process.stderr.unwrap().contains("stderr message")); + + // Test command with large output + let result = run_shell_command( + "for i in {1..10}; do echo \"Line $i\"; done".to_string(), + RunOptions { shell: true, capture_output: true } + ); + assert!(result.is_ok()); + let process = result.unwrap(); + assert_eq!(process.returncode, 0); + assert!(process.stdout.is_some()); + let stdout = process.stdout.unwrap(); + assert!(stdout.contains("Line 1")); + assert!(stdout.contains("Line 10")); + } + + #[test] + fn test_wait_success() { + // Test command exits successfully with code 0 + let mut processo = Processo { + processo: Command::new("true").spawn().unwrap(), + }; + let exit_code = processo.wait().unwrap(); + assert_eq!(exit_code, 0); + } + + #[test] + fn test_wait_failure() { + // Test command exits with failure code (usually 1) + let mut processo = Processo { + processo: Command::new("false").spawn().unwrap(), + }; + let exit_code = processo.wait().unwrap(); + assert_eq!(exit_code, 1); + } + + #[test] + fn test_wait_killed() { + // Test command is killed before finishing; should return -1 + let mut processo = Processo { + processo: Command::new("sleep").arg("5").spawn().unwrap(), + }; + processo.processo.kill().unwrap(); + let exit_code = processo.wait().unwrap(); + assert_eq!(exit_code, -1); // May vary by OS + } + // Terminate tests + fn create_long_running_command() -> Command { + if cfg!(windows) { + let mut cmd = Command::new("timeout"); + cmd.arg("/T").arg("30"); + cmd + } else { + let mut cmd = Command::new("sleep"); + cmd.arg("30"); + cmd + } + } + + #[test] + fn test_terminate_a_running_process() { + let child = create_long_running_command() + .spawn() + .expect("Falha ao iniciar processo para o teste de terminate"); + + let mut processo = Processo { processo: child }; + + thread::sleep(Duration::from_millis(100)); + + processo.terminate().expect("Falha ao chamar terminate"); + + let exit_code = processo.wait().expect("Falha ao esperar pelo processo terminado"); + + let expected_code = if cfg!(windows) { 1 } else { -1 }; + assert_eq!(exit_code, expected_code, "O código de saída após terminate não foi o esperado."); + } + + #[test] + fn test_kill_a_running_process() { + let child = create_long_running_command() + .spawn() + .expect("Falha ao iniciar processo para o teste de kill"); + + + let mut processo = Processo { processo: child }; + + thread::sleep(Duration::from_millis(100)); + + processo.kill().expect("Falha ao chamar kill"); + + + let exit_code = processo.wait().expect("Falha ao esperar pelo processo morto"); + + let expected_code = if cfg!(windows) { 1 } else { -1 }; + assert_eq!(exit_code, expected_code, "O código de saída após kill não foi o esperado."); + } + + #[test] + fn test_wait_on_a_process_that_finishes_normally() { + let mut command = if cfg!(windows) { + let mut cmd = Command::new("timeout"); + cmd.arg("/T").arg("1"); + cmd + } else { + let mut cmd = Command::new("sleep"); + cmd.arg("1"); + cmd + }; + + let child = command.spawn().expect("Falha ao iniciar processo curto"); + let mut processo = Processo { processo: child }; + + let exit_code = processo.wait().expect("Falha ao esperar pelo processo"); + assert_eq!(exit_code, 0); + } + + use std::io::{Read, Write}; + + #[test] + fn test_popen_cat_stdin_stdout() { + // Comando que apenas reflete a entrada + let mut process = popen_command( + vec!["cat".to_string()], + RunOptions { shell: false, capture_output: true } + ).expect("Falha ao iniciar processo"); + + let input = "Mensagem via stdin\nOutra linha\n"; + + // Escreve no stdin do processo + if let Some(stdin) = process.stdin.as_mut() { + stdin.write_all(input.as_bytes()).expect("Falha ao escrever no stdin"); + } + + // Fecha stdin para que o processo finalize (cat só sai quando stdin fecha) + drop(process.stdin.take()); + + // Espera a saída do processo + let output = process.child.wait_with_output().expect("Falha ao esperar processo"); + + // Verifica se a saída é igual à entrada + let stdout = String::from_utf8_lossy(&output.stdout); + assert_eq!(stdout, input); + + // stderr deve estar vazio + assert!(output.stderr.is_empty()); + assert_eq!(output.status.code().unwrap_or(-1), 0); + } + + #[test] + fn test_popen_error_output() { + let mut process = popen_command( + vec!["ls".to_string(), "/naoexiste".to_string()], + RunOptions { shell: false, capture_output: true } + ).expect("Falha ao iniciar processo"); + + let output = process.child.wait_with_output().unwrap(); + + assert_ne!(output.status.code().unwrap_or(-1), 0); + let stderr = String::from_utf8_lossy(&output.stderr); + assert!(stderr.contains("No such file") || stderr.contains("não existe")); + } + +} \ No newline at end of file diff --git a/src/stdlib/subprocess/types.rs b/src/stdlib/subprocess/types.rs new file mode 100644 index 0000000..be0b23e --- /dev/null +++ b/src/stdlib/subprocess/types.rs @@ -0,0 +1,74 @@ +#[derive(Debug, Clone, PartialEq)] +pub struct CompletedProcess { + pub returncode: i32, + pub stdout: Option, + pub stderr: Option, +} + +#[derive(Debug, Clone)] +pub struct RunOptions { + pub shell: bool, + pub capture_output: bool, +} + +impl Default for RunOptions { + fn default() -> Self { + RunOptions { + shell: false, + capture_output: false, + } + } +} + +/// Comprehensive error types for subprocess operations +#[derive(Debug, Clone, PartialEq)] +pub enum SubprocessError { + /// Invalid arguments provided to subprocess.run() + InvalidArguments(String), + /// Command executable not found in PATH + CommandNotFound(String), + /// Permission denied when trying to execute command + PermissionDenied(String), + /// General execution failure + ExecutionFailed(String), + /// Error capturing command output + OutputCaptureError(String), +} + +impl std::fmt::Display for SubprocessError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + SubprocessError::InvalidArguments(msg) => write!(f, "Invalid arguments: {}", msg), + SubprocessError::CommandNotFound(cmd) => write!(f, "Command not found: {}", cmd), + SubprocessError::PermissionDenied(cmd) => write!(f, "Permission denied: {}", cmd), + SubprocessError::ExecutionFailed(msg) => write!(f, "Execution failed: {}", msg), + SubprocessError::OutputCaptureError(msg) => write!(f, "Output capture error: {}", msg), + } + } +} + +impl std::error::Error for SubprocessError {} + +/// Convert SubprocessError to String for RPython's Result type system +impl From for String { + fn from(error: SubprocessError) -> String { + error.to_string() + } +} + +/// Convert std::io::Error to SubprocessError with context +impl SubprocessError { + pub fn from_io_error(error: std::io::Error, command: &str) -> Self { + match error.kind() { + std::io::ErrorKind::NotFound => { + SubprocessError::CommandNotFound(command.to_string()) + } + std::io::ErrorKind::PermissionDenied => { + SubprocessError::PermissionDenied(command.to_string()) + } + _ => { + SubprocessError::ExecutionFailed(format!("{}: {}", command, error)) + } + } + } +} \ No newline at end of file