DEV: Add --profile=[COUNT] option for turbo_rspec

Why is this change required?

By default, `RSpec` comes with a `--profile=[COUNT]` option as well but
enabling that option means that the entire test suite needs to be
executed. This does not work so well for `turbo_rspec` which splits our
test files into various "buckets" for the tests to be executed in
multiple processes. Therefore, this commit adds a similar
`--profile=[COUNT]` option to `turbo_rspec` but will only profile the
tests being executed. Examples:

`LOAD_PLUGINS=1 bin/turbo_rspec --profile plugins/*/spec/system`

or

`LOAD_PLUGINS=1 bin/turbo_rspec --profile=20 plugins/*/spec/system`
This commit is contained in:
Alan Guo Xiang Tan
2023-05-30 09:52:46 +08:00
parent b580f04d34
commit b00edf3ea0
8 changed files with 96 additions and 15 deletions

View File

@ -11,6 +11,8 @@ formatters = []
verbose = false verbose = false
fail_fast = nil fail_fast = nil
seed = rand(2**16) seed = rand(2**16)
profile = false
profile_print_slowest_examples_count = 10
OptionParser OptionParser
.new do |opts| .new do |opts|
@ -27,6 +29,15 @@ OptionParser
opts.on("-v", "--verbose", "More output") { verbose = true } opts.on("-v", "--verbose", "More output") { verbose = true }
opts.on(
"-p",
"--profile=[COUNT]",
"Benchmark the runtime of each example and list the slowest examples (default: 10)",
) do |count|
profile = true
profile_print_slowest_examples_count = count.to_i if count
end
opts.on("--fail-fast=[N]") do |n| opts.on("--fail-fast=[N]") do |n|
n = n =
begin begin
@ -67,6 +78,8 @@ success =
fail_fast: fail_fast, fail_fast: fail_fast,
use_runtime_info: use_runtime_info, use_runtime_info: use_runtime_info,
seed: seed.to_s, seed: seed.to_s,
profile:,
profile_print_slowest_examples_count:,
) )
if success if success

View File

@ -0,0 +1,23 @@
# frozen_string_literal: true
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
RSpec::Support.require_rspec_core "formatters/console_codes"
module TurboTests
class BaseFormatter < RSpec::Core::Formatters::BaseTextFormatter
RSpec::Core::Formatters.register(self, :dump_summary)
def dump_summary(notification, timings)
if timings.present?
output.puts "\nTop #{timings.size} Slowest examples:"
timings.each do |(full_description, source_location, duration)|
output.puts " #{full_description}"
output.puts " #{RSpec::Core::Formatters::ConsoleCodes.wrap(duration.to_s + "ms", :bold)} #{source_location}"
end
end
super(notification)
end
end
end

View File

@ -1,41 +1,51 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
module TurboTests module TurboTests
# An RSpec formatter that prepends the process id to all messages # An RSpec formatter that prepends the process id to all messages
class DocumentationFormatter < RSpec::Core::Formatters::BaseTextFormatter class DocumentationFormatter < ::TurboTests::BaseFormatter
RSpec::Core::Formatters.register(self, :example_failed, :example_passed, :example_pending) RSpec::Core::Formatters.register(self, :example_failed, :example_passed, :example_pending)
def example_passed(notification) def example_passed(notification)
output.puts RSpec::Core::Formatters::ConsoleCodes.wrap( output.puts RSpec::Core::Formatters::ConsoleCodes.wrap(
"[#{notification.example.process_id}] #{notification.example.full_description}", output_example(notification.example),
:success, :success,
) )
output.flush output.flush
end end
def example_pending(notification) def example_pending(notification)
message = notification.example.execution_result.pending_message message = notification.example.execution_result.pending_message
output.puts RSpec::Core::Formatters::ConsoleCodes.wrap( output.puts RSpec::Core::Formatters::ConsoleCodes.wrap(
"[#{notification.example.process_id}] #{notification.example.full_description}" \ "#{output_example(notification.example)} (PENDING: #{message})",
" (PENDING: #{message})",
:pending, :pending,
) )
output.flush output.flush
end end
def example_failed(notification) def example_failed(notification)
output.puts RSpec::Core::Formatters::ConsoleCodes.wrap( output.puts RSpec::Core::Formatters::ConsoleCodes.wrap(
"[#{notification.example.process_id}] #{notification.example.full_description}" \ "#{output_example(notification.example)} (FAILED - #{next_failure_index})",
" (FAILED - #{next_failure_index})",
:failure, :failure,
) )
output.flush output.flush
end end
private private
def output_example(example)
output = +"[#{example.process_id}] #{example.full_description}"
if run_duration_ms = example.metadata[:run_duration_ms]
output << " (#{run_duration_ms}ms)"
end
output
end
def next_failure_index def next_failure_index
@next_failure_index ||= 0 @next_failure_index ||= 0
@next_failure_index += 1 @next_failure_index += 1

