Skip to content

Commit e6ff07f

Browse files
author
Shriyansh Agnihotri
committed
Adding skip browser close to carry forward context from one test to another, not recommended feature
1 parent 45487c8 commit e6ff07f

File tree

4 files changed

+117
-34
lines changed

4 files changed

+117
-34
lines changed

testzeus_hercules/__main__.py

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -8,6 +8,7 @@
88
get_junit_xml_base_path,
99
get_source_log_folder_path,
1010
set_default_test_id,
11+
get_dont_close_browser,
1112
)
1213
from testzeus_hercules.core.runner import SingleCommandInputRunner
1314
from testzeus_hercules.telemetry import EventData, EventType, add_event
@@ -40,13 +41,16 @@ def sequential_process() -> None:
4041
6. Merges all JUnit XML results into a single file.
4142
7. Logs the location of the final result file.
4243
"""
43-
list_of_feats = process_feature_file()
44+
dont_close_browser = get_dont_close_browser()
45+
list_of_feats = process_feature_file(pass_background_to_all=dont_close_browser)
4446
input_gherkin_file_path = get_input_gherkin_file_path()
4547
# get name of the feature file using os package
4648
feature_file_name = os.path.basename(input_gherkin_file_path)
4749

4850
result_of_tests = []
49-
final_result_file_name = f"{get_junit_xml_base_path()}/{feature_file_name}_result.xml"
51+
final_result_file_name = (
52+
f"{get_junit_xml_base_path()}/{feature_file_name}_result.xml"
53+
)
5054
add_event(EventType.RUN, EventData(detail="Total Runs: " + str(len(list_of_feats))))
5155
for feat in list_of_feats:
5256
file_path = feat["output_file"]
@@ -65,6 +69,7 @@ def sequential_process() -> None:
6569
runner = SingleCommandInputRunner(
6670
stake_id=stake_id,
6771
command=cmd,
72+
dont_terminate_browser_after_run=dont_close_browser,
6873
)
6974
asyncio.run(runner.start())
7075

@@ -94,14 +99,17 @@ def sequential_process() -> None:
9499
proofs_video_path=runner.browser_manager.get_latest_video_path(),
95100
network_logs_path=runner.browser_manager.request_response_log_file,
96101
logs_path=get_source_log_folder_path(stake_id),
97-
planner_thoughts_path=get_source_log_folder_path(stake_id) + "/chat_messages.json",
102+
planner_thoughts_path=get_source_log_folder_path(stake_id)
103+
+ "/chat_messages.json",
98104
)
99105
)
100106
JUnitXMLGenerator.merge_junit_xml(result_of_tests, final_result_file_name)
101107
logger.info(f"Results published in junitxml file: {final_result_file_name}")
102108

103109
# building html from junitxml
104-
final_result_html_file_name = f"{get_junit_xml_base_path()}/{feature_file_name}_result.html"
110+
final_result_html_file_name = (
111+
f"{get_junit_xml_base_path()}/{feature_file_name}_result.html"
112+
)
105113
prepare_html([final_result_file_name, final_result_html_file_name])
106114
logger.info(f"Results published in html file: {final_result_html_file_name}")
107115

testzeus_hercules/config.py

Lines changed: 43 additions & 12 deletions
Original file line numberDiff line numberDiff line change
@@ -16,9 +16,15 @@
1616

1717

1818
def arguments() -> None:
19-
parser = argparse.ArgumentParser(description="Hercules: The World's First Open-Source AI Agent for End-to-End Testing")
20-
parser.add_argument("--input-file", type=str, help="Path to the input file.", required=False)
21-
parser.add_argument("--output-path", type=str, help="Path to the output directory.", required=False)
19+
parser = argparse.ArgumentParser(
20+
description="Hercules: The World's First Open-Source AI Agent for End-to-End Testing"
21+
)
22+
parser.add_argument(
23+
"--input-file", type=str, help="Path to the input file.", required=False
24+
)
25+
parser.add_argument(
26+
"--output-path", type=str, help="Path to the output directory.", required=False
27+
)
2228
parser.add_argument(
2329
"--test-data-path",
2430
type=str,
@@ -76,11 +82,17 @@ def arguments() -> None:
7682
agents_llm_config_file_ref_key = os.environ.get("AGENTS_LLM_CONFIG_FILE_REF_KEY")
7783

7884

79-
if (llm_model_name and llm_model_api_key) and (agents_llm_config_file or agents_llm_config_file_ref_key):
80-
logger.error("Provide either LLM_MODEL_NAME and LLM_MODEL_API_KEY together, or AGENTS_LLM_CONFIG_FILE and AGENTS_LLM_CONFIG_FILE_REF_KEY together, not both.")
85+
if (llm_model_name and llm_model_api_key) and (
86+
agents_llm_config_file or agents_llm_config_file_ref_key
87+
):
88+
logger.error(
89+
"Provide either LLM_MODEL_NAME and LLM_MODEL_API_KEY together, or AGENTS_LLM_CONFIG_FILE and AGENTS_LLM_CONFIG_FILE_REF_KEY together, not both."
90+
)
8191
exit(1)
8292

83-
if (not llm_model_name or not llm_model_api_key) and (not agents_llm_config_file or not agents_llm_config_file_ref_key):
93+
if (not llm_model_name or not llm_model_api_key) and (
94+
not agents_llm_config_file or not agents_llm_config_file_ref_key
95+
):
8496
logger.error(
8597
"Either LLM_MODEL_NAME and LLM_MODEL_API_KEY must be set together, or AGENTS_LLM_CONFIG_FILE and AGENTS_LLM_CONFIG_FILE_REF_KEY must be set together. user --llm-model and --llm-model-api-key in hercules command"
8698
)
@@ -97,12 +109,18 @@ def arguments() -> None:
97109
PROJECT_TEMP_PATH = os.path.join(PROJECT_ROOT, "temp")
98110
PROJECT_TEST_ROOT = os.path.join(PROJECT_ROOT, "test")
99111

100-
INPUT_GHERKIN_FILE_PATH = os.environ.get("INPUT_GHERKIN_FILE_PATH") or os.path.join(PROJECT_ROOT, "input/test.feature")
112+
INPUT_GHERKIN_FILE_PATH = os.environ.get("INPUT_GHERKIN_FILE_PATH") or os.path.join(
113+
PROJECT_ROOT, "input/test.feature"
114+
)
101115
TMP_GHERKIN_PATH = os.path.join(PROJECT_ROOT, "gherkin_files")
102-
JUNIT_XML_BASE_PATH = os.environ.get("JUNIT_XML_BASE_PATH") or os.path.join(PROJECT_ROOT, "output")
116+
JUNIT_XML_BASE_PATH = os.environ.get("JUNIT_XML_BASE_PATH") or os.path.join(
117+
PROJECT_ROOT, "output"
118+
)
103119

104120
SOURCE_LOG_FOLDER_PATH = os.path.join(PROJECT_ROOT, "log_files")
105-
TEST_DATA_PATH = os.environ.get("TEST_DATA_PATH") or os.path.join(PROJECT_ROOT, "test_data")
121+
TEST_DATA_PATH = os.environ.get("TEST_DATA_PATH") or os.path.join(
122+
PROJECT_ROOT, "test_data"
123+
)
106124
SCREEN_SHOT_PATH = os.path.join(PROJECT_ROOT, "proofs")
107125

108126
if "HF_HOME" not in os.environ:
@@ -112,6 +130,13 @@ def arguments() -> None:
112130
os.environ["TOKENIZERS_PARALLELISM"] = "false"
113131

114132

133+
def get_dont_close_browser() -> bool:
134+
"""
135+
Check if the system should close the browser after running the test.
136+
"""
137+
return os.environ.get("DONT_CLOSE_BROWSER", "false").lower().strip() == "true"
138+
139+
115140
def get_cdp_config() -> dict | None:
116141
"""
117142
Get the CDP config.
@@ -192,7 +217,9 @@ def get_input_gherkin_file_path() -> str:
192217
base_path = os.path.dirname(INPUT_GHERKIN_FILE_PATH)
193218
if not os.path.exists(base_path):
194219
os.makedirs(base_path)
195-
logger.info(f"Created INPUT_GHERKIN_FILE_PATH folder at: {INPUT_GHERKIN_FILE_PATH}")
220+
logger.info(
221+
f"Created INPUT_GHERKIN_FILE_PATH folder at: {INPUT_GHERKIN_FILE_PATH}"
222+
)
196223
return INPUT_GHERKIN_FILE_PATH
197224

