shrinkray 25.12.26.2__tar.gz → 25.12.27.0__tar.gz
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- {shrinkray-25.12.26.2/src/shrinkray.egg-info → shrinkray-25.12.27.0}/PKG-INFO +1 -1
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/pyproject.toml +1 -3
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/__main__.py +14 -14
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/sat.py +6 -6
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/reducer.py +6 -2
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/state.py +10 -10
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/client.py +2 -2
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/worker.py +32 -23
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/tui.py +25 -15
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/work.py +11 -8
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0/src/shrinkray.egg-info}/PKG-INFO +1 -1
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_clang_delta.py +18 -18
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_cli.py +24 -16
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_definitions.py +1 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_dimacs_cnf.py +8 -4
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_formatting.py +6 -4
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_generic_language.py +0 -2
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_main.py +25 -37
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_problem.py +9 -13
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_reducer.py +11 -38
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_reduction_passes.py +10 -21
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_state.py +8 -10
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_client.py +15 -28
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_integration.py +3 -12
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_worker.py +24 -44
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_tui.py +60 -117
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_tui_snapshots.py +6 -2
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_work.py +17 -10
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/LICENSE +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/README.md +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/setup.cfg +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/__init__.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/cli.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/display.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/formatting.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/__init__.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/bytes.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/clangdelta.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/definitions.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/genericlanguages.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/json.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/patching.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/python.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/passes/sequences.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/problem.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/process.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/py.typed +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/__init__.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/subprocess/protocol.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray/ui.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/SOURCES.txt +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/dependency_links.txt +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/entry_points.txt +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/requires.txt +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/src/shrinkray.egg-info/top_level.txt +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_byte_reduction_passes.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_display.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_generic_shrinking_properties.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_json_passes.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_misc_reduction_performance.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_patching.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_process.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_python_reducers.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_sat.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_subprocess_protocol.py +0 -0
- {shrinkray-25.12.26.2 → shrinkray-25.12.27.0}/tests/test_ui.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shrinkray"
|
|
3
|
-
version = "25.12.
|
|
3
|
+
version = "25.12.27.0"
|
|
4
4
|
description = "Shrink Ray"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "David R. MacIver", email = "david@drmaciver.com"}
|
|
@@ -77,10 +77,8 @@ ignore = [
|
|
|
77
77
|
"B007", # unused loop variable
|
|
78
78
|
"B023", # function definition in loop
|
|
79
79
|
"B904", # raise without from - intentional in some exception handling
|
|
80
|
-
"C408", # unnecessary dict() call - sometimes more readable
|
|
81
80
|
"UP031", # use format specifiers - % format is fine
|
|
82
81
|
"B018", # useless expression - sometimes intentional for side effects
|
|
83
|
-
"C403", # unnecessary list comprehension - sometimes more readable
|
|
84
82
|
"UP022", # prefer capture_output - explicit is fine
|
|
85
83
|
]
|
|
86
84
|
|
|
@@ -271,20 +271,20 @@ def main(
|
|
|
271
271
|
if not backup:
|
|
272
272
|
backup = filename + os.extsep + "bak"
|
|
273
273
|
|
|
274
|
-
state_kwargs: dict[str, Any] =
|
|
275
|
-
input_type
|
|
276
|
-
in_place
|
|
277
|
-
test
|
|
278
|
-
timeout
|
|
279
|
-
base
|
|
280
|
-
parallelism
|
|
281
|
-
filename
|
|
282
|
-
formatter
|
|
283
|
-
trivial_is_error
|
|
284
|
-
seed
|
|
285
|
-
volume
|
|
286
|
-
clang_delta_executable
|
|
287
|
-
|
|
274
|
+
state_kwargs: dict[str, Any] = {
|
|
275
|
+
"input_type": input_type,
|
|
276
|
+
"in_place": in_place,
|
|
277
|
+
"test": test,
|
|
278
|
+
"timeout": timeout,
|
|
279
|
+
"base": os.path.basename(filename),
|
|
280
|
+
"parallelism": parallelism,
|
|
281
|
+
"filename": filename,
|
|
282
|
+
"formatter": formatter,
|
|
283
|
+
"trivial_is_error": trivial_is_error,
|
|
284
|
+
"seed": seed,
|
|
285
|
+
"volume": volume,
|
|
286
|
+
"clang_delta_executable": clang_delta_executable,
|
|
287
|
+
}
|
|
288
288
|
|
|
289
289
|
state: ShrinkRayState[Any]
|
|
290
290
|
ui: ShrinkRayUI[Any]
|
|
@@ -165,14 +165,14 @@ async def renumber_variables(problem: ReductionProblem[SAT]) -> None:
|
|
|
165
165
|
result: SAT = []
|
|
166
166
|
for clause in sat:
|
|
167
167
|
new_clause: Clause = sorted(
|
|
168
|
-
|
|
169
|
-
|
|
168
|
+
{
|
|
169
|
+
(
|
|
170
170
|
(renumbering[lit] if lit > 0 else -renumbering[-lit])
|
|
171
171
|
if abs(lit) in renumbering
|
|
172
172
|
else lit
|
|
173
|
-
|
|
174
|
-
|
|
175
|
-
|
|
173
|
+
)
|
|
174
|
+
for lit in clause
|
|
175
|
+
}
|
|
176
176
|
)
|
|
177
177
|
if len(set(map(abs, new_clause))) == len(new_clause):
|
|
178
178
|
result.append(new_clause)
|
|
@@ -290,7 +290,7 @@ class BooleanEquivalence(UnionFind[int]):
|
|
|
290
290
|
|
|
291
291
|
def find(self, value: int) -> int:
|
|
292
292
|
if not value:
|
|
293
|
-
raise ValueError("Invalid variable
|
|
293
|
+
raise ValueError(f"Invalid variable {value!r}")
|
|
294
294
|
return super().find(value)
|
|
295
295
|
|
|
296
296
|
def merge(self, left: int, right: int) -> None:
|
|
@@ -59,7 +59,9 @@ class Reducer[T](ABC):
|
|
|
59
59
|
# Optional pass statistics tracking (implemented by ShrinkRay)
|
|
60
60
|
pass_stats: "PassStatsTracker | None" = attrs.field(default=None, init=False)
|
|
61
61
|
# Optional current pass tracking (implemented by ShrinkRay)
|
|
62
|
-
current_reduction_pass: "ReductionPass[T] | None" = attrs.field(
|
|
62
|
+
current_reduction_pass: "ReductionPass[T] | None" = attrs.field(
|
|
63
|
+
default=None, init=False
|
|
64
|
+
)
|
|
63
65
|
|
|
64
66
|
@contextmanager
|
|
65
67
|
def backtrack(self, restart: T) -> Generator[None, None, None]:
|
|
@@ -192,7 +194,9 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
192
194
|
# Pass control: disabled passes and skip functionality
|
|
193
195
|
disabled_passes: set[str] = attrs.Factory(set)
|
|
194
196
|
_skip_requested: bool = attrs.field(default=False, init=False)
|
|
195
|
-
_current_pass_scope: "trio.CancelScope | None" = attrs.field(
|
|
197
|
+
_current_pass_scope: "trio.CancelScope | None" = attrs.field(
|
|
198
|
+
default=None, init=False
|
|
199
|
+
)
|
|
196
200
|
_passes_were_skipped: bool = attrs.field(default=False, init=False)
|
|
197
201
|
|
|
198
202
|
def disable_pass(self, pass_name: str) -> None:
|
|
@@ -105,12 +105,12 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
105
105
|
else:
|
|
106
106
|
command = self.test
|
|
107
107
|
|
|
108
|
-
kwargs: dict[str, Any] =
|
|
109
|
-
universal_newlines
|
|
110
|
-
preexec_fn
|
|
111
|
-
cwd
|
|
112
|
-
check
|
|
113
|
-
|
|
108
|
+
kwargs: dict[str, Any] = {
|
|
109
|
+
"universal_newlines": False,
|
|
110
|
+
"preexec_fn": os.setsid,
|
|
111
|
+
"cwd": cwd,
|
|
112
|
+
"check": False,
|
|
113
|
+
}
|
|
114
114
|
if self.input_type.enabled(self._InputType.stdin) and not os.path.isdir(
|
|
115
115
|
working
|
|
116
116
|
):
|
|
@@ -562,10 +562,10 @@ class ShrinkRayDirectoryState(ShrinkRayState[dict[str, bytes]]):
|
|
|
562
562
|
sorted((k, shortlex(v)) for k, v in test_case.items()),
|
|
563
563
|
)
|
|
564
564
|
|
|
565
|
-
return
|
|
566
|
-
sort_key
|
|
567
|
-
size
|
|
568
|
-
|
|
565
|
+
return {
|
|
566
|
+
"sort_key": dict_sort_key,
|
|
567
|
+
"size": dict_size,
|
|
568
|
+
}
|
|
569
569
|
|
|
570
570
|
def new_reducer(
|
|
571
571
|
self, problem: ReductionProblem[dict[str, bytes]]
|
|
@@ -4,7 +4,7 @@ import asyncio
|
|
|
4
4
|
import sys
|
|
5
5
|
import traceback
|
|
6
6
|
import uuid
|
|
7
|
-
from collections.abc import
|
|
7
|
+
from collections.abc import AsyncGenerator
|
|
8
8
|
from typing import Any
|
|
9
9
|
|
|
10
10
|
from shrinkray.subprocess.protocol import (
|
|
@@ -200,7 +200,7 @@ class SubprocessClient:
|
|
|
200
200
|
traceback.print_exc()
|
|
201
201
|
return Response(id="", error="Failed to skip pass")
|
|
202
202
|
|
|
203
|
-
async def get_progress_updates(self) ->
|
|
203
|
+
async def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]:
|
|
204
204
|
"""Yield progress updates as they arrive."""
|
|
205
205
|
while not self._completed:
|
|
206
206
|
try:
|
|
@@ -4,6 +4,7 @@ import os
|
|
|
4
4
|
import sys
|
|
5
5
|
import time
|
|
6
6
|
import traceback
|
|
7
|
+
from contextlib import aclosing
|
|
7
8
|
from typing import Any, Protocol
|
|
8
9
|
|
|
9
10
|
import trio
|
|
@@ -25,6 +26,7 @@ class InputStream(Protocol):
|
|
|
25
26
|
|
|
26
27
|
def __aiter__(self) -> "InputStream": ...
|
|
27
28
|
async def __anext__(self) -> bytes | bytearray: ...
|
|
29
|
+
async def aclose(self) -> None: ...
|
|
28
30
|
|
|
29
31
|
|
|
30
32
|
class OutputStream(Protocol):
|
|
@@ -88,12 +90,13 @@ class ReducerWorker:
|
|
|
88
90
|
stream = trio.lowlevel.FdStream(os.dup(sys.stdin.fileno()))
|
|
89
91
|
|
|
90
92
|
buffer = b""
|
|
91
|
-
async
|
|
92
|
-
|
|
93
|
-
|
|
94
|
-
|
|
95
|
-
|
|
96
|
-
|
|
93
|
+
async with aclosing(stream) as aiter:
|
|
94
|
+
async for chunk in aiter:
|
|
95
|
+
buffer += chunk
|
|
96
|
+
while b"\n" in buffer:
|
|
97
|
+
line, buffer = buffer.split(b"\n", 1)
|
|
98
|
+
if line:
|
|
99
|
+
await self.handle_line(line.decode("utf-8"))
|
|
97
100
|
|
|
98
101
|
async def handle_line(self, line: str) -> None:
|
|
99
102
|
"""Handle a single command line."""
|
|
@@ -124,7 +127,9 @@ class ReducerWorker:
|
|
|
124
127
|
case "skip_pass":
|
|
125
128
|
return self._handle_skip_pass(request.id)
|
|
126
129
|
case _:
|
|
127
|
-
return Response(
|
|
130
|
+
return Response(
|
|
131
|
+
id=request.id, error=f"Unknown command: {request.command}"
|
|
132
|
+
)
|
|
128
133
|
|
|
129
134
|
async def _handle_start(self, request_id: str, params: dict) -> Response:
|
|
130
135
|
"""Start the reduction process."""
|
|
@@ -181,20 +186,20 @@ class ReducerWorker:
|
|
|
181
186
|
if clang_delta_path:
|
|
182
187
|
clang_delta_executable = ClangDelta(clang_delta_path)
|
|
183
188
|
|
|
184
|
-
state_kwargs: dict[str, Any] =
|
|
185
|
-
input_type
|
|
186
|
-
in_place
|
|
187
|
-
test
|
|
188
|
-
timeout
|
|
189
|
-
base
|
|
190
|
-
parallelism
|
|
191
|
-
filename
|
|
192
|
-
formatter
|
|
193
|
-
trivial_is_error
|
|
194
|
-
seed
|
|
195
|
-
volume
|
|
196
|
-
clang_delta_executable
|
|
197
|
-
|
|
189
|
+
state_kwargs: dict[str, Any] = {
|
|
190
|
+
"input_type": input_type,
|
|
191
|
+
"in_place": in_place,
|
|
192
|
+
"test": test,
|
|
193
|
+
"timeout": timeout,
|
|
194
|
+
"base": os.path.basename(filename),
|
|
195
|
+
"parallelism": parallelism,
|
|
196
|
+
"filename": filename,
|
|
197
|
+
"formatter": formatter,
|
|
198
|
+
"trivial_is_error": trivial_is_error,
|
|
199
|
+
"seed": seed,
|
|
200
|
+
"volume": volume,
|
|
201
|
+
"clang_delta_executable": clang_delta_executable,
|
|
202
|
+
}
|
|
198
203
|
|
|
199
204
|
if os.path.isdir(filename):
|
|
200
205
|
files = [os.path.join(d, f) for d, _, fs in os.walk(filename) for f in fs]
|
|
@@ -263,7 +268,9 @@ class ReducerWorker:
|
|
|
263
268
|
|
|
264
269
|
if self.reducer is not None and hasattr(self.reducer, "disable_pass"):
|
|
265
270
|
self.reducer.disable_pass(pass_name)
|
|
266
|
-
return Response(
|
|
271
|
+
return Response(
|
|
272
|
+
id=request_id, result={"status": "disabled", "pass_name": pass_name}
|
|
273
|
+
)
|
|
267
274
|
return Response(id=request_id, error="Reducer does not support pass control")
|
|
268
275
|
|
|
269
276
|
def _handle_enable_pass(self, request_id: str, params: dict) -> Response:
|
|
@@ -281,7 +288,9 @@ class ReducerWorker:
|
|
|
281
288
|
|
|
282
289
|
if self.reducer is not None and hasattr(self.reducer, "enable_pass"):
|
|
283
290
|
self.reducer.enable_pass(pass_name)
|
|
284
|
-
return Response(
|
|
291
|
+
return Response(
|
|
292
|
+
id=request_id, result={"status": "enabled", "pass_name": pass_name}
|
|
293
|
+
)
|
|
285
294
|
return Response(id=request_id, error="Reducer does not support pass control")
|
|
286
295
|
|
|
287
296
|
def _handle_skip_pass(self, request_id: str) -> Response:
|
|
@@ -1,7 +1,9 @@
|
|
|
1
1
|
"""Textual-based TUI for Shrink Ray."""
|
|
2
2
|
|
|
3
3
|
import os
|
|
4
|
-
|
|
4
|
+
import traceback
|
|
5
|
+
from collections.abc import AsyncGenerator
|
|
6
|
+
from contextlib import aclosing
|
|
5
7
|
from datetime import timedelta
|
|
6
8
|
from typing import Literal, Protocol
|
|
7
9
|
|
|
@@ -114,7 +116,7 @@ class ReductionClientProtocol(Protocol):
|
|
|
114
116
|
|
|
115
117
|
@property
|
|
116
118
|
def error_message(self) -> str | None: ...
|
|
117
|
-
def get_progress_updates(self) ->
|
|
119
|
+
def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]: ...
|
|
118
120
|
@property
|
|
119
121
|
def is_completed(self) -> bool: ...
|
|
120
122
|
|
|
@@ -485,7 +487,9 @@ class PassStatsScreen(ModalScreen[None]):
|
|
|
485
487
|
reductions = str(ps.successful_reductions)
|
|
486
488
|
success = f"{ps.success_rate:.1f}%"
|
|
487
489
|
|
|
488
|
-
table.add_row(
|
|
490
|
+
table.add_row(
|
|
491
|
+
checkbox, name, runs, bytes_del, tests, reductions, success
|
|
492
|
+
)
|
|
489
493
|
|
|
490
494
|
# Restore cursor and scroll position after rebuilding
|
|
491
495
|
# Only restore if the saved position is still valid
|
|
@@ -510,7 +514,9 @@ class PassStatsScreen(ModalScreen[None]):
|
|
|
510
514
|
# Update footer with disabled count
|
|
511
515
|
disabled_count = len(self.disabled_passes)
|
|
512
516
|
if disabled_count > 0:
|
|
513
|
-
footer_text =
|
|
517
|
+
footer_text = (
|
|
518
|
+
f"Showing {len(self.pass_stats)} passes ({disabled_count} disabled)"
|
|
519
|
+
)
|
|
514
520
|
else:
|
|
515
521
|
footer_text = f"Showing {len(self.pass_stats)} passes in run order"
|
|
516
522
|
footer = self.query_one("#stats-footer", Static)
|
|
@@ -713,18 +719,21 @@ class ShrinkRayApp(App[None]):
|
|
|
713
719
|
stats_display = self.query_one("#stats-display", StatsDisplay)
|
|
714
720
|
content_preview = self.query_one("#content-preview", ContentPreview)
|
|
715
721
|
|
|
716
|
-
async
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
722
|
+
async with aclosing(self._client.get_progress_updates()) as updates:
|
|
723
|
+
async for update in updates:
|
|
724
|
+
stats_display.update_stats(update)
|
|
725
|
+
content_preview.update_content(
|
|
726
|
+
update.content_preview, update.hex_mode
|
|
727
|
+
)
|
|
728
|
+
self._latest_pass_stats = update.pass_stats
|
|
729
|
+
self._current_pass_name = update.current_pass_name
|
|
730
|
+
self._disabled_passes = update.disabled_passes
|
|
722
731
|
|
|
723
|
-
|
|
724
|
-
|
|
732
|
+
# Check if all passes are disabled
|
|
733
|
+
self._check_all_passes_disabled()
|
|
725
734
|
|
|
726
|
-
|
|
727
|
-
|
|
735
|
+
if self._client.is_completed:
|
|
736
|
+
break
|
|
728
737
|
|
|
729
738
|
self._completed = True
|
|
730
739
|
|
|
@@ -739,6 +748,7 @@ class ShrinkRayApp(App[None]):
|
|
|
739
748
|
self.update_status("Reduction completed! Press 'q' to exit.")
|
|
740
749
|
|
|
741
750
|
except Exception as e:
|
|
751
|
+
traceback.print_exc()
|
|
742
752
|
self.exit(return_code=1, message=f"Error: {e}")
|
|
743
753
|
finally:
|
|
744
754
|
if self._owns_client and self._client:
|
|
@@ -750,7 +760,7 @@ class ShrinkRayApp(App[None]):
|
|
|
750
760
|
all_pass_names = {ps.pass_name for ps in self._latest_pass_stats}
|
|
751
761
|
if all_pass_names and all_pass_names <= set(self._disabled_passes):
|
|
752
762
|
self.update_status(
|
|
753
|
-
"Reduction paused (all passes disabled) -
|
|
763
|
+
"Reduction paused (all passes disabled) - [p] to re-enable passes"
|
|
754
764
|
)
|
|
755
765
|
|
|
756
766
|
def update_status(self, message: str) -> None:
|
|
@@ -13,7 +13,7 @@ Key concepts:
|
|
|
13
13
|
|
|
14
14
|
import heapq
|
|
15
15
|
from collections.abc import Awaitable, Callable, Sequence
|
|
16
|
-
from contextlib import asynccontextmanager
|
|
16
|
+
from contextlib import aclosing, asynccontextmanager
|
|
17
17
|
from enum import IntEnum
|
|
18
18
|
from itertools import islice
|
|
19
19
|
from random import Random
|
|
@@ -100,8 +100,9 @@ class WorkContext:
|
|
|
100
100
|
async with parallel_map(
|
|
101
101
|
values, f, parallelism=min(self.parallelism, n)
|
|
102
102
|
) as result:
|
|
103
|
-
async
|
|
104
|
-
|
|
103
|
+
async with aclosing(result) as aiter:
|
|
104
|
+
async for v in aiter:
|
|
105
|
+
await send.send(v)
|
|
105
106
|
|
|
106
107
|
n *= 2
|
|
107
108
|
else:
|
|
@@ -122,9 +123,10 @@ class WorkContext:
|
|
|
122
123
|
@nursery.start_soon
|
|
123
124
|
async def _():
|
|
124
125
|
async with self.map(ls, apply) as results:
|
|
125
|
-
async
|
|
126
|
-
|
|
127
|
-
|
|
126
|
+
async with aclosing(results) as aiter:
|
|
127
|
+
async for x, v in aiter:
|
|
128
|
+
if v:
|
|
129
|
+
await send.send(x)
|
|
128
130
|
send.close()
|
|
129
131
|
|
|
130
132
|
yield receive
|
|
@@ -139,8 +141,9 @@ class WorkContext:
|
|
|
139
141
|
Will run in parallel if parallelism is enabled.
|
|
140
142
|
"""
|
|
141
143
|
async with self.filter(ls, f) as filtered:
|
|
142
|
-
async
|
|
143
|
-
|
|
144
|
+
async with aclosing(filtered) as aiter:
|
|
145
|
+
async for x in aiter:
|
|
146
|
+
return x
|
|
144
147
|
raise NotFound()
|
|
145
148
|
|
|
146
149
|
async def find_large_integer(self, f: Callable[[int], Awaitable[bool]]) -> int:
|
|
@@ -166,9 +166,8 @@ async def test_apply_transformation_raises_clang_delta_error():
|
|
|
166
166
|
error.stdout = b"Some error"
|
|
167
167
|
error.stderr = b""
|
|
168
168
|
|
|
169
|
-
with patch("trio.run_process", side_effect=error):
|
|
170
|
-
|
|
171
|
-
await cd.apply_transformation("rename-var", 1, source)
|
|
169
|
+
with patch("trio.run_process", side_effect=error), pytest.raises(ClangDeltaError):
|
|
170
|
+
await cd.apply_transformation("rename-var", 1, source)
|
|
172
171
|
|
|
173
172
|
|
|
174
173
|
async def test_pump_handles_assertion_failure():
|
|
@@ -238,14 +237,16 @@ def test_clang_delta_works_when_not_found():
|
|
|
238
237
|
def test_clang_delta_works_when_execution_fails():
|
|
239
238
|
"""Test clang_delta_works returns False when clang_delta exists but fails to run."""
|
|
240
239
|
clang_delta_works.cache_clear()
|
|
241
|
-
with
|
|
242
|
-
|
|
243
|
-
|
|
244
|
-
|
|
240
|
+
with (
|
|
241
|
+
patch(
|
|
242
|
+
"shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
|
|
243
|
+
),
|
|
244
|
+
patch(
|
|
245
245
|
"shrinkray.passes.clangdelta.subprocess.run",
|
|
246
246
|
side_effect=OSError("Library not found"),
|
|
247
|
-
)
|
|
248
|
-
|
|
247
|
+
),
|
|
248
|
+
):
|
|
249
|
+
assert clang_delta_works() is False
|
|
249
250
|
|
|
250
251
|
|
|
251
252
|
def test_clang_delta_works_when_returns_nonzero():
|
|
@@ -254,13 +255,13 @@ def test_clang_delta_works_when_returns_nonzero():
|
|
|
254
255
|
mock_result = MagicMock()
|
|
255
256
|
mock_result.returncode = 1
|
|
256
257
|
|
|
257
|
-
with
|
|
258
|
-
|
|
258
|
+
with (
|
|
259
|
+
patch(
|
|
260
|
+
"shrinkray.passes.clangdelta.find_clang_delta", return_value="/usr/bin/fake"
|
|
261
|
+
),
|
|
262
|
+
patch("shrinkray.passes.clangdelta.subprocess.run", return_value=mock_result),
|
|
259
263
|
):
|
|
260
|
-
|
|
261
|
-
"shrinkray.passes.clangdelta.subprocess.run", return_value=mock_result
|
|
262
|
-
):
|
|
263
|
-
assert clang_delta_works() is False
|
|
264
|
+
assert clang_delta_works() is False
|
|
264
265
|
|
|
265
266
|
|
|
266
267
|
def test_find_clang_delta_when_found_in_path():
|
|
@@ -306,9 +307,8 @@ async def test_query_instances_raises_clang_delta_error():
|
|
|
306
307
|
error.stdout = b"Some other error"
|
|
307
308
|
error.stderr = b"More error info"
|
|
308
309
|
|
|
309
|
-
with patch("trio.run_process", side_effect=error):
|
|
310
|
-
|
|
311
|
-
await cd.query_instances("rename-var", b"int main() {}")
|
|
310
|
+
with patch("trio.run_process", side_effect=error), pytest.raises(ClangDeltaError):
|
|
311
|
+
await cd.query_instances("rename-var", b"int main() {}")
|
|
312
312
|
|
|
313
313
|
|
|
314
314
|
async def test_query_instances_handles_assertion_failure():
|
|
@@ -103,28 +103,36 @@ def test_validate_ui_returns_value_when_provided():
|
|
|
103
103
|
|
|
104
104
|
|
|
105
105
|
def test_validate_ui_returns_textual_for_tty():
|
|
106
|
-
with
|
|
107
|
-
|
|
108
|
-
|
|
109
|
-
|
|
106
|
+
with (
|
|
107
|
+
patch.object(sys.stdin, "isatty", return_value=True),
|
|
108
|
+
patch.object(sys.stdout, "isatty", return_value=True),
|
|
109
|
+
):
|
|
110
|
+
result = validate_ui(None, None, None)
|
|
111
|
+
assert result == UIType.textual
|
|
110
112
|
|
|
111
113
|
|
|
112
114
|
def test_validate_ui_returns_basic_for_non_tty_stdin():
|
|
113
|
-
with
|
|
114
|
-
|
|
115
|
-
|
|
116
|
-
|
|
115
|
+
with (
|
|
116
|
+
patch.object(sys.stdin, "isatty", return_value=False),
|
|
117
|
+
patch.object(sys.stdout, "isatty", return_value=True),
|
|
118
|
+
):
|
|
119
|
+
result = validate_ui(None, None, None)
|
|
120
|
+
assert result == UIType.basic
|
|
117
121
|
|
|
118
122
|
|
|
119
123
|
def test_validate_ui_returns_basic_for_non_tty_stdout():
|
|
120
|
-
with
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
+
with (
|
|
125
|
+
patch.object(sys.stdin, "isatty", return_value=True),
|
|
126
|
+
patch.object(sys.stdout, "isatty", return_value=False),
|
|
127
|
+
):
|
|
128
|
+
result = validate_ui(None, None, None)
|
|
129
|
+
assert result == UIType.basic
|
|
124
130
|
|
|
125
131
|
|
|
126
132
|
def test_validate_ui_returns_basic_for_non_tty_both():
|
|
127
|
-
with
|
|
128
|
-
|
|
129
|
-
|
|
130
|
-
|
|
133
|
+
with (
|
|
134
|
+
patch.object(sys.stdin, "isatty", return_value=False),
|
|
135
|
+
patch.object(sys.stdout, "isatty", return_value=False),
|
|
136
|
+
):
|
|
137
|
+
result = validate_ui(None, None, None)
|
|
138
|
+
assert result == UIType.basic
|
|
@@ -41,6 +41,7 @@ def test_format_is_valid_for_unparseable():
|
|
|
41
41
|
|
|
42
42
|
async def test_compose_returns_early_on_parse_error():
|
|
43
43
|
"""Test that compose returns early when parsing fails after problem changes."""
|
|
44
|
+
|
|
44
45
|
# Create a format that parses strings starting with "VALID:" successfully
|
|
45
46
|
class SelectiveFormat(Format[str, str]):
|
|
46
47
|
def parse(self, input: str) -> str:
|
|
@@ -213,10 +213,14 @@ def test_negating_map_repr():
|
|
|
213
213
|
nm[3] = 4
|
|
214
214
|
r = repr(nm)
|
|
215
215
|
# Should contain both positive and negative mappings
|
|
216
|
-
assert "1" in r
|
|
217
|
-
assert "
|
|
218
|
-
assert "
|
|
219
|
-
assert "
|
|
216
|
+
assert "1" in r
|
|
217
|
+
assert "-1" in r
|
|
218
|
+
assert "2" in r
|
|
219
|
+
assert "-2" in r
|
|
220
|
+
assert "3" in r
|
|
221
|
+
assert "-3" in r
|
|
222
|
+
assert "4" in r
|
|
223
|
+
assert "-4" in r
|
|
220
224
|
|
|
221
225
|
|
|
222
226
|
def test_unit_propagator_duplicate_unit():
|
|
@@ -44,10 +44,12 @@ def test_find_python_command_checks_python_bin_directory():
|
|
|
44
44
|
|
|
45
45
|
|
|
46
46
|
def test_find_python_command_returns_none_when_not_in_bin_dir():
|
|
47
|
-
with
|
|
48
|
-
|
|
49
|
-
|
|
50
|
-
|
|
47
|
+
with (
|
|
48
|
+
patch("shrinkray.formatting.which", return_value=None),
|
|
49
|
+
patch("os.path.exists", return_value=False),
|
|
50
|
+
):
|
|
51
|
+
result = find_python_command("nonexistent")
|
|
52
|
+
assert result is None
|
|
51
53
|
|
|
52
54
|
|
|
53
55
|
# === try_decode tests ===
|
|
@@ -266,7 +266,6 @@ def test_normalize_identifiers_identifier_disappears():
|
|
|
266
266
|
def test_regex_pass_with_compiled_pattern():
|
|
267
267
|
"""Test regex_pass with an already compiled pattern."""
|
|
268
268
|
|
|
269
|
-
|
|
270
269
|
# Create a pass with a pre-compiled pattern
|
|
271
270
|
pattern = re.compile(rb"[0-9]+")
|
|
272
271
|
|
|
@@ -345,7 +344,6 @@ async def test_normalize_identifiers_target_disappears():
|
|
|
345
344
|
This can happen in concurrent scenarios or with modified problem classes.
|
|
346
345
|
"""
|
|
347
346
|
|
|
348
|
-
|
|
349
347
|
# Create a mock problem that changes its current_test_case
|
|
350
348
|
mock_problem = MagicMock()
|
|
351
349
|
mock_problem.work = WorkContext(parallelism=1)
|