From a31d3502ef4bfc569788ed8c5af214a36a4ba7e7 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Freitas=20Fran=C3=A7a?= <127356730+PedroFFranca@users.noreply.github.com> Date: Wed, 16 Jul 2025 12:11:30 -0300 Subject: [PATCH 01/11] =?UTF-8?q?Adi=C3=A7=C3=A3o=20do=20terminate=20e=20k?= =?UTF-8?q?ill?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stdlib/process.rs | 115 ++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 115 insertions(+) create mode 100644 src/stdlib/process.rs diff --git a/src/stdlib/process.rs b/src/stdlib/process.rs new file mode 100644 index 0000000..b2acaab --- /dev/null +++ b/src/stdlib/process.rs @@ -0,0 +1,115 @@ +use std::process::{Child, ExitStatus}; +use std::io; + +pub struct Processo { + processo: Child, +} + +impl Processo { + #[cfg(windows)] + pub fn terminate(&mut self) -> io::Result<()> { + // No Windows, .kill() já usa TerminateProcess, que é o comportamento + // esperado para terminate() e kill() do Python nessa plataforma. + self.processo.kill() + } + + #[cfg(not(windows))] + pub fn terminate(&mut self) -> io::Result<()> { + // Em sistemas POSIX (Linux, macOS), precisamos enviar SIGTERM manualmente. + // O .kill() padrão do Rust enviaria SIGKILL, que é muito agressivo. + + // 1. Pega o PID (Process ID) do processo filho. + let pid = Pid::from_raw(self.processo.id() as i32); + + // 2. Envia o sinal SIGTERM para o PID. + match signal::kill(pid, Signal::SIGTERM) { + Ok(_) => Ok(()), + Err(e) => { + // 3. Converte o erro da crate 'nix' para um erro padrão 'std::io::Error'. + Err(io::Error::new(io::ErrorKind::Other, e)) + } + } + + } + #[cfg(test)] + mod tests { + use super::*; // Importa tudo do módulo pai (Processo, etc.) + use std::thread; + use std::time::Duration; + + #[test] + fn test_process_creation_and_wait_success() { + // Testa o caso mais básico: criar um processo que termina com sucesso. + // O comando `sleep 0.1` é rápido e deve sair com código 0. + let mut processo = Processo::new("sleep", &["0.1"]).expect("Falha ao criar processo 'sleep'"); + + let exit_code = processo.wait().expect("Falha ao esperar pelo processo"); + + // Assert: Verificamos se o código de saída é 0 (sucesso). + assert_eq!(exit_code, 0); + } + + #[test] + fn test_process_creation_fails_for_invalid_command() { + // Testa se a criação falha quando o comando não existe. + let resultado = Processo::new("comando_que_definitivamente_nao_existe_123", &[]); + + // Assert: Verificamos que o resultado é um erro. + assert!(resultado.is_err()); + } + + #[test] + fn test_terminate_long_running_process() { + // 1. SETUP: Inicia um processo que demoraria muito para terminar sozinho. + let mut processo = Processo::new("sleep", &["30"]).expect("Falha ao criar processo 'sleep 30'"); + // Dá um tempinho para o SO realmente iniciar o processo. + thread::sleep(Duration::from_millis(100)); + + // 2. ACTION: Chama o método que queremos testar. + processo.terminate().expect("Falha ao enviar sinal de terminate"); + + // 3. ASSERT: Verifica o resultado. + let exit_code = processo.wait().expect("Falha ao esperar pelo processo terminado"); + + #[cfg(not(windows))] + { + // Em Linux/macOS, um processo terminado por sinal não tem um código de saída. + // Nossa função wait() converte isso para -1. Esta é a verificação correta! + assert_eq!(exit_code, -1, "Em POSIX, o código de saída de um processo terminado por sinal deve ser -1 (na nossa implementação)"); + } + #[cfg(windows)] + { + // No Windows, TerminateProcess força um código de saída, que geralmente é 1. + assert_eq!(exit_code, 1, "No Windows, o código de saída de um processo terminado geralmente é 1"); + } + } + + #[test] + fn test_kill_long_running_process() { + // O teste para kill() é quase idêntico ao de terminate(), + // pois ambos resultam em um encerramento anormal. + + // 1. SETUP + let mut processo = Processo::new("sleep", &["30"]).expect("Falha ao criar processo 'sleep 30'"); + thread::sleep(Duration::from_millis(100)); + + // 2. ACTION + processo.kill().expect("Falha ao enviar sinal de kill"); + + // 3. ASSERT + let exit_code = processo.wait().expect("Falha ao esperar pelo processo morto"); + + #[cfg(not(windows))] + { + // SIGKILL também resulta em um código de saída "None", que mapeamos para -1. + assert_eq!(exit_code, -1, "Em POSIX, o código de saída de um processo morto por sinal deve ser -1"); + } + #[cfg(windows)] + { + // O comportamento é o mesmo de terminate() no Windows. + assert_eq!(exit_code, 1, "No Windows, o código de saída de um processo morto geralmente é 1"); + } + } + } + +} \ No newline at end of file From 848c94727152a94ec87f6a4aa339cd0351bc9e4a Mon Sep 17 00:00:00 2001 From: rockethm Date: Fri, 18 Jul 2025 10:58:47 -0300 Subject: [PATCH 02/11] Created subprocess.run with shell, output capture, basic command execution and return code handling all based on the python subprocess module but simplified --- .gitignore | 3 +- example.py | 8 + src/interpreter/builtins.rs | 388 +++++++++++++++++++++ src/interpreter/expression_eval.rs | 7 + src/interpreter/integration_test.rs | 340 +++++++++++++++++++ src/interpreter/mod.rs | 3 + src/ir/ast.rs | 8 + src/lib.rs | 4 + src/main.rs | 1 + src/stdlib/mod.rs | 3 + src/stdlib/subprocess/mod.rs | 5 + src/stdlib/subprocess/process.rs | 505 ++++++++++++++++++++++++++++ src/stdlib/subprocess/types.rs | 74 ++++ 13 files changed, 1348 insertions(+), 1 deletion(-) create mode 100755 example.py create mode 100644 src/interpreter/builtins.rs create mode 100644 src/interpreter/integration_test.rs create mode 100644 src/stdlib/mod.rs create mode 100644 src/stdlib/subprocess/mod.rs create mode 100644 src/stdlib/subprocess/process.rs create mode 100644 src/stdlib/subprocess/types.rs 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/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..b039410 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -1,5 +1,8 @@ pub mod expression_eval; pub mod statement_execute; +pub mod builtins; +pub mod integration_test; pub use expression_eval::eval; pub use statement_execute::{execute, run}; +pub use builtins::{register_builtins, eval_builtin_function}; 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..b411e45 --- /dev/null +++ b/src/stdlib/subprocess/process.rs @@ -0,0 +1,505 @@ +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)) + } + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[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")); + } +} \ 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 From fe776ab30a129907b053d904dfa8c571895d88b2 Mon Sep 17 00:00:00 2001 From: GabLopes12 Date: Sat, 19 Jul 2025 10:07:53 -0300 Subject: [PATCH 03/11] Gerenciamento de erros --- src/interpreter/mod.rs | 1 + src/interpreter/subprocess_errors.rs | 134 +++++++++++++++++++++++++++ 2 files changed, 135 insertions(+) create mode 100644 src/interpreter/subprocess_errors.rs diff --git a/src/interpreter/mod.rs b/src/interpreter/mod.rs index b039410..76ee10a 100644 --- a/src/interpreter/mod.rs +++ b/src/interpreter/mod.rs @@ -2,6 +2,7 @@ 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}; 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 From e42cf73c9c91f97f02cf48c0145d7536b5f47aa7 Mon Sep 17 00:00:00 2001 From: anauehara Date: Sat, 19 Jul 2025 11:00:27 -0300 Subject: [PATCH 04/11] Add wait() --- src/stdlib/subprocess/process.rs | 51 ++++++++++++++++++++++++++++++++ 1 file changed, 51 insertions(+) diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index b411e45..5be43e5 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -125,6 +125,25 @@ pub fn run_shell_command( } } +use std::process::{Child, ExitStatus}; +use std::io; + +/// 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(test)] mod tests { use super::*; @@ -502,4 +521,36 @@ mod tests { 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 + } + } \ No newline at end of file From 3697cc4252d64f120f53125a03a8abfa2847ee4d Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Freitas=20Fran=C3=A7a?= <127356730+PedroFFranca@users.noreply.github.com> Date: Sat, 19 Jul 2025 15:43:03 -0300 Subject: [PATCH 05/11] Refactoring dos testes --- src/stdlib/process.rs | 177 ++++++++++++++++++++---------------------- 1 file changed, 85 insertions(+), 92 deletions(-) diff --git a/src/stdlib/process.rs b/src/stdlib/process.rs index b2acaab..dddd30f 100644 --- a/src/stdlib/process.rs +++ b/src/stdlib/process.rs @@ -1,115 +1,108 @@ -use std::process::{Child, ExitStatus}; use std::io; +use std::process::{Child, ExitStatus}; +#[cfg(not(windows))] +use nix::{ + sys::signal::{self, Signal}, + unistd::Pid, +}; pub struct Processo { - processo: Child, + pub processo: Child, } impl Processo { + #[cfg(windows)] pub fn terminate(&mut self) -> io::Result<()> { - // No Windows, .kill() já usa TerminateProcess, que é o comportamento - // esperado para terminate() e kill() do Python nessa plataforma. self.processo.kill() } #[cfg(not(windows))] pub fn terminate(&mut self) -> io::Result<()> { - // Em sistemas POSIX (Linux, macOS), precisamos enviar SIGTERM manualmente. - // O .kill() padrão do Rust enviaria SIGKILL, que é muito agressivo. - - // 1. Pega o PID (Process ID) do processo filho. let pid = Pid::from_raw(self.processo.id() as i32); - - // 2. Envia o sinal SIGTERM para o PID. match signal::kill(pid, Signal::SIGTERM) { Ok(_) => Ok(()), - Err(e) => { - // 3. Converte o erro da crate 'nix' para um erro padrão 'std::io::Error'. - Err(io::Error::new(io::ErrorKind::Other, e)) - } + Err(e) => Err(io::Error::new(io::ErrorKind::Other, e)), } - } - #[cfg(test)] - mod tests { - use super::*; // Importa tudo do módulo pai (Processo, etc.) - use std::thread; - use std::time::Duration; - - #[test] - fn test_process_creation_and_wait_success() { - // Testa o caso mais básico: criar um processo que termina com sucesso. - // O comando `sleep 0.1` é rápido e deve sair com código 0. - let mut processo = Processo::new("sleep", &["0.1"]).expect("Falha ao criar processo 'sleep'"); - - let exit_code = processo.wait().expect("Falha ao esperar pelo processo"); - - // Assert: Verificamos se o código de saída é 0 (sucesso). - assert_eq!(exit_code, 0); - } - #[test] - fn test_process_creation_fails_for_invalid_command() { - // Testa se a criação falha quando o comando não existe. - let resultado = Processo::new("comando_que_definitivamente_nao_existe_123", &[]); - - // Assert: Verificamos que o resultado é um erro. - assert!(resultado.is_err()); - } + pub fn kill(&mut self) -> io::Result<()> { + self.processo.kill() + } + +} +mod tests { + use super::*; + use std::process::Command; + use std::thread; + use std::time::Duration; - #[test] - fn test_terminate_long_running_process() { - // 1. SETUP: Inicia um processo que demoraria muito para terminar sozinho. - let mut processo = Processo::new("sleep", &["30"]).expect("Falha ao criar processo 'sleep 30'"); - // Dá um tempinho para o SO realmente iniciar o processo. - thread::sleep(Duration::from_millis(100)); - - // 2. ACTION: Chama o método que queremos testar. - processo.terminate().expect("Falha ao enviar sinal de terminate"); - - // 3. ASSERT: Verifica o resultado. - let exit_code = processo.wait().expect("Falha ao esperar pelo processo terminado"); - - #[cfg(not(windows))] - { - // Em Linux/macOS, um processo terminado por sinal não tem um código de saída. - // Nossa função wait() converte isso para -1. Esta é a verificação correta! - assert_eq!(exit_code, -1, "Em POSIX, o código de saída de um processo terminado por sinal deve ser -1 (na nossa implementação)"); - } - #[cfg(windows)] - { - // No Windows, TerminateProcess força um código de saída, que geralmente é 1. - assert_eq!(exit_code, 1, "No Windows, o código de saída de um processo terminado geralmente é 1"); - } + 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_kill_long_running_process() { - // O teste para kill() é quase idêntico ao de terminate(), - // pois ambos resultam em um encerramento anormal. - - // 1. SETUP - let mut processo = Processo::new("sleep", &["30"]).expect("Falha ao criar processo 'sleep 30'"); - thread::sleep(Duration::from_millis(100)); - - // 2. ACTION - processo.kill().expect("Falha ao enviar sinal de kill"); - - // 3. ASSERT - let exit_code = processo.wait().expect("Falha ao esperar pelo processo morto"); - - #[cfg(not(windows))] - { - // SIGKILL também resulta em um código de saída "None", que mapeamos para -1. - assert_eq!(exit_code, -1, "Em POSIX, o código de saída de um processo morto por sinal deve ser -1"); - } - #[cfg(windows)] - { - // O comportamento é o mesmo de terminate() no Windows. - assert_eq!(exit_code, 1, "No Windows, o código de saída de um processo morto geralmente é 1"); - } - } - } - + #[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); + } } \ No newline at end of file From 0f3b7d8559eda5cd2128b9ad4f88fc92f555ed88 Mon Sep 17 00:00:00 2001 From: anauehara <120783772+anauehara@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:01:49 -0300 Subject: [PATCH 06/11] Revert "Terminate" --- src/stdlib/process.rs | 108 ------------------------------------------ 1 file changed, 108 deletions(-) delete mode 100644 src/stdlib/process.rs diff --git a/src/stdlib/process.rs b/src/stdlib/process.rs deleted file mode 100644 index dddd30f..0000000 --- a/src/stdlib/process.rs +++ /dev/null @@ -1,108 +0,0 @@ -use std::io; -use std::process::{Child, ExitStatus}; -#[cfg(not(windows))] -use nix::{ - sys::signal::{self, Signal}, - unistd::Pid, -}; - -pub struct Processo { - pub processo: Child, -} - -impl Processo { - - #[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() - } - -} -mod tests { - use super::*; - use std::process::Command; - use std::thread; - use std::time::Duration; - - 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); - } -} \ No newline at end of file From 37a8a7252408187523e52ccd1ea3ee4daedcfb5c Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?Pedro=20Freitas=20Fran=C3=A7a?= <127356730+PedroFFranca@users.noreply.github.com> Date: Sat, 19 Jul 2025 17:42:32 -0300 Subject: [PATCH 07/11] added terminate --- Cargo.lock | 37 ++++++++++++ Cargo.toml | 2 + src/stdlib/subprocess/process.rs | 96 ++++++++++++++++++++++++++++++++ 3 files changed, 135 insertions(+) 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/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index 5be43e5..379dad2 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -127,6 +127,11 @@ pub fn run_shell_command( 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. @@ -141,12 +146,35 @@ impl Processo { 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() + } + + } #[cfg(test)] mod tests { use super::*; + use std::process::Command; + use std::thread; + use std::time::Duration; #[test] fn test_basic_command_execution() { @@ -552,5 +580,73 @@ mod tests { 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); + } } \ No newline at end of file From 6d5fb3f1b934fe7aa79b99157be15b298e8a2694 Mon Sep 17 00:00:00 2001 From: marinapmoreno Date: Sun, 20 Jul 2025 11:47:42 -0300 Subject: [PATCH 08/11] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20do=20process=20co?= =?UTF-8?q?m=20o=20novo=20m=C3=A9todo=20popen=20e=20os=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stdlib/subprocess/process.rs | 688 +++---------------------------- 1 file changed, 65 insertions(+), 623 deletions(-) diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index 379dad2..e5076db 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -1,28 +1,18 @@ -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() - } - } +use std::process::{Child, 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, } -/// Execute a command directly without shell interpretation -pub fn run_command( - command: Vec, - options: RunOptions -) -> Result { +/// 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())); } @@ -32,621 +22,73 @@ pub fn run_command( let mut cmd = Command::new(program); cmd.args(args); + cmd.stdin(Stdio::piped()); - // 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 - }; + 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 }; - let returncode = output.status.code().unwrap_or(-1); - - Ok(CompletedProcess { - returncode, + Ok(PopenProcess { + child, + stdin, stdout, stderr, }) } - Err(e) => { - Err(SubprocessError::from_io_error(e, program)) - } + 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 - }; +use std::io::{Read, Write}; - let stderr = if options.capture_output { - Some(bytes_to_string(&output.stderr)) - } else { - None - }; + #[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 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)) - } + let input = "Mensagem via stdin\nOutra linha\n"; - #[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() - } - - -} - - -#[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); - } -} \ No newline at end of file + // 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 From 378dd788a3d8318e258e1d41f76adec8c885fdd5 Mon Sep 17 00:00:00 2001 From: marinapmoreno Date: Sun, 20 Jul 2025 11:59:45 -0300 Subject: [PATCH 09/11] =?UTF-8?q?Atualiza=C3=A7=C3=A3o=20do=20process=20co?= =?UTF-8?q?m=20o=20novo=20m=C3=A9todo=20popen()=20e=20os=20testes?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stdlib/subprocess/process.rs | 663 ++++++++++++++++++++++++++++++- 1 file changed, 659 insertions(+), 4 deletions(-) diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index e5076db..196857b 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -1,3 +1,173 @@ +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::{Child, ChildStdin, ChildStdout, ChildStderr}; /// Representa um processo em execução com acesso a stdin, stdout e stderr. @@ -24,7 +194,8 @@ pub fn popen_command( cmd.args(args); cmd.stdin(Stdio::piped()); - if options.capture_output { + /// Redireciona stdout/stderr para pipes conforme solicitado + if options.capture_output { cmd.stdout(Stdio::piped()); cmd.stderr(Stdio::piped()); } @@ -47,7 +218,489 @@ pub fn popen_command( } -use std::io::{Read, Write}; + +#[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() { @@ -58,7 +711,7 @@ use std::io::{Read, Write}; ).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"); @@ -91,4 +744,6 @@ use std::io::{Read, Write}; 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 + } + +} \ No newline at end of file From acf4d0fc7e8f5888bbaa26be238b8122a2f3710e Mon Sep 17 00:00:00 2001 From: Marina Pimentel Moreno <118771371+marinapmoreno@users.noreply.github.com> Date: Sun, 20 Jul 2025 15:15:09 -0300 Subject: [PATCH 10/11] =?UTF-8?q?Revert=20"Adicionando=20m=C3=A9todo=20pop?= =?UTF-8?q?en"?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stdlib/subprocess/process.rs | 97 -------------------------------- 1 file changed, 97 deletions(-) diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index 196857b..379dad2 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -168,56 +168,6 @@ impl Processo { } -use std::process::{Child, 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 { @@ -699,51 +649,4 @@ mod tests { 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 From 43fb9d795e0ba2438dd26dcacc22e81e13e7eea9 Mon Sep 17 00:00:00 2001 From: marinapmoreno Date: Sun, 20 Jul 2025 15:50:10 -0300 Subject: [PATCH 11/11] =?UTF-8?q?Novo=20c=C3=B3digo=20do=20m=C3=A9todo=20p?= =?UTF-8?q?open?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- src/stdlib/subprocess/process.rs | 97 ++++++++++++++++++++++++++++++++ 1 file changed, 97 insertions(+) diff --git a/src/stdlib/subprocess/process.rs b/src/stdlib/subprocess/process.rs index 379dad2..7f71257 100644 --- a/src/stdlib/subprocess/process.rs +++ b/src/stdlib/subprocess/process.rs @@ -168,6 +168,56 @@ impl Processo { } +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 { @@ -649,4 +699,51 @@ mod tests { 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