shrinkray 0.0.0__py3-none-any.whl
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- shrinkray/__init__.py +1 -0
- shrinkray/__main__.py +1205 -0
- shrinkray/learning.py +221 -0
- shrinkray/passes/__init__.py +0 -0
- shrinkray/passes/bytes.py +547 -0
- shrinkray/passes/clangdelta.py +230 -0
- shrinkray/passes/definitions.py +52 -0
- shrinkray/passes/genericlanguages.py +277 -0
- shrinkray/passes/json.py +91 -0
- shrinkray/passes/patching.py +280 -0
- shrinkray/passes/python.py +176 -0
- shrinkray/passes/sat.py +176 -0
- shrinkray/passes/sequences.py +69 -0
- shrinkray/problem.py +318 -0
- shrinkray/py.typed +0 -0
- shrinkray/reducer.py +430 -0
- shrinkray/work.py +217 -0
- shrinkray-0.0.0.dist-info/LICENSE +21 -0
- shrinkray-0.0.0.dist-info/METADATA +170 -0
- shrinkray-0.0.0.dist-info/RECORD +22 -0
- shrinkray-0.0.0.dist-info/WHEEL +4 -0
- shrinkray-0.0.0.dist-info/entry_points.txt +3 -0
shrinkray/problem.py
ADDED
|
@@ -0,0 +1,318 @@
|
|
|
1
|
+
import hashlib
|
|
2
|
+
import time
|
|
3
|
+
from abc import ABC, abstractmethod, abstractproperty
|
|
4
|
+
from datetime import timedelta
|
|
5
|
+
from typing import Any, Awaitable, Callable, Generic, Optional, TypeVar, cast
|
|
6
|
+
|
|
7
|
+
import attrs
|
|
8
|
+
import trio
|
|
9
|
+
from attrs import define
|
|
10
|
+
from humanize import naturalsize, precisedelta
|
|
11
|
+
|
|
12
|
+
from shrinkray.work import WorkContext
|
|
13
|
+
|
|
14
|
+
S = TypeVar("S")
|
|
15
|
+
T = TypeVar("T")
|
|
16
|
+
|
|
17
|
+
|
|
18
|
+
def shortlex(value: Any) -> Any:
|
|
19
|
+
return (len(value), value)
|
|
20
|
+
|
|
21
|
+
|
|
22
|
+
def default_sort_key(value: Any):
|
|
23
|
+
if isinstance(value, (str, bytes)):
|
|
24
|
+
return shortlex(value)
|
|
25
|
+
else:
|
|
26
|
+
return shortlex(repr(value))
|
|
27
|
+
|
|
28
|
+
|
|
29
|
+
def default_display(value: Any) -> str:
|
|
30
|
+
r = repr(value)
|
|
31
|
+
if len(r) < 50:
|
|
32
|
+
return f"{r} (size {len(value)})"
|
|
33
|
+
return f"value of size {len(value)}"
|
|
34
|
+
|
|
35
|
+
|
|
36
|
+
def default_size(value: Any) -> int:
|
|
37
|
+
try:
|
|
38
|
+
return len(value)
|
|
39
|
+
except TypeError:
|
|
40
|
+
return 0
|
|
41
|
+
|
|
42
|
+
|
|
43
|
+
@define
|
|
44
|
+
class ReductionStats:
|
|
45
|
+
reductions: int = 0
|
|
46
|
+
failed_reductions: int = 0
|
|
47
|
+
|
|
48
|
+
calls: int = 0
|
|
49
|
+
interesting_calls: int = 0
|
|
50
|
+
wasted_interesting_calls: int = 0
|
|
51
|
+
|
|
52
|
+
time_of_last_reduction: float = 0.0
|
|
53
|
+
start_time: float = attrs.Factory(time.time)
|
|
54
|
+
|
|
55
|
+
initial_test_case_size: int = 0
|
|
56
|
+
current_test_case_size: int = 0
|
|
57
|
+
|
|
58
|
+
def time_since_last_reduction(self) -> float:
|
|
59
|
+
return time.time() - self.time_of_last_reduction
|
|
60
|
+
|
|
61
|
+
def display_stats(self) -> str:
|
|
62
|
+
runtime = time.time() - self.start_time
|
|
63
|
+
if self.reductions > 0:
|
|
64
|
+
reduction_percentage = (
|
|
65
|
+
1.0 - self.current_test_case_size / self.initial_test_case_size
|
|
66
|
+
) * 100
|
|
67
|
+
reduction_rate = (
|
|
68
|
+
self.initial_test_case_size - self.current_test_case_size
|
|
69
|
+
) / runtime
|
|
70
|
+
reduction_msg = (
|
|
71
|
+
f"Current test case size: {naturalsize(self.current_test_case_size)} "
|
|
72
|
+
f"({reduction_percentage:.2f}% reduction, {naturalsize(reduction_rate)} / second)"
|
|
73
|
+
)
|
|
74
|
+
else:
|
|
75
|
+
reduction_msg = (
|
|
76
|
+
f"Current test case size: {self.current_test_case_size} bytes"
|
|
77
|
+
)
|
|
78
|
+
|
|
79
|
+
return "\n".join(
|
|
80
|
+
[
|
|
81
|
+
reduction_msg,
|
|
82
|
+
f"Total runtime: {precisedelta(timedelta(seconds=runtime))}",
|
|
83
|
+
(
|
|
84
|
+
(
|
|
85
|
+
f"Calls to interestingness test: {self.calls} ({self.calls / runtime:.2f} calls / second, "
|
|
86
|
+
f"{self.interesting_calls / self.calls * 100.0:.2f}% interesting, "
|
|
87
|
+
f"{self.wasted_interesting_calls / self.calls * 100:.2f}% wasted)"
|
|
88
|
+
)
|
|
89
|
+
if self.calls > 0
|
|
90
|
+
else "Not yet called interestingness test"
|
|
91
|
+
),
|
|
92
|
+
(
|
|
93
|
+
f"Time since last reduction: {self.time_since_last_reduction():.2f}s ({self.reductions / runtime:.2f} reductions / second)"
|
|
94
|
+
if self.reductions
|
|
95
|
+
else "No reductions yet"
|
|
96
|
+
),
|
|
97
|
+
]
|
|
98
|
+
)
|
|
99
|
+
|
|
100
|
+
|
|
101
|
+
@define(slots=False)
|
|
102
|
+
class ReductionProblem(Generic[T], ABC):
|
|
103
|
+
work: WorkContext
|
|
104
|
+
|
|
105
|
+
def __attrs_post_init__(self) -> None:
|
|
106
|
+
self.__view_cache: dict[Any, ReductionProblem[Any]] = {}
|
|
107
|
+
|
|
108
|
+
def view(
|
|
109
|
+
self, format: "Format[T, S] | type[Format[T, S]]"
|
|
110
|
+
) -> "ReductionProblem[S]":
|
|
111
|
+
try:
|
|
112
|
+
return cast(ReductionProblem[S], self.__view_cache[format])
|
|
113
|
+
except KeyError:
|
|
114
|
+
pass
|
|
115
|
+
|
|
116
|
+
concrete_format: Format[T, S] = format() if isinstance(format, type) else format
|
|
117
|
+
|
|
118
|
+
result: View[T, S] = View(
|
|
119
|
+
problem=self,
|
|
120
|
+
work=self.work,
|
|
121
|
+
dump=concrete_format.dumps,
|
|
122
|
+
parse=concrete_format.parse,
|
|
123
|
+
)
|
|
124
|
+
|
|
125
|
+
return cast(ReductionProblem[S], self.__view_cache.setdefault(format, result))
|
|
126
|
+
|
|
127
|
+
async def setup(self) -> None:
|
|
128
|
+
pass
|
|
129
|
+
|
|
130
|
+
@abstractproperty
|
|
131
|
+
def current_test_case(self) -> T: ...
|
|
132
|
+
|
|
133
|
+
@abstractmethod
|
|
134
|
+
async def is_interesting(self, test_case: T) -> bool:
|
|
135
|
+
pass
|
|
136
|
+
|
|
137
|
+
async def is_reduction(self, test_case: T) -> bool:
|
|
138
|
+
if test_case == self.current_test_case:
|
|
139
|
+
return True
|
|
140
|
+
if self.sort_key(test_case) > self.sort_key(self.current_test_case):
|
|
141
|
+
return False
|
|
142
|
+
return await self.is_interesting(test_case)
|
|
143
|
+
|
|
144
|
+
@abstractmethod
|
|
145
|
+
def sort_key(self, test_case: T) -> Any: ...
|
|
146
|
+
|
|
147
|
+
@abstractmethod
|
|
148
|
+
def size(self, test_case: T) -> int:
|
|
149
|
+
return len(test_case) # type: ignore
|
|
150
|
+
|
|
151
|
+
@property
|
|
152
|
+
def current_size(self) -> int:
|
|
153
|
+
return self.size(self.current_test_case)
|
|
154
|
+
|
|
155
|
+
@abstractmethod
|
|
156
|
+
def display(self, value: T) -> str: ...
|
|
157
|
+
|
|
158
|
+
def backtrack(self, new_test_case: T) -> "ReductionProblem[T]":
|
|
159
|
+
return BasicReductionProblem(
|
|
160
|
+
initial=new_test_case,
|
|
161
|
+
is_interesting=self.is_interesting,
|
|
162
|
+
work=self.work,
|
|
163
|
+
sort_key=self.sort_key,
|
|
164
|
+
size=self.size,
|
|
165
|
+
display=self.display,
|
|
166
|
+
)
|
|
167
|
+
|
|
168
|
+
|
|
169
|
+
class InvalidInitialExample(ValueError):
|
|
170
|
+
pass
|
|
171
|
+
|
|
172
|
+
|
|
173
|
+
def default_cache_key(value: Any) -> str:
|
|
174
|
+
if not isinstance(value, bytes):
|
|
175
|
+
if not isinstance(value, str):
|
|
176
|
+
value = repr(value)
|
|
177
|
+
value = value.encode("utf-8")
|
|
178
|
+
|
|
179
|
+
hex = hashlib.sha1(value).hexdigest()[:8]
|
|
180
|
+
return f"{len(value)}:{hex}"
|
|
181
|
+
|
|
182
|
+
|
|
183
|
+
class BasicReductionProblem(ReductionProblem[T]):
|
|
184
|
+
def __init__(
|
|
185
|
+
self,
|
|
186
|
+
initial: T,
|
|
187
|
+
is_interesting: Callable[[T], Awaitable[bool]],
|
|
188
|
+
work: WorkContext,
|
|
189
|
+
sort_key: Callable[[T], Any] = default_sort_key,
|
|
190
|
+
size: Callable[[T], int] = default_size,
|
|
191
|
+
display: Callable[[T], str] = default_display,
|
|
192
|
+
stats: Optional[ReductionStats] = None,
|
|
193
|
+
cache_key: Callable[[Any], str] = default_cache_key,
|
|
194
|
+
):
|
|
195
|
+
super().__init__(work=work)
|
|
196
|
+
self.__current = initial
|
|
197
|
+
self.__sort_key = sort_key
|
|
198
|
+
self.__size = size
|
|
199
|
+
self.__display = display
|
|
200
|
+
if stats is None:
|
|
201
|
+
self.stats = ReductionStats()
|
|
202
|
+
self.stats.initial_test_case_size = self.size(initial)
|
|
203
|
+
self.stats.current_test_case_size = self.size(initial)
|
|
204
|
+
else:
|
|
205
|
+
self.stats = stats
|
|
206
|
+
|
|
207
|
+
self.__is_interesting_cache: dict[str, bool] = {}
|
|
208
|
+
self.__cache_key = cache_key
|
|
209
|
+
self.__is_interesting = is_interesting
|
|
210
|
+
self.__on_reduce_callbacks: list[Callable[[T], Awaitable[None]]] = []
|
|
211
|
+
self.__current = initial
|
|
212
|
+
self.__has_set_up = False
|
|
213
|
+
|
|
214
|
+
async def setup(self) -> None:
|
|
215
|
+
if self.__has_set_up:
|
|
216
|
+
return
|
|
217
|
+
self.__has_set_up = True
|
|
218
|
+
if not await self.__is_interesting(self.current_test_case):
|
|
219
|
+
raise InvalidInitialExample(
|
|
220
|
+
f"Initial example ({self.display(self.current_test_case)}) does not satisfy interestingness test."
|
|
221
|
+
)
|
|
222
|
+
|
|
223
|
+
def display(self, value: T) -> str:
|
|
224
|
+
return self.__display(value)
|
|
225
|
+
|
|
226
|
+
def sort_key(self, test_case: T) -> Any:
|
|
227
|
+
return self.__sort_key(test_case)
|
|
228
|
+
|
|
229
|
+
def size(self, test_case: T) -> int:
|
|
230
|
+
return self.__size(test_case)
|
|
231
|
+
|
|
232
|
+
def on_reduce(self, callback: Callable[[T], Awaitable[None]]) -> None:
|
|
233
|
+
"""Every time `is_interesting` is called with a successful reduction,
|
|
234
|
+
call `fn` with the new value. Note that these are called outside the lock."""
|
|
235
|
+
self.__on_reduce_callbacks.append(callback)
|
|
236
|
+
|
|
237
|
+
async def is_interesting(self, value: T) -> bool:
|
|
238
|
+
"""Returns true if this value is interesting."""
|
|
239
|
+
await trio.lowlevel.checkpoint()
|
|
240
|
+
if value == self.current_test_case:
|
|
241
|
+
return True
|
|
242
|
+
cache_key = self.__cache_key(value)
|
|
243
|
+
try:
|
|
244
|
+
return self.__is_interesting_cache[cache_key]
|
|
245
|
+
except KeyError:
|
|
246
|
+
pass
|
|
247
|
+
result = await self.__is_interesting(value)
|
|
248
|
+
self.__is_interesting_cache[cache_key] = result
|
|
249
|
+
self.stats.failed_reductions += 1
|
|
250
|
+
self.stats.calls += 1
|
|
251
|
+
if result:
|
|
252
|
+
self.stats.interesting_calls += 1
|
|
253
|
+
if self.sort_key(value) < self.sort_key(self.current_test_case):
|
|
254
|
+
self.__is_interesting_cache.clear()
|
|
255
|
+
self.stats.failed_reductions -= 1
|
|
256
|
+
self.stats.reductions += 1
|
|
257
|
+
self.stats.time_of_last_reduction = time.time()
|
|
258
|
+
self.stats.current_test_case_size = self.size(value)
|
|
259
|
+
self.__current = value
|
|
260
|
+
for f in self.__on_reduce_callbacks:
|
|
261
|
+
await f(value)
|
|
262
|
+
else:
|
|
263
|
+
self.stats.wasted_interesting_calls += 1
|
|
264
|
+
return result
|
|
265
|
+
|
|
266
|
+
@property
|
|
267
|
+
def current_test_case(self) -> T:
|
|
268
|
+
return self.__current
|
|
269
|
+
|
|
270
|
+
|
|
271
|
+
class View(ReductionProblem[T], Generic[S, T]):
|
|
272
|
+
def __init__(
|
|
273
|
+
self,
|
|
274
|
+
problem: ReductionProblem[S],
|
|
275
|
+
parse: Callable[[S], T],
|
|
276
|
+
dump: Callable[[T], S],
|
|
277
|
+
work: Optional[WorkContext] = None,
|
|
278
|
+
sort_key: Optional[Callable[[T], Any]] = None,
|
|
279
|
+
):
|
|
280
|
+
super().__init__(work=work or problem.work)
|
|
281
|
+
self.__problem = problem
|
|
282
|
+
self.__parse = parse
|
|
283
|
+
self.__dump = dump
|
|
284
|
+
self.__sort_key = sort_key
|
|
285
|
+
|
|
286
|
+
current = problem.current_test_case
|
|
287
|
+
self.__prev = current
|
|
288
|
+
self.__current = parse(current)
|
|
289
|
+
|
|
290
|
+
def display(self, value: T) -> str:
|
|
291
|
+
return default_display(value)
|
|
292
|
+
|
|
293
|
+
@property
|
|
294
|
+
def stats(self) -> ReductionStats:
|
|
295
|
+
return self.__problem.stats # type: ignore
|
|
296
|
+
|
|
297
|
+
@property
|
|
298
|
+
def current_test_case(self) -> T:
|
|
299
|
+
current = self.__problem.current_test_case
|
|
300
|
+
if current != self.__prev:
|
|
301
|
+
self.__prev = current
|
|
302
|
+
new_value = self.__parse(current)
|
|
303
|
+
if self.__sort_key is None or self.__sort_key(new_value) < self.__sort_key(
|
|
304
|
+
self.__current
|
|
305
|
+
):
|
|
306
|
+
self.__current = new_value
|
|
307
|
+
return self.__current
|
|
308
|
+
|
|
309
|
+
async def is_interesting(self, test_case: T) -> bool:
|
|
310
|
+
return await self.__problem.is_interesting(self.__dump(test_case))
|
|
311
|
+
|
|
312
|
+
def sort_key(self, test_case: T) -> Any:
|
|
313
|
+
if self.__sort_key is not None:
|
|
314
|
+
return self.__sort_key(test_case)
|
|
315
|
+
return self.__problem.sort_key(self.__dump(test_case))
|
|
316
|
+
|
|
317
|
+
def size(self, test_case: T) -> int:
|
|
318
|
+
return self.__problem.size(self.__dump(test_case))
|
shrinkray/py.typed
ADDED
|
File without changes
|