Skip to content
This repository was archived by the owner on Nov 30, 2024. It is now read-only.

Commit 86c4fd4

Browse files
committed
Merge pull request #1997 from urbanautomaton/recursive-bisect-strategy
Recursive bisect strategy
2 parents 9e801c3 + 5a2f476 commit 86c4fd4

File tree

9 files changed

+306
-188
lines changed

9 files changed

+306
-188
lines changed

features/command_line/bisect.feature

Lines changed: 16 additions & 20 deletions
Original file line numberDiff line numberDiff line change
@@ -54,12 +54,12 @@ Feature: Bisect
5454
Bisect started using options: "--seed 1234"
5555
Running suite to find failures... (0.16755 seconds)
5656
Starting bisect with 1 failing example and 9 non-failing examples.
57+
Checking that failure(s) are order-dependent... failure appears to be order-dependent
5758
58-
Round 1: searching for 5 non-failing examples (of 9) to ignore: .. (0.30166 seconds)
59-
Round 2: searching for 3 non-failing examples (of 5) to ignore: .. (0.30306 seconds)
60-
Round 3: searching for 2 non-failing examples (of 3) to ignore: .. (0.33292 seconds)
61-
Round 4: searching for 1 non-failing example (of 2) to ignore: . (0.16476 seconds)
62-
Round 5: searching for 1 non-failing example (of 1) to ignore: . (0.15329 seconds)
59+
Round 1: bisecting over non-failing examples 1-9 .. ignoring examples 6-9 (0.30166 seconds)
60+
Round 2: bisecting over non-failing examples 1-5 .. ignoring examples 4-5 (0.30306 seconds)
61+
Round 3: bisecting over non-failing examples 1-3 .. ignoring example 3 (0.33292 seconds)
62+
Round 4: bisecting over non-failing examples 1-2 . ignoring example 1 (0.16476 seconds)
6363
Bisect complete! Reduced necessary non-failing examples from 9 to 1 in 1.26 seconds.
6464
6565
The minimal reproduction command is:
@@ -75,10 +75,11 @@ Feature: Bisect
7575
Bisect started using options: "--seed 1234"
7676
Running suite to find failures... (0.17102 seconds)
7777
Starting bisect with 1 failing example and 9 non-failing examples.
78+
Checking that failure(s) are order-dependent... failure appears to be order-dependent
7879
79-
Round 1: searching for 5 non-failing examples (of 9) to ignore: .. (0.32943 seconds)
80-
Round 2: searching for 3 non-failing examples (of 5) to ignore: .. (0.3154 seconds)
81-
Round 3: searching for 2 non-failing examples (of 3) to ignore: ..
80+
Round 1: bisecting over non-failing examples 1-9 .. ignoring examples 6-9 (0.32943 seconds)
81+
Round 2: bisecting over non-failing examples 1-5 .. ignoring examples 4-5 (0.3154 seconds)
82+
Round 3: bisecting over non-failing examples 1-3 .. ignoring example 3 (0.2175 seconds)
8283
8384
Bisect aborted!
8485
@@ -106,8 +107,10 @@ Feature: Bisect
106107
- ./spec/calculator_7_spec.rb[1:1]
107108
- ./spec/calculator_8_spec.rb[1:1]
108109
- ./spec/calculator_9_spec.rb[1:1]
109-
110-
Round 1: searching for 5 non-failing examples (of 9) to ignore:
110+
Checking that failure(s) are order-dependent..
111+
- Running: rspec ./spec/calculator_1_spec.rb[1:1] --seed 1234 (n.nnnn seconds)
112+
- Failure appears to be order-dependent
113+
Round 1: bisecting over non-failing examples 1-9
111114
- Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_6_spec.rb[1:1] ./spec/calculator_7_spec.rb[1:1] ./spec/calculator_8_spec.rb[1:1] ./spec/calculator_9_spec.rb[1:1] --seed 1234 (0.15302 seconds)
112115
- Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] ./spec/calculator_4_spec.rb[1:1] ./spec/calculator_5_spec.rb[1:1] --seed 1234 (0.19708 seconds)
113116
- Examples we can safely ignore (4):
@@ -121,8 +124,7 @@ Feature: Bisect
121124
- ./spec/calculator_3_spec.rb[1:1]
122125
- ./spec/calculator_4_spec.rb[1:1]
123126
- ./spec/calculator_5_spec.rb[1:1]
124-
- Round finished (0.35172 seconds)
125-
Round 2: searching for 3 non-failing examples (of 5) to ignore:
127+
Round 2: bisecting over non-failing examples 1-5
126128
- Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_4_spec.rb[1:1] ./spec/calculator_5_spec.rb[1:1] --seed 1234 (0.15836 seconds)
127129
- Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234 (0.19065 seconds)
128130
- Examples we can safely ignore (2):
@@ -132,26 +134,20 @@ Feature: Bisect
132134
- ./spec/calculator_10_spec.rb[1:1]
133135
- ./spec/calculator_2_spec.rb[1:1]
134136
- ./spec/calculator_3_spec.rb[1:1]
135-
- Round finished (0.35022 seconds)
136-
Round 3: searching for 2 non-failing examples (of 3) to ignore:
137+
Round 3: bisecting over non-failing examples 1-3
137138
- Running: rspec ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_2_spec.rb[1:1] --seed 1234 (0.21028 seconds)
138139
- Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] ./spec/calculator_3_spec.rb[1:1] --seed 1234 (0.1975 seconds)
139140
- Examples we can safely ignore (1):
140141
- ./spec/calculator_2_spec.rb[1:1]
141142
- Remaining non-failing examples (2):
142143
- ./spec/calculator_10_spec.rb[1:1]
143144
- ./spec/calculator_3_spec.rb[1:1]
144-
- Round finished (0.40882 seconds)
145-
Round 4: searching for 1 non-failing example (of 2) to ignore:
145+
Round 4: bisecting over non-failing examples 1-2
146146
- Running: rspec ./spec/calculator_10_spec.rb[1:1] ./spec/calculator_1_spec.rb[1:1] --seed 1234 (0.17173 seconds)
147147
- Examples we can safely ignore (1):
148148
- ./spec/calculator_3_spec.rb[1:1]
149149
- Remaining non-failing examples (1):
150150
- ./spec/calculator_10_spec.rb[1:1]
151-
- Round finished (0.17234 seconds)
152-
Round 5: searching for 1 non-failing example (of 1) to ignore:
153-
- Running: rspec ./spec/calculator_1_spec.rb[1:1] --seed 1234 (0.18279 seconds)
154-
- Round finished (0.18312 seconds)
155151
Bisect complete! Reduced necessary non-failing examples from 9 to 1 in 1.47 seconds.
156152
157153
The minimal reproduction command is:

