Skip to content

Commit 7f34712

Browse files
committed
Add watch-mode to integration tests
1 parent 2708fb6 commit 7f34712

File tree

1 file changed

+100
-200
lines changed

1 file changed

+100
-200
lines changed

integration/local.py

Lines changed: 100 additions & 200 deletions
Original file line numberDiff line numberDiff line change
@@ -3,8 +3,6 @@
33
# /// script
44
# requires-python = ">=3.8"
55
# dependencies = [
6-
# "requests",
7-
# "urllib3",
86
# "watchdog",
97
# ]
108
# ///
@@ -23,14 +21,11 @@
2321
import sys
2422
import time
2523

26-
from subprocess import CalledProcessError
2724
from watchdog.events import PatternMatchingEventHandler
2825
from watchdog.observers import Observer
2926

3027
from argparse import RawTextHelpFormatter
3128

32-
# from dataclasses import dataclass
33-
# from datetime import datetime
3429
from pathlib import Path
3530
from typing import Any, Callable, Dict, List
3631

@@ -76,31 +71,21 @@
7671
###########
7772

7873

79-
def run(args: List[str]) -> str:
74+
def run(
75+
cmd: List[str], check: bool = True, *args, **kwargs
76+
) -> subprocess.CompletedProcess:
8077
"""
8178
Executes a shell command, checks for success, and returns its stdout.
8279
8380
Args:
8481
args: A list of strings representing the command and its arguments.
8582
86-
Returns:
87-
The standard output of the command as a string.
88-
8983
Raises:
9084
subprocess.CalledProcessError: If the command returns a non-zero exit code.
9185
"""
92-
log.debug(f"Running command: {' '.join(args)}")
93-
result = subprocess.run(args, check=True, text=True, capture_output=True)
94-
return result.stdout
95-
96-
97-
def is_docker_container_running(container_name: str) -> bool:
98-
"""Checks if a Docker container is currently running."""
99-
try:
100-
output = run(["docker", "inspect", "-f", "{{.State.Running}}", container_name]).strip()
101-
return output.lower() == "true"
102-
except CalledProcessError:
103-
return False
86+
log.debug(f"Running command: {' '.join(cmd)}")
87+
res = subprocess.run(cmd, *args, check=check, text=True, **kwargs)
88+
return res
10489

10590

10691
def doc(dic: Dict[str, Callable[..., Any]]) -> str:
@@ -111,226 +96,141 @@ def doc(dic: Dict[str, Callable[..., Any]]) -> str:
11196
return doc_string
11297

11398

114-
def cleanup_containers(full_cleanup: bool) -> None:
115-
"""Remove Docker containers and network conditionally.
99+
def cleanup_lila() -> None:
100+
"""Remove Lila Docker container and network unconditionally."""
101+
log.info(f"Cleaning up Lila container...")
102+
run(["docker", "rm", "--force", BDIT_LILA])
116103

117-
:param full_cleanup: If True, remove BDIT_LILA and BDIT_NETWORK. Always attempts to remove BDIT_APP.
118-
"""
119-
log.info(f"Cleaning up containers (full_cleanup={full_cleanup})...")
120-
121-
try:
122-
run(["docker", "rm", "--force", BDIT_APP])
123-
log.info(f"Removed container: {BDIT_APP}")
124-
except CalledProcessError as e:
125-
if "No such container" in e.stderr:
126-
log.debug(f"Container {BDIT_APP} not found, skipping removal.")
127-
else:
128-
log.warning(f"Failed to remove {BDIT_APP}: {e.stderr}")
129-
130-
if full_cleanup:
131-
try:
132-
run(["docker", "rm", "--force", BDIT_LILA])
133-
log.info(f"Removed container: {BDIT_LILA}")
134-
except CalledProcessError as e:
135-
if "No such container" in e.stderr:
136-
log.debug(f"Container {BDIT_LILA} not found, skipping removal.")
137-
else:
138-
log.warning(f"Failed to remove {BDIT_LILA}: {e.stderr}")
139104

140-
try:
141-
run(["docker", "network", "rm", BDIT_NETWORK])
142-
log.info(f"Removed network: {BDIT_NETWORK}")
143-
except CalledProcessError as e:
144-
if "no such network" in e.stderr:
145-
log.debug(f"Network {BDIT_NETWORK} not found, skipping removal.")
146-
else:
147-
log.warning(f"Failed to remove {BDIT_NETWORK}: {e.stderr}")
148-
else:
149-
log.info(f"Leaving {BDIT_LILA} and {BDIT_NETWORK} intact.")
105+
def cleanup_containers() -> None:
106+
"""Remove Docker containers and network unconditionally."""
107+
log.info(f"Cleaning up containers...")
108+
run(["docker", "rm", "--force", BDIT_APP])
109+
cleanup_lila()
110+
run(["docker", "network", "rm", "--force", BDIT_NETWORK])
150111
log.info("Cleanup complete.")
151112

