Skip to content
Merged
8 changes: 8 additions & 0 deletions CHANGELOG.md
Original file line number Diff line number Diff line change
@@ -1,5 +1,13 @@
# Changelog

### 5.7.0

* Performance improvement: don't run `rake knapsack_pro:rspec_test_example_detector` when no slow test files are detected for RSpec.

https://github.com/KnapsackPro/knapsack_pro-ruby/pull/225

https://github.com/KnapsackPro/knapsack_pro-ruby/compare/v5.6.0...v5.7.0

### 5.6.0

* Use `frozen_string_literal: true` to reduce memory usage
Expand Down
8 changes: 8 additions & 0 deletions lib/knapsack_pro/adapters/base_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,14 @@ def self.adapter_bind_method_called_file
"#{KnapsackPro::Config::TempFiles::TEMP_DIRECTORY_PATH}/#{adapter_name}-bind_method_called_for_node_#{KnapsackPro::Config::Env.ci_node_index}.txt"
end

def self.split_by_test_cases_enabled?
false
end

def self.test_file_cases_for(slow_test_files)
raise NotImplementedError
end

def self.slow_test_file?(adapter_class, test_file_path)
@slow_test_file_paths ||=
begin
Expand Down
29 changes: 29 additions & 0 deletions lib/knapsack_pro/adapters/rspec_adapter.rb
Original file line number Diff line number Diff line change
Expand Up @@ -5,6 +5,35 @@ module Adapters
class RSpecAdapter < BaseAdapter
TEST_DIR_PATTERN = 'spec/**{,/*/**}/*_spec.rb'

def self.split_by_test_cases_enabled?
return false unless KnapsackPro::Config::Env.rspec_split_by_test_examples?

require 'rspec/core/version'
unless Gem::Version.new(::RSpec::Core::Version::STRING) >= Gem::Version.new('3.3.0')
raise "RSpec >= 3.3.0 is required to split test files by test examples. Learn more: #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}"
end

true
end

def self.test_file_cases_for(slow_test_files)
KnapsackPro.logger.info("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual test cases). Thanks to that, a single slow test file can be split across parallel CI nodes. Analyzing #{slow_test_files.size} slow test files.")

# generate the RSpec JSON report in a separate process to not pollute the RSpec state
cmd = [
'RACK_ENV=test',
'RAILS_ENV=test',
KnapsackPro::Config::Env.rspec_test_example_detector_prefix,
'rake knapsack_pro:rspec_test_example_detector',
].join(' ')
unless Kernel.system(cmd)
raise "Could not generate JSON report for RSpec. Rake task failed when running #{cmd}"
end

# read the JSON report
KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new.test_file_example_paths
end

def self.ensure_no_tag_option_when_rspec_split_by_test_examples_enabled!(cli_args)
if KnapsackPro::Config::Env.rspec_split_by_test_examples? && has_tag_option?(cli_args)
error_message = "It is not allowed to use the RSpec tag option together with the RSpec split by test examples feature. Please see: #{KnapsackPro::Urls::RSPEC__SPLIT_BY_TEST_EXAMPLES__TAG}"
Expand Down
31 changes: 6 additions & 25 deletions lib/knapsack_pro/base_allocator_builder.rb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Have you considered moving the logic specific to an adapter inside the adapter_class?

We could prolly replace most of fast_and_slow_test_files_to_run and its if adapter_class == KnapsackPro::Adapters::RSpecAdapter && KnapsackPro::Config::Env.rspec_split_by_test_examples? with something like:

adapter_class.new.some_name_that_makes_sense(args)

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Good idea. I extracted everything related to the RSpec to the adapter class. Other generic things stay in base allocator builder so that we have option to extend other adapters to also support test cases splitting in the future.

Original file line number Diff line number Diff line change
Expand Up @@ -35,35 +35,16 @@ def fallback_mode_test_files
def fast_and_slow_test_files_to_run
test_files_to_run = all_test_files_to_run

if adapter_class == KnapsackPro::Adapters::RSpecAdapter && KnapsackPro::Config::Env.rspec_split_by_test_examples?
require 'rspec/core/version'
unless Gem::Version.new(::RSpec::Core::Version::STRING) >= Gem::Version.new('3.3.0')
raise "RSpec >= 3.3.0 is required to split test files by test examples. Learn more: #{KnapsackPro::Urls::SPLIT_BY_TEST_EXAMPLES}"
end

if adapter_class.split_by_test_cases_enabled?
slow_test_files = get_slow_test_files
return test_files_to_run if slow_test_files.empty?