features/support/send_sigint_during_bisect.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -5,8 +5,8 @@ module RSpec::Core::Formatters
55
BisectProgressFormatter = Class.new(remove_const :BisectProgressFormatter) do
66
RSpec::Core::Formatters.register self
77

8-
def bisect_round_finished(notification)
9-
return super unless notification.round == 3
8+
def bisect_round_started(notification)
9+
return super unless @round_count == 3
1010

1111
Process.kill("INT", Process.pid)
1212
# Process.kill is not a synchronous call, so to ensure the output

lib/rspec/core/bisect/example_minimizer.rb

Lines changed: 78 additions & 39 deletions
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,3 @@
1-
RSpec::Support.require_rspec_core "bisect/subset_enumerator"
2-
31
module RSpec
42
module Core
53
module Bisect
@@ -18,20 +16,65 @@ def initialize(runner, reporter)
1816
def find_minimal_repro
1917
prep
2018

21-
self.remaining_ids = non_failing_example_ids
19+
_, duration = track_duration do
20+
bisect(non_failing_example_ids)
21+
end
22+
23+
notify(:bisect_complete, :duration => duration,
24+
:original_non_failing_count => non_failing_example_ids.size,
25+
:remaining_count => remaining_ids.size)
26+
27+
remaining_ids + failed_example_ids
28+
end
29+
30+
def bisect(candidate_ids)
31+
notify(:bisect_dependency_check_started)
32+
if get_expected_failures_for?([])
33+
notify(:bisect_dependency_check_failed)
34+
self.remaining_ids = []
35+
return
36+
end
37+
notify(:bisect_dependency_check_passed)
38+
39+
bisect_over(candidate_ids)
40+
end
41+
42+
def bisect_over(candidate_ids)
43+
return if candidate_ids.one?
44+
45+
notify(
46+
:bisect_round_started,
47+
:candidate_range => example_range(candidate_ids),
48+
:candidates_count => candidate_ids.size
49+
)
2250