198225

@@ -236,7 +263,9 @@ def get_source_log_folder_path(test_id: Optional[str] = None) -> str:
236263
source_log_folder_path = os.path.join(SOURCE_LOG_FOLDER_PATH, test_id)
237264
if not os.path.exists(source_log_folder_path):
238265
os.makedirs(source_log_folder_path)
239-
logger.info(f"Created source_log_folder_path folder at: {source_log_folder_path}")
266+
logger.info(
267+
f"Created source_log_folder_path folder at: {source_log_folder_path}"
268+
)
240269
return source_log_folder_path
241270

242271

@@ -299,4 +328,6 @@ def get_project_temp_path(test_id: Optional[str] = None) -> str:
299328
"BROWSER_TYPE": get_browser_type(),
300329
"CAPTURE_NETWORK": should_capture_network(),
301330
}
302-
add_event(EventType.CONFIG, EventData(detail="General Config", additional_data=config_brief))
331+
add_event(
332+
EventType.CONFIG, EventData(detail="General Config", additional_data=config_brief)
333+
)

testzeus_hercules/core/runner.py

Lines changed: 39 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -27,15 +27,19 @@ def __init__(
2727
planner_max_chat_round: int = 50,
2828
browser_nav_max_chat_round: int = 10,
2929
stake_id: str | None = None,
30+
dont_terminate_browser_after_run: bool = False,
3031
):
3132
self.planner_number_of_rounds = planner_max_chat_round
3233
self.browser_number_of_rounds = browser_nav_max_chat_round
3334
self.browser_manager = None
3435
self.autogen_wrapper = None
3536
self.is_running = False
3637
self.stake_id = stake_id
38+
self.dont_terminate_browser_after_run = dont_terminate_browser_after_run
3739

