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

Commit d37db19

Browse files
committed
Merge pull request #1971 from rspec/refactor_issue_1878
Refactor group profile to include before hooks
2 parents 0efd67b + 2e59a6e commit d37db19

File tree

14 files changed

+166
-26
lines changed

14 files changed

+166
-26
lines changed

features/configuration/profile.feature

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -218,3 +218,25 @@ Feature: Profile examples
218218
When I run `rspec spec --fail-fast --profile`
219219
Then the output should not contain "Top 2 slowest examples"
220220
And the output should not contain "example 1"
221+
222+
Scenario: Using `--profile` with slow before hooks includes hook execution time
223+
Given a file named "spec/example_spec.rb" with:
224+
"""ruby
225+
RSpec.describe "slow before context hook" do
226+
before(:context) do
227+
sleep 0.2
228+
end
229+
it "example" do
230+
expect(10).to eq(10)
231+
end
232+
end
233+
234+
RSpec.describe "slow example" do
235+
it "slow example" do
236+
sleep 0.1
237+
expect(10).to eq(10)
238+
end
239+
end
240+
"""
241+
When I run `rspec spec --profile 1`
242+
Then the output should contain "slow before context hook"

lib/rspec/core.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -137,6 +137,7 @@ def self.world
137137
# Namespace for the rspec-core code.
138138
module Core
139139
autoload :ExampleStatusPersister, "rspec/core/example_status_persister"
140+
autoload :Profiler, "rspec/core/profiler"
140141

141142
# @private
142143
# This avoids issues with reporting time caused by examples that

lib/rspec/core/formatters.rb

Lines changed: 5 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -126,7 +126,11 @@ def setup_default(output_stream, deprecation_stream)
126126
add FallbackMessageFormatter, output_stream
127127
end
128128

129-
return unless RSpec.configuration.profile_examples? && !existing_formatter_implements?(:dump_profile)
129+
return unless RSpec.configuration.profile_examples?
130+
131+
@reporter.setup_profiler
132+
133+
return if existing_formatter_implements?(:dump_profile)
130134

131135
add RSpec::Core::Formatters::ProfileFormatter, output_stream
132136
end

lib/rspec/core/formatters/json_formatter.rb

Lines changed: 1 addition & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -60,8 +60,7 @@ def dump_profile(profile)
6060
# @api private
6161
def dump_profile_slowest_examples(profile)
6262
@output_hash[:profile] = {}
63-
sorted_examples = profile.slowest_examples
64-
@output_hash[:profile][:examples] = sorted_examples.map do |example|
63+
@output_hash[:profile][:examples] = profile.slowest_examples.map do |example|
6564
format_example(example).tap do |hash|
6665
hash[:run_time] = example.execution_result.run_time
6766
end

lib/rspec/core/notifications.rb

Lines changed: 12 additions & 16 deletions
Original file line numberDiff line numberDiff line change
@@ -400,8 +400,16 @@ def duplicate_rerun_locations
400400
# @attr duration [Float] the time taken (in seconds) to run the suite
401401
# @attr examples [Array<RSpec::Core::Example>] the examples run
402402
# @attr number_of_examples [Fixnum] the number of examples to profile
403-
ProfileNotification = Struct.new(:duration, :examples, :number_of_examples)
403+
# @attr example_groups [Array<RSpec::Core::Profiler>] example groups run
404404
class ProfileNotification
405+
def initialize(duration, examples, number_of_examples, example_groups)
406+
@duration = duration
407+
@examples = examples
408+
@number_of_examples = number_of_examples
409+
@example_groups = example_groups
410+
end
411+
attr_reader :duration, :examples, :number_of_examples
412+
405413
# @return [Array<RSpec::Core::Example>] the slowest examples
406414
def slowest_examples
407415
@slowest_examples ||=
@@ -435,26 +443,14 @@ def slowest_groups
435443
private
436444

437445
def calculate_slowest_groups
438-
example_groups = {}
439-
440-
examples.each do |example|
441-
location = example.example_group.parent_groups.last.metadata[:location]
442-
443-
location_hash = example_groups[location] ||= Hash.new(0)
444-
location_hash[:total_time] += example.execution_result.run_time
445-
location_hash[:count] += 1
446-
next if location_hash.key?(:description)
447-
location_hash[:description] = example.example_group.top_level_description
448-
end
449-
450446
# stop if we've only one example group
451-
return {} if example_groups.keys.length <= 1
447+
return {} if @example_groups.keys.length <= 1
452448

