shrinkray 25.12.26__tar.gz → 25.12.26.2__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/src/shrinkray.egg-info → shrinkray-25.12.26.2}/PKG-INFO +21 -17
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/README.md +10 -6
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/pyproject.toml +11 -11
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/problem.py +27 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/reducer.py +129 -3
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/subprocess/client.py +34 -1
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/subprocess/protocol.py +49 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/subprocess/worker.py +104 -11
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/tui.py +324 -4
- {shrinkray-25.12.26 → shrinkray-25.12.26.2/src/shrinkray.egg-info}/PKG-INFO +21 -17
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray.egg-info/requires.txt +10 -10
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_clang_delta.py +1 -1
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_reducer.py +359 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_subprocess_client.py +87 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_subprocess_protocol.py +88 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_subprocess_worker.py +311 -0
- shrinkray-25.12.26.2/tests/test_tui.py +3750 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_tui_snapshots.py +80 -1
- shrinkray-25.12.26/tests/test_tui.py +0 -1720
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/LICENSE +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/setup.cfg +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/__init__.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/__main__.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/cli.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/display.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/formatting.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/__init__.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/bytes.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/clangdelta.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/definitions.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/genericlanguages.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/json.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/patching.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/python.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/sat.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/passes/sequences.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/process.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/py.typed +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/state.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/subprocess/__init__.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/ui.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray/work.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray.egg-info/SOURCES.txt +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray.egg-info/dependency_links.txt +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray.egg-info/entry_points.txt +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/src/shrinkray.egg-info/top_level.txt +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_byte_reduction_passes.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_cli.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_definitions.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_dimacs_cnf.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_display.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_formatting.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_generic_language.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_generic_shrinking_properties.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_json_passes.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_main.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_misc_reduction_performance.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_patching.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_problem.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_process.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_python_reducers.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_reduction_passes.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_sat.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_state.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_subprocess_integration.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_ui.py +0 -0
- {shrinkray-25.12.26 → shrinkray-25.12.26.2}/tests/test_work.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrinkray
|
|
3
|
-
Version: 25.12.26
|
|
3
|
+
Version: 25.12.26.2
|
|
4
4
|
Summary: Shrink Ray
|
|
5
5
|
Author-email: "David R. MacIver" <david@drmaciver.com>
|
|
6
6
|
License: MIT
|
|
@@ -20,19 +20,19 @@ Requires-Dist: humanize>=4.9.0
|
|
|
20
20
|
Requires-Dist: libcst>=1.1.0
|
|
21
21
|
Requires-Dist: exceptiongroup>=1.2.0
|
|
22
22
|
Requires-Dist: binaryornot>=0.4.4
|
|
23
|
-
Requires-Dist: black
|
|
23
|
+
Requires-Dist: black>=24.1.0
|
|
24
24
|
Provides-Extra: dev
|
|
25
|
-
Requires-Dist: coverage>=7.
|
|
25
|
+
Requires-Dist: coverage>=7.4.0; extra == "dev"
|
|
26
26
|
Requires-Dist: hypothesis>=6.92.1; extra == "dev"
|
|
27
27
|
Requires-Dist: hypothesmith>=0.3.1; extra == "dev"
|
|
28
|
-
Requires-Dist: pytest; extra == "dev"
|
|
29
|
-
Requires-Dist: pytest-trio; extra == "dev"
|
|
30
|
-
Requires-Dist: pytest-asyncio; extra == "dev"
|
|
31
|
-
Requires-Dist: pytest-textual-snapshot; extra == "dev"
|
|
32
|
-
Requires-Dist: coverage[toml]; extra == "dev"
|
|
33
|
-
Requires-Dist: pygments; extra == "dev"
|
|
34
|
-
Requires-Dist: basedpyright; extra == "dev"
|
|
35
|
-
Requires-Dist: ruff; extra == "dev"
|
|
28
|
+
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
29
|
+
Requires-Dist: pytest-trio>=0.8.0; extra == "dev"
|
|
30
|
+
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
31
|
+
Requires-Dist: pytest-textual-snapshot>=1.0.0; extra == "dev"
|
|
32
|
+
Requires-Dist: coverage[toml]>=7.4.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pygments>=2.17.0; extra == "dev"
|
|
34
|
+
Requires-Dist: basedpyright>=1.1.0; extra == "dev"
|
|
35
|
+
Requires-Dist: ruff>=0.1.0; extra == "dev"
|
|
36
36
|
Requires-Dist: pexpect>=4.9.0; extra == "dev"
|
|
37
37
|
Requires-Dist: pyte>=0.8.2; extra == "dev"
|
|
38
38
|
Dynamic: license-file
|
|
@@ -89,15 +89,19 @@ Most test-case reducers only work well on a few formats. Shrink Ray is designed
|
|
|
89
89
|
|
|
90
90
|
It's designed to be highly parallel, and work with a very wide variety of formats, through a mix of good generic algorithms and format-specific reduction passes.
|
|
91
91
|
|
|
92
|
-
|
|
92
|
+
## Versioning and Releases
|
|
93
93
|
|
|
94
|
-
|
|
94
|
+
Shrink Ray uses calendar versioning (calver) in the format YY.M.D.N (e.g., 25.12.26.0 for the first release on December 26, 2025, 25.12.26.1 for the second, etc.).
|
|
95
|
+
|
|
96
|
+
New releases are published automatically when changes are pushed to main if there are any changes to the source code or pyproject.toml since the previous release.
|
|
97
|
+
|
|
98
|
+
Shrinkray makes no particularly strong backwards compatibility guarantees. I aim to keep its behaviour relatively stable between releases, but for example will not be particularly shy about dropping old versions of Python or adding new dependencies. The basic workflow of running a simple reduction will rarely, if ever, change, but the UI is likely to be continuously evolving for some time.
|
|
95
99
|
|
|
96
100
|
## Installation
|
|
97
101
|
|
|
98
|
-
Shrink Ray requires Python 3.
|
|
102
|
+
Shrink Ray requires Python 3.12 or later, and can be installed using pip or uv like any other python package.
|
|
99
103
|
|
|
100
|
-
|
|
104
|
+
You can install the latest release from PyPI or run directly from the main branch:
|
|
101
105
|
|
|
102
106
|
```
|
|
103
107
|
pipx install shrinkray
|
|
@@ -107,8 +111,8 @@ pipx install git+https://github.com/DRMacIver/shrinkray.git
|
|
|
107
111
|
|
|
108
112
|
(if you don't have or want [pipx](https://pypa.github.io/pipx/) you could also do this with pip or `uv pip` and it would work fine)
|
|
109
113
|
|
|
110
|
-
Shrink Ray requires Python 3.
|
|
111
|
-
on versions it's incompatible with. If you do not have Python 3.
|
|
114
|
+
Shrink Ray requires Python 3.12 or later and won't work on earlier versions. If everything is working correctly, it should refuse to install
|
|
115
|
+
on versions it's incompatible with. If you do not have Python 3.12 installed, I recommend [pyenv](https://github.com/pyenv/pyenv) for managing
|
|
112
116
|
Python installs.
|
|
113
117
|
|
|
114
118
|
If you want to use it from the git repo directly, you can do the following:
|
|
@@ -50,15 +50,19 @@ Most test-case reducers only work well on a few formats. Shrink Ray is designed
|
|
|
50
50
|
|
|
51
51
|
It's designed to be highly parallel, and work with a very wide variety of formats, through a mix of good generic algorithms and format-specific reduction passes.
|
|
52
52
|
|
|
53
|
-
|
|
53
|
+
## Versioning and Releases
|
|
54
54
|
|
|
55
|
-
|
|
55
|
+
Shrink Ray uses calendar versioning (calver) in the format YY.M.D.N (e.g., 25.12.26.0 for the first release on December 26, 2025, 25.12.26.1 for the second, etc.).
|
|
56
|
+
|
|
57
|
+
New releases are published automatically when changes are pushed to main if there are any changes to the source code or pyproject.toml since the previous release.
|
|
58
|
+
|
|
59
|
+
Shrinkray makes no particularly strong backwards compatibility guarantees. I aim to keep its behaviour relatively stable between releases, but for example will not be particularly shy about dropping old versions of Python or adding new dependencies. The basic workflow of running a simple reduction will rarely, if ever, change, but the UI is likely to be continuously evolving for some time.
|
|
56
60
|
|
|
57
61
|
## Installation
|
|
58
62
|
|
|
59
|
-
Shrink Ray requires Python 3.
|
|
63
|
+
Shrink Ray requires Python 3.12 or later, and can be installed using pip or uv like any other python package.
|
|
60
64
|
|
|
61
|
-
|
|
65
|
+
You can install the latest release from PyPI or run directly from the main branch:
|
|
62
66
|
|
|
63
67
|
```
|
|
64
68
|
pipx install shrinkray
|
|
@@ -68,8 +72,8 @@ pipx install git+https://github.com/DRMacIver/shrinkray.git
|
|
|
68
72
|
|
|
69
73
|
(if you don't have or want [pipx](https://pypa.github.io/pipx/) you could also do this with pip or `uv pip` and it would work fine)
|
|
70
74
|
|
|
71
|
-
Shrink Ray requires Python 3.
|
|
72
|
-
on versions it's incompatible with. If you do not have Python 3.
|
|
75
|
+
Shrink Ray requires Python 3.12 or later and won't work on earlier versions. If everything is working correctly, it should refuse to install
|
|
76
|
+
on versions it's incompatible with. If you do not have Python 3.12 installed, I recommend [pyenv](https://github.com/pyenv/pyenv) for managing
|
|
73
77
|
Python installs.
|
|
74
78
|
|
|
75
79
|
If you want to use it from the git repo directly, you can do the following:
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shrinkray"
|
|
3
|
-
version = "25.12.26"
|
|
3
|
+
version = "25.12.26.2"
|
|
4
4
|
description = "Shrink Ray"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "David R. MacIver", email = "david@drmaciver.com"}
|
|
@@ -20,7 +20,7 @@ dependencies = [
|
|
|
20
20
|
"libcst>=1.1.0",
|
|
21
21
|
"exceptiongroup>=1.2.0",
|
|
22
22
|
"binaryornot>=0.4.4",
|
|
23
|
-
"black",
|
|
23
|
+
"black>=24.1.0",
|
|
24
24
|
]
|
|
25
25
|
|
|
26
26
|
[project.urls]
|
|
@@ -35,17 +35,17 @@ shrinkray-worker = "shrinkray.__main__:worker_main"
|
|
|
35
35
|
|
|
36
36
|
[project.optional-dependencies]
|
|
37
37
|
dev = [
|
|
38
|
-
"coverage>=7.
|
|
38
|
+
"coverage>=7.4.0",
|
|
39
39
|
"hypothesis>=6.92.1",
|
|
40
40
|
"hypothesmith>=0.3.1",
|
|
41
|
-
"pytest",
|
|
42
|
-
"pytest-trio",
|
|
43
|
-
"pytest-asyncio",
|
|
44
|
-
"pytest-textual-snapshot",
|
|
45
|
-
"coverage[toml]",
|
|
46
|
-
"pygments",
|
|
47
|
-
"basedpyright",
|
|
48
|
-
"ruff",
|
|
41
|
+
"pytest>=8.0.0",
|
|
42
|
+
"pytest-trio>=0.8.0",
|
|
43
|
+
"pytest-asyncio>=0.21.0",
|
|
44
|
+
"pytest-textual-snapshot>=1.0.0",
|
|
45
|
+
"coverage[toml]>=7.4.0",
|
|
46
|
+
"pygments>=2.17.0",
|
|
47
|
+
"basedpyright>=1.1.0",
|
|
48
|
+
"ruff>=0.1.0",
|
|
49
49
|
"pexpect>=4.9.0",
|
|
50
50
|
"pyte>=0.8.2",
|
|
51
51
|
]
|
|
@@ -19,6 +19,7 @@ from datetime import timedelta
|
|
|
19
19
|
from typing import (
|
|
20
20
|
TYPE_CHECKING,
|
|
21
21
|
Any,
|
|
22
|
+
Protocol,
|
|
22
23
|
TypeVar,
|
|
23
24
|
cast,
|
|
24
25
|
)
|
|
@@ -38,6 +39,18 @@ S = TypeVar("S")
|
|
|
38
39
|
T = TypeVar("T")
|
|
39
40
|
|
|
40
41
|
|
|
42
|
+
class PassStatsProtocol(Protocol):
|
|
43
|
+
"""Protocol for pass statistics tracking.
|
|
44
|
+
|
|
45
|
+
This allows problem.py to track stats without importing from reducer.py,
|
|
46
|
+
avoiding circular dependencies.
|
|
47
|
+
"""
|
|
48
|
+
|
|
49
|
+
test_evaluations: int
|
|
50
|
+
successful_reductions: int
|
|
51
|
+
bytes_deleted: int
|
|
52
|
+
|
|
53
|
+
|
|
41
54
|
def shortlex[SizedT: Sized](value: SizedT) -> tuple[int, SizedT]:
|
|
42
55
|
"""Return a comparison key for shortlex ordering.
|
|
43
56
|
|
|
@@ -162,6 +175,8 @@ class ReductionProblem[T](ABC):
|
|
|
162
175
|
"""
|
|
163
176
|
|
|
164
177
|
work: WorkContext
|
|
178
|
+
# Track current pass stats for real-time updates (set by reducer)
|
|
179
|
+
current_pass_stats: PassStatsProtocol | None = None
|
|
165
180
|
|
|
166
181
|
def __attrs_post_init__(self) -> None:
|
|
167
182
|
# Cache of View objects for each Format, to avoid re-parsing
|
|
@@ -377,6 +392,11 @@ class BasicReductionProblem(ReductionProblem[T]):
|
|
|
377
392
|
self.__is_interesting_cache[cache_key] = result
|
|
378
393
|
self.stats.failed_reductions += 1
|
|
379
394
|
self.stats.calls += 1
|
|
395
|
+
|
|
396
|
+
# Update current pass stats if a pass is running
|
|
397
|
+
if self.current_pass_stats is not None:
|
|
398
|
+
self.current_pass_stats.test_evaluations += 1
|
|
399
|
+
|
|
380
400
|
if result:
|
|
381
401
|
self.stats.interesting_calls += 1
|
|
382
402
|
if self.sort_key(test_case) < self.sort_key(self.current_test_case):
|
|
@@ -384,6 +404,13 @@ class BasicReductionProblem(ReductionProblem[T]):
|
|
|
384
404
|
self.stats.failed_reductions -= 1
|
|
385
405
|
self.stats.reductions += 1
|
|
386
406
|
self.stats.time_of_last_reduction = time.time()
|
|
407
|
+
|
|
408
|
+
# Update current pass stats for reductions
|
|
409
|
+
if self.current_pass_stats is not None:
|
|
410
|
+
self.current_pass_stats.successful_reductions += 1
|
|
411
|
+
size_diff = self.size(self.current_test_case) - self.size(test_case)
|
|
412
|
+
self.current_pass_stats.bytes_deleted += size_diff
|
|
413
|
+
|
|
387
414
|
self.stats.current_test_case_size = self.size(test_case)
|
|
388
415
|
self.__current = test_case
|
|
389
416
|
for f in self.__on_reduce_callbacks:
|
|
@@ -56,6 +56,11 @@ from shrinkray.problem import ReductionProblem, ReductionStats, shortlex
|
|
|
56
56
|
class Reducer[T](ABC):
|
|
57
57
|
target: ReductionProblem[T]
|
|
58
58
|
|
|
59
|
+
# Optional pass statistics tracking (implemented by ShrinkRay)
|
|
60
|
+
pass_stats: "PassStatsTracker | None" = attrs.field(default=None, init=False)
|
|
61
|
+
# Optional current pass tracking (implemented by ShrinkRay)
|
|
62
|
+
current_reduction_pass: "ReductionPass[T] | None" = attrs.field(default=None, init=False)
|
|
63
|
+
|
|
59
64
|
@contextmanager
|
|
60
65
|
def backtrack(self, restart: T) -> Generator[None, None, None]:
|
|
61
66
|
current = self.target
|
|
@@ -72,6 +77,20 @@ class Reducer[T](ABC):
|
|
|
72
77
|
def status(self) -> str:
|
|
73
78
|
return ""
|
|
74
79
|
|
|
80
|
+
@property
|
|
81
|
+
def disabled_passes(self) -> set[str]:
|
|
82
|
+
"""Set of disabled pass names. Override in subclasses for pass control."""
|
|
83
|
+
return set()
|
|
84
|
+
|
|
85
|
+
def disable_pass(self, pass_name: str) -> None: # noqa: B027
|
|
86
|
+
"""Disable a pass by name. Override in subclasses for pass control."""
|
|
87
|
+
|
|
88
|
+
def enable_pass(self, pass_name: str) -> None: # noqa: B027
|
|
89
|
+
"""Enable a pass by name. Override in subclasses for pass control."""
|
|
90
|
+
|
|
91
|
+
def skip_current_pass(self) -> None: # noqa: B027
|
|
92
|
+
"""Skip the currently running pass. Override in subclasses for pass control."""
|
|
93
|
+
|
|
75
94
|
|
|
76
95
|
@define
|
|
77
96
|
class BasicReducer[T](Reducer[T]):
|
|
@@ -117,14 +136,88 @@ class RestartPass(Exception):
|
|
|
117
136
|
pass
|
|
118
137
|
|
|
119
138
|
|
|
139
|
+
@define
|
|
140
|
+
class PassStats:
|
|
141
|
+
"""Statistics for a single reduction pass."""
|
|
142
|
+
|
|
143
|
+
pass_name: str
|
|
144
|
+
bytes_deleted: int = 0
|
|
145
|
+
run_count: int = 0
|
|
146
|
+
test_evaluations: int = 0
|
|
147
|
+
successful_reductions: int = 0
|
|
148
|
+
|
|
149
|
+
@property
|
|
150
|
+
def success_rate(self) -> float:
|
|
151
|
+
"""Percentage of test evaluations that led to a reduction."""
|
|
152
|
+
if self.test_evaluations == 0:
|
|
153
|
+
return 0.0
|
|
154
|
+
return (self.successful_reductions / self.test_evaluations) * 100.0
|
|
155
|
+
|
|
156
|
+
|
|
157
|
+
@define
|
|
158
|
+
class PassStatsTracker:
|
|
159
|
+
"""Tracks statistics for all reduction passes.
|
|
160
|
+
|
|
161
|
+
Python 3.7+ dicts maintain insertion order, so stats are returned
|
|
162
|
+
in the order passes were first run.
|
|
163
|
+
"""
|
|
164
|
+
|
|
165
|
+
_stats: dict[str, PassStats] = attrs.Factory(dict)
|
|
166
|
+
|
|
167
|
+
def get_or_create(self, pass_name: str) -> PassStats:
|
|
168
|
+
if pass_name not in self._stats:
|
|
169
|
+
self._stats[pass_name] = PassStats(pass_name=pass_name)
|
|
170
|
+
return self._stats[pass_name]
|
|
171
|
+
|
|
172
|
+
def get_stats_in_order(self) -> list[PassStats]:
|
|
173
|
+
"""Get stats in the order passes were first run."""
|
|
174
|
+
return list(self._stats.values())
|
|
175
|
+
|
|
176
|
+
|
|
177
|
+
class SkipPass(Exception):
|
|
178
|
+
"""Raised to skip the current pass."""
|
|
179
|
+
|
|
180
|
+
pass
|
|
181
|
+
|
|
182
|
+
|
|
120
183
|
@define
|
|
121
184
|
class ShrinkRay(Reducer[bytes]):
|
|
122
185
|
clang_delta: ClangDelta | None = None
|
|
123
186
|
|
|
124
|
-
current_reduction_pass: ReductionPass[bytes] | None = None
|
|
125
187
|
current_pump: ReductionPump[bytes] | None = None
|
|
126
188
|
|
|
127
189
|
unlocked_ok_passes: bool = False
|
|
190
|
+
pass_stats: PassStatsTracker | None = attrs.Factory(PassStatsTracker)
|
|
191
|
+
|
|
192
|
+
# Pass control: disabled passes and skip functionality
|
|
193
|
+
disabled_passes: set[str] = attrs.Factory(set)
|
|
194
|
+
_skip_requested: bool = attrs.field(default=False, init=False)
|
|
195
|
+
_current_pass_scope: "trio.CancelScope | None" = attrs.field(default=None, init=False)
|
|
196
|
+
_passes_were_skipped: bool = attrs.field(default=False, init=False)
|
|
197
|
+
|
|
198
|
+
def disable_pass(self, pass_name: str) -> None:
|
|
199
|
+
"""Disable a pass by name. If it's currently running, skip it."""
|
|
200
|
+
self.disabled_passes.add(pass_name)
|
|
201
|
+
# If this pass is currently running, skip it
|
|
202
|
+
if (
|
|
203
|
+
self.current_reduction_pass is not None
|
|
204
|
+
and self.current_reduction_pass.__name__ == pass_name
|
|
205
|
+
):
|
|
206
|
+
self.skip_current_pass()
|
|
207
|
+
|
|
208
|
+
def enable_pass(self, pass_name: str) -> None:
|
|
209
|
+
"""Enable a previously disabled pass."""
|
|
210
|
+
self.disabled_passes.discard(pass_name)
|
|
211
|
+
|
|
212
|
+
def is_pass_disabled(self, pass_name: str) -> bool:
|
|
213
|
+
"""Check if a pass is disabled."""
|
|
214
|
+
return pass_name in self.disabled_passes
|
|
215
|
+
|
|
216
|
+
def skip_current_pass(self) -> None:
|
|
217
|
+
"""Request to skip the currently running pass."""
|
|
218
|
+
self._skip_requested = True
|
|
219
|
+
if self._current_pass_scope is not None:
|
|
220
|
+
self._current_pass_scope.cancel()
|
|
128
221
|
|
|
129
222
|
initial_cuts: list[ReductionPass[bytes]] = attrs.Factory(
|
|
130
223
|
lambda: [
|
|
@@ -222,12 +315,39 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
222
315
|
return f"Running reduction pump {self.current_pump.__name__}"
|
|
223
316
|
|
|
224
317
|
async def run_pass(self, rp: ReductionPass[bytes]) -> None:
|
|
318
|
+
pass_name = rp.__name__
|
|
319
|
+
|
|
320
|
+
# Skip if pass is disabled
|
|
321
|
+
if self.is_pass_disabled(pass_name):
|
|
322
|
+
return
|
|
323
|
+
|
|
225
324
|
try:
|
|
226
325
|
assert self.current_reduction_pass is None
|
|
227
326
|
self.current_reduction_pass = rp
|
|
228
|
-
|
|
327
|
+
self._skip_requested = False
|
|
328
|
+
|
|
329
|
+
# Get or create stats entry for this pass
|
|
330
|
+
assert self.pass_stats is not None # Always set by Factory
|
|
331
|
+
stats_entry = self.pass_stats.get_or_create(pass_name)
|
|
332
|
+
stats_entry.run_count += 1
|
|
333
|
+
|
|
334
|
+
# Set current pass stats on the problem for real-time updates
|
|
335
|
+
self.target.current_pass_stats = stats_entry
|
|
336
|
+
|
|
337
|
+
# Run the pass with a cancel scope that can be externally cancelled
|
|
338
|
+
with trio.CancelScope() as scope:
|
|
339
|
+
self._current_pass_scope = scope
|
|
340
|
+
await rp(self.target)
|
|
341
|
+
|
|
342
|
+
# If the pass was cancelled/skipped, mark that passes were skipped
|
|
343
|
+
if scope.cancelled_caught:
|
|
344
|
+
self._passes_were_skipped = True
|
|
345
|
+
|
|
229
346
|
finally:
|
|
230
347
|
self.current_reduction_pass = None
|
|
348
|
+
self.target.current_pass_stats = None
|
|
349
|
+
self._current_pass_scope = None
|
|
350
|
+
self._skip_requested = False
|
|
231
351
|
|
|
232
352
|
async def pump(self, rp: ReductionPump[bytes]) -> None:
|
|
233
353
|
try:
|
|
@@ -357,6 +477,9 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
357
477
|
await self.initial_cut()
|
|
358
478
|
|
|
359
479
|
while True:
|
|
480
|
+
# Reset skip tracking for this iteration
|
|
481
|
+
self._passes_were_skipped = False
|
|
482
|
+
|
|
360
483
|
prev = self.target.current_test_case
|
|
361
484
|
await self.run_some_passes()
|
|
362
485
|
if self.target.current_test_case != prev:
|
|
@@ -364,7 +487,10 @@ class ShrinkRay(Reducer[bytes]):
|
|
|
364
487
|
for pump in self.pumps:
|
|
365
488
|
await self.pump(pump)
|
|
366
489
|
if self.target.current_test_case == prev:
|
|
367
|
-
|
|
490
|
+
# Only terminate if no passes were skipped
|
|
491
|
+
# If passes were skipped, we need another full run to be sure
|
|
492
|
+
if not self._passes_were_skipped:
|
|
493
|
+
break
|
|
368
494
|
|
|
369
495
|
|
|
370
496
|
class UpdateKeys(Patches[dict[str, bytes], dict[str, bytes]]):
|
|
@@ -2,6 +2,7 @@
|
|
|
2
2
|
|
|
3
3
|
import asyncio
|
|
4
4
|
import sys
|
|
5
|
+
import traceback
|
|
5
6
|
import uuid
|
|
6
7
|
from collections.abc import AsyncIterator
|
|
7
8
|
from typing import Any
|
|
@@ -58,6 +59,7 @@ class SubprocessClient:
|
|
|
58
59
|
if line:
|
|
59
60
|
await self._handle_message(line.decode("utf-8"))
|
|
60
61
|
except Exception:
|
|
62
|
+
traceback.print_exc()
|
|
61
63
|
break
|
|
62
64
|
|
|
63
65
|
async def _handle_message(self, line: str) -> None:
|
|
@@ -65,6 +67,7 @@ class SubprocessClient:
|
|
|
65
67
|
try:
|
|
66
68
|
msg = deserialize(line)
|
|
67
69
|
except Exception:
|
|
70
|
+
traceback.print_exc()
|
|
68
71
|
return
|
|
69
72
|
|
|
70
73
|
if isinstance(msg, ProgressUpdate):
|
|
@@ -167,6 +170,36 @@ class SubprocessClient:
|
|
|
167
170
|
except Exception:
|
|
168
171
|
return Response(id="", result={"status": "cancelled"})
|
|
169
172
|
|
|
173
|
+
async def disable_pass(self, pass_name: str) -> Response:
|
|
174
|
+
"""Disable a reduction pass by name."""
|
|
175
|
+
if self._completed:
|
|
176
|
+
return Response(id="", result={"status": "already_completed"})
|
|
177
|
+
try:
|
|
178
|
+
return await self.send_command("disable_pass", {"pass_name": pass_name})
|
|
179
|
+
except Exception:
|
|
180
|
+
traceback.print_exc()
|
|
181
|
+
return Response(id="", error="Failed to disable pass")
|
|
182
|
+
|
|
183
|
+
async def enable_pass(self, pass_name: str) -> Response:
|
|
184
|
+
"""Enable a previously disabled reduction pass."""
|
|
185
|
+
if self._completed:
|
|
186
|
+
return Response(id="", result={"status": "already_completed"})
|
|
187
|
+
try:
|
|
188
|
+
return await self.send_command("enable_pass", {"pass_name": pass_name})
|
|
189
|
+
except Exception:
|
|
190
|
+
traceback.print_exc()
|
|
191
|
+
return Response(id="", error="Failed to enable pass")
|
|
192
|
+
|
|
193
|
+
async def skip_current_pass(self) -> Response:
|
|
194
|
+
"""Skip the currently running pass."""
|
|
195
|
+
if self._completed:
|
|
196
|
+
return Response(id="", result={"status": "already_completed"})
|
|
197
|
+
try:
|
|
198
|
+
return await self.send_command("skip_pass")
|
|
199
|
+
except Exception:
|
|
200
|
+
traceback.print_exc()
|
|
201
|
+
return Response(id="", error="Failed to skip pass")
|
|
202
|
+
|
|
170
203
|
async def get_progress_updates(self) -> AsyncIterator[ProgressUpdate]:
|
|
171
204
|
"""Yield progress updates as they arrive."""
|
|
172
205
|
while not self._completed:
|
|
@@ -200,7 +233,7 @@ class SubprocessClient:
|
|
|
200
233
|
try:
|
|
201
234
|
self._process.stdin.close()
|
|
202
235
|
except Exception:
|
|
203
|
-
|
|
236
|
+
traceback.print_exc()
|
|
204
237
|
# Only terminate if still running
|
|
205
238
|
if self._process.returncode is None:
|
|
206
239
|
try:
|
|
@@ -50,6 +50,24 @@ class ProgressUpdate:
|
|
|
50
50
|
content_preview: str = ""
|
|
51
51
|
# Whether content is hex mode
|
|
52
52
|
hex_mode: bool = False
|
|
53
|
+
# Pass statistics (only passes with test evaluations)
|
|
54
|
+
pass_stats: list["PassStatsData"] = field(default_factory=list)
|
|
55
|
+
# Currently running pass name (for highlighting)
|
|
56
|
+
current_pass_name: str = ""
|
|
57
|
+
# List of disabled pass names
|
|
58
|
+
disabled_passes: list[str] = field(default_factory=list)
|
|
59
|
+
|
|
60
|
+
|
|
61
|
+
@dataclass
|
|
62
|
+
class PassStatsData:
|
|
63
|
+
"""Statistics for a single pass (serializable)."""
|
|
64
|
+
|
|
65
|
+
pass_name: str
|
|
66
|
+
bytes_deleted: int
|
|
67
|
+
run_count: int
|
|
68
|
+
test_evaluations: int
|
|
69
|
+
successful_reductions: int
|
|
70
|
+
success_rate: float
|
|
53
71
|
|
|
54
72
|
|
|
55
73
|
def serialize(msg: Request | Response | ProgressUpdate) -> str:
|
|
@@ -84,6 +102,19 @@ def serialize(msg: Request | Response | ProgressUpdate) -> str:
|
|
|
84
102
|
"time_since_last_reduction": msg.time_since_last_reduction,
|
|
85
103
|
"content_preview": msg.content_preview,
|
|
86
104
|
"hex_mode": msg.hex_mode,
|
|
105
|
+
"pass_stats": [
|
|
106
|
+
{
|
|
107
|
+
"pass_name": ps.pass_name,
|
|
108
|
+
"bytes_deleted": ps.bytes_deleted,
|
|
109
|
+
"run_count": ps.run_count,
|
|
110
|
+
"test_evaluations": ps.test_evaluations,
|
|
111
|
+
"successful_reductions": ps.successful_reductions,
|
|
112
|
+
"success_rate": ps.success_rate,
|
|
113
|
+
}
|
|
114
|
+
for ps in msg.pass_stats
|
|
115
|
+
],
|
|
116
|
+
"current_pass_name": msg.current_pass_name,
|
|
117
|
+
"disabled_passes": msg.disabled_passes,
|
|
87
118
|
},
|
|
88
119
|
}
|
|
89
120
|
else:
|
|
@@ -98,6 +129,21 @@ def deserialize(line: str) -> Request | Response | ProgressUpdate:
|
|
|
98
129
|
# Check for progress update (has "type" field)
|
|
99
130
|
if data.get("type") == "progress":
|
|
100
131
|
d = data["data"]
|
|
132
|
+
|
|
133
|
+
# Parse pass stats
|
|
134
|
+
pass_stats_data = []
|
|
135
|
+
for ps_dict in d.get("pass_stats", []):
|
|
136
|
+
pass_stats_data.append(
|
|
137
|
+
PassStatsData(
|
|
138
|
+
pass_name=ps_dict["pass_name"],
|
|
139
|
+
bytes_deleted=ps_dict["bytes_deleted"],
|
|
140
|
+
run_count=ps_dict["run_count"],
|
|
141
|
+
test_evaluations=ps_dict["test_evaluations"],
|
|
142
|
+
successful_reductions=ps_dict["successful_reductions"],
|
|
143
|
+
success_rate=ps_dict["success_rate"],
|
|
144
|
+
)
|
|
145
|
+
)
|
|
146
|
+
|
|
101
147
|
return ProgressUpdate(
|
|
102
148
|
status=d["status"],
|
|
103
149
|
size=d["size"],
|
|
@@ -113,6 +159,9 @@ def deserialize(line: str) -> Request | Response | ProgressUpdate:
|
|
|
113
159
|
time_since_last_reduction=d.get("time_since_last_reduction", 0.0),
|
|
114
160
|
content_preview=d.get("content_preview", ""),
|
|
115
161
|
hex_mode=d.get("hex_mode", False),
|
|
162
|
+
pass_stats=pass_stats_data,
|
|
163
|
+
current_pass_name=d.get("current_pass_name", ""),
|
|
164
|
+
disabled_passes=d.get("disabled_passes", []),
|
|
116
165
|
)
|
|
117
166
|
|
|
118
167
|
# Check for response (has "result" or "error" field)
|