23-
each_bisect_round do |subsets|
24-
ids_to_ignore = subsets.find do |ids|
51+
slice_size = (candidate_ids.length / 2.0).ceil
52+
lhs, rhs = candidate_ids.each_slice(slice_size).to_a
53+
54+
ids_to_ignore, duration = track_duration do
55+
[lhs, rhs].find do |ids|
2556
get_expected_failures_for?(remaining_ids - ids)
2657
end
58+
end
2759

28-
next :done unless ids_to_ignore
29-
60+
if ids_to_ignore
3061
self.remaining_ids -= ids_to_ignore
31-
notify(:bisect_ignoring_ids, :ids_to_ignore => ids_to_ignore, :remaining_ids => remaining_ids)
62+
notify(
63+
:bisect_round_ignoring_ids,
64+
:ids_to_ignore => ids_to_ignore,
65+
:ignore_range => example_range(ids_to_ignore),
66+
:remaining_ids => remaining_ids,
67+
:duration => duration
68+
)
69+
bisect_over(candidate_ids - ids_to_ignore)
70+
else
71+
notify(
72+
:bisect_round_detected_multiple_culprits,
73+
:duration => duration
74+
)
75+
bisect_over(lhs)
76+
bisect_over(rhs)
3277
end
33-
34-
currently_needed_ids
3578
end
3679

3780
def currently_needed_ids
@@ -43,15 +86,35 @@ def repro_command_for_currently_needed_ids
4386
"(Not yet enough information to provide any repro command)"
4487
end
4588

89+
# @private
90+
# Convenience class for describing a subset of the candidate examples
91+
ExampleRange = Struct.new(:start, :finish) do
92+
def description
93+
if start == finish
94+
"example #{start}"
95+
else
96+
"examples #{start}-#{finish}"
97+
end
98+
end
99+
end
100+
46101
private
47102

103+
def example_range(ids)
104+
ExampleRange.new(
105+
non_failing_example_ids.find_index(ids.first) + 1,
106+
non_failing_example_ids.find_index(ids.last) + 1
107+
)
108+
end
109+
48110
def prep
49111
notify(:bisect_starting, :original_cli_args => runner.original_cli_args)
50112

51113
_, duration = track_duration do
52114
original_results = runner.original_results
53115
@all_example_ids = original_results.all_example_ids
54116
@failed_example_ids = original_results.failed_example_ids
117+
@remaining_ids = non_failing_example_ids
55118
end
56119

57120
if @failed_example_ids.empty?
@@ -70,7 +133,11 @@ def non_failing_example_ids
70133

71134
def get_expected_failures_for?(ids)
72135
ids_to_run = ids + failed_example_ids
73-
notify(:bisect_individual_run_start, :command => runner.repro_command_from(ids_to_run))
136+
notify(
137+
:bisect_individual_run_start,
138+
:command => runner.repro_command_from(ids_to_run),
139+
:ids_to_run => ids_to_run
140+
)
74141

75142
results, duration = track_duration { runner.run(ids_to_run) }
76143
notify(:bisect_individual_run_complete, :duration => duration, :results => results)
@@ -79,34 +146,6 @@ def get_expected_failures_for?(ids)
79146
(failed_example_ids & results.failed_example_ids) == failed_example_ids
80147
end
81148

82-
INFINITY = (1.0 / 0) # 1.8.7 doesn't define Float::INFINITY so we define our own...
83-
84-
def each_bisect_round(&block)
85-
last_round, duration = track_duration do
86-
1.upto(INFINITY) do |round|
87-
break if :done == bisect_round(round, &block)
88-
end
89-
end
90-
91-
notify(:bisect_complete, :round => last_round, :duration => duration,
92-
:original_non_failing_count => non_failing_example_ids.size,
93-
:remaining_count => remaining_ids.size)
94-
end
95-
96-
def bisect_round(round)
97-
value, duration = track_duration do
98-
subsets = SubsetEnumerator.new(remaining_ids)
99-
notify(:bisect_round_started, :round => round,
100-
:subset_size => subsets.subset_size,
101-
:remaining_count => remaining_ids.size)
102-
103-
yield subsets
104-
end
105-
106-
notify(:bisect_round_finished, :duration => duration, :round => round)
107-
value
108-
end
109-
110149
def track_duration
111150
start = ::RSpec::Core::Time.now
112151
[yield, ::RSpec::Core::Time.now - start]

lib/rspec/core/bisect/subset_enumerator.rb

Lines changed: 0 additions & 39 deletions
This file was deleted.

0 commit comments

Comments
 (0)