Skip to content

Commit b421f6e

Browse files
Copilot0xkarmacoma
andcommitted
Address feedback: make tag required and simplify global executor init
Co-authored-by: 0xkarmacoma <[email protected]>
1 parent 00b767f commit b421f6e

File tree

5 files changed

+51
-48
lines changed

5 files changed

+51
-48
lines changed

src/halmos/__main__.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -430,8 +430,8 @@ def setup(ctx: FunctionContext) -> Exec:
430430
path_ctx = PathContext(
431431
args=args,
432432
path_id=path_id,
433-
query=query,
434433
solving_ctx=ctx.solving_ctx,
434+
query=query,
435435
tag=ctx.info.sig,
436436
)
437437
solver_output = solve_low_level(path_ctx)
@@ -874,8 +874,8 @@ def handle_assertion_violation(
874874
path_ctx = PathContext(
875875
args=args,
876876
path_id=path_id,
877-
query=query,
878877
solving_ctx=ctx.solving_ctx,
878+
query=query,
879879
tag=probe_tag if probe_tag else ctx.info.sig,
880880
)
881881

@@ -1094,8 +1094,8 @@ def run_test(ctx: FunctionContext) -> TestResult:
10941094
path_ctx = PathContext(
10951095
args=args,
10961096
path_id=path_id,
1097-
query=ex.path.to_smt2(args),
10981097
solving_ctx=ctx.solving_ctx,
1098+
query=ex.path.to_smt2(args),
10991099
tag=ctx.info.sig,
11001100
)
11011101
solver_output = solve_low_level(path_ctx)

src/halmos/processes.py

Lines changed: 14 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -34,7 +34,7 @@ def shutdown_all(self):
3434
class PopenFuture(concurrent.futures.Future):
3535
cmd: list[str]
3636
timeout: float | None # in seconds, None means no timeout
37-
tag: str | None # optional tag for grouping and selective cancellation
37+
tag: str # tag for grouping and selective cancellation
3838
process: subprocess.Popen | None
3939
stdout: str | None
4040
stderr: str | None
@@ -43,10 +43,9 @@ class PopenFuture(concurrent.futures.Future):
4343
end_time: float | None
4444
_exception: Exception | None
4545

46-
def __init__(
47-
self, cmd: list[str], timeout: float | None = None, tag: str | None = None
48-
):
46+
def __init__(self, cmd: list[str], tag: str, timeout: float | None = None):
4947
super().__init__()
48+
assert tag, "tag cannot be empty"
5049
self.cmd = cmd
5150
self.timeout = timeout
5251
self.tag = tag
@@ -199,18 +198,17 @@ def submit(self, future: PopenFuture) -> PopenFuture:
199198

200199
def interrupt(self, tag: str) -> None:
201200
"""Interrupts all futures with the specified tag.
202-
201+
203202
Args:
204-
tag: The tag identifying futures to interrupt.
205-
Futures without tags are not affected.
203+
tag: The tag identifying futures to interrupt.
204+
Futures with a different tag are not affected.
206205
"""
207-
if not tag:
208-
return
209-
206+
assert tag, "tag cannot be empty"
207+
210208
with self._lock:
211209
# Find all futures with the matching tag and cancel them
212210
futures_to_cancel = [f for f in self._futures if f.tag == tag]
213-
211+
214212
# Cancel outside the lock to avoid deadlocks
215213
for future in futures_to_cancel:
216214
future.cancel()
@@ -251,22 +249,13 @@ def _join(self):
251249

252250

253251
# Global PopenExecutor instance for shared use across all tests and probes
254-
_global_executor: PopenExecutor | None = None
255-
_global_executor_lock = threading.Lock()
252+
_executor = PopenExecutor()
253+
ExecutorRegistry().register(_executor)
256254

257255

258256
def get_global_executor() -> PopenExecutor:
259-
"""Get the global PopenExecutor instance, creating it if necessary."""
260-
global _global_executor
261-
262-
if _global_executor is None:
263-
with _global_executor_lock:
264-
if _global_executor is None:
265-
_global_executor = PopenExecutor()
266-
# Register with the ExecutorRegistry so it gets shut down properly
267-
ExecutorRegistry().register(_global_executor)
268-
269-
return _global_executor
257+
"""Get the global PopenExecutor instance."""
258+
return _executor
270259

271260

272261
def main():
@@ -293,7 +282,7 @@ def done_callback(future: PopenFuture):
293282
]
294283

295284
futures = [
296-
PopenFuture(command.split(), tag=f"test-{i}")
285+
PopenFuture(command.split(), f"test-{i}")
297286
for i, command in enumerate(commands)
298287
]
299288

src/halmos/solve.py

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -274,8 +274,8 @@ class PathContext:
274274
path_id: int
275275
solving_ctx: SolvingContext
276276
query: SMTQuery
277+
tag: str # tag for grouping solver queries
277278
is_refined: bool = False
278-
tag: str | None = None # optional tag for grouping solver queries
279279

