shrinkray 25.12.26.2__py3-none-any.whl → 25.12.27.1__py3-none-any.whl
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/__main__.py +22 -20
- shrinkray/passes/sat.py +6 -6
- shrinkray/reducer.py +6 -2
- shrinkray/state.py +58 -16
- shrinkray/subprocess/client.py +6 -5
- shrinkray/subprocess/worker.py +33 -24
- shrinkray/tui.py +30 -19
- shrinkray/work.py +11 -8
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/METADATA +3 -2
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/RECORD +14 -14
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/WHEEL +0 -0
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/entry_points.txt +0 -0
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/licenses/LICENSE +0 -0
- {shrinkray-25.12.26.2.dist-info → shrinkray-25.12.27.1.dist-info}/top_level.txt +0 -0
shrinkray/__main__.py
CHANGED
|
@@ -75,12 +75,14 @@ async def run_shrink_ray(
|
|
|
75
75
|
)
|
|
76
76
|
@click.option(
|
|
77
77
|
"--timeout",
|
|
78
|
-
default=
|
|
78
|
+
default=None,
|
|
79
79
|
type=click.FLOAT,
|
|
80
80
|
help=(
|
|
81
|
-
"Time out subprocesses after this many seconds. If
|
|
82
|
-
"
|
|
83
|
-
"
|
|
81
|
+
"Time out subprocesses after this many seconds. If not specified, "
|
|
82
|
+
"runs the interestingness test once and sets timeout to 10x the "
|
|
83
|
+
"measured time (capped at 5 minutes). If set to <= 0 then no timeout "
|
|
84
|
+
"will be used. Any commands that time out will be treated as failing "
|
|
85
|
+
"the test"
|
|
84
86
|
),
|
|
85
87
|
)
|
|
86
88
|
@click.option(
|
|
@@ -212,7 +214,7 @@ def main(
|
|
|
212
214
|
backup: str,
|
|
213
215
|
filename: str,
|
|
214
216
|
test: list[str],
|
|
215
|
-
timeout: float,
|
|
217
|
+
timeout: float | None,
|
|
216
218
|
in_place: bool,
|
|
217
219
|
parallelism: int,
|
|
218
220
|
seed: int,
|
|
@@ -225,7 +227,7 @@ def main(
|
|
|
225
227
|
ui_type: UIType,
|
|
226
228
|
theme: str,
|
|
227
229
|
) -> None:
|
|
228
|
-
if timeout <= 0:
|
|
230
|
+
if timeout is not None and timeout <= 0:
|
|
229
231
|
timeout = float("inf")
|
|
230
232
|
|
|
231
233
|
if not os.access(test[0], os.X_OK):
|
|
@@ -271,20 +273,20 @@ def main(
|
|
|
271
273
|
if not backup:
|
|
272
274
|
backup = filename + os.extsep + "bak"
|
|
273
275
|
|
|
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
|
-
|
|
276
|
+
state_kwargs: dict[str, Any] = {
|
|
277
|
+
"input_type": input_type,
|
|
278
|
+
"in_place": in_place,
|
|
279
|
+
"test": test,
|
|
280
|
+
"timeout": timeout,
|
|
281
|
+
"base": os.path.basename(filename),
|
|
282
|
+
"parallelism": parallelism,
|
|
283
|
+
"filename": filename,
|
|
284
|
+
"formatter": formatter,
|
|
285
|
+
"trivial_is_error": trivial_is_error,
|
|
286
|
+
"seed": seed,
|
|
287
|
+
"volume": volume,
|
|
288
|
+
"clang_delta_executable": clang_delta_executable,
|
|
289
|
+
}
|
|
288
290
|
|
|
289
291
|
state: ShrinkRayState[Any]
|
|
290
292
|
ui: ShrinkRayUI[Any]
|
shrinkray/passes/sat.py
CHANGED
|
@@ -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:
|
shrinkray/reducer.py
CHANGED
|
@@ -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:
|
shrinkray/state.py
CHANGED
|
@@ -36,13 +36,32 @@ class TimeoutExceededOnInitial(InvalidInitialExample):
|
|
|
36
36
|
)
|
|
37
37
|
|
|
38
38
|
|
|
39
|
+
# Constants for dynamic timeout
|
|
40
|
+
DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT = 300.0 # 5 minutes for first call
|
|
41
|
+
DYNAMIC_TIMEOUT_MULTIPLIER = 10
|
|
42
|
+
DYNAMIC_TIMEOUT_MAX = 300.0 # 5 minutes maximum
|
|
43
|
+
DYNAMIC_TIMEOUT_MIN = 1.0 # 1 second minimum to prevent edge cases
|
|
44
|
+
|
|
45
|
+
|
|
46
|
+
def compute_dynamic_timeout(runtime: float) -> float:
|
|
47
|
+
"""Compute dynamic timeout based on measured runtime.
|
|
48
|
+
|
|
49
|
+
The timeout is set to 10x the measured runtime, clamped between
|
|
50
|
+
DYNAMIC_TIMEOUT_MIN and DYNAMIC_TIMEOUT_MAX.
|
|
51
|
+
"""
|
|
52
|
+
return max(
|
|
53
|
+
DYNAMIC_TIMEOUT_MIN,
|
|
54
|
+
min(runtime * DYNAMIC_TIMEOUT_MULTIPLIER, DYNAMIC_TIMEOUT_MAX),
|
|
55
|
+
)
|
|
56
|
+
|
|
57
|
+
|
|
39
58
|
@define(slots=False)
|
|
40
59
|
class ShrinkRayState[TestCase](ABC):
|
|
41
60
|
input_type: Any # InputType from __main__
|
|
42
61
|
in_place: bool
|
|
43
62
|
test: list[str]
|
|
44
63
|
filename: str
|
|
45
|
-
timeout: float
|
|
64
|
+
timeout: float | None
|
|
46
65
|
base: str
|
|
47
66
|
parallelism: int
|
|
48
67
|
initial: TestCase
|
|
@@ -105,12 +124,12 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
105
124
|
else:
|
|
106
125
|
command = self.test
|
|
107
126
|
|
|
108
|
-
kwargs: dict[str, Any] =
|
|
109
|
-
universal_newlines
|
|
110
|
-
preexec_fn
|
|
111
|
-
cwd
|
|
112
|
-
check
|
|
113
|
-
|
|
127
|
+
kwargs: dict[str, Any] = {
|
|
128
|
+
"universal_newlines": False,
|
|
129
|
+
"preexec_fn": os.setsid,
|
|
130
|
+
"cwd": cwd,
|
|
131
|
+
"check": False,
|
|
132
|
+
}
|
|
114
133
|
if self.input_type.enabled(self._InputType.stdin) and not os.path.isdir(
|
|
115
134
|
working
|
|
116
135
|
):
|
|
@@ -127,7 +146,8 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
127
146
|
completed = await trio.run_process(command, **kwargs)
|
|
128
147
|
runtime = time.time() - start_time
|
|
129
148
|
|
|
130
|
-
|
|
149
|
+
# Check for timeout violation (only when timeout is explicitly set)
|
|
150
|
+
if self.timeout is not None and runtime >= self.timeout and self.first_call:
|
|
131
151
|
self.initial_exit_code = completed.returncode
|
|
132
152
|
self.first_call = False
|
|
133
153
|
raise TimeoutExceededOnInitial(
|
|
@@ -137,6 +157,9 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
137
157
|
|
|
138
158
|
if self.first_call:
|
|
139
159
|
self.initial_exit_code = completed.returncode
|
|
160
|
+
# Set dynamic timeout if not explicitly specified
|
|
161
|
+
if self.timeout is None:
|
|
162
|
+
self.timeout = compute_dynamic_timeout(runtime)
|
|
140
163
|
self.first_call = False
|
|
141
164
|
|
|
142
165
|
# Store captured output
|
|
@@ -168,9 +191,19 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
168
191
|
sp = await nursery.start(call_with_kwargs)
|
|
169
192
|
|
|
170
193
|
try:
|
|
171
|
-
|
|
172
|
-
|
|
173
|
-
|
|
194
|
+
# Determine effective timeout for this call
|
|
195
|
+
if self.first_call:
|
|
196
|
+
# For first call: use calibration timeout if dynamic, otherwise 10x explicit timeout
|
|
197
|
+
if self.timeout is None:
|
|
198
|
+
effective_timeout = DYNAMIC_TIMEOUT_CALIBRATION_TIMEOUT
|
|
199
|
+
else:
|
|
200
|
+
effective_timeout = self.timeout * 10
|
|
201
|
+
else:
|
|
202
|
+
# For subsequent calls, timeout must be set (either explicit or computed)
|
|
203
|
+
assert self.timeout is not None
|
|
204
|
+
effective_timeout = self.timeout
|
|
205
|
+
|
|
206
|
+
with trio.move_on_after(effective_timeout):
|
|
174
207
|
await sp.wait()
|
|
175
208
|
|
|
176
209
|
runtime = time.time() - start_time
|
|
@@ -179,7 +212,12 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
179
212
|
# Process didn't terminate before timeout - kill it
|
|
180
213
|
await self._interrupt_wait_and_kill(sp)
|
|
181
214
|
|
|
182
|
-
|
|
215
|
+
# Check for timeout violation (only when timeout is explicitly set)
|
|
216
|
+
if (
|
|
217
|
+
self.timeout is not None
|
|
218
|
+
and runtime >= self.timeout
|
|
219
|
+
and self.first_call
|
|
220
|
+
):
|
|
183
221
|
raise TimeoutExceededOnInitial(
|
|
184
222
|
timeout=self.timeout,
|
|
185
223
|
runtime=runtime,
|
|
@@ -187,6 +225,10 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
187
225
|
finally:
|
|
188
226
|
if self.first_call:
|
|
189
227
|
self.initial_exit_code = sp.returncode
|
|
228
|
+
# Set dynamic timeout if not explicitly specified
|
|
229
|
+
if self.timeout is None:
|
|
230
|
+
runtime = time.time() - start_time
|
|
231
|
+
self.timeout = compute_dynamic_timeout(runtime)
|
|
190
232
|
self.first_call = False
|
|
191
233
|
|
|
192
234
|
result: int | None = sp.returncode
|
|
@@ -562,10 +604,10 @@ class ShrinkRayDirectoryState(ShrinkRayState[dict[str, bytes]]):
|
|
|
562
604
|
sorted((k, shortlex(v)) for k, v in test_case.items()),
|
|
563
605
|
)
|
|
564
606
|
|
|
565
|
-
return
|
|
566
|
-
sort_key
|
|
567
|
-
size
|
|
568
|
-
|
|
607
|
+
return {
|
|
608
|
+
"sort_key": dict_sort_key,
|
|
609
|
+
"size": dict_size,
|
|
610
|
+
}
|
|
569
611
|
|
|
570
612
|
def new_reducer(
|
|
571
613
|
self, problem: ReductionProblem[dict[str, bytes]]
|
shrinkray/subprocess/client.py
CHANGED
|
@@ -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 (
|
|
@@ -127,7 +127,7 @@ class SubprocessClient:
|
|
|
127
127
|
file_path: str,
|
|
128
128
|
test: list[str],
|
|
129
129
|
parallelism: int | None = None,
|
|
130
|
-
timeout: float =
|
|
130
|
+
timeout: float | None = None,
|
|
131
131
|
seed: int = 0,
|
|
132
132
|
input_type: str = "all",
|
|
133
133
|
in_place: bool = False,
|
|
@@ -138,10 +138,9 @@ class SubprocessClient:
|
|
|
138
138
|
trivial_is_error: bool = True,
|
|
139
139
|
) -> Response:
|
|
140
140
|
"""Start the reduction process."""
|
|
141
|
-
params = {
|
|
141
|
+
params: dict[str, Any] = {
|
|
142
142
|
"file_path": file_path,
|
|
143
143
|
"test": test,
|
|
144
|
-
"timeout": timeout,
|
|
145
144
|
"seed": seed,
|
|
146
145
|
"input_type": input_type,
|
|
147
146
|
"in_place": in_place,
|
|
@@ -153,6 +152,8 @@ class SubprocessClient:
|
|
|
153
152
|
}
|
|
154
153
|
if parallelism is not None:
|
|
155
154
|
params["parallelism"] = parallelism
|
|
155
|
+
if timeout is not None:
|
|
156
|
+
params["timeout"] = timeout
|
|
156
157
|
return await self.send_command("start", params)
|
|
157
158
|
|
|
158
159
|
async def get_status(self) -> Response:
|
|
@@ -200,7 +201,7 @@ class SubprocessClient:
|
|
|
200
201
|
traceback.print_exc()
|
|
201
202
|
return Response(id="", error="Failed to skip pass")
|
|
202
203
|
|
|
203
|
-
async def get_progress_updates(self) ->
|
|
204
|
+
async def get_progress_updates(self) -> AsyncGenerator[ProgressUpdate, None]:
|
|
204
205
|
"""Yield progress updates as they arrive."""
|
|
205
206
|
while not self._completed:
|
|
206
207
|
try:
|
shrinkray/subprocess/worker.py
CHANGED
|
@@ -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."""
|
|
@@ -164,7 +169,7 @@ class ReducerWorker:
|
|
|
164
169
|
filename = params["file_path"]
|
|
165
170
|
test = params["test"]
|
|
166
171
|
parallelism = params.get("parallelism", os.cpu_count() or 1)
|
|
167
|
-
timeout = params.get("timeout"
|
|
172
|
+
timeout = params.get("timeout") # None means dynamic timeout
|
|
168
173
|
seed = params.get("seed", 0)
|
|
169
174
|
input_type = InputType[params.get("input_type", "all")]
|
|
170
175
|
in_place = params.get("in_place", False)
|
|
@@ -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:
|
shrinkray/tui.py
CHANGED
|
@@ -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
|
|
|
@@ -96,7 +98,7 @@ class ReductionClientProtocol(Protocol):
|
|
|
96
98
|
file_path: str,
|
|
97
99
|
test: list[str],
|
|
98
100
|
parallelism: int | None = None,
|
|
99
|
-
timeout: float =
|
|
101
|
+
timeout: float | None = None,
|
|
100
102
|
seed: int = 0,
|
|
101
103
|
input_type: str = "all",
|
|
102
104
|
in_place: bool = False,
|
|
@@ -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)
|
|
@@ -610,7 +616,7 @@ class ShrinkRayApp(App[None]):
|
|
|
610
616
|
file_path: str,
|
|
611
617
|
test: list[str],
|
|
612
618
|
parallelism: int | None = None,
|
|
613
|
-
timeout: float =
|
|
619
|
+
timeout: float | None = None,
|
|
614
620
|
seed: int = 0,
|
|
615
621
|
input_type: str = "all",
|
|
616
622
|
in_place: bool = False,
|
|
@@ -651,6 +657,7 @@ class ShrinkRayApp(App[None]):
|
|
|
651
657
|
yield Label(
|
|
652
658
|
"Shrink Ray - [h] help, [p] passes, [c] skip pass, [q] quit",
|
|
653
659
|
id="status-label",
|
|
660
|
+
markup=False,
|
|
654
661
|
)
|
|
655
662
|
with Vertical(id="stats-container"):
|
|
656
663
|
yield StatsDisplay(id="stats-display")
|
|
@@ -713,18 +720,21 @@ class ShrinkRayApp(App[None]):
|
|
|
713
720
|
stats_display = self.query_one("#stats-display", StatsDisplay)
|
|
714
721
|
content_preview = self.query_one("#content-preview", ContentPreview)
|
|
715
722
|
|
|
716
|
-
async
|
|
717
|
-
|
|
718
|
-
|
|
719
|
-
|
|
720
|
-
|
|
721
|
-
|
|
723
|
+
async with aclosing(self._client.get_progress_updates()) as updates:
|
|
724
|
+
async for update in updates:
|
|
725
|
+
stats_display.update_stats(update)
|
|
726
|
+
content_preview.update_content(
|
|
727
|
+
update.content_preview, update.hex_mode
|
|
728
|
+
)
|
|
729
|
+
self._latest_pass_stats = update.pass_stats
|
|
730
|
+
self._current_pass_name = update.current_pass_name
|
|
731
|
+
self._disabled_passes = update.disabled_passes
|
|
722
732
|
|
|
723
|
-
|
|
724
|
-
|
|
733
|
+
# Check if all passes are disabled
|
|
734
|
+
self._check_all_passes_disabled()
|
|
725
735
|
|
|
726
|
-
|
|
727
|
-
|
|
736
|
+
if self._client.is_completed:
|
|
737
|
+
break
|
|
728
738
|
|
|
729
739
|
self._completed = True
|
|
730
740
|
|
|
@@ -739,6 +749,7 @@ class ShrinkRayApp(App[None]):
|
|
|
739
749
|
self.update_status("Reduction completed! Press 'q' to exit.")
|
|
740
750
|
|
|
741
751
|
except Exception as e:
|
|
752
|
+
traceback.print_exc()
|
|
742
753
|
self.exit(return_code=1, message=f"Error: {e}")
|
|
743
754
|
finally:
|
|
744
755
|
if self._owns_client and self._client:
|
|
@@ -750,7 +761,7 @@ class ShrinkRayApp(App[None]):
|
|
|
750
761
|
all_pass_names = {ps.pass_name for ps in self._latest_pass_stats}
|
|
751
762
|
if all_pass_names and all_pass_names <= set(self._disabled_passes):
|
|
752
763
|
self.update_status(
|
|
753
|
-
"Reduction paused (all passes disabled) -
|
|
764
|
+
"Reduction paused (all passes disabled) - [p] to re-enable passes"
|
|
754
765
|
)
|
|
755
766
|
|
|
756
767
|
def update_status(self, message: str) -> None:
|
|
@@ -797,7 +808,7 @@ async def _validate_initial_example(
|
|
|
797
808
|
file_path: str,
|
|
798
809
|
test: list[str],
|
|
799
810
|
parallelism: int | None,
|
|
800
|
-
timeout: float,
|
|
811
|
+
timeout: float | None,
|
|
801
812
|
seed: int,
|
|
802
813
|
input_type: str,
|
|
803
814
|
in_place: bool,
|
|
@@ -845,7 +856,7 @@ def run_textual_ui(
|
|
|
845
856
|
file_path: str,
|
|
846
857
|
test: list[str],
|
|
847
858
|
parallelism: int | None = None,
|
|
848
|
-
timeout: float =
|
|
859
|
+
timeout: float | None = None,
|
|
849
860
|
seed: int = 0,
|
|
850
861
|
input_type: str = "all",
|
|
851
862
|
in_place: bool = False,
|
shrinkray/work.py
CHANGED
|
@@ -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:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrinkray
|
|
3
|
-
Version: 25.12.
|
|
3
|
+
Version: 25.12.27.1
|
|
4
4
|
Summary: Shrink Ray
|
|
5
5
|
Author-email: "David R. MacIver" <david@drmaciver.com>
|
|
6
6
|
License: MIT
|
|
@@ -28,7 +28,8 @@ Requires-Dist: hypothesmith>=0.3.1; extra == "dev"
|
|
|
28
28
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
29
29
|
Requires-Dist: pytest-trio>=0.8.0; extra == "dev"
|
|
30
30
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
31
|
-
Requires-Dist:
|
|
31
|
+
Requires-Dist: syrupy>=5.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: jinja2>=3.0.0; extra == "dev"
|
|
32
33
|
Requires-Dist: coverage[toml]>=7.4.0; extra == "dev"
|
|
33
34
|
Requires-Dist: pygments>=2.17.0; extra == "dev"
|
|
34
35
|
Requires-Dist: basedpyright>=1.1.0; extra == "dev"
|
|
@@ -1,16 +1,16 @@
|
|
|
1
1
|
shrinkray/__init__.py,sha256=b5MvcvhsEGYya3GRXNbCJAlAL5IZHSsETLK_vtfmXRY,18
|
|
2
|
-
shrinkray/__main__.py,sha256=
|
|
2
|
+
shrinkray/__main__.py,sha256=K3_s96Tyoi7SxNOyoZXkfiEoSxVBL__TJ3o2Cefadmg,11093
|
|
3
3
|
shrinkray/cli.py,sha256=1-qjaIchyCDd-YCdGWtK7q9j9qr6uX6AqtwW8m5QCQg,1697
|
|
4
4
|
shrinkray/display.py,sha256=WYN05uqmUVpZhwi2pxr1U-wLHWZ9KiL0RUlTCBJ1N3E,2430
|
|
5
5
|
shrinkray/formatting.py,sha256=tXCGnhJn-WJGpHMaLHRCAXK8aKJBbrOdiW9QGERrQEk,3121
|
|
6
6
|
shrinkray/problem.py,sha256=Kp7QN10E4tzjdpoqJve8_RT26VpywzQwY0gX2VkBGCo,17277
|
|
7
7
|
shrinkray/process.py,sha256=-eP8h5X0ESbkcTic8FFEzkd4-vwaZ0YI5tLxUR25L8U,1599
|
|
8
8
|
shrinkray/py.typed,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
9
|
-
shrinkray/reducer.py,sha256=
|
|
10
|
-
shrinkray/state.py,sha256=
|
|
11
|
-
shrinkray/tui.py,sha256=
|
|
9
|
+
shrinkray/reducer.py,sha256=xhLo_GF7qrIVoiLHed6Wt4nxjdE-9jj_7K9F76un89o,19877
|
|
10
|
+
shrinkray/state.py,sha256=HQ7VivCXWiP2L53gB0lh6dHcdru6waWZkbPg6rYO7z4,24290
|
|
11
|
+
shrinkray/tui.py,sha256=3RskLo6JvKdUQIHi40R5ka-F_1GkBXyA_d_SkYbLlCw,31601
|
|
12
12
|
shrinkray/ui.py,sha256=xuDUwU-MM3AetvwUB7bfzav0P_drUsBrKFPhON_Nr-k,2251
|
|
13
|
-
shrinkray/work.py,sha256=
|
|
13
|
+
shrinkray/work.py,sha256=GEZ14Kk3bvwUxAnACvY-wom2lVWaGrELMNxrDjv03dk,8110
|
|
14
14
|
shrinkray/passes/__init__.py,sha256=47DEQpj8HBSa-_TImW-5JCeuQeRkm5NMpJWZG3hSuFU,0
|
|
15
15
|
shrinkray/passes/bytes.py,sha256=pX2kBeH38SM4j55f8bNF_2PVBCDo6XdUj73EvOFH9f8,23904
|
|
16
16
|
shrinkray/passes/clangdelta.py,sha256=t9EQ_kc159HRs48JwB5JvlJCsiCscrZgf2nhHCZRZX0,8419
|
|
@@ -19,15 +19,15 @@ shrinkray/passes/genericlanguages.py,sha256=qbTuJgUieHqWtf7cly2tm0qdbrVXzVoWcAnF
|
|
|
19
19
|
shrinkray/passes/json.py,sha256=AcmroHgb38Aa3aApwcuYNzmyya_vnCi2RFSVqomiDg8,2586
|
|
20
20
|
shrinkray/passes/patching.py,sha256=1uOTir3IbywKmsg6IIhSnxHFovZTdUCS-8PSwzgza00,8936
|
|
21
21
|
shrinkray/passes/python.py,sha256=3WN1lZTf5oVL8FCTGomhrCuE04wIX9ocKcmFV86NMZA,6875
|
|
22
|
-
shrinkray/passes/sat.py,sha256=
|
|
22
|
+
shrinkray/passes/sat.py,sha256=5Zv4IgGfg3SYplMAAaPLkbBNh4fuVpci4F9GdVacayA,19504
|
|
23
23
|
shrinkray/passes/sequences.py,sha256=jCK1fWBxCz79u7JWSps9wf7Yru7W_FAsJwdgg--CLxU,3040
|
|
24
24
|
shrinkray/subprocess/__init__.py,sha256=FyV2y05uwQ1RTZGwREI0aAVaLX1jiwRcWsdsksFmdbM,451
|
|
25
|
-
shrinkray/subprocess/client.py,sha256=
|
|
25
|
+
shrinkray/subprocess/client.py,sha256=erqnPglPO0YNdwEKlmhB3yDo6Mfc00Lxh4T85lZhsDo,9341
|
|
26
26
|
shrinkray/subprocess/protocol.py,sha256=LuHl0IkKpDzYhAGZz_EiTHNqDNq_v1ozg5aUSl7UzE4,6203
|
|
27
|
-
shrinkray/subprocess/worker.py,sha256=
|
|
28
|
-
shrinkray-25.12.
|
|
29
|
-
shrinkray-25.12.
|
|
30
|
-
shrinkray-25.12.
|
|
31
|
-
shrinkray-25.12.
|
|
32
|
-
shrinkray-25.12.
|
|
33
|
-
shrinkray-25.12.
|
|
27
|
+
shrinkray/subprocess/worker.py,sha256=ke-9DYFH117EpJEntkucTrn7ep7pygzmV-VXkRe1o-E,19294
|
|
28
|
+
shrinkray-25.12.27.1.dist-info/licenses/LICENSE,sha256=iMKX79AuokJfIZUnGUARdUp30vVAoIPOJ7ek8TY63kk,1072
|
|
29
|
+
shrinkray-25.12.27.1.dist-info/METADATA,sha256=bB-_7Y4Gk9a-QiXEFKy_iTDy3nWM72YOYXhq0W_PJ7o,9721
|
|
30
|
+
shrinkray-25.12.27.1.dist-info/WHEEL,sha256=_zCd3N1l69ArxyTb8rzEoP9TpbYXkqRFSNOD5OuxnTs,91
|
|
31
|
+
shrinkray-25.12.27.1.dist-info/entry_points.txt,sha256=wIZvnGyOdVeaLTiv2klnSyTe-EKkkwn4SwHh9bmJ7qk,104
|
|
32
|
+
shrinkray-25.12.27.1.dist-info/top_level.txt,sha256=fLif8-rFoFOnf5h8-vs3ECkKNWQopTQh3xvl1s7pchQ,10
|
|
33
|
+
shrinkray-25.12.27.1.dist-info/RECORD,,
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|