REFACTOR handle user defined test cmd (#59)

This commit is contained in:
Jose M Leon 2025-01-27 08:41:02 -05:00 committed by GitHub
parent d89f8990a3
commit 90b8875a73
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
3 changed files with 272 additions and 176 deletions

View File

@ -1,6 +1,7 @@
"""Utilities for executing and managing user-defined test commands.""" """Utilities for executing and managing user-defined test commands."""
from typing import Dict, Any, Tuple, Optional from typing import Dict, Any, Tuple
import subprocess
from dataclasses import dataclass from dataclasses import dataclass
from rich.console import Console from rich.console import Console
from rich.markdown import Markdown from rich.markdown import Markdown
@ -20,105 +21,146 @@ class TestState:
auto_test: bool auto_test: bool
should_break: bool = False should_break: bool = False
def display_test_failure(attempts: int, max_retries: int) -> None: class TestCommandExecutor:
"""Display test failure message. """Class for executing and managing test commands."""
Args: def __init__(self, config: Dict[str, Any], original_prompt: str, test_attempts: int = 0, auto_test: bool = False):
attempts: Current number of attempts """Initialize the test command executor.
max_retries: Maximum allowed retries
""" Args:
console.print( config: Configuration dictionary containing test settings
Panel( original_prompt: The original prompt to append errors to
Markdown(f"Test failed. Attempt number {attempts} of {max_retries}. Retrying and informing of failure output"), test_attempts: Current number of test attempts
title="🔎 User Defined Test", auto_test: Whether auto-test mode is enabled
border_style="red bold" """
self.config = config
self.state = TestState(
prompt=original_prompt,
test_attempts=test_attempts,
auto_test=auto_test,
should_break=False
) )
) self.max_retries = config.get("max_test_cmd_retries", 5)
def handle_test_failure(state: TestState, original_prompt: str, test_result: Dict[str, Any]) -> TestState: def display_test_failure(self) -> None:
"""Handle test command failure. """Display test failure message."""
console.print(
Panel(
Markdown(f"Test failed. Attempt number {self.state.test_attempts} of {self.max_retries}. Retrying and informing of failure output"),
title="🔎 User Defined Test",
border_style="red bold"
)
)
Args: def handle_test_failure(self, original_prompt: str, test_result: Dict[str, Any]) -> None:
state: Current test state """Handle test command failure.
original_prompt: Original prompt text
test_result: Test command result
Returns: Args:
Updated test state original_prompt: Original prompt text
""" test_result: Test command result
state.prompt = f"{original_prompt}. Previous attempt failed with: <test_cmd_stdout>{test_result['output']}</test_cmd_stdout>" """
display_test_failure(state.test_attempts, 5) # Default max retries self.state.prompt = f"{original_prompt}. Previous attempt failed with: <test_cmd_stdout>{test_result['output']}</test_cmd_stdout>"
state.should_break = False self.display_test_failure()
return state self.state.should_break = False
def run_test_command(cmd: str, state: TestState, original_prompt: str) -> TestState: def run_test_command(self, cmd: str, original_prompt: str) -> None:
"""Run test command and handle result. """Run test command and handle result.
Args: Args:
cmd: Test command to execute cmd: Test command to execute
state: Current test state original_prompt: Original prompt text
original_prompt: Original prompt text """
timeout = self.config.get('timeout', 30)
try:
logger.info(f"Executing test command: {cmd} with timeout {timeout}s")
test_result = run_shell_command(cmd, timeout=timeout)
self.state.test_attempts += 1
Returns: if not test_result["success"]:
Updated test state self.handle_test_failure(original_prompt, test_result)
""" return
try:
test_result = run_shell_command(cmd)
state.test_attempts += 1
if not test_result["success"]: self.state.should_break = True
return handle_test_failure(state, original_prompt, test_result) logger.info("Test command executed successfully")
state.should_break = True except subprocess.TimeoutExpired as e:
return state logger.warning(f"Test command timed out after {timeout}s: {cmd}")
self.state.test_attempts += 1
self.state.prompt = f"{original_prompt}. Previous attempt timed out after {timeout} seconds"
self.display_test_failure()
except Exception as e: except subprocess.CalledProcessError as e:
logger.warning(f"Test command execution failed: {str(e)}") logger.error(f"Test command failed with exit code {e.returncode}: {cmd}\nOutput: {e.output}")
state.test_attempts += 1 self.state.test_attempts += 1
state.should_break = True self.state.prompt = f"{original_prompt}. Previous attempt failed with exit code {e.returncode}: {e.output}"
return state self.display_test_failure()
def handle_user_response(response: str, state: TestState, cmd: str, original_prompt: str) -> TestState: except Exception as e:
"""Handle user's response to test prompt. logger.warning(f"Test command execution failed: {str(e)}")
self.state.test_attempts += 1
self.state.should_break = True
Args: def handle_user_response(self, response: str, cmd: str, original_prompt: str) -> None:
response: User's response (y/n/a) """Handle user's response to test prompt.
state: Current test state Args:
cmd: Test command response: User's response (y/n/a)
original_prompt: Original prompt text cmd: Test command
original_prompt: Original prompt text
"""
response = response.strip().lower()
Returns: if response == "n":
Updated test state self.state.should_break = True
""" return
response = response.strip().lower()
if response == "n": if response == "a":
state.should_break = True self.state.auto_test = True
return state self.run_test_command(cmd, original_prompt)
return
if response == "a": if response == "y":
state.auto_test = True self.run_test_command(cmd, original_prompt)
return run_test_command(cmd, state, original_prompt)
if response == "y": def check_max_retries(self) -> bool:
return run_test_command(cmd, state, original_prompt) """Check if max retries reached.
return state Returns:
True if max retries reached
"""
if self.state.test_attempts >= self.max_retries:
logger.warning("Max test retries reached")
return True
return False
def check_max_retries(attempts: int, max_retries: int) -> bool: def execute(self) -> Tuple[bool, str, bool, int]:
"""Check if max retries reached. """Execute test command and handle retries.
Args: Returns:
attempts: Current number of attempts Tuple containing:
max_retries: Maximum allowed retries - bool: Whether to break the retry loop
- str: Updated prompt
- bool: Updated auto_test flag
- int: Updated test_attempts count
"""
if not self.config.get("test_cmd"):
self.state.should_break = True
return self.state.should_break, self.state.prompt, self.state.auto_test, self.state.test_attempts
Returns: cmd = self.config["test_cmd"]
True if max retries reached
""" if not self.state.auto_test:
if attempts >= max_retries: print()
logger.warning("Max test retries reached") response = ask_human.invoke({"question": "Would you like to run the test command? (y=yes, n=no, a=enable auto-test)"})
return True self.handle_user_response(response, cmd, self.state.prompt)
return False else:
if self.check_max_retries():
logger.error(f"Maximum number of test retries ({self.max_retries}) reached. Stopping test execution.")
console.print(Panel(f"Maximum retries ({self.max_retries}) reached. Test execution stopped.", title="⚠️ Test Execution", border_style="yellow bold"))
self.state.should_break = True
else:
self.run_test_command(cmd, self.state.prompt)
return self.state.should_break, self.state.prompt, self.state.auto_test, self.state.test_attempts
def execute_test_command( def execute_test_command(
config: Dict[str, Any], config: Dict[str, Any],
@ -141,27 +183,5 @@ def execute_test_command(
- bool: Updated auto_test flag - bool: Updated auto_test flag
- int: Updated test_attempts count - int: Updated test_attempts count
""" """
state = TestState( executor = TestCommandExecutor(config, original_prompt, test_attempts, auto_test)
prompt=original_prompt, return executor.execute()
test_attempts=test_attempts,
auto_test=auto_test
)
if not config.get("test_cmd"):
state.should_break = True
return state.should_break, state.prompt, state.auto_test, state.test_attempts
max_retries = config.get("max_test_cmd_retries", 5)
cmd = config["test_cmd"]
if not auto_test:
print()
response = ask_human.invoke({"question": "Would you like to run the test command? (y=yes, n=no, a=enable auto-test)"})
state = handle_user_response(response, state, cmd, original_prompt)
else:
if check_max_retries(test_attempts, max_retries):
state.should_break = True
else:
state = run_test_command(cmd, state, original_prompt)
return state.should_break, state.prompt, state.auto_test, state.test_attempts

View File

@ -190,7 +190,8 @@ def test_execute_test_command(
if auto_test and test_attempts < config.get("max_test_cmd_retries", 5): if auto_test and test_attempts < config.get("max_test_cmd_retries", 5):
if config.get("test_cmd"): if config.get("test_cmd"):
mock_run_cmd.assert_called_once_with(config["test_cmd"]) # Verify run_shell_command called with command and default timeout
mock_run_cmd.assert_called_once_with(config["test_cmd"], timeout=config.get('timeout', 30))
# Verify logging for max retries # Verify logging for max retries
if test_attempts >= config.get("max_test_cmd_retries", 5): if test_attempts >= config.get("max_test_cmd_retries", 5):

View File

@ -2,13 +2,11 @@
import pytest import pytest
from unittest.mock import patch, Mock from unittest.mock import patch, Mock
import subprocess
from ra_aid.tools.handle_user_defined_test_cmd_execution import ( from ra_aid.tools.handle_user_defined_test_cmd_execution import (
TestState, TestState,
execute_test_command, TestCommandExecutor,
handle_test_failure, execute_test_command
run_test_command,
handle_user_response,
check_max_retries
) )
@pytest.fixture @pytest.fixture
@ -17,93 +15,170 @@ def test_state():
return TestState( return TestState(
prompt="test prompt", prompt="test prompt",
test_attempts=0, test_attempts=0,
auto_test=False auto_test=False,
should_break=False
) )
def test_check_max_retries(): @pytest.fixture
"""Test max retries check.""" def test_executor():
assert not check_max_retries(2, 3) """Create a test executor fixture."""
assert check_max_retries(3, 3) config = {"test_cmd": "test", "max_test_cmd_retries": 3}
assert check_max_retries(4, 3) return TestCommandExecutor(config, "test prompt")
def test_handle_test_failure(test_state): def test_check_max_retries(test_executor):
"""Test max retries check."""
test_executor.state.test_attempts = 2
assert not test_executor.check_max_retries()
test_executor.state.test_attempts = 3
assert test_executor.check_max_retries()
test_executor.state.test_attempts = 4
assert test_executor.check_max_retries()
def test_handle_test_failure(test_executor):
"""Test handling of test failures.""" """Test handling of test failures."""
test_result = {"output": "error message"} test_result = {"output": "error message"}
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.display_test_failure"): with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.console.print"):
state = handle_test_failure(test_state, "original", test_result) test_executor.handle_test_failure("original", test_result)
assert not state.should_break assert not test_executor.state.should_break
assert "error message" in state.prompt assert "error message" in test_executor.state.prompt
def test_run_test_command_success(test_state): def test_run_test_command_success(test_executor):
"""Test successful test command execution.""" """Test successful test command execution."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run: with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
mock_run.return_value = {"success": True, "output": ""} mock_run.return_value = {"success": True, "output": ""}
state = run_test_command("test", test_state, "original") test_executor.run_test_command("test", "original")
assert state.should_break assert test_executor.state.should_break
assert state.test_attempts == 1 assert test_executor.state.test_attempts == 1
def test_run_test_command_failure(test_state): def test_run_test_command_failure(test_executor):
"""Test failed test command execution.""" """Test failed test command execution."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run: with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
mock_run.return_value = {"success": False, "output": "error"} mock_run.return_value = {"success": False, "output": "error"}
state = run_test_command("test", test_state, "original") test_executor.run_test_command("test", "original")
assert not state.should_break assert not test_executor.state.should_break
assert state.test_attempts == 1 assert test_executor.state.test_attempts == 1
assert "error" in state.prompt assert "error" in test_executor.state.prompt
def test_run_test_command_error(test_state): def test_run_test_command_error(test_executor):
"""Test test command execution error.""" """Test test command execution error."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run: with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run:
mock_run.side_effect = Exception("Command failed") mock_run.side_effect = Exception("Generic error")
state = run_test_command("test", test_state, "original") test_executor.run_test_command("test", "original")
assert state.should_break assert test_executor.state.should_break
assert state.test_attempts == 1 assert test_executor.state.test_attempts == 1
def test_handle_user_response_no(test_state): def test_run_test_command_timeout(test_executor):
"""Test test command timeout handling."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run,\
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger.warning") as mock_logger:
# Create a TimeoutExpired exception
timeout_exc = subprocess.TimeoutExpired(cmd="test", timeout=30)
mock_run.side_effect = timeout_exc
test_executor.run_test_command("test", "original")
# Verify state updates
assert not test_executor.state.should_break
assert test_executor.state.test_attempts == 1
assert "timed out after 30 seconds" in test_executor.state.prompt
# Verify logging
mock_logger.assert_called_once()
def test_run_test_command_called_process_error(test_executor):
"""Test handling of CalledProcessError exception."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_shell_command") as mock_run,\
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.logger.error") as mock_logger:
# Create a CalledProcessError exception
process_error = subprocess.CalledProcessError(
returncode=1,
cmd="test",
output="Command failed output"
)
mock_run.side_effect = process_error
test_executor.run_test_command("test", "original")
# Verify state updates
assert not test_executor.state.should_break
assert test_executor.state.test_attempts == 1
assert "failed with exit code 1" in test_executor.state.prompt
# Verify logging
mock_logger.assert_called_once()
def test_handle_user_response_no(test_executor):
"""Test handling of 'no' response.""" """Test handling of 'no' response."""
state = handle_user_response("n", test_state, "test", "original") test_executor.handle_user_response("n", "test", "original")
assert state.should_break assert test_executor.state.should_break
assert not state.auto_test assert not test_executor.state.auto_test
def test_handle_user_response_auto(test_state): def test_handle_user_response_auto(test_executor):
"""Test handling of 'auto' response.""" """Test handling of 'auto' response."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_test_command") as mock_run: with patch.object(test_executor, "run_test_command") as mock_run:
mock_state = TestState("prompt", 1, True, True) test_executor.handle_user_response("a", "test", "original")
mock_run.return_value = mock_state assert test_executor.state.auto_test
state = handle_user_response("a", test_state, "test", "original") mock_run.assert_called_once_with("test", "original")
assert state.auto_test
mock_run.assert_called_once()
def test_handle_user_response_yes(test_state): def test_handle_user_response_yes(test_executor):
"""Test handling of 'yes' response.""" """Test handling of 'yes' response."""
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_test_command") as mock_run: with patch.object(test_executor, "run_test_command") as mock_run:
mock_state = TestState("prompt", 1, False, True) test_executor.handle_user_response("y", "test", "original")
mock_run.return_value = mock_state assert not test_executor.state.auto_test
state = handle_user_response("y", test_state, "test", "original") mock_run.assert_called_once_with("test", "original")
assert not state.auto_test
mock_run.assert_called_once()
def test_execute_test_command_no_cmd(): def test_execute_no_cmd():
"""Test execution with no test command.""" """Test execution with no test command."""
result = execute_test_command({}, "prompt") executor = TestCommandExecutor({}, "prompt")
result = executor.execute()
assert result == (True, "prompt", False, 0) assert result == (True, "prompt", False, 0)
def test_execute_test_command_manual(): def test_execute_manual():
"""Test manual test execution.""" """Test manual test execution."""
config = {"test_cmd": "test"} config = {"test_cmd": "test"}
executor = TestCommandExecutor(config, "prompt")
def mock_handle_response(response, cmd, prompt):
# Simulate the behavior of handle_user_response and run_test_command
executor.state.should_break = True
executor.state.test_attempts = 1
executor.state.prompt = "new prompt"
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human") as mock_ask, \ with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.ask_human") as mock_ask, \
patch("ra_aid.tools.handle_user_defined_test_cmd_execution.handle_user_response") as mock_handle: patch.object(executor, "handle_user_response", side_effect=mock_handle_response) as mock_handle:
mock_ask.invoke.return_value = "y" mock_ask.invoke.return_value = "y"
mock_state = TestState("new prompt", 1, False, True)
mock_handle.return_value = mock_state result = executor.execute()
result = execute_test_command(config, "prompt") mock_handle.assert_called_once_with("y", "test", "prompt")
assert result == (True, "new prompt", False, 1) assert result == (True, "new prompt", False, 1)
def test_execute_test_command_auto(): def test_execute_auto():
"""Test auto test execution.""" """Test auto test execution."""
config = {"test_cmd": "test", "max_test_cmd_retries": 3} config = {"test_cmd": "test", "max_test_cmd_retries": 3}
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.run_test_command") as mock_run: executor = TestCommandExecutor(config, "prompt", auto_test=True)
mock_state = TestState("new prompt", 1, True, True)
mock_run.return_value = mock_state # Set up state before creating mock
executor.state.test_attempts = 1
executor.state.should_break = True
with patch.object(executor, "run_test_command") as mock_run:
result = executor.execute()
assert result == (True, "prompt", True, 1)
mock_run.assert_called_once_with("test", "prompt")
def test_execute_test_command_function():
"""Test the execute_test_command function."""
config = {"test_cmd": "test"}
with patch("ra_aid.tools.handle_user_defined_test_cmd_execution.TestCommandExecutor") as mock_executor_class:
mock_executor = Mock()
mock_executor.execute.return_value = (True, "new prompt", True, 1)
mock_executor_class.return_value = mock_executor
result = execute_test_command(config, "prompt", auto_test=True) result = execute_test_command(config, "prompt", auto_test=True)
assert result == (True, "new prompt", True, 1) assert result == (True, "new prompt", True, 1)
mock_executor_class.assert_called_once_with(config, "prompt", 0, True)
mock_executor.execute.assert_called_once()