11
11
import math
12
12
from collections import defaultdict
13
13
from collections .abc import Sequence
14
- from typing import TYPE_CHECKING , Callable , Optional , Union , cast
14
+ from typing import TYPE_CHECKING , Callable , Literal , Optional , Union , cast
15
15
16
16
import attr
17
17
@@ -136,7 +136,7 @@ class Shrinker:
136
136
manage the associated state of a particular shrink problem. That is, we
137
137
have some initial ConjectureData object and some property of interest
138
138
that it satisfies, and we want to find a ConjectureData object with a
139
- shortlex (see sort_key above) smaller buffer that exhibits the same
139
+ shortlex (see sort_key above) smaller choice sequence that exhibits the same
140
140
property.
141
141
142
142
Currently the only property of interest we use is that the status is
@@ -160,7 +160,7 @@ class Shrinker:
160
160
=======================
161
161
162
162
Generally a shrink pass is just any function that calls
163
- cached_test_function and/or incorporate_new_buffer a number of times,
163
+ cached_test_function and/or consider_new_nodes a number of times,
164
164
but there are a couple of useful things to bear in mind.
165
165
166
166
A shrink pass *makes progress* if running it changes self.shrink_target
@@ -202,22 +202,22 @@ class Shrinker:
202
202
are carefully designed to do the right thing in the case that no
203
203
shrinks occurred and try to adapt to any changes to do a reasonable
204
204
job. e.g. say we wanted to write a shrink pass that tried deleting
205
- each individual byte (this isn't an especially good choice ,
205
+ each individual choice (this isn't an especially good pass ,
206
206
but it leads to a simple illustrative example), we might do it
207
- by iterating over the buffer like so:
207
+ by iterating over the choice sequence like so:
208
208
209
209
.. code-block:: python
210
210
211
211
i = 0
212
- while i < len(self.shrink_target.buffer ):
213
- if not self.incorporate_new_buffer (
214
- self.shrink_target.buffer [:i] + self.shrink_target.buffer [i + 1 :]
212
+ while i < len(self.shrink_target.nodes ):
213
+ if not self.consider_new_nodes (
214
+ self.shrink_target.nodes [:i] + self.shrink_target.nodes [i + 1 :]
215
215
):
216
216
i += 1
217
217
218
218
The reason for writing the loop this way is that i is always a
219
- valid index into the current buffer , even if the current buffer
220
- changes as a result of our actions. When the buffer changes,
219
+ valid index into the current choice sequence , even if the current sequence
220
+ changes as a result of our actions. When the choice sequence changes,
221
221
we leave the index where it is rather than restarting from the
222
222
beginning, and carry on. This means that the number of steps we
223
223
run in this case is always bounded above by the number of steps
@@ -308,10 +308,8 @@ def __init__(
308
308
self .__predicate = predicate or (lambda data : True )
309
309
self .__allow_transition = allow_transition or (lambda source , destination : True )
310
310
self .__derived_values : dict = {}
311
- self .__pending_shrink_explanation = None
312
311
313
312
self .initial_size = len (initial .choices )
314
-
315
313
# We keep track of the current best example on the shrink_target
316
314
# attribute.
317
315
self .shrink_target = initial
@@ -331,7 +329,7 @@ def __init__(
331
329
332
330
# Because the shrinker is also used to `pareto_optimise` in the target phase,
333
331
# we sometimes want to allow extending buffers instead of aborting at the end.
334
- self .__extend = "full" if in_target_phase else 0
332
+ self .__extend : Union [ Literal [ "full" ], int ] = "full" if in_target_phase else 0
335
333
self .should_explain = explain
336
334
337
335
@derived_value # type: ignore
@@ -383,32 +381,32 @@ def check_calls(self) -> None:
383
381
if self .calls - self .calls_at_last_shrink >= self .max_stall :
384
382
raise StopShrinking
385
383
386
- def cached_test_function (self , nodes ):
384
+ def cached_test_function (
385
+ self , nodes : Sequence [ChoiceNode ]
386
+ ) -> tuple [bool , Optional [Union [ConjectureResult , _Overrun ]]]:
387
+ nodes = nodes [: len (self .nodes )]
388
+
389
+ if startswith (nodes , self .nodes ):
390
+ return (True , None )
391
+
392
+ if sort_key (self .nodes ) < sort_key (nodes ):
393
+ return (False , None )
394
+
387
395
# sometimes our shrinking passes try obviously invalid things. We handle
388
396
# discarding them in one place here.
389
- for node in nodes :
390
- if not choice_permitted (node .value , node .kwargs ):
391
- return None
397
+ if any (not choice_permitted (node .value , node .kwargs ) for node in nodes ):
398
+ return (False , None )
392
399
393
400
result = self .engine .cached_test_function (
394
401
[n .value for n in nodes ], extend = self .__extend
395
402
)
403
+ previous = self .shrink_target
396
404
self .incorporate_test_data (result )
397
405
self .check_calls ()
398
- return result
406
+ return ( previous is not self . shrink_target , result )
399
407
400
408
def consider_new_nodes (self , nodes : Sequence [ChoiceNode ]) -> bool :
401
- nodes = nodes [: len (self .nodes )]
402
-
403
- if startswith (nodes , self .nodes ):
404
- return True
405
-
406
- if sort_key (self .nodes ) < sort_key (nodes ):
407
- return False
408
-
409
- previous = self .shrink_target
410
- self .cached_test_function (nodes )
411
- return previous is not self .shrink_target
409
+ return self .cached_test_function (nodes )[0 ]
412
410
413
411
def incorporate_test_data (self , data ):
414
412
"""Takes a ConjectureData or Overrun object updates the current
@@ -458,8 +456,8 @@ def s(n):
458
456
"Shrink pass profiling\n "
459
457
"---------------------\n \n "
460
458
f"Shrinking made a total of { calls } call{ s (calls )} of which "
461
- f"{ self .shrinks } shrank and { misaligned } were misaligned. This deleted { total_deleted } choices out "
462
- f"of { self .initial_size } ."
459
+ f"{ self .shrinks } shrank and { misaligned } were misaligned. This "
460
+ f"deleted { total_deleted } choices out of { self .initial_size } ."
463
461
)
464
462
for useful in [True , False ]:
465
463
self .debug ("" )
@@ -700,7 +698,7 @@ def reduce_each_alternative(self):
700
698
# previous values to no longer be valid in its position.
701
699
zero_attempt = self .cached_test_function (
702
700
nodes [:i ] + (nodes [i ].copy (with_value = 0 ),) + nodes [i + 1 :]
703
- )
701
+ )[ 1 ]
704
702
if (
705
703
zero_attempt is not self .shrink_target
706
704
and zero_attempt is not None
@@ -731,10 +729,9 @@ def try_lower_node_as_alternative(self, i, v):
731
729
while rerandomising and attempting to repair any subsequent
732
730
changes to the shape of the test case that this causes."""
733
731
nodes = self .shrink_target .nodes
734
- initial_attempt = self .cached_test_function (
732
+ if self .consider_new_nodes (
735
733
nodes [:i ] + (nodes [i ].copy (with_value = v ),) + nodes [i + 1 :]
736
- )
737
- if initial_attempt is self .shrink_target :
734
+ ):
738
735
return True
739
736
740
737
prefix = nodes [:i ] + (nodes [i ].copy (with_value = v ),)
@@ -1090,7 +1087,7 @@ def try_shrinking_nodes(self, nodes, n):
1090
1087
[(node .index , node .index + 1 , [node .copy (with_value = n )]) for node in nodes ],
1091
1088
)
1092
1089
1093
- attempt = self .cached_test_function (initial_attempt )
1090
+ attempt = self .cached_test_function (initial_attempt )[ 1 ]
1094
1091
1095
1092
if attempt is None :
1096
1093
return False
@@ -1149,8 +1146,7 @@ def try_shrinking_nodes(self, nodes, n):
1149
1146
# attempts which increase min_size tend to overrun rather than
1150
1147
# be misaligned, making a covering case difficult.
1151
1148
return False # pragma: no cover
1152
- # the size decreased in our attempt. Try again, but replace with
1153
- # the min_size that we would have gotten, and truncate the value
1149
+ # the size decreased in our attempt. Try again, but truncate the value
1154
1150
# to that size by removing any elements past min_size.
1155
1151
return self .consider_new_nodes (
1156
1152
initial_attempt [: node .index ]
@@ -1534,7 +1530,7 @@ def try_trivial_spans(self, chooser):
1534
1530
]
1535
1531
)
1536
1532
suffix = nodes [ex .end :]
1537
- attempt = self .cached_test_function (prefix + replacement + suffix )
1533
+ attempt = self .cached_test_function (prefix + replacement + suffix )[ 1 ]
1538
1534
1539
1535
if self .shrink_target is not prev :
1540
1536
return
@@ -1598,7 +1594,7 @@ def minimize_individual_choices(self, chooser):
1598
1594
+ (node .copy (with_value = node .value - 1 ),)
1599
1595
+ self .nodes [node .index + 1 :]
1600
1596
)
1601
- attempt = self .cached_test_function (lowered )
1597
+ attempt = self .cached_test_function (lowered )[ 1 ]
1602
1598
if (
1603
1599
attempt is None
1604
1600
or attempt .status < Status .VALID
0 commit comments