Skip to content

Commit 1cc952d

Browse files
authored
✨ feat(fill): watch mode (#2173)
Co-authored-by: raxhvl <[email protected]>
1 parent 3c92a7d commit 1cc952d

File tree

6 files changed

+169
-4
lines changed

6 files changed

+169
-4
lines changed

docs/CHANGELOG.md

Lines changed: 3 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -14,9 +14,10 @@ Test fixtures for use by clients are available for each release on the [Github r
1414

1515
#### `fill`
1616

17-
- Move pytest marker registration for `fill` and `execute-*` from their respective ini files to the shared `pytest_plugins.shared.execute_fill` pytest plugin ([#2110](https://github.com/ethereum/execution-spec-tests/pull/2110)).
1817
- ✨ Added `--optimize-gas`, `--optimize-gas-output` and `--optimize-gas-post-processing` flags that allow to binary search the minimum gas limit value for a transaction in a test that still yields the same test result ([#1979](https://github.com/ethereum/execution-spec-tests/pull/1979)).
19-
- Upgraded ckzg version to 2.1.3 or newer for correct handling of points at infinity ([#2171](https://github.com/ethereum/execution-spec-tests/pull/2171)).
18+
- ✨ Added `--watch` flag that monitors test files for changes and automatically re-runs the fill command when developing tests ([#2173](https://github.com/ethereum/execution-spec-tests/pull/2173)).
19+
- 🔀 Upgraded ckzg version to 2.1.3 or newer for correct handling of points at infinity ([#2171](https://github.com/ethereum/execution-spec-tests/pull/2171)).
20+
- 🔀 Move pytest marker registration for `fill` and `execute-*` from their respective ini files to the shared `pytest_plugins.shared.execute_fill` pytest plugin ([#2110](https://github.com/ethereum/execution-spec-tests/pull/2110)).
2021

2122
#### `consume`
2223

docs/filling_tests/filling_tests_command_line.md

Lines changed: 46 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,6 +123,52 @@ This flag automatically performs a two-phase execution:
123123

124124
The `--evm-dump-dir` flag can be used to dump the inputs and outputs of every call made to the `t8n` command for debugging purposes, see [Debugging Transition Tools](./debugging_t8n_tools.md).
125125

126+
## Watch Mode for Development
127+
128+
!!! tip "Development workflow"
129+
Use `--watch` or `--watcherfall` during test development to get immediate feedback on your changes without manually re-running the fill command.
130+
131+
### Standard Watch Mode (`--watch`)
132+
133+
This will:
134+
135+
1. Run the initial fill command.
136+
2. Monitor all Python files in the `tests/` and `src/` directories for changes.
137+
3. Automatically re-run the fill command when changes are detected.
138+
4. Clear the screen and show which files changed.
139+
140+
```console
141+
uv run fill tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py --clean --until Amsterdam --watch
142+
✓ Fill completed
143+
144+
Watching for changes...
145+
146+
```
147+
148+
### Watcherfall Watch Mode (`--watcherfall`)
149+
150+
!!! info "Watcherfall mode"
151+
A verbose mode; like watch but the logs keep flowing - perfect when you want to see the full history of runs without clearing the terminal.
152+
153+
Same as `--watch` but without clearing the terminal between runs, so you can see the full output history:
154+
155+
```console
156+
uv run fill tests/amsterdam/eip7928_block_level_access_lists/test_block_access_lists.py --clean --until Amsterdam --watcherfall
157+
Starting watcherfall mode (verbose)...
158+
✓ Fill completed
159+
160+
Watching for changes...
161+
162+
File changes detected, re-running...
163+
164+
✓ Fill completed
165+
166+
Watching for changes...
167+
168+
```
169+
170+
Exit either watch mode with Ctrl+C
171+
126172
## Other Useful Pytest Command-Line Options
127173

128174
```console

src/cli/pytest_commands/fill.py

Lines changed: 24 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -5,7 +5,8 @@
55
import click
66

77
from .base import PytestCommand, PytestExecution, common_pytest_options
8-
from .processors import HelpFlagsProcessor, StdoutFlagsProcessor
8+
from .processors import HelpFlagsProcessor, StdoutFlagsProcessor, WatchFlagsProcessor
9+
from .watcher import FileWatcher
910

1011

1112
class FillCommand(PytestCommand):
@@ -18,6 +19,7 @@ def __init__(self, **kwargs):
1819
argument_processors=[
1920
HelpFlagsProcessor("fill"),
2021
StdoutFlagsProcessor(),
22+
WatchFlagsProcessor(),
2123
],
2224
**kwargs,
2325
)
@@ -174,6 +176,27 @@ def _is_tarball_output(self, args: List[str]) -> bool:
174176
return str(output_path).endswith(".tar.gz")
175177
return False
176178

179+
def _is_watch_mode(self, args: List[str]) -> bool:
180+
"""Check if any watch flag is present in arguments."""
181+
return any(flag in args for flag in ["--watch", "--watcherfall"])
182+
183+
def _is_verbose_watch_mode(self, args: List[str]) -> bool:
184+
"""Check if verbose watch flag (--watcherfall) is present in arguments."""
185+
return "--watcherfall" in args
186+
187+
def execute(self, pytest_args: List[str]) -> None:
188+
"""Execute the command with optional watch mode support."""
189+
if self._is_watch_mode(pytest_args):
190+
self._execute_with_watch(pytest_args)
191+
else:
192+
super().execute(pytest_args)
193+
194+
def _execute_with_watch(self, pytest_args: List[str]) -> None:
195+
"""Execute fill command in watch mode."""
196+
verbose = self._is_verbose_watch_mode(pytest_args)
197+
watcher = FileWatcher(console=self.runner.console, verbose=verbose)
198+
watcher.run_with_watch(pytest_args)
199+
177200

178201
class PhilCommand(FillCommand):
179202
"""Friendly fill command with emoji reporting."""

src/cli/pytest_commands/processors.py

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -118,6 +118,14 @@ def _has_parallelism_flag(self, args: List[str]) -> bool:
118118
return "-n" in args
119119

120120

121+
class WatchFlagsProcessor(ArgumentProcessor):
122+
"""Processes --watch and --watcherfall flags for file watching functionality."""
123+
124+
def process_args(self, args: List[str]) -> List[str]:
125+
"""Remove --watch and --watcherfall flags from args passed to pytest."""
126+
return [arg for arg in args if arg not in ["--watch", "--watcherfall"]]
127+
128+
121129
class ConsumeCommandProcessor(ArgumentProcessor):
122130
"""Processes consume-specific command arguments."""
123131

src/cli/pytest_commands/watcher.py

Lines changed: 86 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,86 @@
1+
"""File watcher implementation for --watch flag functionality."""
2+
3+
import os
4+
import subprocess
5+
import time
6+
from pathlib import Path
7+
from typing import Dict
8+
9+
from rich.console import Console
10+
11+
12+
class FileWatcher:
13+
"""Simple file watcher that re-runs the fill command on changes."""
14+
15+
def __init__(self, console=None, verbose=False):
16+
"""Initialize the file watcher."""
17+
self.console = console or Console(highlight=False)
18+
self.verbose = verbose
19+
20+
def run_with_watch(self, args):
21+
"""Watch for file changes and re-run fill command."""
22+
file_mtimes: Dict[Path, float] = {}
23+
24+
def get_file_mtimes():
25+
"""Get current modification times of all test and source files."""
26+
mtimes = {}
27+
# Watch tests directory
28+
tests_dir = Path("tests")
29+
if tests_dir.exists():
30+
for py_file in tests_dir.rglob("*.py"):
31+
try:
32+
mtimes[py_file] = py_file.stat().st_mtime
33+
except (OSError, FileNotFoundError):
34+
pass
35+
# Watch src directory
36+
src_dir = Path("src")
37+
if src_dir.exists():
38+
for py_file in src_dir.rglob("*.py"):
39+
try:
40+
mtimes[py_file] = py_file.stat().st_mtime
41+
except (OSError, FileNotFoundError):
42+
pass
43+
return mtimes
44+
45+
def run_fill():
46+
"""Run fill command without --watch / --watcherfall flag."""
47+
clean_args = [arg for arg in args if arg not in ["--watch", "--watcherfall"]]
48+
cmd = ["uv", "run", "fill"] + clean_args
49+
result = subprocess.run(cmd)
50+
51+
if result.returncode == 0:
52+
self.console.print("[green]✓ Fill completed[/green]")
53+
else:
54+
self.console.print(f"[red]✗ Fill failed (exit {result.returncode})[/red]")
55+
56+
# Setup
57+
mode_desc = "watcherfall mode (verbose)" if self.verbose else "watch mode"
58+
self.console.print(f"[blue]Starting {mode_desc}...[/blue]")
59+
file_mtimes = get_file_mtimes()
60+
61+
# Initial run
62+
self.console.print("[green]Running initial fill...[/green]")
63+
run_fill()
64+
65+
file_count = len(file_mtimes)
66+
self.console.print(
67+
f"[blue]Watching {file_count} files in tests/ and src/ directories."
68+
"\n Press Ctrl+C to stop.[/blue]"
69+
)
70+
71+
# Watch loop
72+
try:
73+
while True:
74+
time.sleep(0.5)
75+
current_mtimes = get_file_mtimes()
76+
77+
if current_mtimes != file_mtimes:
78+
if not self.verbose:
79+
os.system("clear" if os.name != "nt" else "cls")
80+
self.console.print("[yellow]File changes detected, re-running...[/yellow]\n")
81+
run_fill()
82+
file_mtimes = current_mtimes
83+
self.console.print("\n[blue]Watching for changes...[/blue]")
84+
85+
except KeyboardInterrupt:
86+
self.console.print("\n[yellow]Watch mode stopped.[/yellow]")

whitelist.txt

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1071,4 +1071,5 @@ CLZ
10711071
Fujisan
10721072
zkevm
10731073
jsign
1074-
Glamsterdam
1074+
Glamsterdam
1075+
Watcherfall

0 commit comments

Comments
 (0)