shrinkray 25.12.27.3__tar.gz → 25.12.29.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.27.3/src/shrinkray.egg-info → shrinkray-25.12.29.0}/PKG-INFO +10 -2
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/README.md +6 -1
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/pyproject.toml +5 -2
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/bytes.py +0 -125
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/clangdelta.py +0 -24
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/genericlanguages.py +0 -20
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/json.py +0 -8
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/patching.py +0 -63
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/sequences.py +0 -25
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/reducer.py +0 -50
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/state.py +43 -51
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/subprocess/__init__.py +0 -4
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/subprocess/protocol.py +10 -12
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/subprocess/worker.py +59 -21
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/tui.py +605 -32
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0/src/shrinkray.egg-info}/PKG-INFO +10 -2
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray.egg-info/SOURCES.txt +0 -2
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray.egg-info/requires.txt +3 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_byte_reduction_passes.py +1 -97
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_clang_delta.py +2 -47
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_generic_language.py +0 -31
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_generic_shrinking_properties.py +0 -15
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_json_passes.py +0 -43
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_patching.py +0 -140
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_python_reducers.py +0 -11
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_reducer.py +1 -13
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_reduction_passes.py +8 -169
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_state.py +129 -39
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_subprocess_protocol.py +0 -32
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_subprocess_worker.py +76 -24
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_tui.py +3836 -1743
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_validation.py +1 -0
- shrinkray-25.12.27.3/src/shrinkray/display.py +0 -75
- shrinkray-25.12.27.3/tests/test_display.py +0 -135
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/LICENSE +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/setup.cfg +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/__init__.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/__main__.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/cli.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/formatting.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/__init__.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/definitions.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/python.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/passes/sat.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/problem.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/process.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/py.typed +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/subprocess/client.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/ui.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/validation.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray/work.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray.egg-info/dependency_links.txt +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray.egg-info/entry_points.txt +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/src/shrinkray.egg-info/top_level.txt +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_cli.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_definitions.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_dimacs_cnf.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_formatting.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_main.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_misc_reduction_performance.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_natural_sort_orders.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_problem.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_process.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_sat.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_subprocess_client.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_subprocess_integration.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_tui_snapshots.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_ui.py +0 -0
- {shrinkray-25.12.27.3 → shrinkray-25.12.29.0}/tests/test_work.py +0 -0
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
Metadata-Version: 2.4
|
|
2
2
|
Name: shrinkray
|
|
3
|
-
Version: 25.12.
|
|
3
|
+
Version: 25.12.29.0
|
|
4
4
|
Summary: Shrink Ray
|
|
5
5
|
Author-email: "David R. MacIver" <david@drmaciver.com>
|
|
6
6
|
License: MIT
|
|
@@ -16,6 +16,7 @@ Requires-Dist: click>=8.0.1
|
|
|
16
16
|
Requires-Dist: chardet>=5.2.0
|
|
17
17
|
Requires-Dist: trio>=0.28.0
|
|
18
18
|
Requires-Dist: textual>=0.47.0
|
|
19
|
+
Requires-Dist: textual-plotext>=0.2.0
|
|
19
20
|
Requires-Dist: humanize>=4.9.0
|
|
20
21
|
Requires-Dist: libcst>=1.1.0
|
|
21
22
|
Requires-Dist: exceptiongroup>=1.2.0
|
|
@@ -28,6 +29,8 @@ Requires-Dist: hypothesmith>=0.3.1; extra == "dev"
|
|
|
28
29
|
Requires-Dist: pytest>=8.0.0; extra == "dev"
|
|
29
30
|
Requires-Dist: pytest-trio>=0.8.0; extra == "dev"
|
|
30
31
|
Requires-Dist: pytest-asyncio>=0.21.0; extra == "dev"
|
|
32
|
+
Requires-Dist: pytest-xdist>=3.5.0; extra == "dev"
|
|
33
|
+
Requires-Dist: pytest-cov>=4.1.0; extra == "dev"
|
|
31
34
|
Requires-Dist: syrupy>=5.0.0; extra == "dev"
|
|
32
35
|
Requires-Dist: jinja2>=3.0.0; extra == "dev"
|
|
33
36
|
Requires-Dist: coverage[toml]>=7.4.0; extra == "dev"
|
|
@@ -78,7 +81,12 @@ shrinkray (or any other test-case reducer) then systematically tries smaller and
|
|
|
78
81
|
|
|
79
82
|
While it runs, you will see the following user interface:
|
|
80
83
|
|
|
81
|
-
|
|
84
|
+
<video controls poster="gallery/enterprise-hello/hello.png">
|
|
85
|
+
<source src="https://drmaciver.github.io/shrinkray/assets/hello.mp4" type="video/mp4">
|
|
86
|
+
Your browser doesn't support video. <a href="gallery/enterprise-hello/hello.gif">View the GIF instead</a>.
|
|
87
|
+
</video>
|
|
88
|
+
|
|
89
|
+
(This is a toy example based on reducing a ridiculously bad version of hello world)
|
|
82
90
|
|
|
83
91
|
When it finishes you will be left with the reduced test case in `mytestcase.py`.
|
|
84
92
|
|
|
@@ -38,7 +38,12 @@ shrinkray (or any other test-case reducer) then systematically tries smaller and
|
|
|
38
38
|
|
|
39
39
|
While it runs, you will see the following user interface:
|
|
40
40
|
|
|
41
|
-
|
|
41
|
+
<video controls poster="gallery/enterprise-hello/hello.png">
|
|
42
|
+
<source src="https://drmaciver.github.io/shrinkray/assets/hello.mp4" type="video/mp4">
|
|
43
|
+
Your browser doesn't support video. <a href="gallery/enterprise-hello/hello.gif">View the GIF instead</a>.
|
|
44
|
+
</video>
|
|
45
|
+
|
|
46
|
+
(This is a toy example based on reducing a ridiculously bad version of hello world)
|
|
42
47
|
|
|
43
48
|
When it finishes you will be left with the reduced test case in `mytestcase.py`.
|
|
44
49
|
|
|
@@ -1,6 +1,6 @@
|
|
|
1
1
|
[project]
|
|
2
2
|
name = "shrinkray"
|
|
3
|
-
version = "25.12.
|
|
3
|
+
version = "25.12.29.0"
|
|
4
4
|
description = "Shrink Ray"
|
|
5
5
|
authors = [
|
|
6
6
|
{name = "David R. MacIver", email = "david@drmaciver.com"}
|
|
@@ -16,6 +16,7 @@ dependencies = [
|
|
|
16
16
|
"chardet>=5.2.0",
|
|
17
17
|
"trio>=0.28.0",
|
|
18
18
|
"textual>=0.47.0",
|
|
19
|
+
"textual-plotext>=0.2.0",
|
|
19
20
|
"humanize>=4.9.0",
|
|
20
21
|
"libcst>=1.1.0",
|
|
21
22
|
"exceptiongroup>=1.2.0",
|
|
@@ -41,6 +42,8 @@ dev = [
|
|
|
41
42
|
"pytest>=8.0.0",
|
|
42
43
|
"pytest-trio>=0.8.0",
|
|
43
44
|
"pytest-asyncio>=0.21.0",
|
|
45
|
+
"pytest-xdist>=3.5.0",
|
|
46
|
+
"pytest-cov>=4.1.0",
|
|
44
47
|
# pytest-textual-snapshot is vendored in tests/pytest_textual_snapshot.py
|
|
45
48
|
# to fix syrupy 5.0 compatibility. These are its dependencies:
|
|
46
49
|
"syrupy>=5.0.0",
|
|
@@ -67,7 +70,7 @@ core = "ctrace"
|
|
|
67
70
|
|
|
68
71
|
[tool.coverage.report]
|
|
69
72
|
show_missing = true
|
|
70
|
-
fail_under
|
|
73
|
+
# fail_under is enforced in justfile to allow split parallel/serial runs
|
|
71
74
|
|
|
72
75
|
[tool.ruff]
|
|
73
76
|
line-length = 88
|
|
@@ -28,26 +28,6 @@ from shrinkray.passes.patching import Cuts, Patches, apply_patches
|
|
|
28
28
|
from shrinkray.problem import Format, ReductionProblem
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
@define(frozen=True)
|
|
32
|
-
class Encoding(Format[bytes, str]):
|
|
33
|
-
"""Format that decodes/encodes bytes using a character encoding."""
|
|
34
|
-
|
|
35
|
-
encoding: str
|
|
36
|
-
|
|
37
|
-
def __repr__(self) -> str:
|
|
38
|
-
return f"Encoding({repr(self.encoding)})"
|
|
39
|
-
|
|
40
|
-
@property
|
|
41
|
-
def name(self) -> str:
|
|
42
|
-
return self.encoding
|
|
43
|
-
|
|
44
|
-
def parse(self, input: bytes) -> str:
|
|
45
|
-
return input.decode(self.encoding)
|
|
46
|
-
|
|
47
|
-
def dumps(self, input: str) -> bytes:
|
|
48
|
-
return input.encode(self.encoding)
|
|
49
|
-
|
|
50
|
-
|
|
51
31
|
@define(frozen=True)
|
|
52
32
|
class Split(Format[bytes, list[bytes]]):
|
|
53
33
|
"""Format that splits bytes by a delimiter.
|
|
@@ -165,9 +145,6 @@ def tokenize(text: bytes) -> list[bytes]:
|
|
|
165
145
|
return result
|
|
166
146
|
|
|
167
147
|
|
|
168
|
-
MAX_DELETE_INTERVAL = 8
|
|
169
|
-
|
|
170
|
-
|
|
171
148
|
async def lexeme_based_deletions(
|
|
172
149
|
problem: ReductionProblem[bytes], min_size: int = 8
|
|
173
150
|
) -> None:
|
|
@@ -601,108 +578,6 @@ async def lower_individual_bytes(problem: ReductionProblem[bytes]) -> None:
|
|
|
601
578
|
await apply_patches(problem, IndividualByteReplacement(), patches)
|
|
602
579
|
|
|
603
580
|
|
|
604
|
-
RegionReplacementPatch = list[tuple[int, int, int]]
|
|
605
|
-
|
|
606
|
-
|
|
607
|
-
class RegionReplacement(Patches[RegionReplacementPatch, bytes]):
|
|
608
|
-
@property
|
|
609
|
-
def empty(self) -> RegionReplacementPatch:
|
|
610
|
-
return []
|
|
611
|
-
|
|
612
|
-
def combine(self, *patches: RegionReplacementPatch) -> RegionReplacementPatch:
|
|
613
|
-
result: RegionReplacementPatch = []
|
|
614
|
-
for p in patches:
|
|
615
|
-
result.extend(p)
|
|
616
|
-
return result
|
|
617
|
-
|
|
618
|
-
def apply(self, patch: RegionReplacementPatch, target: bytes) -> bytes:
|
|
619
|
-
result = bytearray(target)
|
|
620
|
-
for i, j, d in patch:
|
|
621
|
-
if d < result[i]:
|
|
622
|
-
for k in range(i, j):
|
|
623
|
-
result[k] = d
|
|
624
|
-
return bytes(result)
|
|
625
|
-
|
|
626
|
-
def size(self, patch: RegionReplacementPatch) -> int:
|
|
627
|
-
return 0
|
|
628
|
-
|
|
629
|
-
|
|
630
|
-
async def short_replacements(problem: ReductionProblem[bytes]) -> None:
|
|
631
|
-
"""Replace short regions with uniform byte values.
|
|
632
|
-
|
|
633
|
-
Tries replacing 1-4 byte regions with uniform values like 0, 1,
|
|
634
|
-
space, newline, or period. Useful for simplifying small sequences.
|
|
635
|
-
"""
|
|
636
|
-
target = problem.current_test_case
|
|
637
|
-
patches = [
|
|
638
|
-
[(i, j, c)]
|
|
639
|
-
for c in [0, 1] + list(b"01 \t\n\r.")
|
|
640
|
-
for i in range(len(target))
|
|
641
|
-
if target[i] > c
|
|
642
|
-
for j in range(i + 1, min(i + 5, len(target) + 1))
|
|
643
|
-
]
|
|
644
|
-
|
|
645
|
-
await apply_patches(problem, RegionReplacement(), patches)
|
|
646
|
-
|
|
647
|
-
|
|
648
|
-
WHITESPACE = b" \t\r\n"
|
|
649
|
-
|
|
650
|
-
|
|
651
|
-
async def sort_whitespace(problem: ReductionProblem[bytes]) -> None:
|
|
652
|
-
"""NB: This is a stupid pass that we only really need for artificial
|
|
653
|
-
test cases, but it's helpful for allowing those artificial test cases
|
|
654
|
-
to expose other issues."""
|
|
655
|
-
|
|
656
|
-
whitespace_up_to = 0
|
|
657
|
-
while (
|
|
658
|
-
whitespace_up_to < len(problem.current_test_case)
|
|
659
|
-
and problem.current_test_case[whitespace_up_to] not in WHITESPACE
|
|
660
|
-
):
|
|
661
|
-
whitespace_up_to += 1
|
|
662
|
-
while (
|
|
663
|
-
whitespace_up_to < len(problem.current_test_case)
|
|
664
|
-
and problem.current_test_case[whitespace_up_to] in WHITESPACE
|
|
665
|
-
):
|
|
666
|
-
whitespace_up_to += 1
|
|
667
|
-
|
|
668
|
-
# If the initial whitespace ends with a newline we want to keep it doing
|
|
669
|
-
# that. This is mostly for Python purposes.
|
|
670
|
-
if (
|
|
671
|
-
whitespace_up_to > 0
|
|
672
|
-
and problem.current_test_case[whitespace_up_to - 1] == b"\n"[0]
|
|
673
|
-
):
|
|
674
|
-
whitespace_up_to -= 1
|
|
675
|
-
|
|
676
|
-
i = whitespace_up_to + 1
|
|
677
|
-
|
|
678
|
-
while i < len(problem.current_test_case):
|
|
679
|
-
if problem.current_test_case[i] not in WHITESPACE:
|
|
680
|
-
i += 1
|
|
681
|
-
continue
|
|
682
|
-
|
|
683
|
-
async def can_move_to_whitespace(k: int) -> bool:
|
|
684
|
-
if i + k > len(problem.current_test_case):
|
|
685
|
-
return False
|
|
686
|
-
|
|
687
|
-
base = problem.current_test_case
|
|
688
|
-
target = base[i : i + k]
|
|
689
|
-
|
|
690
|
-
if any(c not in WHITESPACE for c in target):
|
|
691
|
-
return False
|
|
692
|
-
|
|
693
|
-
prefix = base[:whitespace_up_to]
|
|
694
|
-
attempt = prefix + target + base[whitespace_up_to:i] + base[i + k :]
|
|
695
|
-
return await problem.is_interesting(attempt)
|
|
696
|
-
|
|
697
|
-
k = await problem.work.find_large_integer(can_move_to_whitespace)
|
|
698
|
-
whitespace_up_to += k
|
|
699
|
-
i += k + 1
|
|
700
|
-
test_case = problem.current_test_case
|
|
701
|
-
await problem.is_interesting(
|
|
702
|
-
bytes(sorted(test_case[:whitespace_up_to])) + test_case[whitespace_up_to:]
|
|
703
|
-
)
|
|
704
|
-
|
|
705
|
-
|
|
706
581
|
# These are some cheat substitutions that are sometimes helpful, but mostly
|
|
707
582
|
# for passing stupid tests.
|
|
708
583
|
STANDARD_SUBSTITUTIONS = [(b"\0\0", b"\1"), (b"\0\0", b"\xff")]
|
|
@@ -1,6 +1,5 @@
|
|
|
1
1
|
import os
|
|
2
2
|
import subprocess
|
|
3
|
-
from functools import lru_cache
|
|
4
3
|
from glob import glob
|
|
5
4
|
from shutil import which
|
|
6
5
|
from tempfile import NamedTemporaryFile
|
|
@@ -26,29 +25,6 @@ def find_clang_delta():
|
|
|
26
25
|
return clang_delta
|
|
27
26
|
|
|
28
27
|
|
|
29
|
-
@lru_cache(maxsize=1)
|
|
30
|
-
def clang_delta_works() -> bool:
|
|
31
|
-
"""Check if clang_delta can actually execute.
|
|
32
|
-
|
|
33
|
-
This verifies not just that the binary exists, but that it can run.
|
|
34
|
-
On some systems (e.g., Ubuntu 24.04), creduce is installed but
|
|
35
|
-
clang_delta fails at runtime due to shared library issues.
|
|
36
|
-
"""
|
|
37
|
-
clang_delta = find_clang_delta()
|
|
38
|
-
if not clang_delta:
|
|
39
|
-
return False
|
|
40
|
-
try:
|
|
41
|
-
# Run a simple test to verify clang_delta works
|
|
42
|
-
result = subprocess.run(
|
|
43
|
-
[clang_delta, "--help"],
|
|
44
|
-
capture_output=True,
|
|
45
|
-
timeout=5,
|
|
46
|
-
)
|
|
47
|
-
return result.returncode == 0
|
|
48
|
-
except (OSError, subprocess.TimeoutExpired):
|
|
49
|
-
return False
|
|
50
|
-
|
|
51
|
-
|
|
52
28
|
TRANSFORMATIONS: list[str] = [
|
|
53
29
|
"aggregate-to-scalar",
|
|
54
30
|
"binop-simplification",
|
|
@@ -9,7 +9,6 @@ from string import ascii_lowercase, ascii_uppercase
|
|
|
9
9
|
from typing import AnyStr
|
|
10
10
|
|
|
11
11
|
import trio
|
|
12
|
-
from attr import define
|
|
13
12
|
|
|
14
13
|
from shrinkray.passes.bytes import ByteReplacement, delete_intervals
|
|
15
14
|
from shrinkray.passes.definitions import ReductionPass
|
|
@@ -24,25 +23,6 @@ from shrinkray.problem import (
|
|
|
24
23
|
from shrinkray.work import NotFound
|
|
25
24
|
|
|
26
25
|
|
|
27
|
-
@define(frozen=True)
|
|
28
|
-
class Substring(Format[AnyStr, AnyStr]):
|
|
29
|
-
prefix: AnyStr
|
|
30
|
-
suffix: AnyStr
|
|
31
|
-
|
|
32
|
-
@property
|
|
33
|
-
def name(self) -> str:
|
|
34
|
-
return f"Substring({len(self.prefix)}, {len(self.suffix)})"
|
|
35
|
-
|
|
36
|
-
def parse(self, input: AnyStr) -> AnyStr:
|
|
37
|
-
if input.startswith(self.prefix) and input.endswith(self.suffix):
|
|
38
|
-
return input[len(self.prefix) : len(input) - len(self.suffix)]
|
|
39
|
-
else:
|
|
40
|
-
raise ParseError()
|
|
41
|
-
|
|
42
|
-
def dumps(self, input: AnyStr) -> AnyStr:
|
|
43
|
-
return self.prefix + input + self.suffix
|
|
44
|
-
|
|
45
|
-
|
|
46
26
|
class RegionReplacingPatches(Patches[dict[int, AnyStr], AnyStr]):
|
|
47
27
|
def __init__(self, regions: list[tuple[int, int]]):
|
|
48
28
|
assert regions
|
|
@@ -9,14 +9,6 @@ from shrinkray.passes.patching import Patches, apply_patches
|
|
|
9
9
|
from shrinkray.problem import Format, ParseError, ReductionProblem
|
|
10
10
|
|
|
11
11
|
|
|
12
|
-
def is_json(s: bytes) -> bool:
|
|
13
|
-
try:
|
|
14
|
-
json.loads(s)
|
|
15
|
-
return True
|
|
16
|
-
except ValueError:
|
|
17
|
-
return False
|
|
18
|
-
|
|
19
|
-
|
|
20
12
|
@define(frozen=True)
|
|
21
13
|
class _JSON(Format[bytes, Any]):
|
|
22
14
|
def __repr__(self) -> str:
|
|
@@ -1,7 +1,5 @@
|
|
|
1
1
|
from abc import ABC, abstractmethod
|
|
2
2
|
from collections.abc import Callable, Iterable, Sequence
|
|
3
|
-
from enum import Enum
|
|
4
|
-
from random import Random
|
|
5
3
|
from typing import Any, TypeVar, cast
|
|
6
4
|
|
|
7
5
|
import trio
|
|
@@ -52,27 +50,6 @@ class SetPatches[T, TargetType](Patches[frozenset[T], TargetType]):
|
|
|
52
50
|
return len(patch)
|
|
53
51
|
|
|
54
52
|
|
|
55
|
-
class ListPatches[T, TargetType](Patches[list[T], TargetType]):
|
|
56
|
-
def __init__(self, apply: Callable[[list[T], TargetType], TargetType]):
|
|
57
|
-
self.__apply = apply
|
|
58
|
-
|
|
59
|
-
@property
|
|
60
|
-
def empty(self):
|
|
61
|
-
return []
|
|
62
|
-
|
|
63
|
-
def combine(self, *patches: list[T]) -> list[T]:
|
|
64
|
-
result = []
|
|
65
|
-
for p in patches:
|
|
66
|
-
result.extend(p)
|
|
67
|
-
return result
|
|
68
|
-
|
|
69
|
-
def apply(self, patch: list[T], target: TargetType) -> TargetType:
|
|
70
|
-
return self.__apply(patch, target)
|
|
71
|
-
|
|
72
|
-
def size(self, patch: list[T]) -> int:
|
|
73
|
-
return len(patch)
|
|
74
|
-
|
|
75
|
-
|
|
76
53
|
class PatchApplier[PatchType, TargetType]:
|
|
77
54
|
def __init__(
|
|
78
55
|
self,
|
|
@@ -174,15 +151,6 @@ class PatchApplier[PatchType, TargetType]:
|
|
|
174
151
|
return await receive_merge_result.receive()
|
|
175
152
|
|
|
176
153
|
|
|
177
|
-
class Direction(Enum):
|
|
178
|
-
LEFT = 0
|
|
179
|
-
RIGHT = 1
|
|
180
|
-
|
|
181
|
-
|
|
182
|
-
class Completed(Exception):
|
|
183
|
-
pass
|
|
184
|
-
|
|
185
|
-
|
|
186
154
|
async def apply_patches[PatchType, TargetType](
|
|
187
155
|
problem: ReductionProblem[TargetType],
|
|
188
156
|
patch_info: Patches[PatchType, TargetType],
|
|
@@ -220,37 +188,6 @@ async def apply_patches[PatchType, TargetType](
|
|
|
220
188
|
await applier.try_apply_patch(patch)
|
|
221
189
|
|
|
222
190
|
|
|
223
|
-
class LazyMutableRange:
|
|
224
|
-
def __init__(self, n: int):
|
|
225
|
-
self.__size = n
|
|
226
|
-
self.__mask: dict[int, int] = {}
|
|
227
|
-
|
|
228
|
-
def __getitem__(self, i: int) -> int:
|
|
229
|
-
return self.__mask.get(i, i)
|
|
230
|
-
|
|
231
|
-
def __setitem__(self, i: int, v: int) -> None:
|
|
232
|
-
self.__mask[i] = v
|
|
233
|
-
|
|
234
|
-
def __len__(self) -> int:
|
|
235
|
-
return self.__size
|
|
236
|
-
|
|
237
|
-
def pop(self) -> int:
|
|
238
|
-
i = len(self) - 1
|
|
239
|
-
result = self[i]
|
|
240
|
-
self.__size = i
|
|
241
|
-
self.__mask.pop(i, None)
|
|
242
|
-
return result
|
|
243
|
-
|
|
244
|
-
|
|
245
|
-
def lazy_shuffle[T](seq: Sequence[T], rnd: Random) -> Iterable[T]:
|
|
246
|
-
indices = LazyMutableRange(len(seq))
|
|
247
|
-
while indices:
|
|
248
|
-
j = len(indices) - 1
|
|
249
|
-
i = rnd.randrange(0, len(indices))
|
|
250
|
-
indices[i], indices[j] = indices[j], indices[i]
|
|
251
|
-
yield seq[indices.pop()]
|
|
252
|
-
|
|
253
|
-
|
|
254
191
|
CutPatch = list[tuple[int, int]]
|
|
255
192
|
|
|
256
193
|
|
|
@@ -18,31 +18,6 @@ async def delete_elements[Seq: Sequence[Any]](problem: ReductionProblem[Seq]) ->
|
|
|
18
18
|
)
|
|
19
19
|
|
|
20
20
|
|
|
21
|
-
def merged_intervals(intervals: list[tuple[int, int]]) -> list[tuple[int, int]]:
|
|
22
|
-
normalized: list[list[int]] = []
|
|
23
|
-
for start, end in sorted(map(tuple, intervals)):
|
|
24
|
-
if normalized and normalized[-1][-1] >= start:
|
|
25
|
-
normalized[-1][-1] = max(normalized[-1][-1], end)
|
|
26
|
-
else:
|
|
27
|
-
normalized.append([start, end])
|
|
28
|
-
return list(map(tuple, normalized)) # type: ignore
|
|
29
|
-
|
|
30
|
-
|
|
31
|
-
def with_deletions[Seq: Sequence[Any]](
|
|
32
|
-
target: Seq, deletions: list[tuple[int, int]]
|
|
33
|
-
) -> Seq:
|
|
34
|
-
result: list[Any] = []
|
|
35
|
-
prev = 0
|
|
36
|
-
total_deleted = 0
|
|
37
|
-
for start, end in deletions:
|
|
38
|
-
total_deleted += end - start
|
|
39
|
-
result.extend(target[prev:start])
|
|
40
|
-
prev = end
|
|
41
|
-
result.extend(target[prev:])
|
|
42
|
-
assert len(result) + total_deleted == len(target)
|
|
43
|
-
return type(target)(result) # type: ignore
|
|
44
|
-
|
|
45
|
-
|
|
46
21
|
def block_deletion[Seq: Sequence[Any]](
|
|
47
22
|
min_block: int, max_block: int
|
|
48
23
|
) -> ReductionPass[Seq]:
|
|
@@ -99,50 +99,6 @@ class Reducer[T](ABC):
|
|
|
99
99
|
"""Skip the currently running pass. Override in subclasses for pass control."""
|
|
100
100
|
|
|
101
101
|
|
|
102
|
-
@define
|
|
103
|
-
class BasicReducer[T](Reducer[T]):
|
|
104
|
-
reduction_passes: Iterable[ReductionPass[T]]
|
|
105
|
-
pumps: Iterable[ReductionPump[T]] = ()
|
|
106
|
-
_status: str = "Starting up"
|
|
107
|
-
|
|
108
|
-
def __attrs_post_init__(self) -> None:
|
|
109
|
-
self.reduction_passes = list(self.reduction_passes)
|
|
110
|
-
|
|
111
|
-
@property
|
|
112
|
-
def status(self) -> str:
|
|
113
|
-
return self._status
|
|
114
|
-
|
|
115
|
-
@status.setter
|
|
116
|
-
def status(self, value: str) -> None:
|
|
117
|
-
self._status = value
|
|
118
|
-
|
|
119
|
-
async def run_pass(self, rp: ReductionPass[T]) -> None:
|
|
120
|
-
await rp(self.target)
|
|
121
|
-
|
|
122
|
-
async def run(self) -> None:
|
|
123
|
-
await self.target.setup()
|
|
124
|
-
|
|
125
|
-
while True:
|
|
126
|
-
prev = self.target.current_test_case
|
|
127
|
-
for rp in self.reduction_passes:
|
|
128
|
-
self.status = f"Running reduction pass {rp.__name__}"
|
|
129
|
-
await self.run_pass(rp)
|
|
130
|
-
for pump in self.pumps:
|
|
131
|
-
self.status = f"Pumping with {pump.__name__}"
|
|
132
|
-
pumped = await pump(self.target)
|
|
133
|
-
if pumped != self.target.current_test_case:
|
|
134
|
-
with self.backtrack(pumped):
|
|
135
|
-
for rp in self.reduction_passes:
|
|
136
|
-
self.status = f"Running reduction pass {rp.__name__} under pump {pump.__name__}"
|
|
137
|
-
await self.run_pass(rp)
|
|
138
|
-
if prev == self.target.current_test_case:
|
|
139
|
-
return
|
|
140
|
-
|
|
141
|
-
|
|
142
|
-
class RestartPass(Exception):
|
|
143
|
-
pass
|
|
144
|
-
|
|
145
|
-
|
|
146
102
|
@define
|
|
147
103
|
class PassStats:
|
|
148
104
|
"""Statistics for a single reduction pass."""
|
|
@@ -181,12 +137,6 @@ class PassStatsTracker:
|
|
|
181
137
|
return list(self._stats.values())
|
|
182
138
|
|
|
183
139
|
|
|
184
|
-
class SkipPass(Exception):
|
|
185
|
-
"""Raised to skip the current pass."""
|
|
186
|
-
|
|
187
|
-
pass
|
|
188
|
-
|
|
189
|
-
|
|
190
140
|
@define
|
|
191
141
|
class ShrinkRay(Reducer[bytes]):
|
|
192
142
|
clang_delta: ClangDelta | None = None
|
|
@@ -11,7 +11,7 @@ from abc import ABC, abstractmethod
|
|
|
11
11
|
from collections import deque
|
|
12
12
|
from datetime import timedelta
|
|
13
13
|
from tempfile import TemporaryDirectory
|
|
14
|
-
from typing import Any
|
|
14
|
+
from typing import Any
|
|
15
15
|
|
|
16
16
|
import humanize
|
|
17
17
|
import trio
|
|
@@ -28,9 +28,6 @@ from shrinkray.reducer import DirectoryShrinkRay, Reducer, ShrinkRay
|
|
|
28
28
|
from shrinkray.work import Volume, WorkContext
|
|
29
29
|
|
|
30
30
|
|
|
31
|
-
T = TypeVar("T")
|
|
32
|
-
|
|
33
|
-
|
|
34
31
|
class TimeoutExceededOnInitial(InvalidInitialExample):
|
|
35
32
|
def __init__(self, runtime: float, timeout: float) -> None:
|
|
36
33
|
self.runtime = runtime
|
|
@@ -60,7 +57,7 @@ def compute_dynamic_timeout(runtime: float) -> float:
|
|
|
60
57
|
|
|
61
58
|
|
|
62
59
|
@define
|
|
63
|
-
class
|
|
60
|
+
class OutputCaptureManager:
|
|
64
61
|
"""Manages temporary files for test output capture.
|
|
65
62
|
|
|
66
63
|
Allocates unique files for each test's stdout/stderr output,
|
|
@@ -77,7 +74,8 @@ class TestOutputManager:
|
|
|
77
74
|
|
|
78
75
|
_sequence: int = 0
|
|
79
76
|
_active_outputs: dict[int, str] = {}
|
|
80
|
-
|
|
77
|
+
# Completed outputs: (test_id, file_path, completion_time, return_code)
|
|
78
|
+
_completed_outputs: deque[tuple[int, str, float, int]] = deque()
|
|
81
79
|
|
|
82
80
|
def __attrs_post_init__(self) -> None:
|
|
83
81
|
# Initialize mutable defaults
|
|
@@ -92,11 +90,13 @@ class TestOutputManager:
|
|
|
92
90
|
self._active_outputs[test_id] = file_path
|
|
93
91
|
return test_id, file_path
|
|
94
92
|
|
|
95
|
-
def mark_completed(self, test_id: int) -> None:
|
|
93
|
+
def mark_completed(self, test_id: int, return_code: int = 0) -> None:
|
|
96
94
|
"""Mark a test as completed and move to completed queue."""
|
|
97
95
|
if test_id in self._active_outputs:
|
|
98
96
|
file_path = self._active_outputs.pop(test_id)
|
|
99
|
-
self._completed_outputs.append(
|
|
97
|
+
self._completed_outputs.append(
|
|
98
|
+
(test_id, file_path, time.time(), return_code)
|
|
99
|
+
)
|
|
100
100
|
self._cleanup_old_files()
|
|
101
101
|
|
|
102
102
|
def _cleanup_old_files(self) -> None:
|
|
@@ -107,65 +107,55 @@ class TestOutputManager:
|
|
|
107
107
|
self._completed_outputs
|
|
108
108
|
and now - self._completed_outputs[0][2] > self.max_age_seconds
|
|
109
109
|
):
|
|
110
|
-
_, file_path, _ = self._completed_outputs.popleft()
|
|
110
|
+
_, file_path, _, _ = self._completed_outputs.popleft()
|
|
111
111
|
self._safe_delete(file_path)
|
|
112
112
|
# Remove excess files beyond max_files
|
|
113
113
|
while len(self._completed_outputs) > self.max_files:
|
|
114
|
-
_, file_path, _ = self._completed_outputs.popleft()
|
|
114
|
+
_, file_path, _, _ = self._completed_outputs.popleft()
|
|
115
115
|
self._safe_delete(file_path)
|
|
116
116
|
|
|
117
|
-
|
|
118
|
-
|
|
119
|
-
|
|
120
|
-
|
|
121
|
-
|
|
122
|
-
|
|
123
|
-
|
|
124
|
-
if not self._completed_outputs:
|
|
125
|
-
return None
|
|
126
|
-
test_id, file_path, completion_time = self._completed_outputs[-1]
|
|
127
|
-
elapsed = time.time() - completion_time
|
|
128
|
-
|
|
129
|
-
# Show completed test during the full display window
|
|
130
|
-
if elapsed < self.min_display_seconds + self.grace_period_seconds:
|
|
131
|
-
return test_id, file_path
|
|
117
|
+
@staticmethod
|
|
118
|
+
def _file_has_content(path: str) -> bool:
|
|
119
|
+
"""Check if a file exists and has non-zero size."""
|
|
120
|
+
try:
|
|
121
|
+
return os.path.getsize(path) > 0
|
|
122
|
+
except OSError:
|
|
123
|
+
return False
|
|
132
124
|
|
|
133
|
-
|
|
125
|
+
def get_current_output(self) -> tuple[str | None, int | None, int | None]:
|
|
126
|
+
"""Get the current output to display.
|
|
134
127
|
|
|
135
|
-
|
|
136
|
-
|
|
128
|
+
Returns (file_path, test_id, return_code) where:
|
|
129
|
+
- file_path: path to the output file to display
|
|
130
|
+
- test_id: the test ID (for display in header)
|
|
131
|
+
- return_code: the return code (None if test is still running)
|
|
137
132
|
|
|
138
|
-
Active tests
|
|
139
|
-
recently completed test output for min_display_seconds,
|
|
140
|
-
an additional grace_period if no new test has started.
|
|
133
|
+
Active tests take priority only if they have produced output.
|
|
134
|
+
Otherwise, shows recently completed test output for min_display_seconds,
|
|
135
|
+
plus an additional grace_period if no new test has started.
|
|
141
136
|
"""
|
|
142
|
-
# Active tests
|
|
137
|
+
# Active tests take priority only if they have content
|
|
143
138
|
if self._active_outputs:
|
|
144
139
|
max_id = max(self._active_outputs.keys())
|
|
145
|
-
|
|
146
|
-
|
|
147
|
-
|
|
148
|
-
|
|
149
|
-
|
|
150
|
-
|
|
140
|
+
active_path = self._active_outputs[max_id]
|
|
141
|
+
if self._file_has_content(active_path):
|
|
142
|
+
# Active test with output - no return code yet
|
|
143
|
+
return active_path, max_id, None
|
|
144
|
+
# Active test has no output yet - fall through to show previous output
|
|
145
|
+
|
|
146
|
+
# Check for recently completed test that should stay visible,
|
|
147
|
+
# or fall back to most recent completed (even if past display window)
|
|
151
148
|
if self._completed_outputs:
|
|
152
|
-
|
|
153
|
-
|
|
154
|
-
|
|
155
|
-
def get_active_test_id(self) -> int | None:
|
|
156
|
-
"""Get the currently running test ID, if any.
|
|
149
|
+
test_id, file_path, _, return_code = self._completed_outputs[-1]
|
|
150
|
+
return file_path, test_id, return_code
|
|
157
151
|
|
|
158
|
-
|
|
159
|
-
"""
|
|
160
|
-
if self._active_outputs:
|
|
161
|
-
return max(self._active_outputs.keys())
|
|
162
|
-
return None
|
|
152
|
+
return None, None, None
|
|
163
153
|
|
|
164
154
|
def cleanup_all(self) -> None:
|
|
165
155
|
"""Clean up all output files (called on shutdown)."""
|
|
166
156
|
for file_path in self._active_outputs.values():
|
|
167
157
|
self._safe_delete(file_path)
|
|
168
|
-
for _, file_path, _ in self._completed_outputs:
|
|
158
|
+
for _, file_path, _, _ in self._completed_outputs:
|
|
169
159
|
self._safe_delete(file_path)
|
|
170
160
|
self._active_outputs.clear()
|
|
171
161
|
self._completed_outputs.clear()
|
|
@@ -212,7 +202,7 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
212
202
|
_last_debug_output: str = ""
|
|
213
203
|
|
|
214
204
|
# Optional output manager for capturing test output (TUI mode)
|
|
215
|
-
output_manager:
|
|
205
|
+
output_manager: OutputCaptureManager | None = None
|
|
216
206
|
|
|
217
207
|
def __attrs_post_init__(self):
|
|
218
208
|
self.is_interesting_limiter = trio.CapacityLimiter(max(self.parallelism, 1))
|
|
@@ -301,6 +291,7 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
301
291
|
# Determine output handling
|
|
302
292
|
test_id: int | None = None
|
|
303
293
|
output_file_handle = None
|
|
294
|
+
exit_code: int | None = None # Track for output manager
|
|
304
295
|
|
|
305
296
|
if self.output_manager is not None:
|
|
306
297
|
# Capture output to a file for TUI display
|
|
@@ -369,6 +360,7 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
369
360
|
|
|
370
361
|
result: int | None = sp.returncode
|
|
371
362
|
assert result is not None
|
|
363
|
+
exit_code = result
|
|
372
364
|
|
|
373
365
|
return result
|
|
374
366
|
finally:
|
|
@@ -376,7 +368,7 @@ class ShrinkRayState[TestCase](ABC):
|
|
|
376
368
|
if output_file_handle is not None:
|
|
377
369
|
output_file_handle.close()
|
|
378
370
|
if test_id is not None and self.output_manager is not None:
|
|
379
|
-
self.output_manager.mark_completed(test_id)
|
|
371
|
+
self.output_manager.mark_completed(test_id, exit_code or 0)
|
|
380
372
|
|
|
381
373
|
async def run_for_exit_code(self, test_case: TestCase, debug: bool = False) -> int:
|
|
382
374
|
# Lazy import
|