Skip to content

Commit

Permalink
Introduce duration provider pattern
Browse files Browse the repository at this point in the history
  • Loading branch information
yuki24 committed Apr 24, 2024
1 parent 2c2dc3f commit ec406db
Show file tree
Hide file tree
Showing 9 changed files with 164 additions and 53 deletions.
2 changes: 2 additions & 0 deletions .rubocop.yml
Original file line number Diff line number Diff line change
Expand Up @@ -27,6 +27,8 @@ Naming/FileName:
- lib/flatware-*.rb
Style/Documentation:
Enabled: false
Metrics/AbcSize:
Max: 20
Metrics/BlockLength:
Exclude:
- spec/**/*_spec.rb
Expand Down
4 changes: 2 additions & 2 deletions lib/flatware/rspec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,8 +11,8 @@ module RSpec

module_function

def extract_jobs_from_args(args, workers:)
JobBuilder.new(args, workers: workers).jobs
def extract_jobs_from_args(args, workers:, duration_provider:)
JobBuilder.new(args, workers: workers, duration_provider: duration_provider).jobs
end

def runner
Expand Down
12 changes: 10 additions & 2 deletions lib/flatware/rspec/cli.rb
Original file line number Diff line number Diff line change
Expand Up @@ -2,20 +2,28 @@

require 'flatware/cli'
require 'flatware/rspec'
require 'flatware/rspec/duration_providers'
require 'flatware/rspec/formatters/console'

module Flatware
# rspec thor command
class CLI
worker_option
method_option(
'sink-endpoint',
type: :string,
default: 'drbunix:flatware-sink'
)
method_option(
:'duration-provider',
aliases: '-d',
type: :string,
default: :example_statuses,
desc: 'Duration provider to use. The default option is "example_statuses".'
)
desc 'rspec [FLATWARE_OPTS]', 'parallelizes rspec'
def rspec(*rspec_args)
jobs = RSpec.extract_jobs_from_args rspec_args, workers: workers
duration_provider = RSpec::DurationProviders.lookup(options['duration-provider'])
jobs = RSpec.extract_jobs_from_args rspec_args, workers: workers, duration_provider: duration_provider