453-
example_groups.each_value do |hash|
449+
@example_groups.each_value do |hash|
454450
hash[:average] = hash[:total_time].to_f / hash[:count]
455451
end
456452

457-
example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples)
453+
@example_groups.sort_by { |_, hash| -hash[:average] }.first(number_of_examples)
458454
end
459455
end
460456

lib/rspec/core/profiler.rb

Lines changed: 28 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,28 @@
1+
module RSpec
2+
module Core
3+
# @private
4+
class Profiler
5+
NOTIFICATIONS = [:example_group_started, :example_group_finished, :example_started]
6+
7+
def initialize
8+
@example_groups = Hash.new { |h, k| h[k] = { :count => 0 } }
9+
end
10+
11+
attr_reader :example_groups
12+
13+
def example_group_started(notification)
14+
@example_groups[notification.group][:start] = Time.now
15+
@example_groups[notification.group][:description] = notification.group.top_level_description
16+
end
17+
18+
def example_group_finished(notification)
19+
@example_groups[notification.group][:total_time] = Time.now - @example_groups[notification.group][:start]
20+
end
21+
22+
def example_started(notification)
23+
group = notification.example.example_group.parent_groups.last
24+
@example_groups[group][:count] += 1
25+
end
26+
end
27+
end
28+
end

lib/rspec/core/reporter.rb

Lines changed: 9 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,13 @@ def reset
2828
@examples = []
2929
@failed_examples = []
3030
@pending_examples = []
31+
@profiler = Profiler.new if defined?(@profiler)
32+
end
33+
34+
# @private
35+
def setup_profiler
36+
@profiler = Profiler.new
37+
register_listener @profiler, *Profiler::NOTIFICATIONS
3138
end
3239

3340
# Registers a listener to a list of notifications. The reporter will send
@@ -149,7 +156,8 @@ def finish
149156
notify :deprecation_summary, Notifications::NullNotification
150157
unless mute_profile_output?
151158
notify :dump_profile, Notifications::ProfileNotification.new(@duration, @examples,
152-
@configuration.profile_examples)
159+
@configuration.profile_examples,
160+
@profiler.example_groups)
153161
end
154162
notify :dump_summary, Notifications::SummaryNotification.new(@duration, @examples, @failed_examples,
155163
@pending_examples, @load_time)

spec/rspec/core/formatters/json_formatter_spec.rb

Lines changed: 2 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -133,8 +133,8 @@ def profile *groups
133133
end
134134

135135
before do
136+
setup_profiler
136137
formatter
137-
config.profile_examples = 10
138138
end
139139

140140
context "with one example group" do
@@ -183,7 +183,7 @@ def profile *groups
183183
end
184184

185185
it "provides information" do
186-
expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location])
186+
expect(formatter.output_hash[:profile][:groups].first.keys).to match_array([:total_time, :count, :description, :average, :location, :start])
187187
end
188188

189189
it "ranks the example groups by average time" do

spec/rspec/core/formatters/profile_formatter_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
include FormatterSupport
55

66
def profile *groups
7+
setup_profiler
78
groups.each { |group| group.run(reporter) }
89
examples = groups.map(&:examples).flatten
910
total_time = examples.map { |e| e.execution_result.run_time }.inject(&:+)