View File

@ -54,6 +54,7 @@ module TurboTests
shared_group_inclusion_backtrace: shared_group_inclusion_backtrace:
example.metadata[:shared_group_inclusion_backtrace].map(&method(:stack_frame_to_json)), example.metadata[:shared_group_inclusion_backtrace].map(&method(:stack_frame_to_json)),
extra_failure_lines: example.metadata[:extra_failure_lines], extra_failure_lines: example.metadata[:extra_failure_lines],
run_duration_ms: example.metadata[:run_duration_ms],
}, },
location_rerun_argument: example.location_rerun_argument, location_rerun_argument: example.location_rerun_argument,
} }

View File

@ -1,9 +1,7 @@
# frozen_string_literal: true # frozen_string_literal: true
RSpec::Support.require_rspec_core "formatters/base_text_formatter"
module TurboTests module TurboTests
class ProgressFormatter < RSpec::Core::Formatters::BaseTextFormatter class ProgressFormatter < ::TurboTests::BaseFormatter
LINE_LENGTH = 80 LINE_LENGTH = 80
RSpec::Core::Formatters.register( RSpec::Core::Formatters.register(

View File

@ -2,8 +2,8 @@
module TurboTests module TurboTests
class Reporter class Reporter
def self.from_config(formatter_config, start_time) def self.from_config(formatter_config, start_time, max_timings_count: nil)
reporter = new(start_time) reporter = new(start_time:, max_timings_count:)
formatter_config.each do |config| formatter_config.each do |config|
name, outputs = config.values_at(:name, :outputs) name, outputs = config.values_at(:name, :outputs)
@ -20,7 +20,7 @@ module TurboTests
attr_reader :failed_examples attr_reader :failed_examples
attr_reader :formatters attr_reader :formatters
def initialize(start_time) def initialize(start_time:, max_timings_count:)
@formatters = [] @formatters = []
@pending_examples = [] @pending_examples = []
@failed_examples = [] @failed_examples = []
@ -28,6 +28,8 @@ module TurboTests
@start_time = start_time @start_time = start_time
@messages = [] @messages = []
@errors_outside_of_examples_count = 0 @errors_outside_of_examples_count = 0
@timings = []
@max_timings_count = max_timings_count
end end
def add(name, outputs) def add(name, outputs)
@ -50,6 +52,7 @@ module TurboTests
delegate_to_formatters(:example_passed, example.notification) delegate_to_formatters(:example_passed, example.notification)
@all_examples << example @all_examples << example
log_timing(example)
end end
def example_pending(example) def example_pending(example)
@ -57,6 +60,7 @@ module TurboTests
@all_examples << example @all_examples << example
@pending_examples << example @pending_examples << example
log_timing(example)
end end
def example_failed(example) def example_failed(example)
@ -64,6 +68,7 @@ module TurboTests
@all_examples << example @all_examples << example
@failed_examples << example @failed_examples << example
log_timing(example)
end end
def message(message) def message(message)
@ -79,14 +84,17 @@ module TurboTests
end_time = Time.now end_time = Time.now
delegate_to_formatters(:start_dump, RSpec::Core::Notifications::NullNotification) delegate_to_formatters(:start_dump, RSpec::Core::Notifications::NullNotification)
delegate_to_formatters( delegate_to_formatters(
:dump_pending, :dump_pending,
RSpec::Core::Notifications::ExamplesNotification.new(self), RSpec::Core::Notifications::ExamplesNotification.new(self),
) )
delegate_to_formatters( delegate_to_formatters(
:dump_failures, :dump_failures,
RSpec::Core::Notifications::ExamplesNotification.new(self), RSpec::Core::Notifications::ExamplesNotification.new(self),
) )
delegate_to_formatters( delegate_to_formatters(
:dump_summary, :dump_summary,
RSpec::Core::Notifications::SummaryNotification.new( RSpec::Core::Notifications::SummaryNotification.new(
@ -97,7 +105,9 @@ module TurboTests
0, 0,
@errors_outside_of_examples_count, @errors_outside_of_examples_count,
), ),
@timings,
) )
delegate_to_formatters(:close, RSpec::Core::Notifications::NullNotification) delegate_to_formatters(:close, RSpec::Core::Notifications::NullNotification)
end end
@ -108,5 +118,15 @@ module TurboTests
formatter.send(method, *args) if formatter.respond_to?(method) formatter.send(method, *args) if formatter.respond_to?(method)
end end
end end
private
def log_timing(example)
if run_duration_ms = example.metadata[:run_duration_ms]
@timings << [example.full_description, example.location, run_duration_ms]
@timings.sort_by! { |timing| -timing.last }
@timings.pop if @timings.size > @max_timings_count
end
end
end end
end end

View File

@ -13,7 +13,12 @@ module TurboTests
STDOUT.puts "VERBOSE" if verbose STDOUT.puts "VERBOSE" if verbose
reporter = Reporter.from_config(formatters, start_time) reporter =
Reporter.from_config(
formatters,
start_time,
max_timings_count: opts[:profile_print_slowest_examples_count],
)
if ENV["GITHUB_ACTIONS"] if ENV["GITHUB_ACTIONS"]
RSpec.configure do |config| RSpec.configure do |config|
@ -31,6 +36,7 @@ module TurboTests
fail_fast: fail_fast, fail_fast: fail_fast,
use_runtime_info: use_runtime_info, use_runtime_info: use_runtime_info,
seed: seed, seed: seed,
profile: opts[:profile],
).run ).run
end end
@ -49,6 +55,7 @@ module TurboTests
@fail_fast = opts[:fail_fast] @fail_fast = opts[:fail_fast]
@use_runtime_info = opts[:use_runtime_info] @use_runtime_info = opts[:use_runtime_info]
@seed = opts[:seed] @seed = opts[:seed]
@profile = opts[:profile]
@failure_count = 0 @failure_count = 0
@messages = Queue.new @messages = Queue.new
@ -168,6 +175,8 @@ module TurboTests
*tests, *tests,
] ]
env["DISCOURSE_RSPEC_PROFILE_EACH_EXAMPLE"] = "1" if @profile
if @verbose if @verbose
command_str = command_str =
[env.map { |k, v| "#{k}=#{v}" }.join(" "), command.join(" ")].select { |x| x.size > 0 } [env.map { |k, v| "#{k}=#{v}" }.join(" "), command.join(" ")].select { |x| x.size > 0 }

View File

@ -369,6 +369,13 @@ RSpec.configure do |config|
end end
end end
if ENV["DISCOURSE_RSPEC_PROFILE_EACH_EXAMPLE"]
config.around :each do |example|
measurement = Benchmark.measure { example.run }
RSpec.current_example.metadata[:run_duration_ms] = (measurement.real * 1000).round(2)
end
end
config.before :each do config.before :each do
# This allows DB.transaction_open? to work in tests. See lib/mini_sql_multisite_connection.rb # This allows DB.transaction_open? to work in tests. See lib/mini_sql_multisite_connection.rb
DB.test_transaction = ActiveRecord::Base.connection.current_transaction DB.test_transaction = ActiveRecord::Base.connection.current_transaction