152113

114+
def run_lila():
115+
run(
116+
[
117+
"docker",
118+
"run",
119+
"--name",
120+
BDIT_LILA,
121+
"--network",
122+
BDIT_NETWORK,
123+
"-d",
124+
BDIT_IMAGE,
125+
]
126+
)
127+
128+
153129
class ChangeHandler(PatternMatchingEventHandler):
154-
def __init__(self, project_root: Path):
130+
def __init__(self, on_change_callback: Callable[[], None]) -> None:
155131
super().__init__(
156132
patterns=["*.py"],
157133
ignore_patterns=[],
158134
ignore_directories=True,
159135
case_sensitive=False,
160136
)
161-
self._should_run_test = False
162-
self._project_root = project_root
137+
self._already_running = False
138+
self._on_change_callback = on_change_callback
163139

164140
def on_any_event(self, event):
165-
# Filter for changes in berserk or integration/tests directories
166-
try:
167-
relative_path = Path(event.src_path).relative_to(self._project_root)
168-
if relative_path.parts[0] == "berserk" or (relative_path.parts[0] == "integration" and relative_path.parts[1] == "tests"):
169-
log.debug(f"Detected change in {event.src_path}: {event.event_type}")
170-
self.trigger_test()
171-
except ValueError:
172-
# Event outside project_root, ignore
173-
pass
174-
175-
def trigger_test(self):
176-
self._should_run_test = True
177-
178-
def should_run_test(self):
179-
return self._should_run_test
180-
181-
def reset_trigger(self):
182-
self._should_run_test = False
141+
# feels like race-conditions are waiting to happen here...
142+
if not self._already_running:
143+
self._already_running = True
144+
self._on_change_callback()
145+
self._already_running = False
183146

184147

185-
def integration_test(_watch: bool) -> None:
148+
def integration_test(watch: bool) -> None:
186149
"""Run the Berserk Docker Image Test (BDIT)."""
187150
log.info("Running integration tests")
188151

189152
project_root = SCRIPT_DIR.parent
190153

191-
# Initial cleanup: Always remove BDIT_APP. For BDIT_LILA/BDIT_NETWORK, depends on watch mode.
192-
cleanup_containers(full_cleanup=not _watch)
193-
194-
# Ensure BDIT_NETWORK exists
195-
try:
196-
run(["docker", "network", "create", BDIT_NETWORK])
197-
log.info(f"Created network: {BDIT_NETWORK}")
198-
except CalledProcessError as e:
199-
if "network with name bdit_lila-network already exists" in e.stderr:
200-
log.debug(f"Network {BDIT_NETWORK} already exists.")
201-
else:
202-
log.error(f"Failed to create network {BDIT_NETWORK}: {e.stderr}")
203-
raise
204-
205-
# Ensure BDIT_LILA is running.
206-
if not is_docker_container_running(BDIT_LILA):
207-
log.info(f"Starting Lila container: {BDIT_LILA} with image {BDIT_IMAGE}")
208-
try:
209-
run(
210-
[
211-
"docker",
212-
"run",
213-
"--name",
214-
BDIT_LILA,
215-
"--network",
216-
BDIT_NETWORK,
217-
"-d",
218-
BDIT_IMAGE,
219-
]
220-
)
221-
except CalledProcessError as e:
222-
if "conflict: container name" in e.stderr:
223-
log.warning(
224-
f"Container {BDIT_LILA} already exists but was not reported as running. Attempting to restart..."
225-
)
226-
run(["docker", "rm", "--force", BDIT_LILA])
227-
run( # Retry after force removal
228-
[
229-
"docker",
230-
"run",
231-
"--name",
232-
BDIT_LILA,
233-
"--network",
234-
BDIT_NETWORK,
235-
"-d",
236-
BDIT_IMAGE,
237-
]
238-
)
239-
else:
240-
log.error(f"Failed to start Lila container {BDIT_LILA}: {e.stderr}")
241-
raise
154+
cleanup_containers()
155+
156+
run(["docker", "network", "create", BDIT_NETWORK])
157+
log.info(f"Created network: {BDIT_NETWORK}")
158+
run_lila()
159+
log.info(f"Started Lila container: {BDIT_LILA}")
242160

