@@ -483,6 +483,7 @@ def shrink(self):
483
483
"""
484
484
485
485
try :
486
+ self .initial_coarse_reduction ()
486
487
self .greedy_shrink ()
487
488
except StopShrinking :
488
489
# If we stopped shrinking because we're making slow progress (instead of
@@ -689,6 +690,123 @@ def greedy_shrink(self):
689
690
]
690
691
)
691
692
693
+ def initial_coarse_reduction (self ):
694
+ """Performs some preliminary reductions that should not be
695
+ repeated as part of the main shrink passes.
696
+
697
+ The main reason why these can't be included as part of shrink
698
+ passes is that they have much more ability to make the test
699
+ case "worse". e.g. they might rerandomise part of it, significantly
700
+ increasing the value of individual nodes, which works in direct
701
+ opposition to the lexical shrinking and will frequently undo
702
+ its work.
703
+ """
704
+ self .reduce_each_alternative ()
705
+
706
+ @derived_value # type: ignore
707
+ def examples_starting_at (self ):
708
+ result = [[] for _ in self .shrink_target .ir_nodes ]
709
+ for i , ex in enumerate (self .examples ):
710
+ # We can have zero-length examples that start at the end
711
+ if ex .ir_start < len (result ):
712
+ result [ex .ir_start ].append (i )
713
+ return tuple (map (tuple , result ))
714
+
715
+ def reduce_each_alternative (self ):
716
+ """This is a pass that is designed to rerandomise use of the
717
+ one_of strategy or things that look like it, in order to try
718
+ to move from later strategies to earlier ones in the branch
719
+ order.
720
+
721
+ It does this by trying to systematically lower each value it
722
+ finds that looks like it might be the branch decision for
723
+ one_of, and then attempts to repair any changes in shape that
724
+ this causes.
725
+ """
726
+ i = 0
727
+ while i < len (self .shrink_target .ir_nodes ):
728
+ nodes = self .shrink_target .ir_nodes
729
+ node = nodes [i ]
730
+ if (
731
+ node .ir_type == "integer"
732
+ and not node .was_forced
733
+ and node .value <= 10
734
+ and node .kwargs ["min_value" ] == 0
735
+ ):
736
+ assert isinstance (node .value , int )
737
+
738
+ # We've found a plausible candidate for a ``one_of`` choice.
739
+ # We now want to see if the shape of the test case actually depends
740
+ # on it. If it doesn't, then we don't need to do this (comparatively
741
+ # costly) pass, and can let much simpler lexicographic reduction
742
+ # handle it later.
743
+ #
744
+ # We test this by trying to set the value to zero and seeing if the
745
+ # shape changes, as measured by either changing the number of subsequent
746
+ # nodes, or changing the nodes in such a way as to cause one of the
747
+ # previous values to no longer be valid in its position.
748
+ zero_attempt = self .cached_test_function_ir (
749
+ nodes [:i ] + (nodes [i ].copy (with_value = 0 ),) + nodes [i + 1 :]
750
+ )
751
+ if (
752
+ zero_attempt is not self .shrink_target
753
+ and zero_attempt is not None
754
+ and zero_attempt .status >= Status .VALID
755
+ ):
756
+ changed_shape = len (zero_attempt .ir_nodes ) != len (nodes )
757
+
758
+ if not changed_shape :
759
+ for j in range (i + 1 , len (nodes )):
760
+ zero_node = zero_attempt .ir_nodes [j ]
761
+ orig_node = nodes [j ]
762
+ if (
763
+ zero_node .ir_type != orig_node .ir_type
764
+ or not ir_value_permitted (
765
+ orig_node .value , zero_node .ir_type , zero_node .kwargs
766
+ )
767
+ ):
768
+ changed_shape = True
769
+ break
770
+ if changed_shape :
771
+ for v in range (node .value ):
772
+ if self .try_lower_node_as_alternative (i , v ):
773
+ break
774
+ i += 1
775
+
776
+ def try_lower_node_as_alternative (self , i , v ):
777
+ """Attempt to lower `self.shrink_target.ir_nodes[i]` to `v`,
778
+ while rerandomising and attempting to repair any subsequent
779
+ changes to the shape of the test case that this causes."""
780
+ nodes = self .shrink_target .ir_nodes
781
+ initial_attempt = self .cached_test_function_ir (
782
+ nodes [:i ] + (nodes [i ].copy (with_value = v ),) + nodes [i + 1 :]
783
+ )
784
+ if initial_attempt is self .shrink_target :
785
+ return True
786
+
787
+ prefix = nodes [:i ] + (nodes [i ].copy (with_value = v ),)
788
+ initial = self .shrink_target
789
+ examples = self .examples_starting_at [i ]
790
+ for _ in range (3 ):
791
+ random_attempt = self .engine .cached_test_function_ir (
792
+ prefix , extend = len (nodes ) * 2
793
+ )
794
+ if random_attempt .status < Status .VALID :
795
+ continue
796
+ self .incorporate_test_data (random_attempt )
797
+ for j in examples :
798
+ initial_ex = initial .examples [j ]
799
+ attempt_ex = random_attempt .examples [j ]
800
+ contents = random_attempt .ir_nodes [
801
+ attempt_ex .ir_start : attempt_ex .ir_end
802
+ ]
803
+ self .consider_new_tree (
804
+ nodes [:i ] + contents + nodes [initial_ex .ir_end :]
805
+ )
806
+ if initial is not self .shrink_target :
807
+ return True
808
+ return False
809
+
692
810
@derived_value # type: ignore
693
811
def shrink_pass_choice_trees (self ):
694
812
return defaultdict (ChoiceTree )
0 commit comments