Skip to content

Commit 067db28

Browse files
Adding max-run-time argument (#306)
* fixing tests and addind arg. * Adding tests. * Running formatter. * Incrementing version.
1 parent 685c7f4 commit 067db28

File tree

10 files changed

+75
-22
lines changed

10 files changed

+75
-22
lines changed

cover_agent/CoverAgent.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -13,7 +13,6 @@
1313
from cover_agent.AICaller import AICaller
1414
from cover_agent.AgentCompletionABC import AgentCompletionABC
1515
from cover_agent.DefaultAgentCompletion import DefaultAgentCompletion
16-
import cover_agent.utils
1716

1817

1918
class CoverAgent:
@@ -106,6 +105,7 @@ def __init__(self, args, agent_completion: AgentCompletionABC = None):
106105
comparison_branch=args.branch,
107106
num_attempts=args.run_tests_multiple_times,
108107
agent_completion=self.agent_completion,
108+
max_run_time=args.max_run_time,
109109
)
110110

111111
def _validate_paths(self):

cover_agent/Runner.py

Lines changed: 8 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,41 +1,35 @@
11
import subprocess
22
import time
33

4-
from cover_agent.settings.config_loader import get_settings
5-
64

75
class Runner:
86
@staticmethod
9-
def run_command(command, cwd=None):
7+
def run_command(command: str, max_run_time: int, cwd: str = None):
108
"""
119
Executes a shell command in a specified working directory and returns its output, error, and exit code.
1210
1311
Parameters:
1412
command (str): The shell command to execute.
13+
max_run_time (int): Maximum allowed runtime in seconds before timeout.
1514
cwd (str, optional): The working directory in which to execute the command. Defaults to None.
1615
1716
Returns:
18-
tuple: A tuple containing the standard output ('stdout'), standard error ('stderr'), exit code ('exit_code'), and the time of the executed command ('command_start_time').
17+
tuple: A tuple containing the standard output ('stdout'), standard error ('stderr'), exit code ('exit_code'),
18+
and the time of the executed command ('command_start_time').
1919
"""
20-
# Get the current time before running the test command, in milliseconds
21-
command_start_time = int(round(time.time() * 1000))
20+
command_start_time = int(
21+
time.time() * 1000
22+
) # Get the current time in milliseconds
2223

23-
max_allowed_runtime_seconds = get_settings().get(
24-
"tests.max_allowed_runtime_seconds", 3600
25-
)
26-
# Ensure the command is executed with shell=True for string commands
2724
try:
2825
result = subprocess.run(
2926
command,
3027
shell=True,
3128
cwd=cwd,
3229
text=True,
3330
capture_output=True,
34-
timeout=max_allowed_runtime_seconds,
31+
timeout=max_run_time,
3532
)
36-
37-
# Return a dictionary with the desired information
3833
return result.stdout, result.stderr, result.returncode, command_start_time
3934
except subprocess.TimeoutExpired:
40-
# Handle the timeout case
4135
return "", "Command timed out", -1, command_start_time

cover_agent/UnitTestValidator.py