formatter = Flatware::RSpec::Formatters::Console.new(
::RSpec.configuration.output_stream,
Expand Down
21 changes: 21 additions & 0 deletions lib/flatware/rspec/duration_providers.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,21 @@
# frozen_string_literal: true

require 'flatware/rspec/duration_providers/example_statuses_provider'

module Flatware
module RSpec
module DurationProviders
module_function

def lookup(provider)
const_get("#{classify(provider)}Provider").new
end

def classify(underscore_name)
underscore_name.to_s.split('_').map(&:capitalize).join
end

private_class_method :classify
end
end
end
54 changes: 54 additions & 0 deletions lib/flatware/rspec/duration_providers/example_statuses_provider.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,54 @@
# frozen_string_literal: true

require 'forwardable'

module Flatware
module RSpec
module DurationProviders
class ExampleStatusesProvider
extend Forwardable
attr_reader :configuration

def_delegators :configuration, :example_status_persistence_file_path

def initialize(configuration: ::RSpec.configuration)
@configuration = configuration
end

def seconds_per_file
sum_seconds(load_persisted_example_statuses)
end

private

def load_persisted_example_statuses
::RSpec::Core::ExampleStatusPersister.load_from(
example_status_persistence_file_path || ''
)
end

def sum_seconds(statuses)
statuses.select(&passing)
.map { |example| parse_example(**example) }
.reduce({}) do |times, example|
times.merge(
example.fetch(:file_name) => example.fetch(:seconds)
) do |_, old = 0, new|
old + new
end
end
end

def passing
->(example) { example.fetch(:status) =~ /pass/i }
end

def parse_example(example_id:, run_time:, **)
seconds = run_time.match(/\d+(\.\d+)?/).to_s.to_f
file_name = ::RSpec::Core::Example.parse_id(example_id).first
{ seconds: seconds, file_name: file_name }
end
end
end
end
end
41 changes: 5 additions & 36 deletions lib/flatware/rspec/job_builder.rb
Original file line number Diff line number Diff line change
Expand Up @@ -9,17 +9,14 @@ module RSpec
# and attempts to ballence the jobs accordingly.
class JobBuilder
extend Forwardable
attr_reader :args, :workers, :configuration
attr_reader :args, :workers, :configuration, :duration_provider

def_delegators(
:configuration,
:files_to_run,
:example_status_persistence_file_path
)
def_delegators :configuration, :files_to_run

def initialize(args, workers:)
def initialize(args, workers:, duration_provider:)
@args = args
@workers = workers
@duration_provider = duration_provider

@configuration = ::RSpec.configuration
configuration.define_singleton_method(:command) { 'rspec' }
Expand All @@ -29,7 +26,7 @@ def initialize(args, workers:)

def jobs
timed_files, untimed_files = timed_and_untimed_files(
sum_seconds(load_persisted_example_statuses)
duration_provider.seconds_per_file
)

balance_jobs(
Expand Down Expand Up @@ -66,34 +63,6 @@ def normalize_path(path)
::RSpec::Core::Metadata.relative_path(File.expand_path(path))
end

def load_persisted_example_statuses
::RSpec::Core::ExampleStatusPersister.load_from(
example_status_persistence_file_path || ''
)
end

def sum_seconds(statuses)
statuses.select(&passing)
.map { |example| parse_example(**example) }
.reduce({}) do |times, example|
times.merge(
example.fetch(:file_name) => example.fetch(:seconds)
) do |_, old = 0, new|
old + new
end
end
end

def passing
->(example) { example.fetch(:status) =~ /pass/i }
end

def parse_example(example_id:, run_time:, **)
seconds = run_time.match(/\d+(\.\d+)?/).to_s.to_f
file_name = ::RSpec::Core::Example.parse_id(example_id).first
{ seconds: seconds, file_name: file_name }
end

def round_robin(count, items)
Array.new(count) { [] }.tap do |groups|
items.each_with_index do |entry, i|
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
# frozen_string_literal: true

require 'spec_helper'
require 'flatware/rspec/duration_providers/example_statuses_provider'

describe Flatware::RSpec::DurationProviders::ExampleStatusesProvider do
before do
allow(RSpec::Core::ExampleStatusPersister).to(
receive(:load_from).and_return(persisted_examples)
)
end

let(:persisted_examples) do
[
{ example_id: './fast_1_spec.rb[1]', run_time: '1 second' },
{ example_id: './fast_2_spec.rb[1]', run_time: '1 second' },
{ example_id: './fast_3_spec.rb[1]', run_time: '1 second' },
{ example_id: './slow_spec.rb[1]', run_time: '2 seconds' }
].map { |example| example.merge status: 'passed' }
end

describe '#seconds_per_file' do
subject { described_class.new.seconds_per_file }

it 'returns an object of the specified provider class' do
expect(subject).to eq(
'./fast_1_spec.rb' => 1.0,
'./fast_2_spec.rb' => 1.0,
'./fast_3_spec.rb' => 1.0,
'./slow_spec.rb' => 2.0
)
end
end
end
26 changes: 26 additions & 0 deletions spec/flatware/rspec/duration_providers_spec.rb
Original file line number Diff line number Diff line change
@@ -0,0 +1,26 @@
# frozen_string_literal: true

require 'spec_helper'
require 'flatware/rspec/duration_providers'

describe Flatware::RSpec::DurationProviders do
describe '#lookup' do
before do
Flatware::RSpec::DurationProviders.const_set(:MockProvider, mock_provider)
end

after do
Flatware::RSpec::DurationProviders.send(:remove_const, :MockProvider)
end

let(:mock_provider) { Class.new }

it 'returns an object of the specified provider class' do
expect(described_class.lookup('mock')).to be_a(mock_provider)
end

it 'works with a string argument' do
expect(described_class.lookup(:mock)).to be_a(mock_provider)
end
end
end
23 changes: 10 additions & 13 deletions spec/flatware/rspec/job_builder_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,30 +5,27 @@

describe Flatware::RSpec::JobBuilder do
before do
allow(RSpec::Core::ExampleStatusPersister).to(
receive(:load_from).and_return(persisted_examples)
)

allow(RSpec.configuration).to(
receive(:files_to_run).and_return(files_to_run)
)
end

let(:persisted_examples) { [] }
let(:duration_provider) { double('duration_provider', seconds_per_file: seconds_per_file) }
let(:seconds_per_file) { [] }
let(:files_to_run) { [] }

subject do
described_class.new([], workers: 2).jobs
described_class.new([], workers: 2, duration_provider: duration_provider).jobs
end

context 'when this run includes persisted examples' do
let(:persisted_examples) do
[
{ example_id: './fast_1_spec.rb[1]', run_time: '1 second' },
{ example_id: './fast_2_spec.rb[1]', run_time: '1 second' },
{ example_id: './fast_3_spec.rb[1]', run_time: '1 second' },
{ example_id: './slow_spec.rb[1]', run_time: '2 seconds' }
].map { |example| example.merge status: 'passed' }
let(:seconds_per_file) do
{
'./fast_1_spec.rb' => 1.0,
'./fast_2_spec.rb' => 1.0,
'./fast_3_spec.rb' => 1.0,
'./slow_spec.rb' => 2.0
}
end

let(:files_to_run) { %w[fast_1_spec.rb fast_2_spec.rb slow_spec.rb] }
Expand Down

0 comments on commit ec406db

Please sign in to comment.