spec/rspec/core/formatters_spec.rb

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -163,6 +163,7 @@ module RSpec::Core::Formatters
163163
context "without an existing profile formatter" do
164164
it "will add the profile formatter" do
165165
allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [] }
166+
expect(reporter).to receive(:setup_profiler)
166167
expect {
167168
setup_default
168169
}.to change { loader.formatters }.
@@ -174,6 +175,7 @@ module RSpec::Core::Formatters
174175
context "when a formatter that implement #dump_profile is added" do
175176
it "wont add the profile formatter" do
176177
allow(reporter).to receive(:registered_listeners).with(:dump_profile) { [:json] }
178+
expect(reporter).to receive(:setup_profiler)
177179
setup_default
178180
expect(
179181
loader.formatters.map(&:class)

spec/rspec/core/profiler_spec.rb

Lines changed: 69 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,69 @@
1+
require 'rspec/core/profiler'
2+
3+
RSpec.describe 'RSpec::Core::Profiler' do
4+
let(:profiler) { RSpec::Core::Profiler.new }
5+
6+
it 'starts with an empty hash of example_groups' do
7+
expect(profiler.example_groups).to be_empty.and be_a Hash
8+
end
9+
10+
context 'when hooked into the reporter' do
11+
include FormatterSupport
12+
13+
let(:description ) { "My Group" }
14+
let(:now) { ::Time.utc(2015, 6, 2) }
15+
16+
before do
17+
allow(::RSpec::Core::Time).to receive(:now) { now }
18+
end
19+
20+
def group
21+
@group ||=
22+
begin
23+
group = super
24+
allow(group).to receive(:top_level_description) { description }
25+
group
26+
end
27+
end
28+
29+
describe '#example_group_started' do
30+
it 'records example groups start time and description via id' do
31+
expect {
32+
profiler.example_group_started group_notification group
33+
}.to change { profiler.example_groups[group] }.
34+
from(a_hash_excluding(:start, :description)).
35+
to(a_hash_including(:start => now, :description => description))
36+
end
37+
end
38+
39+
describe '#example_group_finished' do
40+
before do
41+
profiler.example_group_started group_notification group
42+
allow(::RSpec::Core::Time).to receive(:now) { now + 1 }
43+
end
44+
45+
it 'records example groups total time and description via id' do
46+
expect {
47+
profiler.example_group_finished group_notification group
48+
}.to change { profiler.example_groups[group] }.
49+
from(a_hash_excluding(:total_time)).
50+
to(a_hash_including(:total_time => 1.0))
51+
end
52+
end
53+
54+
describe '#example_started' do
55+
let(:example) { new_example }
56+
before do
57+
allow(example).to receive(:example_group) { group }
58+
allow(group).to receive(:parent_groups) { [group] }
59+
profiler.example_group_started group_notification group
60+
end
61+
62+
it 'increments the count of examples for its parent group' do
63+
expect {
64+
profiler.example_started example_notification example
65+
}.to change { profiler.example_groups[group][:count] }.by 1
66+
end
67+
end
68+
end
69+
end

spec/rspec/core/reporter_spec.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -25,6 +25,7 @@ module RSpec::Core
2525
it "dumps the failure summary after the profile and deprecation summary so failures don't scroll off the screen and get missed" do
2626
config.profile_examples = 10
2727
formatter = instance_double("RSpec::Core::Formatter::ProgressFormatter")
28+
reporter.setup_profiler
2829
reporter.register_listener(formatter, :dump_summary, :dump_profile, :deprecation_summary)
2930

3031
expect(formatter).to receive(:deprecation_summary).ordered

spec/support/formatter_support.rb

Lines changed: 12 additions & 4 deletions
Original file line numberDiff line numberDiff line change
@@ -185,6 +185,11 @@ def setup_reporter(*streams)
185185
@reporter = config.reporter
186186
end
187187

188+
def setup_profiler
189+
config.profile_examples = true
190+
reporter.setup_profiler
191+
end
192+
188193
def formatter_output
189194
@formatter_output ||= StringIO.new
190195
end
@@ -220,6 +225,7 @@ def new_example(metadata = {})
220225
instance_double(RSpec::Core::Example,
221226
:description => "Example",
222227
:full_description => "Example",
228+
:example_group => group,
223229
:execution_result => result,
224230
:location => "",
225231
:location_rerun_argument => "",
@@ -234,7 +240,9 @@ def examples(n)
234240
end
235241

236242
def group
237-
class_double "RSpec::Core::ExampleGroup", :description => "Group"
243+
group = class_double "RSpec::Core::ExampleGroup", :description => "Group"
244+
allow(group).to receive(:parent_groups) { [group] }
245+
group
238246
end
239247

240248
def start_notification(count)
@@ -249,8 +257,8 @@ def example_notification(specific_example = new_example)
249257
::RSpec::Core::Notifications::ExampleNotification.for specific_example
250258
end
251259

252-
def group_notification
253-
::RSpec::Core::Notifications::GroupNotification.new group
260+
def group_notification group_to_notify = group
261+
::RSpec::Core::Notifications::GroupNotification.new group_to_notify
254262
end
255263

256264
def message_notification(message)
@@ -274,7 +282,7 @@ def summary_notification(duration, examples, failed, pending, time)
274282
end
275283

276284
def profile_notification(duration, examples, number)
277-
::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number
285+
::RSpec::Core::Notifications::ProfileNotification.new duration, examples, number, reporter.instance_variable_get('@profiler').example_groups
278286
end
279287

280288
end

spec/support/matchers.rb

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -123,3 +123,4 @@ def failure_reason(example)
123123
RSpec::Matchers.define_negated_matcher :exclude, :include
124124
RSpec::Matchers.define_negated_matcher :excluding, :include
125125
RSpec::Matchers.define_negated_matcher :avoid_changing, :change
126+
RSpec::Matchers.define_negated_matcher :a_hash_excluding, :include

0 commit comments

Comments
 (0)