Lines changed: 6 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@ def __init__(
2424
code_coverage_report_path: str,
2525
test_command: str,
2626
llm_model: str,
27+
max_run_time: int,
2728
agent_completion: AgentCompletionABC,
2829
test_command_dir: str = os.getcwd(),
2930
included_files: list = None,
@@ -45,6 +46,7 @@ def __init__(
4546
code_coverage_report_path (str): The path to the code coverage report file.
4647
test_command (str): The command to run tests.
4748
llm_model (str): The language model to be used for test generation.
49+
max_run_time (int): The maximum time in seconds to run the test command.
4850
agent_completion (AgentCompletionABC): The agent completion object to use for test generation.
4951
api_base (str, optional): The base API url to use in case model is set to Ollama or Hugging Face. Defaults to an empty string.
5052
test_command_dir (str, optional): The directory where the test command should be executed. Defaults to the current working directory.
@@ -81,6 +83,7 @@ def __init__(
8183
self.comparison_branch = comparison_branch
8284
self.num_attempts = num_attempts
8385
self.agent_completion = agent_completion
86+
self.max_run_time = max_run_time
8487

8588
# Get the logger instance from CustomLogger
8689
self.logger = CustomLogger.get_logger(__name__)
@@ -292,7 +295,9 @@ def run_coverage(self):
292295
f'Running build/test command to generate coverage report: "{self.test_command}"'
293296
)
294297
stdout, stderr, exit_code, time_of_test_command = Runner.run_command(
295-
command=self.test_command, cwd=self.test_command_dir
298+
command=self.test_command,
299+
max_run_time=self.max_run_time,
300+
cwd=self.test_command_dir,
296301
)
297302
assert (
298303
exit_code == 0

cover_agent/main.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -71,6 +71,12 @@ def parse_args():
7171
default=10,
7272
help="The maximum number of iterations. Default: %(default)s.",
7373
)
74+
parser.add_argument(
75+
"--max-run-time",
76+
type=int,
77+
default=30,
78+
help="Maximum time (in seconds) allowed for test execution. Overrides the value in configuration.toml if provided. Defaults to 30 seconds.",
79+
)
7480
parser.add_argument(
7581
"--additional-instructions",
7682
default="",

cover_agent/settings/configuration.toml

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -3,4 +3,4 @@ limit_tokens=true
33
max_tokens=20000
44

55
[tests]
6-
max_allowed_runtime_seconds=3600
6+
max_allowed_runtime_seconds=30

cover_agent/version.txt

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1 +1 @@
1-
0.3.4
1+
0.3.5

tests/test_CoverAgent.py

Lines changed: 6 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -55,6 +55,7 @@ def test_agent_source_file_not_found(self, mock_isfile, mock_unit_cover_agent):
5555
report_filepath="test_results.html",
5656
desired_coverage=90,
5757
max_iterations=10,
58+
max_run_time=30,
5859
)
5960
parse_args = lambda: args
6061
mock_isfile.return_value = False
@@ -88,6 +89,7 @@ def test_agent_test_file_not_found(
8889
desired_coverage=90,
8990
max_iterations=10,
9091
prompt_only=False,
92+
max_run_time=30,
9193
)
9294
parse_args = lambda: args
9395
mock_isfile.side_effect = [True, False]
@@ -128,6 +130,7 @@ def test_duplicate_test_file_without_output_path(self, mock_isfile):
128130
diff_coverage=False,
129131
branch="main",
130132
run_tests_multiple_times=1,
133+
max_run_time=30,
131134
)
132135

133136
with pytest.raises(AssertionError) as exc_info:
@@ -183,6 +186,7 @@ def test_run_max_iterations_strict_coverage(
183186
strict_coverage=True,
184187
diff_coverage=False,
185188
branch="main",
189+
max_run_time=30,
186190
)
187191
# Mock the methods used in run
188192
validator = mock_unit_test_validator.return_value
@@ -215,6 +219,7 @@ def test_project_root_not_found(self, mock_isdir, mock_isfile):
215219
report_filepath="test_results.html",
216220
desired_coverage=90,
217221
max_iterations=10,
222+
max_run_time=30,
218223
)
219224

220225
with pytest.raises(FileNotFoundError) as exc_info:
@@ -259,6 +264,7 @@ def test_run_diff_coverage(
259264
strict_coverage=False,
260265
diff_coverage=True,
261266
branch="main",
267+
max_run_time=30,
262268
)
263269
mock_test_validator.return_value.current_coverage = 0.5
264270
mock_test_validator.return_value.desired_coverage = 90

tests/test_Runner.py

Lines changed: 14 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -1,21 +1,23 @@
11
import pytest
22
from unittest.mock import patch
3-
from cover_agent.Runner import Runner # Adjust the import path as necessary
3+
from cover_agent.Runner import Runner
44

55

66
class TestRunner:
77
def test_run_command_success(self):
88
"""Test the run_command method with a command that succeeds."""
99
command = 'echo "Hello, World!"'
10-
stdout, stderr, exit_code, _ = Runner.run_command(command)
10+
stdout, stderr, exit_code, _ = Runner.run_command(command, max_run_time=10)
1111
assert stdout.strip() == "Hello, World!"
1212
assert stderr == ""
1313
assert exit_code == 0
1414

1515
def test_run_command_with_cwd(self):
1616
"""Test the run_command method with a specified working directory."""
1717
command = 'echo "Working Directory"'
18-
stdout, stderr, exit_code, _ = Runner.run_command(command, cwd="/tmp")
18+
stdout, stderr, exit_code, _ = Runner.run_command(
19+
command, cwd="/tmp", max_run_time=10
20+
)
1921
assert stdout.strip() == "Working Directory"
2022
assert stderr == ""
2123
assert exit_code == 0
@@ -24,10 +26,18 @@ def test_run_command_failure(self):
2426
"""Test the run_command method with a command that fails."""
2527
# Use a command that is guaranteed to fail
2628
command = "command_that_does_not_exist"
27-
stdout, stderr, exit_code, _ = Runner.run_command(command)
29+
stdout, stderr, exit_code, _ = Runner.run_command(command, max_run_time=10)
2830
assert stdout == ""
2931
assert (
3032
"command_that_does_not_exist: not found" in stderr
3133
or "command_that_does_not_exist: command not found" in stderr
3234
)
3335
assert exit_code != 0
36+
37+
def test_run_command_timeout(self):
38+
"""Test that a command exceeding the max_run_time times out."""
39+
command = "sleep 2" # A command that takes longer than the timeout
40+
stdout, stderr, exit_code, _ = Runner.run_command(command, max_run_time=1)
41+
assert stdout == ""
42+
assert stderr == "Command timed out"
43+
assert exit_code == -1