280280
@property
281281
def dump_file(self) -> Path:
@@ -290,8 +290,8 @@ def refine(self) -> "PathContext":
290290
path_id=self.path_id,
291291
solving_ctx=self.solving_ctx,
292292
query=refine(self.query),
293-
is_refined=True,
294293
tag=self.tag,
294+
is_refined=True,
295295
)
296296

297297

@@ -494,7 +494,7 @@ def solve_low_level(path_ctx: PathContext) -> SolverOutput:
494494
else args.solver_command
495495
)
496496
cmd_with_file = cmd_list + [smt2_filename]
497-
future = PopenFuture(cmd_with_file, timeout=timeout_seconds, tag=path_ctx.tag)
497+
future = PopenFuture(cmd_with_file, path_ctx.tag, timeout=timeout_seconds)
498498

499499
# starts the subprocess asynchronously
500500
get_global_executor().submit(future)

tests/test_global_executor.py

Lines changed: 24 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -38,19 +38,30 @@ def test_popen_future_with_tag(self):
3838
cmd = ["echo", "hello"]
3939
tag = "test-tag"
4040

41-
future = PopenFuture(cmd, tag=tag)
41+
future = PopenFuture(cmd, tag)
4242

4343
assert future.cmd == cmd
4444
assert future.tag == tag
4545

46-
def test_popen_future_without_tag(self):
47-
"""Test that PopenFuture works without tag parameter."""
46+
def test_popen_future_with_minimal_args(self):
47+
"""Test that PopenFuture works with minimal required parameters."""
4848
cmd = ["echo", "hello"]
49+
tag = "test-minimal"
4950

50-
future = PopenFuture(cmd)
51+
future = PopenFuture(cmd, tag)
5152

5253
assert future.cmd == cmd
53-
assert future.tag is None
54+
assert future.tag == tag
55+
56+
def test_popen_future_empty_tag_assertion(self):
57+
"""Test that PopenFuture raises assertion error for empty tag."""
58+
cmd = ["echo", "hello"]
59+
60+
try:
61+
PopenFuture(cmd, "")
62+
raise AssertionError("Expected AssertionError for empty tag")
63+
except AssertionError:
64+
pass # Expected
5465

5566
def test_interrupt_by_tag(self):
5667
"""Test that interrupt() cancels futures with matching tags."""
@@ -64,7 +75,7 @@ def test_interrupt_by_tag(self):
6475
future3 = Mock(spec=PopenFuture)
6576
future3.tag = "tag1"
6677
future4 = Mock(spec=PopenFuture)
67-
future4.tag = None
78+
future4.tag = "tag3"
6879

6980
# Add to executor's futures list
7081
executor._futures = [future1, future2, future3, future4]
@@ -79,16 +90,20 @@ def test_interrupt_by_tag(self):
7990
future4.cancel.assert_not_called()
8091

8192
def test_interrupt_with_empty_tag(self):
82-
"""Test that interrupt() with empty tag does nothing."""
93+
"""Test that interrupt() with empty tag raises assertion error."""
8394
executor = get_global_executor()
8495

8596
# Create mock future
8697
future = Mock(spec=PopenFuture)
8798
future.tag = "some-tag"
8899
executor._futures = [future]
89100

90-
# Interrupt with empty tag
91-
executor.interrupt("")
101+
# Interrupt with empty tag should raise assertion error
102+
try:
103+
executor.interrupt("")
104+
raise AssertionError("Expected AssertionError for empty tag")
105+
except AssertionError:
106+
pass # Expected
92107

93108
# No futures should be cancelled
94109
future.cancel.assert_not_called()

tests/test_solver_integration.py

Lines changed: 7 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -1,13 +1,14 @@
11
# SPDX-License-Identifier: AGPL-3.0
22

3-
from unittest.mock import Mock, patch
3+
import contextlib
44
import tempfile
55
from pathlib import Path
6+
from unittest.mock import Mock, patch
67

7-
from halmos.solve import SolvingContext, PathContext, solve_low_level
8-
from halmos.sevm import SMTQuery
98
from halmos.config import Config as HalmosConfig
10-
from halmos.processes import get_global_executor, PopenFuture
9+
from halmos.processes import PopenFuture, get_global_executor
10+
from halmos.sevm import SMTQuery
11+
from halmos.solve import PathContext, SolvingContext, solve_low_level
1112

1213

1314
class TestSolverIntegration:
@@ -51,11 +52,9 @@ def mock_submit(future):
5152

5253
# Mock the global executor's submit method
5354
with patch.object(get_global_executor(), 'submit', side_effect=mock_submit):
54-
try:
55-
solve_low_level(path_ctx)
56-
except Exception:
55+
with contextlib.suppress(Exception):
5756
# We expect this might fail due to mocking, that's OK
58-
pass
57+
solve_low_level(path_ctx)
5958

6059
# Verify that a future was submitted with the correct tag
6160
assert len(submitted_futures) == 1

0 commit comments

Comments
 (0)