243161
# Build the application image (always rebuild to ensure latest changes)
244162
dockerfile_path = SCRIPT_DIR / "Dockerfile"
245-
uv_cache_dir = run(["uv", "cache", "dir"]).strip()
163+
uv_cache_dir = run(["uv", "cache", "dir"], capture_output=True).stdout.strip()
246164
log.info(
247165
f"Building Docker image: {BDIT_APP_IMAGE} from {project_root} using {dockerfile_path}"
248166
)
249-
run(
250-
[
251-
"docker",
252-
"build",
253-
"-f",
254-
str(dockerfile_path),
255-
str(project_root),
256-
"--build-arg",
257-
f"UV_CACHE_DIR={uv_cache_dir}",
258-
"-t",
259-
BDIT_APP_IMAGE,
260-
]
261-
)
262167

263-
def run_app_test():
264-
"""Helper to run the BDIT_APP container."""
168+
def build_and_run_test_image():
169+
run(
170+
[
171+
"docker",
172+
"build",
173+
"-f",
174+
str(dockerfile_path),
175+
str(project_root),
176+
"--build-arg",
177+
f"UV_CACHE_DIR={uv_cache_dir}",
178+
"-t",
179+
BDIT_APP_IMAGE,
180+
]
181+
)
265182
log.info(f"Running app container: {BDIT_APP} with image {BDIT_APP_IMAGE}")
266-
try:
267-
run(
268-
[
269-
"docker",
270-
"run",
271-
"--rm", # Always remove after execution
272-
"--name",
273-
BDIT_APP,
274-
"--network",
275-
BDIT_NETWORK,
276-
BDIT_APP_IMAGE,
277-
]
278-
)
279-
log.info("App container finished successfully.")
280-
except CalledProcessError as e:
281-
log.error(f"App container {BDIT_APP} failed with exit code {e.returncode}:")
282-
log.error(f"stdout: {e.stdout}")
283-
log.error(f"stderr: {e.stderr}")
284-
if _watch:
285-
log.info("Continuing in watch mode...")
286-
else:
287-
raise
288-
289-
if _watch:
290-
log.info("Entering watch mode. Monitoring files in 'berserk/' and 'integration/tests/'. Press Ctrl+C to stop.")
291-
292-
event_handler = ChangeHandler(project_root)
183+
run(
184+
[
185+
"docker",
186+
"run",
187+
"--rm", # Always remove after execution
188+
"--name",
189+
BDIT_APP,
190+
"--network",
191+
BDIT_NETWORK,
192+
BDIT_APP_IMAGE,
193+
],
194+
)
195+
log.info("App container finished successfully.")
196+
197+
# Initial test
198+
build_and_run_test_image()
199+
200+
if watch:
201+
# the tests need a fresh lila everytime, due to some actions not being idempotent
202+
# like kid mode or bot upgrade
203+
cleanup_lila()
204+
run_lila()
205+
log.info(
206+
"Entering watch mode. Monitoring files in 'berserk/' and 'integration/tests/'. Press Ctrl+C to stop."
207+
)
208+
209+
def safe_build_and_run_test_image():
210+
try:
211+
build_and_run_test_image()
212+
except subprocess.CalledProcessError as e:
213+
log.error(f"Error during build or test run: {e}")
214+
cleanup_lila()
215+
run_lila()
216+
217+
event_handler = ChangeHandler(safe_build_and_run_test_image)
293218
observer = Observer()
294219
observer.schedule(event_handler, project_root / "berserk", recursive=True)
295-
observer.schedule(event_handler, project_root / "integration" / "tests", recursive=True)
220+
observer.schedule(
221+
event_handler, project_root / "integration" / "tests", recursive=True
222+
)
296223
observer.start()
297224

298225
try:
299-
# Run initial test
300-
run_app_test()
301226
while True:
302-
if event_handler.should_run_test():
303-
event_handler.reset_trigger()
304-
log.info("File change detected, rebuilding and re-running app container...")
305-
# Rebuild the app image on change
306-
run(
307-
[
308-
"docker",
309-
"build",
310-
"-f",
311-
str(dockerfile_path),
312-
str(project_root),
313-
"--build-arg",
314-
f"UV_CACHE_DIR={uv_cache_dir}",
315-
"-t",
316-
BDIT_APP_IMAGE,
317-
]
318-
)
319-
run_app_test()
320227
time.sleep(1)
321228
except KeyboardInterrupt:
322229
log.info("Watch mode stopped by user.")
323230
finally:
324231
observer.stop()
325232
observer.join()
326-
log.info(f"Watch mode exited. To clean up {BDIT_LILA} and {BDIT_NETWORK}, run without --watch.")
327-
328-
else:
329-
# Non-watch mode: run once, then perform full cleanup
330-
run_app_test()
331-
cleanup_containers(full_cleanup=True)
332-
333-
log.info("✅ Done")
233+
cleanup_containers()
334234

335235

336236
def main() -> None:

0 commit comments

Comments
 (0)