Skip to content

Commit a82d1c2

Browse files
Merge pull request #8 from GoWithitRoger/feat/AIO-context-manager
refactor: move chromedriver cleanup into managed_webdriver_session co…
2 parents 08ee061 + 0cded6b commit a82d1c2

3 files changed

Lines changed: 93 additions & 116 deletions

File tree

main.py

Lines changed: 54 additions & 81 deletions
Original file line numberDiff line numberDiff line change
@@ -56,29 +56,6 @@ def set_chromedriver_pid(self, pid: int) -> None:
5656
self.last_chromedriver_pid = pid
5757

5858

59-
def cleanup_old_processes(debug_logger: Optional["DebugLogger"] = None) -> None:
60-
"""Find and terminate any lingering chromedriver processes from previous runs.
61-
62-
This is a safeguard against resource leaks from crashed/orphaned sessions.
63-
"""
64-
print("Running pre-emptive cleanup of old chromedriver processes...")
65-
if debug_logger:
66-
debug_logger.log("cleanup_old_processes: START")
67-
try:
68-
# Use pkill to find and kill processes by name. -f checks the full command line.
69-
# The [c]haracter class is a trick to prevent pkill from finding its own process.
70-
command = "pkill -f '[c]hromedriver'"
71-
result = subprocess.run(command, shell=True, check=False)
72-
if debug_logger:
73-
rc = getattr(result, "returncode", "NA")
74-
debug_logger.log(f"cleanup_old_processes: END (rc={rc})")
75-
print("Cleanup complete.")
76-
except Exception as e:
77-
if debug_logger:
78-
debug_logger.log(f"cleanup_old_processes: ERROR {e}")
79-
print(f"Notice: Pre-emptive cleanup failed. This is non-critical. Error: {e}")
80-
81-
8259
def log_running_chromedriver_processes(debug_logger: DebugLogger) -> None:
8360
"""Logs any active chromedriver processes using `ps`.
8461
@@ -102,52 +79,64 @@ def log_running_chromedriver_processes(debug_logger: DebugLogger) -> None:
10279
debug_logger.log(f"Error while checking chromedriver processes: {e}")
10380

10481

82+
def cleanup_old_processes() -> None:
83+
"""Kill lingering chromedriver processes from prior runs (best-effort)."""
84+
try:
85+
subprocess.run("pkill -f '[c]hromedriver'", shell=True, check=False)
86+
except Exception:
87+
# Ignore any error; this is a best-effort cleanup.
88+
pass
89+
90+
10591
@contextmanager
10692
def managed_webdriver_session(chrome_options: Options, debug_logger: DebugLogger):
107-
"""A self-cleaning context manager for the Selenium WebDriver.
93+
"""A self-contained, resilient context manager for Selenium WebDriver.
10894
109-
Sets up the driver and guarantees the chromedriver service process is
110-
terminated on exit, aggressively if needed to avoid orphans.
95+
It handles pre-emptive cleanup, robust initialization, and guaranteed teardown.
11196
"""
112-
service: ChromeService = ChromeService()
97+
debug_logger.log("managed_webdriver_session: START")
98+
print("Running pre-emptive cleanup of old chromedriver processes...")
99+
try:
100+
subprocess.run("pkill -f '[c]hromedriver'", shell=True, check=False)
101+
print("Cleanup complete.")
102+
except Exception as e:
103+
print(f"Notice: Pre-emptive cleanup failed. This is non-critical. Error: {e}")
104+
113105
driver: Optional[WebDriver] = None
106+
service: Optional[ChromeService] = None
114107
print("Setting up WebDriver for gateway tests...")
115108
try:
109+
service = ChromeService()
116110
driver = webdriver.Chrome(service=service, options=chrome_options)
117-
# Track and log the chromedriver PID if available
118-
try:
119-
if service and service.process and service.process.pid:
120-
debug_logger.set_chromedriver_pid(service.process.pid)
121-
debug_logger.log(
122-
f"WebDriver service started with PID: {service.process.pid}"
123-
)
124-
except Exception as e: # pragma: no cover - best effort logging
125-
debug_logger.log(f"Unable to retrieve WebDriver PID: {e}")
111+
if service and service.process and service.process.pid:
112+
debug_logger.set_chromedriver_pid(service.process.pid)
113+
debug_logger.log(f"WebDriver service started with PID: {service.process.pid}")
126114
yield driver
115+
except Exception as e:
116+
print(f"CRITICAL: Failed to initialize WebDriver session. Error: {e}")
117+
yield None
127118
finally:
128119
print("Shutting down WebDriver session...")
129120
if driver:
130121
debug_logger.log("WebDriver quit: START")
131122
try:
132-
driver.quit() # Graceful shutdown of browser and driver
123+
driver.quit()
133124
finally:
134125
debug_logger.log("WebDriver quit: END")
135-
# Aggressive kill of chromedriver service process
136126
if service and getattr(service, "process", None):
137-
pid = getattr(service.process, "pid", None)
138-
if pid:
139-
debug_logger.log(
140-
f"Forcefully terminating chromedriver service (PID: {pid})..."
141-
)
142127
try:
143-
service.process.kill() # Force kill to prevent orphans
144-
service.process.wait(timeout=5) # Confirm termination
145-
debug_logger.log("Service terminated successfully.")
146-
except Exception as e: # pragma: no cover - defensive
147-
debug_logger.log(
148-
"Notice: Could not kill service process; it may have already exited. "
128+
pid = getattr(service.process, "pid", None)
129+
if pid:
130+
print(f"Forcefully terminating chromedriver service (PID: {pid})...")
131+
service.process.kill()
132+
service.process.wait(timeout=5)
133+
print("Service terminated successfully.")
134+
except Exception as e:
135+
print(
136+
"Notice: Could not kill service process, it may have already exited. "
149137
f"Error: {e}"
150138
)
139+
debug_logger.log("managed_webdriver_session: END")
151140
# Verify process state after teardown
152141
log_running_chromedriver_processes(debug_logger)
153142

@@ -451,11 +440,8 @@ def run_ping_test_task(driver: WebDriver) -> Optional[GatewayPingResults]:
451440
return None
452441
return parse_gateway_ping_results(results_text)
453442
except Exception as e:
454-
print(f"Error during gateway ping test: {e}")
455-
error_time = datetime.now().strftime("%Y%m%d_%H%M%S")
456-
screenshot_file = f"error_screenshot_gateway_ping_{error_time}.png"
457-
driver.save_screenshot(screenshot_file)
458-
print(f"Saved screenshot to {screenshot_file} for debugging.")
443+
# Just log the error and return. Don't try to interact with a potentially dead driver.
444+
print(f"An error occurred during the task: {e}")
459445
return None
460446

461447

@@ -504,7 +490,8 @@ def run_speed_test_task(driver: WebDriver, access_code: str) -> Optional[SpeedRe
504490
break
505491
return results if results else None
506492
except Exception as e:
507-
print(f"Error during gateway speed test: {e}")
493+
# Just log the error and return. Don't try to interact with a potentially dead driver.
494+
print(f"An error occurred during the task: {e}")
508495
return None
509496

510497

@@ -713,14 +700,14 @@ def perform_checks() -> None:
713700
master_results: dict[str, str | float | int | None] = {}
714701
debug_log = DebugLogger(start_time=time.time())
715702
debug_log.log("perform_checks: START")
716-
# Pre-emptive scavenger to ensure no orphaned chromedriver processes linger
717-
cleanup_old_processes(debug_log)
718703
print(
719704
f"\n[{datetime.now().strftime('%Y-%m-%d %H:%M:%S')}] "
720705
f"Starting checks (Run #{run_counter})..."
721706
)
722707

723-
# Check for any lingering chromedriver processes from previous runs
708+
# Pre-emptive cleanup should run first
709+
cleanup_old_processes()
710+
# Optionally log existing chromedriver processes after cleanup in debug mode
724711
log_running_chromedriver_processes(debug_log)
725712

726713
# --- Run Local Tests (No Browser Required) ---
@@ -763,34 +750,20 @@ def perform_checks() -> None:
763750
chrome_options.add_argument("--no-sandbox")
764751
chrome_options.add_argument("--disable-dev-shm-usage")
765752

766-
try:
767-
debug_log.log("Selenium setup: START")
768-
with managed_webdriver_session(chrome_options, debug_log) as driver:
769-
debug_log.log("Selenium setup: END")
770-
driver.get(config.GATEWAY_URL)
771-
time.sleep(2)
753+
# The 'try...except' is removed, as the context manager handles its own errors.
754+
with managed_webdriver_session(chrome_options, debug_log) as driver:
755+
if driver:
756+
# Safely run tasks if the driver was created successfully
772757
debug_log.log("run_ping_test_task: START")
773-
gateway_ping_results = run_ping_test_task(driver)
758+
master_results.update(run_ping_test_task(driver) or {})
774759
debug_log.log("run_ping_test_task: END")
775-
if gateway_ping_results:
776-
master_results.update(gateway_ping_results)
777760
if should_run_gateway_speed_test:
778761
debug_log.log("run_speed_test_task: START")
779-
gateway_speed_results = run_speed_test_task(driver, DEVICE_ACCESS_CODE)
762+
master_results.update(run_speed_test_task(driver, DEVICE_ACCESS_CODE) or {})
780763
debug_log.log("run_speed_test_task: END")
781-
if gateway_speed_results:
782-
master_results.update(gateway_speed_results)
783-
except Exception as e:
784-
print(f"A critical error occurred in the WebDriver session: {e}")
785-
# Best-effort screenshot if driver existed during context
786-
try:
787-
if 'driver' in locals() and driver:
788-
error_time = datetime.now().strftime("%Y%m%d_%H%M%S")
789-
screenshot_file = f"error_screenshot_{error_time}.png"
790-
driver.save_screenshot(screenshot_file)
791-
print(f"Saved screenshot to {screenshot_file} for debugging.")
792-
except Exception:
793-
pass
764+
else:
765+
# This 'else' block will run if the context manager yields None
766+
print("Skipping gateway tests because WebDriver session failed to start.")
794767

795768
debug_log.log("perform_checks: END")
796769
log_results(master_results)

tests/test_cleanup.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -1,25 +1,46 @@
11
import os
22
import sys
3-
from unittest.mock import patch
3+
import time
4+
from unittest.mock import MagicMock, patch
45

56
# Ensure the main module can be imported
67
sys.path.insert(0, os.path.abspath(os.path.join(os.path.dirname(__file__), "..")))
78

89
import main
910

1011

11-
def test_cleanup_old_processes_invokes_pkill():
12-
with patch("main.subprocess.run") as mock_run:
13-
main.cleanup_old_processes()
14-
mock_run.assert_called()
15-
args, kwargs = mock_run.call_args
16-
# Ensure pkill is targeted at chromedriver with -f and shell=True
17-
assert "pkill -f '[c]hromedriver'" in args[0]
18-
assert kwargs.get("shell") is True
19-
assert kwargs.get("check") is False
12+
def test_context_manager_runs_pkill_preemptively():
13+
debug_logger = main.DebugLogger(start_time=time.time())
2014

15+
fake_service = MagicMock()
16+
fake_service.process = MagicMock()
17+
fake_driver = MagicMock()
2118

22-
def test_cleanup_old_processes_handles_exception():
23-
with patch("main.subprocess.run", side_effect=RuntimeError("boom")):
24-
# Should not raise
25-
main.cleanup_old_processes()
19+
with (
20+
patch("main.subprocess.run") as mock_run,
21+
patch.object(main, "ChromeService", return_value=fake_service),
22+
patch.object(main.webdriver, "Chrome", return_value=fake_driver),
23+
patch.object(main, "log_running_chromedriver_processes"),
24+
):
25+
opts = main.Options()
26+
with main.managed_webdriver_session(opts, debug_logger):
27+
pass
28+
# Pre-emptive cleanup should have been invoked
29+
mock_run.assert_any_call("pkill -f '[c]hromedriver'", shell=True, check=False)
30+
31+
32+
def test_context_manager_yields_none_on_setup_failure():
33+
debug_logger = main.DebugLogger(start_time=time.time())
34+
35+
with (
36+
patch("main.subprocess.run") as mock_run,
37+
patch.object(main, "ChromeService") as mock_service,
38+
patch.object(main.webdriver, "Chrome", side_effect=RuntimeError("boom")),
39+
patch.object(main, "log_running_chromedriver_processes"),
40+
):
41+
mock_service.return_value.process = MagicMock()
42+
opts = main.Options()
43+
with main.managed_webdriver_session(opts, debug_logger) as driver:
44+
assert driver is None
45+
# Ensure pkill was attempted even when setup fails
46+
mock_run.assert_any_call("pkill -f '[c]hromedriver'", shell=True, check=False)

tests/test_main_flow.py

Lines changed: 4 additions & 21 deletions
Original file line numberDiff line numberDiff line change
@@ -23,7 +23,7 @@ def mock_tasks():
2323
patch("main.run_local_speed_test_task", return_value={}) as mock_local_speed,
2424
patch("main.run_ping_test_task", return_value={}) as mock_gateway_ping,
2525
patch("main.run_speed_test_task", return_value={}) as mock_gateway_speed,
26-
patch("main.cleanup_old_processes") as mock_cleanup,
26+
patch("main.subprocess.run") as mock_subproc_run,
2727
patch("main.log_results") as mock_log,
2828
patch("main.webdriver.Chrome", return_value=MagicMock()) as mock_chrome,
2929
patch("main.get_access_code", return_value="test-code") as mock_access_code,
@@ -38,7 +38,7 @@ def mock_tasks():
3838
"local_speed": mock_local_speed,
3939
"gateway_ping": mock_gateway_ping,
4040
"gateway_speed": mock_gateway_speed,
41-
"cleanup": mock_cleanup,
41+
"subprocess_run": mock_subproc_run,
4242
"log": mock_log,
4343
"chrome": mock_chrome,
4444
"access_code": mock_access_code,
@@ -104,25 +104,8 @@ def test_perform_checks_gateway_speed_test_interval(mock_tasks, monkeypatch):
104104
perform_checks() # Run 1
105105
mock_tasks["gateway_speed"].assert_not_called()
106106

107-
108-
def test_perform_checks_calls_cleanup_first(mock_tasks, monkeypatch):
109-
"""Ensure the pre-emptive cleanup runs at the beginning of perform_checks."""
110-
# Configure toggles so at least one downstream task runs
111-
monkeypatch.setattr(config_module, "RUN_LOCAL_PING_TEST", True)
112-
monkeypatch.setattr(config_module, "RUN_LOCAL_GATEWAY_PING_TEST", False)
113-
monkeypatch.setattr(config_module, "RUN_LOCAL_SPEED_TEST", False)
114-
115-
events: list[str] = []
116-
mock_tasks["cleanup"].side_effect = lambda *_, **__: events.append("cleanup")
117-
mock_tasks["wifi"].side_effect = lambda: (events.append("wifi"), {})[1]
118-
119-
perform_checks()
120-
121-
# cleanup should be called and recorded before wifi diagnostics
122-
assert mock_tasks["cleanup"].call_count == 1
123-
assert events and events[0] == "cleanup"
124-
125-
# Do not assert interval behavior here; covered by dedicated test
107+
# Note: Pre-emptive cleanup now occurs within the context manager.
108+
# Dedicated tests cover that behavior in tests/test_cleanup.py.
126109

127110

128111
def test_perform_checks_gateway_speed_test_disabled(mock_tasks, monkeypatch):

0 commit comments

Comments
 (0)