@@ -154,11 +154,30 @@ def timing_report(self) -> str:
154154 return "\n " .join (out )
155155
156156
157+ # Stop when 99% confident the true valid rate is below 1%.
158+ # For k valid examples, we need n invalid such that:
159+ # P(seeing <= k valid in n+k trials | rate=1%) <= 1%
160+ # k=0: (0.99)^n <= 0.01 -> n >= ln(0.01)/ln(0.99)
161+ # Each additional valid example adds ~ln(0.01)/ln(0.99)/3 to threshold.
162+ def _calculate_thresholds (
163+ confidence : float = 0.99 , min_valid_rate : float = 0.01
164+ ) -> tuple [int , int ]:
165+ log_confidence = math .log (1 - confidence )
166+ log_invalid_rate = math .log (1 - min_valid_rate )
167+ base = math .ceil (log_confidence / log_invalid_rate )
168+ # Approximate increase per valid example (from binomial CDF)
169+ per_valid = math .ceil (base / 3 )
170+ return base , per_valid
171+
172+
173+ INVALID_THRESHOLD_BASE , INVALID_PER_VALID = _calculate_thresholds ()
174+
175+
157176class ExitReason (Enum ):
158177 max_examples = "settings.max_examples={s.max_examples}"
159178 max_iterations = (
160179 "settings.max_examples={s.max_examples}, "
161- "but < 10 % of examples satisfied assumptions"
180+ "but < 1 % of examples satisfied assumptions"
162181 )
163182 max_shrinks = f"shrunk example { MAX_SHRINKS } times"
164183 finished = "nothing left to do"
@@ -724,12 +743,11 @@ def _backend_cannot_proceed(
724743 # while in the other case below we just want to move on to shrinking.)
725744 if self .valid_examples >= self .settings .max_examples :
726745 self .exit_with (ExitReason .max_examples )
727- if self .call_count >= max (
728- self .settings .max_examples * 10 ,
729- # We have a high-ish default max iterations, so that tests
730- # don't become flaky when max_examples is too low.
731- 1000 ,
732- ):
746+ # Stop when we're 99% confident the true valid rate is below 1%.
747+ invalid_threshold = (
748+ INVALID_THRESHOLD_BASE + INVALID_PER_VALID * self .valid_examples
749+ )
750+ if (self .invalid_examples + self .overrun_examples ) > invalid_threshold :
733751 self .exit_with (ExitReason .max_iterations )
734752
735753 if self .__tree_is_exhausted ():
@@ -1088,8 +1106,12 @@ def should_generate_more(self) -> bool:
10881106 # but with the important distinction that this clause will move on to
10891107 # the shrinking phase having found one or more bugs, while the other
10901108 # will exit having found zero bugs.
1091- if self .valid_examples >= self .settings .max_examples or self .call_count >= max (
1092- self .settings .max_examples * 10 , 1000
1109+ invalid_threshold = (
1110+ INVALID_THRESHOLD_BASE + INVALID_PER_VALID * self .valid_examples
1111+ )
1112+ if (
1113+ self .valid_examples >= self .settings .max_examples
1114+ or (self .invalid_examples + self .overrun_examples ) > invalid_threshold
10931115 ): # pragma: no cover
10941116 return False
10951117
0 commit comments