38-
self.save_chat_logs_to_files = os.getenv("SAVE_CHAT_LOGS_TO_FILE", "True").lower() in ["true", "1"]
40+
self.save_chat_logs_to_files = os.getenv(
41+
"SAVE_CHAT_LOGS_TO_FILE", "True"
42+
).lower() in ["true", "1"]
3943

4044
self.planner_agent_name = "planner_agent"
4145
self.shutdown_event = asyncio.Event()
@@ -56,7 +60,9 @@ async def initialize(self) -> None:
5660
browser_nav_max_chat_round=self.browser_number_of_rounds,
5761
)
5862

59-
self.browser_manager = PlaywrightManager(gui_input_mode=False, stake_id=self.stake_id)
63+
self.browser_manager = PlaywrightManager(
64+
gui_input_mode=False, stake_id=self.stake_id
65+
)
6066
await self.browser_manager.async_initialize()
6167

6268
async def process_command(self, command: str) -> tuple[Any, float]:
@@ -80,13 +86,19 @@ async def process_command(self, command: str) -> tuple[Any, float]:
8086
if command:
8187
self.is_running = True
8288
start_time = time.time()
83-
current_url = await self.browser_manager.get_current_url() if self.browser_manager else None
89+
current_url = (
90+
await self.browser_manager.get_current_url()
91+
if self.browser_manager
92+
else None
93+
)
8494
self.browser_manager.log_user_message(command) # type: ignore
8595
result = None
8696
logger.info(f"Processing command: {command}")
8797
if self.autogen_wrapper:
8898
await self.browser_manager.update_processing_state("processing") # type: ignore
89-
result = await self.autogen_wrapper.process_command(command, current_url)
99+
result = await self.autogen_wrapper.process_command(
100+
command, current_url
101+
)
90102
await self.browser_manager.update_processing_state("done") # type: ignore
91103
end_time = time.time()
92104
elapsed_time = round(end_time - start_time, 2)
@@ -95,7 +107,11 @@ async def process_command(self, command: str) -> tuple[Any, float]:
95107
if result is not None:
96108
chat_history = result.chat_history # type: ignore
97109
last_message = chat_history[-1] if chat_history else None # type: ignore
98-
if last_message and "terminate" in last_message and last_message["terminate"] == "yes":
110+
if (
111+
last_message
112+
and "terminate" in last_message
113+
and last_message["terminate"] == "yes"
114+
):
99115
await self.browser_manager.notify_user(last_message, "answer") # type: ignore
100116

101117
await self.browser_manager.notify_user(f"Task Completed ({elapsed_time}s).", "info") # type: ignore
@@ -107,7 +123,9 @@ async def save_planner_chat_messages(self) -> None:
107123
"""
108124
Saves chat messages to a file or logs them based on configuration.
109125
"""
110-
messages = self.autogen_wrapper.agents_map[self.planner_agent_name].chat_messages
126+
messages = self.autogen_wrapper.agents_map[
127+
self.planner_agent_name
128+
].chat_messages
111129
messages_str_keys = {str(key): value for key, value in messages.items()}
112130
res_output_thoughts_logs_di = {}
113131
for key, value in messages_str_keys.items():
@@ -126,7 +144,9 @@ async def save_planner_chat_messages(self) -> None:
126144
try:
127145
res_content = json.loads(content)
128146
except json.JSONDecodeError:
129-
logger.debug(f"Failed to decode JSON: {content}, keeping as multiline string")
147+
logger.debug(
148+
f"Failed to decode JSON: {content}, keeping as multiline string"
149+
)
130150
res_content = content
131151
res_output_thoughts_logs_di[key][idx]["content"] = res_content
132152

@@ -180,7 +200,9 @@ async def start(self) -> None:
180200
"""
181201
await self.initialize()
182202
while not self.is_running:
183-
command: str = await async_input("Enter your command (or type 'exit' to quit): ")
203+
command: str = await async_input(
204+
"Enter your command (or type 'exit' to quit): "
205+
)
184206
await self.process_command(command)
185207
if self.shutdown_event.is_set():
186208
break
@@ -192,7 +214,12 @@ class SingleCommandInputRunner(BaseRunner):
192214
A runner that handles input command and return the result.
193215
"""
194216