KnapsackPro.logger.info("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual 'it's. Thanks to that a single slow test file can be split across parallel CI nodes). Analyzing #{slow_test_files.size} slow test files.")

# generate RSpec JSON report in separate process to not pollute RSpec state
cmd = [
'RACK_ENV=test',
'RAILS_ENV=test',
KnapsackPro::Config::Env.rspec_test_example_detector_prefix,
'rake knapsack_pro:rspec_test_example_detector',
].join(' ')
unless Kernel.system(cmd)
raise "Could not generate JSON report for RSpec. Rake task failed when running #{cmd}"
end
test_file_cases = adapter_class.test_file_cases_for(slow_test_files)

# read JSON report
detector = KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector.new
test_file_example_paths = detector.test_file_example_paths

KnapsackPro::TestFilesWithTestCasesComposer.call(test_files_to_run, slow_test_files, test_file_example_paths)
else
test_files_to_run
return KnapsackPro::TestFilesWithTestCasesComposer.call(test_files_to_run, slow_test_files, test_file_cases)
end

test_files_to_run
end

private
Expand Down
12 changes: 12 additions & 0 deletions spec/knapsack_pro/adapters/base_adapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -42,6 +42,18 @@
end
end

describe '.split_by_test_cases_enabled?' do
subject { described_class.split_by_test_cases_enabled? }

it { expect(subject).to be false }
end

describe '.test_file_cases_for' do
subject { described_class.test_file_cases_for([]) }

it { expect { subject }.to raise_error NotImplementedError }
end

describe '.slow_test_file?' do
let(:adapter_class) { double }
let(:slow_test_files) do
Expand Down
75 changes: 75 additions & 0 deletions spec/knapsack_pro/adapters/rspec_adapter_spec.rb
Original file line number Diff line number Diff line change
Expand Up @@ -12,6 +12,81 @@
it_behaves_like 'adapter'
end

describe '.split_by_test_cases_enabled?' do
subject { described_class.split_by_test_cases_enabled? }

before do
expect(KnapsackPro::Config::Env).to receive(:rspec_split_by_test_examples?).and_return(rspec_split_by_test_examples_enabled)
end

context 'when the RSpec split by test examples is enabled' do
let(:rspec_split_by_test_examples_enabled) { true }

it { expect(subject).to be true }

context 'when the RSpec version is < 3.3.0' do
before do
stub_const('RSpec::Core::Version::STRING', '3.2.0')
end

it do
expect { subject }.to raise_error RuntimeError, 'RSpec >= 3.3.0 is required to split test files by test examples. Learn more: https://knapsackpro.com/perma/ruby/split-by-test-examples'
end
end
end

context 'when the RSpec split by test examples is disabled' do
let(:rspec_split_by_test_examples_enabled) { false }

it { expect(subject).to be false }
end
end

describe '.test_file_cases_for' do
let(:slow_test_files) do
[
'1_spec.rb',
'2_spec.rb',
'3_spec.rb',
'4_spec.rb',
'5_spec.rb',
]
end

subject { described_class.test_file_cases_for(slow_test_files) }

before do
logger = instance_double(Logger)
expect(KnapsackPro).to receive(:logger).and_return(logger)
expect(logger).to receive(:info).with("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual test cases). Thanks to that, a single slow test file can be split across parallel CI nodes. Analyzing 5 slow test files.")

cmd = 'RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector'
expect(Kernel).to receive(:system).with(cmd).and_return(cmd_result)
end

context 'when the rake task to detect RSpec test examples succeeded' do
let(:cmd_result) { true }

it 'returns test example paths for slow test files' do
rspec_test_example_detector = instance_double(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector)
expect(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector).to receive(:new).and_return(rspec_test_example_detector)

test_file_example_paths = double
expect(rspec_test_example_detector).to receive(:test_file_example_paths).and_return(test_file_example_paths)

expect(subject).to eq test_file_example_paths
end
end

context 'when the rake task to detect RSpec test examples failed' do
let(:cmd_result) { false }

it do
expect { subject }.to raise_error(RuntimeError, 'Could not generate JSON report for RSpec. Rake task failed when running RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector')
end
end
end

describe '.ensure_no_tag_option_when_rspec_split_by_test_examples_enabled!' do
let(:cli_args) { double }

Expand Down
70 changes: 22 additions & 48 deletions spec/knapsack_pro/base_allocator_builder_spec.rb
Copy link
Copy Markdown
Contributor

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Are we covering the empty?: true case?

