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/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