195-
def __init__(self, command: str, *args: Any, **kwargs: Any) -> None:
217+
def __init__(
218+
self,
219+
command: str,
220+
*args: Any,
221+
**kwargs: Any,
222+
) -> None:
196223
super().__init__(*args, **kwargs)
197224
self.command = command
198225
self.result = None
@@ -204,5 +231,6 @@ async def start(self) -> None:
204231
"""
205232
await self.initialize()
206233
self.result, self.execution_time = await self.process_command(self.command)
207-
_ = await self.process_command("exit")
208-
await self.wait_for_exit()
234+
if not self.dont_terminate_browser_after_run:
235+
_ = await self.process_command("exit")
236+
await self.wait_for_exit()

testzeus_hercules/utils/gherkin_helper.py

Lines changed: 23 additions & 7 deletions
Original file line numberDiff line numberDiff line change
@@ -9,14 +9,17 @@
99
input_gherkin_file_path = get_input_gherkin_file_path()
1010

1111

12-
def split_feature_file(input_file: str, output_dir: str) -> List[Dict[str, str]]:
12+
def split_feature_file(
13+
input_file: str, output_dir: str, dont_append_header: bool = False
14+
) -> List[Dict[str, str]]:
1315
"""
1416
Splits a single BDD feature file into multiple feature files, with each file containing a single scenario.
1517
The script preserves the feature-level content that should be shared across all scenario files.
1618
1719
Parameters:
1820
input_file (str): Path to the input BDD feature file.
1921
output_dir (str): Path to the directory where the split feature files will be saved.
22+
dont_append_header (bool): If True, the Feature header is only added to the first extracted scenario file.
2023
2124
Returns:
2225
list: A list of dictionaries containing feature, scenario, and output file path.
@@ -67,13 +70,22 @@ def split_feature_file(input_file: str, output_dir: str) -> List[Dict[str, str]]
6770
f_scenario = f_scenario.replace(comment_line, "")
6871

6972
if already_visited_scenarios[scenario_title] > 0:
70-
scenario_title = f"{scenario_title} - {already_visited_scenarios[scenario_title]}"
73+
scenario_title = (
74+
f"{scenario_title} - {already_visited_scenarios[scenario_title]}"
75+
)
7176
scenario_filename = f"{scenario_title.replace(' ', '_')}_{already_visited_scenarios[scenario_title]}.feature"
7277
output_file = os.path.join(output_dir, scenario_filename)
7378
already_visited_scenarios[scenario_title] += 1
7479

80+
if dont_append_header and i > 0:
81+
file_content = (
82+
f"{prev_comment_lines}\n{all_scenarios[i]}{scenario_title}{f_scenario}"
83+
)
84+
else:
85+
file_content = f"{feature_header}\n\n{prev_comment_lines}\n{all_scenarios[i]}{scenario_title}{f_scenario}"
86+
7587
with open(output_file, "w") as f:
76-
f.write(f"{feature_header}\n\n{prev_comment_lines}\n{all_scenarios[i]}{scenario_title}{f_scenario}")
88+
f.write(file_content)
7789
prev_comment_lines = comment_lines
7890

7991
scenario_di = {
@@ -105,17 +117,21 @@ def serialize_feature_file(file_path: str) -> str:
105117
return feature_content
106118

107119

108-
def process_feature_file() -> List[Dict[str, str]]:
120+
def process_feature_file(pass_background_to_all: bool = True) -> List[Dict[str, str]]:
109121
"""
110122
Processes a Gherkin feature file by splitting it into smaller parts.
111123
112124
Returns:
113125
List[Dict[str, str]]: A list of dictionaries containing the split parts of the feature file.
114126
"""
115-
return split_feature_file(input_gherkin_file_path, tmp_gherkin_path)
127+
return split_feature_file(
128+
input_gherkin_file_path,
129+
tmp_gherkin_path,
130+
dont_append_header=not pass_background_to_all,
131+
)
116132

117133

118-
def split_test() -> None:
134+
def split_test(pass_background_to_all: bool = True) -> None:
119135
"""
120136
Parses command line arguments and splits the feature file into individual scenario files.
121137
"""
@@ -135,7 +151,7 @@ def split_test() -> None:
135151
# feature_file_path = args.feature_file_path
136152
# output_dir = args.output_dir
137153
# list_of_feats = split_feature_file(feature_file_path, output_dir)
138-
list_of_feats = process_feature_file()
154+
list_of_feats = process_feature_file(pass_background_to_all=pass_background_to_all)
139155
for feat in list_of_feats:
140156
file_path = feat["output_file"]
141157
print(serialize_feature_file(file_path))

0 commit comments

Comments
 (0)