Copy link
Copy Markdown
Member Author

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Yes. It's covered now.

Original file line number Diff line number Diff line change
Expand Up @@ -117,8 +117,8 @@
describe '#fast_and_slow_test_files_to_run' do
subject { allocator_builder.fast_and_slow_test_files_to_run }

context 'when looking for test files on disk by default' do
it do
context 'when split by test cases disabled' do
it 'returns test files to run based on test files on the disk' do
test_file_pattern = double
expect(KnapsackPro::TestFilePattern).to receive(:call).with(adapter_class).and_return(test_file_pattern)

Expand All @@ -129,70 +129,44 @@
end
end

context 'when RSpec adapter AND rspec split by test examples is enabled' do
let(:adapter_class) { KnapsackPro::Adapters::RSpecAdapter }
context 'when split by test cases enabled' do
let(:test_files_to_run) { double }
let(:cmd) { 'RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector' }

before do
expect(KnapsackPro::Config::Env).to receive(:rspec_split_by_test_examples?).and_return(true)
before do
expect(adapter_class).to receive(:split_by_test_cases_enabled?).and_return(true)

test_file_pattern = double
expect(KnapsackPro::TestFilePattern).to receive(:call).with(adapter_class).and_return(test_file_pattern)

expect(KnapsackPro::TestFileFinder).to receive(:call).with(test_file_pattern).and_return(test_files_to_run)
end

context 'when RSpec version < 3.3.0' do
before do
stub_const('RSpec::Core::Version::STRING', '3.2.0')
end

it do
expect { subject }.to raise_error RuntimeError, 'RSpec >= 3.3.0 is required to split test files by test examples. Learn more: https://knapsackpro.com/perma/ruby/split-by-test-examples'
end
expect(allocator_builder).to receive(:get_slow_test_files).and_return(slow_test_files)
end

context 'when rake task to detect RSpec test examples works' do
let(:slow_test_files) { double(size: 5) }
let(:cmd_result) { true }
let(:test_file_example_paths) { double }
let(:logger) { instance_double(Logger) }
let(:test_files_with_test_cases) { double }

before do
expect(allocator_builder).to receive(:get_slow_test_files).and_return(slow_test_files)

expect(KnapsackPro).to receive(:logger).and_return(logger)

expect(Kernel).to receive(:system).with(cmd).and_return(cmd_result)

rspec_test_example_detector = instance_double(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector)
expect(KnapsackPro::TestCaseDetectors::RSpecTestExampleDetector).to receive(:new).and_return(rspec_test_example_detector)
expect(rspec_test_example_detector).to receive(:test_file_example_paths).and_return(test_file_example_paths)

expect(KnapsackPro::TestFilesWithTestCasesComposer).to receive(:call).with(test_files_to_run, slow_test_files, test_file_example_paths).and_return(test_files_with_test_cases)
context 'when slow test files are detected' do
let(:slow_test_files) do
[
'1_spec.rb',
'2_spec.rb',
]
end

it do
expect(logger).to receive(:info).with("Generating RSpec test examples JSON report for slow test files to prepare it to be split by test examples (by individual 'it's. Thanks to that a single slow test file can be split across parallel CI nodes). Analyzing 5 slow test files.")
it 'returns test files with test cases' do
test_file_cases = double
expect(adapter_class).to receive(:test_file_cases_for).with(slow_test_files).and_return(test_file_cases)

test_files_with_test_cases = double
expect(KnapsackPro::TestFilesWithTestCasesComposer).to receive(:call).with(test_files_to_run, slow_test_files, test_file_cases).and_return(test_files_with_test_cases)

expect(subject).to eq test_files_with_test_cases
end
end

context 'when rake task to detect RSpec test examples failed' do
let(:slow_test_files) { double(size: 5) }
let(:cmd_result) { false }

before do
expect(allocator_builder).to receive(:get_slow_test_files).and_return(slow_test_files)

expect(Kernel).to receive(:system).with(cmd).and_return(cmd_result)
end
context 'when slow test files are not detected' do
let(:slow_test_files) { [] }

it do
expect { subject }.to raise_error(RuntimeError, 'Could not generate JSON report for RSpec. Rake task failed when running RACK_ENV=test RAILS_ENV=test bundle exec rake knapsack_pro:rspec_test_example_detector')
it 'returns test files without test cases' do
expect(subject).to eq test_files_to_run
end
end
end
Expand Down