tests/test_UnitTestValidator.py

Lines changed: 10 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ def test_extract_error_message_exception_handling(self):
2525
test_command="pytest",
2626
llm_model="gpt-3",
2727
agent_completion=mock_agent_completion,
28+
max_run_time=30,
2829
)
2930

3031
# Simulate agent_completion raising an exception
@@ -53,6 +54,7 @@ def test_run_coverage_with_report_coverage_flag(self):
5354
llm_model="gpt-3",
5455
agent_completion=MagicMock(),
5556
use_report_coverage_feature_flag=True,
57+
max_run_time=30,
5658
)
5759
with patch.object(
5860
Runner, "run_command", return_value=("", "", 0, datetime.datetime.now())
@@ -79,6 +81,7 @@ def test_extract_error_message_with_prompt_builder(self):
7981
test_command="pytest",
8082
llm_model="gpt-3",
8183
agent_completion=mock_agent_completion,
84+
max_run_time=30,
8285
)
8386

8487
mock_response = """
@@ -138,6 +141,7 @@ def test_validate_test_pass_no_coverage_increase_with_prompt(self):
138141
test_command="pytest",
139142
llm_model="gpt-3",
140143
agent_completion=MagicMock(),
144+
max_run_time=30,
141145
)
142146

143147
# Setup initial state
@@ -181,6 +185,7 @@ def test_initial_test_suite_analysis_with_agent_completion(self):
181185
test_command="pytest",
182186
llm_model="gpt-3",
183187
agent_completion=mock_agent_completion,
188+
max_run_time=30,
184189
)
185190

186191
# Mock responses from agent_completion
@@ -222,6 +227,7 @@ def test_post_process_coverage_report_with_report_coverage_flag(self):
222227
llm_model="gpt-3",
223228
agent_completion=MagicMock(),
224229
use_report_coverage_feature_flag=True,
230+
max_run_time=30,
225231
)
226232
with patch.object(
227233
CoverageProcessor,
@@ -246,6 +252,7 @@ def test_post_process_coverage_report_with_diff_coverage(self):
246252
llm_model="gpt-3",
247253
agent_completion=MagicMock(),
248254
diff_coverage=True,
255+
max_run_time=30,
249256
)
250257
with patch.object(generator, "generate_diff_coverage_report"), patch.object(
251258
CoverageProcessor, "process_coverage_report", return_value=([], [], 0.8)
@@ -266,6 +273,7 @@ def test_post_process_coverage_report_without_flags(self):
266273
test_command="pytest",
267274
llm_model="gpt-3",
268275
agent_completion=MagicMock(),
276+
max_run_time=30,
269277
)
270278
with patch.object(
271279
CoverageProcessor, "process_coverage_report", return_value=([], [], 0.7)
@@ -288,6 +296,7 @@ def test_generate_diff_coverage_report_success(self):
288296
agent_completion=MagicMock(),
289297
diff_coverage=True,
290298
comparison_branch="main",
299+
max_run_time=30,
291300
)
292301
with patch(
293302
"cover_agent.UnitTestValidator.diff_cover_main"
@@ -316,6 +325,7 @@ def test_generate_diff_coverage_report_failure(self):
316325
agent_completion=MagicMock(),
317326
diff_coverage=True,
318327
comparison_branch="main",
328+
max_run_time=30,
319329
)
320330
with patch(
321331
"cover_agent.UnitTestValidator.diff_cover_main",

tests/test_main.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -128,3 +128,25 @@ def test_main_calls_agent_run(self, mock_isfile, mock_parse_args, mock_cover_age
128128

129129
mock_cover_agent.assert_called_once_with(args)
130130
mock_agent_instance.run.assert_called_once()
131+
132+
def test_parse_args_with_max_run_time(self):
133+
with patch(
134+
"sys.argv",
135+
[
136+
"program.py",
137+
"--source-file-path",
138+
"test_source.py",
139+
"--test-file-path",
140+
"test_file.py",
141+
"--code-coverage-report-path",
142+
"coverage_report.xml",
143+
"--test-command",
144+
"pytest",
145+
"--max-iterations",
146+
"10",
147+
"--max-run-time",
148+
"45",
149+
],
150+
):
151+
args = parse_args()
152+
assert args.max_run_time == 45

0 commit comments

Comments
 (0)