shrinkray 25.12.27.0__tar.gz → 25.12.27.1__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.27.0/src/shrinkray.egg-info → shrinkray-25.12.27.1}/PKG-INFO +3 -2
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/pyproject.toml +5 -2
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/__main__.py +8 -6
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/state.py +48 -6
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/subprocess/client.py +4 -3
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/subprocess/worker.py +1 -1
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/tui.py +5 -4
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1/src/shrinkray.egg-info}/PKG-INFO +3 -2
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray.egg-info/requires.txt +2 -1
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_state.py +82 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_tui.py +1 -1
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_tui_snapshots.py +71 -29
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/LICENSE +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/README.md +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/setup.cfg +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/__init__.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/cli.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/display.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/formatting.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/__init__.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/bytes.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/clangdelta.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/definitions.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/genericlanguages.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/json.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/patching.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/python.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/sat.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/passes/sequences.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/problem.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/process.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/py.typed +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/reducer.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/subprocess/__init__.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/subprocess/protocol.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/ui.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray/work.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray.egg-info/SOURCES.txt +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray.egg-info/dependency_links.txt +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray.egg-info/entry_points.txt +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/src/shrinkray.egg-info/top_level.txt +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_byte_reduction_passes.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_clang_delta.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_cli.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_definitions.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_dimacs_cnf.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_display.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_formatting.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_generic_language.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_generic_shrinking_properties.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_json_passes.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_main.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_misc_reduction_performance.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_patching.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_problem.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_process.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_python_reducers.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_reducer.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_reduction_passes.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_sat.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_subprocess_client.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_subprocess_integration.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_subprocess_protocol.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_subprocess_worker.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_ui.py +0 -0
- {shrinkray-25.12.27.0 → shrinkray-25.12.27.1}/tests/test_work.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrinkray
|
|
3
|
-
Version: 25.12.27.
|
|
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,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shrinkray"
|
|
3
|
-
version = "25.12.27.
|
|
3
|
+
version = "25.12.27.1"
|
|
4
4
|
description = "Shrink Ray"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "David R. MacIver", email = "david@drmaciver.com"}
|
|
@@ -41,7 +41,10 @@ dev = [
|
|
|
41
41
|
"pytest>=8.0.0",
|
|
42
42
|
"pytest-trio>=0.8.0",
|
|
43
43
|
"pytest-asyncio>=0.21.0",
|
|
44
|
-
|
|
44
|
+
# pytest-textual-snapshot is vendored in tests/pytest_textual_snapshot.py
|
|
45
|
+
# to fix syrupy 5.0 compatibility. These are its dependencies:
|
|
46
|
+
"syrupy>=5.0.0",
|
|
47
|
+
"jinja2>=3.0.0",
|
|
45
48
|
"coverage[toml]>=7.4.0",
|
|
46
49
|
"pygments>=2.17.0",
|
|
47
50
|
"basedpyright>=1.1.0",
|
|
@@ -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):
|
|
@@ -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
|
|
@@ -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
|
|
@@ -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:
|
|
@@ -169,7 +169,7 @@ class ReducerWorker:
|
|
|
169
169
|
filename = params["file_path"]
|
|
170
170
|
test = params["test"]
|
|
171
171
|
parallelism = params.get("parallelism", os.cpu_count() or 1)
|
|
172
|
-
timeout = params.get("timeout"
|
|
172
|
+
timeout = params.get("timeout") # None means dynamic timeout
|
|
173
173
|
seed = params.get("seed", 0)
|
|
174
174
|
input_type = InputType[params.get("input_type", "all")]
|
|
175
175
|
in_place = params.get("in_place", False)
|
|
@@ -98,7 +98,7 @@ class ReductionClientProtocol(Protocol):
|
|
|
98
98
|
file_path: str,
|
|
99
99
|
test: list[str],
|
|
100
100
|
parallelism: int | None = None,
|
|
101
|
-
timeout: float =
|
|
101
|
+
timeout: float | None = None,
|
|
102
102
|
seed: int = 0,
|
|
103
103
|
input_type: str = "all",
|
|
104
104
|
in_place: bool = False,
|
|
@@ -616,7 +616,7 @@ class ShrinkRayApp(App[None]):
|
|
|
616
616
|
file_path: str,
|
|
617
617
|
test: list[str],
|
|
618
618
|
parallelism: int | None = None,
|
|
619
|
-
timeout: float =
|
|
619
|
+
timeout: float | None = None,
|
|
620
620
|
seed: int = 0,
|
|
621
621
|
input_type: str = "all",
|
|
622
622
|
in_place: bool = False,
|
|
@@ -657,6 +657,7 @@ class ShrinkRayApp(App[None]):
|
|
|
657
657
|
yield Label(
|
|
658
658
|
"Shrink Ray - [h] help, [p] passes, [c] skip pass, [q] quit",
|
|
659
659
|
id="status-label",
|
|
660
|
+
markup=False,
|
|
660
661
|
)
|
|
661
662
|
with Vertical(id="stats-container"):
|
|
662
663
|
yield StatsDisplay(id="stats-display")
|
|
@@ -807,7 +808,7 @@ async def _validate_initial_example(
|
|
|
807
808
|
file_path: str,
|
|
808
809
|
test: list[str],
|
|
809
810
|
parallelism: int | None,
|
|
810
|
-
timeout: float,
|
|
811
|
+
timeout: float | None,
|
|
811
812
|
seed: int,
|
|
812
813
|
input_type: str,
|
|
813
814
|
in_place: bool,
|
|
@@ -855,7 +856,7 @@ def run_textual_ui(
|
|
|
855
856
|
file_path: str,
|
|
856
857
|
test: list[str],
|
|
857
858
|
parallelism: int | None = None,
|
|
858
|
-
timeout: float =
|
|
859
|
+
timeout: float | None = None,
|
|
859
860
|
seed: int = 0,
|
|
860
861
|
input_type: str = "all",
|
|
861
862
|
in_place: bool = False,
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrinkray
|
|
3
|
-
Version: 25.12.27.
|
|
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"
|
|
@@ -9,6 +9,7 @@ import trio
|
|
|
9
9
|
from shrinkray.cli import InputType
|
|
10
10
|
from shrinkray.problem import InvalidInitialExample
|
|
11
11
|
from shrinkray.state import (
|
|
12
|
+
DYNAMIC_TIMEOUT_MIN,
|
|
12
13
|
ShrinkRayDirectoryState,
|
|
13
14
|
ShrinkRayStateSingleFile,
|
|
14
15
|
TimeoutExceededOnInitial,
|
|
@@ -1640,6 +1641,87 @@ async def test_run_for_exit_code_debug_mode_timeout_on_first_call(tmp_path):
|
|
|
1640
1641
|
assert state.first_call is False
|
|
1641
1642
|
|
|
1642
1643
|
|
|
1644
|
+
async def test_run_for_exit_code_debug_mode_dynamic_timeout(tmp_path):
|
|
1645
|
+
"""Test dynamic timeout computation in debug mode on first call.
|
|
1646
|
+
|
|
1647
|
+
Exercises the dynamic timeout computation path in debug mode.
|
|
1648
|
+
"""
|
|
1649
|
+
script = tmp_path / "test.sh"
|
|
1650
|
+
script.write_text("#!/bin/bash\nexit 0")
|
|
1651
|
+
script.chmod(0o755)
|
|
1652
|
+
|
|
1653
|
+
target = tmp_path / "test.txt"
|
|
1654
|
+
target.write_text("hello")
|
|
1655
|
+
|
|
1656
|
+
state = ShrinkRayStateSingleFile(
|
|
1657
|
+
input_type=InputType.arg,
|
|
1658
|
+
in_place=False,
|
|
1659
|
+
test=[str(script)],
|
|
1660
|
+
filename=str(target),
|
|
1661
|
+
timeout=None, # Dynamic timeout
|
|
1662
|
+
base="test.txt",
|
|
1663
|
+
parallelism=1,
|
|
1664
|
+
initial=b"hello",
|
|
1665
|
+
formatter="none",
|
|
1666
|
+
trivial_is_error=True,
|
|
1667
|
+
seed=0,
|
|
1668
|
+
volume=Volume.quiet,
|
|
1669
|
+
clang_delta_executable=None,
|
|
1670
|
+
)
|
|
1671
|
+
|
|
1672
|
+
# First call in debug mode with None timeout should compute dynamic timeout
|
|
1673
|
+
assert state.timeout is None
|
|
1674
|
+
exit_code = await state.run_for_exit_code(b"hello", debug=True)
|
|
1675
|
+
assert exit_code == 0
|
|
1676
|
+
# After first call, timeout should be computed
|
|
1677
|
+
assert state.timeout is not None
|
|
1678
|
+
assert state.timeout > 0
|
|
1679
|
+
# first_call should be False after this
|
|
1680
|
+
assert state.first_call is False
|
|
1681
|
+
|
|
1682
|
+
|
|
1683
|
+
async def test_run_for_exit_code_dynamic_timeout_non_debug(tmp_path):
|
|
1684
|
+
"""Test dynamic timeout computation in non-debug mode on first call.
|
|
1685
|
+
|
|
1686
|
+
Exercises the dynamic timeout computation path in non-debug mode,
|
|
1687
|
+
which uses run_script_on_file instead of the debug path.
|
|
1688
|
+
"""
|
|
1689
|
+
script = tmp_path / "test.sh"
|
|
1690
|
+
script.write_text("#!/bin/bash\nexit 0")
|
|
1691
|
+
script.chmod(0o755)
|
|
1692
|
+
|
|
1693
|
+
target = tmp_path / "test.txt"
|
|
1694
|
+
target.write_text("hello")
|
|
1695
|
+
|
|
1696
|
+
state = ShrinkRayStateSingleFile(
|
|
1697
|
+
input_type=InputType.arg,
|
|
1698
|
+
in_place=False,
|
|
1699
|
+
test=[str(script)],
|
|
1700
|
+
filename=str(target),
|
|
1701
|
+
timeout=None, # Dynamic timeout
|
|
1702
|
+
base="test.txt",
|
|
1703
|
+
parallelism=1,
|
|
1704
|
+
initial=b"hello",
|
|
1705
|
+
formatter="none",
|
|
1706
|
+
trivial_is_error=True,
|
|
1707
|
+
seed=0,
|
|
1708
|
+
volume=Volume.quiet,
|
|
1709
|
+
clang_delta_executable=None,
|
|
1710
|
+
)
|
|
1711
|
+
|
|
1712
|
+
# First call in non-debug mode with None timeout should compute dynamic timeout
|
|
1713
|
+
assert state.timeout is None
|
|
1714
|
+
exit_code = await state.run_for_exit_code(b"hello", debug=False)
|
|
1715
|
+
assert exit_code == 0
|
|
1716
|
+
# After first call, timeout should be computed
|
|
1717
|
+
assert state.timeout is not None
|
|
1718
|
+
assert state.timeout > 0
|
|
1719
|
+
# Verify minimum timeout is respected
|
|
1720
|
+
assert state.timeout >= DYNAMIC_TIMEOUT_MIN
|
|
1721
|
+
# first_call should be False after this
|
|
1722
|
+
assert state.first_call is False
|
|
1723
|
+
|
|
1724
|
+
|
|
1643
1725
|
async def test_run_for_exit_code_debug_mode_captures_stdout(tmp_path):
|
|
1644
1726
|
"""Test that debug mode captures stdout output.
|
|
1645
1727
|
|
|
@@ -1,9 +1,33 @@
|
|
|
1
1
|
"""Snapshot tests for the textual TUI."""
|
|
2
2
|
|
|
3
|
+
import asyncio
|
|
4
|
+
from pathlib import Path
|
|
5
|
+
|
|
3
6
|
import pytest
|
|
4
7
|
|
|
5
|
-
from shrinkray.subprocess.protocol import PassStatsData, ProgressUpdate
|
|
6
|
-
from shrinkray.tui import ShrinkRayApp
|
|
8
|
+
from shrinkray.subprocess.protocol import PassStatsData, ProgressUpdate, Response
|
|
9
|
+
from shrinkray.tui import ShrinkRayApp, StatsDisplay
|
|
10
|
+
|
|
11
|
+
|
|
12
|
+
# Directory where snapshots are stored
|
|
13
|
+
SNAPSHOTS_DIR = Path(__file__).parent / "__snapshots__" / "test_tui_snapshots"
|
|
14
|
+
|
|
15
|
+
|
|
16
|
+
def pytest_sessionfinish(session, exitstatus):
|
|
17
|
+
"""Verify all snapshots contain expected content after tests complete."""
|
|
18
|
+
if not SNAPSHOTS_DIR.exists():
|
|
19
|
+
return
|
|
20
|
+
|
|
21
|
+
# Using vendored pytest_textual_snapshot with syrupy 5.0 compatibility
|
|
22
|
+
for snapshot_file in SNAPSHOTS_DIR.glob("*.svg"):
|
|
23
|
+
content = snapshot_file.read_text()
|
|
24
|
+
# Each snapshot should contain "Reducer" which appears in StatsDisplay
|
|
25
|
+
# Note: SVG uses   for non-breaking spaces, so we just check for "Reducer"
|
|
26
|
+
if "Reducer" not in content:
|
|
27
|
+
raise AssertionError(
|
|
28
|
+
f"Snapshot {snapshot_file.name} does not contain expected text 'Reducer'. "
|
|
29
|
+
"The snapshot may have been captured before the app fully rendered."
|
|
30
|
+
)
|
|
7
31
|
|
|
8
32
|
|
|
9
33
|
class FakeReductionClientForSnapshots:
|
|
@@ -12,7 +36,8 @@ class FakeReductionClientForSnapshots:
|
|
|
12
36
|
def __init__(self, updates: list[ProgressUpdate]):
|
|
13
37
|
self._updates = updates
|
|
14
38
|
self._update_index = 0
|
|
15
|
-
self.
|
|
39
|
+
self._cancelled = False
|
|
40
|
+
self._updates_consumed = False
|
|
16
41
|
|
|
17
42
|
async def start(self) -> None:
|
|
18
43
|
pass
|
|
@@ -22,7 +47,7 @@ class FakeReductionClientForSnapshots:
|
|
|
22
47
|
file_path: str,
|
|
23
48
|
test: list[str],
|
|
24
49
|
parallelism: int | None = None,
|
|
25
|
-
timeout: float =
|
|
50
|
+
timeout: float | None = None,
|
|
26
51
|
seed: int = 0,
|
|
27
52
|
input_type: str = "all",
|
|
28
53
|
in_place: bool = False,
|
|
@@ -31,34 +56,24 @@ class FakeReductionClientForSnapshots:
|
|
|
31
56
|
no_clang_delta: bool = False,
|
|
32
57
|
clang_delta: str = "",
|
|
33
58
|
trivial_is_error: bool = True,
|
|
34
|
-
):
|
|
35
|
-
from shrinkray.subprocess.protocol import Response
|
|
36
|
-
|
|
59
|
+
) -> Response:
|
|
37
60
|
return Response(id="start", result={"status": "started"})
|
|
38
61
|
|
|
39
|
-
async def cancel(self):
|
|
40
|
-
|
|
41
|
-
|
|
42
|
-
self._completed = True
|
|
62
|
+
async def cancel(self) -> Response:
|
|
63
|
+
self._cancelled = True
|
|
43
64
|
return Response(id="cancel", result={"status": "cancelled"})
|
|
44
65
|
|
|
45
|
-
async def disable_pass(self, pass_name: str):
|
|
46
|
-
from shrinkray.subprocess.protocol import Response
|
|
47
|
-
|
|
66
|
+
async def disable_pass(self, pass_name: str) -> Response:
|
|
48
67
|
return Response(
|
|
49
68
|
id="disable", result={"status": "disabled", "pass_name": pass_name}
|
|
50
69
|
)
|
|
51
70
|
|
|
52
|
-
async def enable_pass(self, pass_name: str):
|
|
53
|
-
from shrinkray.subprocess.protocol import Response
|
|
54
|
-
|
|
71
|
+
async def enable_pass(self, pass_name: str) -> Response:
|
|
55
72
|
return Response(
|
|
56
73
|
id="enable", result={"status": "enabled", "pass_name": pass_name}
|
|
57
74
|
)
|
|
58
75
|
|
|
59
|
-
async def skip_current_pass(self):
|
|
60
|
-
from shrinkray.subprocess.protocol import Response
|
|
61
|
-
|
|
76
|
+
async def skip_current_pass(self) -> Response:
|
|
62
77
|
return Response(id="skip", result={"status": "skipped"})
|
|
63
78
|
|
|
64
79
|
async def close(self) -> None:
|
|
@@ -66,14 +81,18 @@ class FakeReductionClientForSnapshots:
|
|
|
66
81
|
|
|
67
82
|
async def get_progress_updates(self):
|
|
68
83
|
for update in self._updates:
|
|
69
|
-
if self.
|
|
84
|
+
if self._cancelled:
|
|
70
85
|
break
|
|
71
86
|
yield update
|
|
72
|
-
self.
|
|
87
|
+
self._updates_consumed = True
|
|
88
|
+
# Keep the app running by waiting indefinitely (until cancelled)
|
|
89
|
+
while not self._cancelled:
|
|
90
|
+
await asyncio.sleep(1)
|
|
73
91
|
|
|
74
92
|
@property
|
|
75
93
|
def is_completed(self) -> bool:
|
|
76
|
-
|
|
94
|
+
# Only complete when cancelled, not when updates are consumed
|
|
95
|
+
return self._cancelled
|
|
77
96
|
|
|
78
97
|
@property
|
|
79
98
|
def error_message(self) -> str | None:
|
|
@@ -180,40 +199,63 @@ def large_file_update() -> ProgressUpdate:
|
|
|
180
199
|
# === TUI snapshot tests ===
|
|
181
200
|
|
|
182
201
|
|
|
202
|
+
async def wait_for_render(pilot):
|
|
203
|
+
"""Wait for the app to fully render with actual content.
|
|
204
|
+
|
|
205
|
+
Checks that the StatsDisplay has received and rendered the update
|
|
206
|
+
by verifying original_size is non-zero (all test fixtures have non-zero size).
|
|
207
|
+
"""
|
|
208
|
+
max_attempts = 50 # Generous limit to avoid flakiness
|
|
209
|
+
for _ in range(max_attempts):
|
|
210
|
+
await pilot.pause()
|
|
211
|
+
stats = pilot.app.query_one("#stats-display", StatsDisplay)
|
|
212
|
+
if stats.original_size > 0:
|
|
213
|
+
# Content has been rendered, give one more pause for layout
|
|
214
|
+
await pilot.pause()
|
|
215
|
+
return
|
|
216
|
+
|
|
217
|
+
# Get final state for error message
|
|
218
|
+
final_stats = pilot.app.query_one("#stats-display", StatsDisplay)
|
|
219
|
+
raise AssertionError(
|
|
220
|
+
"App did not render within expected time. "
|
|
221
|
+
f"StatsDisplay.original_size is still {final_stats.original_size}"
|
|
222
|
+
)
|
|
223
|
+
|
|
224
|
+
|
|
183
225
|
def test_initial_state(snap_compare, initial_state_update):
|
|
184
226
|
"""Snapshot test for initial app state."""
|
|
185
227
|
app = make_app_with_updates([initial_state_update])
|
|
186
|
-
assert snap_compare(app, terminal_size=(120, 40))
|
|
228
|
+
assert snap_compare(app, terminal_size=(120, 40), run_before=wait_for_render)
|
|
187
229
|
|
|
188
230
|
|
|
189
231
|
def test_mid_reduction(snap_compare, mid_reduction_update):
|
|
190
232
|
"""Snapshot test for mid-reduction state."""
|
|
191
233
|
app = make_app_with_updates([mid_reduction_update])
|
|
192
|
-
assert snap_compare(app, terminal_size=(120, 40))
|
|
234
|
+
assert snap_compare(app, terminal_size=(120, 40), run_before=wait_for_render)
|
|
193
235
|
|
|
194
236
|
|
|
195
237
|
def test_hex_mode(snap_compare, hex_mode_update):
|
|
196
238
|
"""Snapshot test for hex mode display."""
|
|
197
239
|
app = make_app_with_updates([hex_mode_update])
|
|
198
|
-
assert snap_compare(app, terminal_size=(120, 40))
|
|
240
|
+
assert snap_compare(app, terminal_size=(120, 40), run_before=wait_for_render)
|
|
199
241
|
|
|
200
242
|
|
|
201
243
|
def test_large_file(snap_compare, large_file_update):
|
|
202
244
|
"""Snapshot test for large file with potential diff."""
|
|
203
245
|
app = make_app_with_updates([large_file_update])
|
|
204
|
-
assert snap_compare(app, terminal_size=(120, 40))
|
|
246
|
+
assert snap_compare(app, terminal_size=(120, 40), run_before=wait_for_render)
|
|
205
247
|
|
|
206
248
|
|
|
207
249
|
def test_small_terminal(snap_compare, mid_reduction_update):
|
|
208
250
|
"""Snapshot test with smaller terminal size."""
|
|
209
251
|
app = make_app_with_updates([mid_reduction_update])
|
|
210
|
-
assert snap_compare(app, terminal_size=(80, 24))
|
|
252
|
+
assert snap_compare(app, terminal_size=(80, 24), run_before=wait_for_render)
|
|
211
253
|
|
|
212
254
|
|
|
213
255
|
def test_wide_terminal(snap_compare, mid_reduction_update):
|
|
214
256
|
"""Snapshot test with wider terminal."""
|
|
215
257
|
app = make_app_with_updates([mid_reduction_update])
|
|
216
|
-
assert snap_compare(app, terminal_size=(160, 50))
|
|
258
|
+
assert snap_compare(app, terminal_size=(160, 50), run_before=wait_for_render)
|
|
217
259
|
|
|
218
260
|
|
|
219
261
|
@pytest.fixture
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|
|
File without changes
|