diff --git a/lib/crystalball.rb b/lib/crystalball.rb index 502afd97..e71c25a3 100644 --- a/lib/crystalball.rb +++ b/lib/crystalball.rb @@ -11,6 +11,7 @@ require 'crystalball/predictor/modified_specs' require 'crystalball/predictor/modified_support_specs' require 'crystalball/predictor/associated_specs' +require 'crystalball/predictor/regex_specs' require 'crystalball/example_group_map' require 'crystalball/execution_map' require 'crystalball/map_generator' diff --git a/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb b/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb index cc7c18ed..ced75879 100644 --- a/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +++ b/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb @@ -15,7 +15,7 @@ def apply! # Overrides `FactoryBot::FactoryRunner#run`. Pushes factory name to # `FactoryBotStrategy.used_factories` and calls original `run` def run(*) - factory = FactoryBotStrategy.factory_bot_constant.factory_by_name(@name) + factory = FactoryBotStrategy.factory_bot_constant.factories.find(@name) FactoryBotStrategy.used_factories << factory.name.to_s super end diff --git a/lib/crystalball/predictor/regex_specs.rb b/lib/crystalball/predictor/regex_specs.rb new file mode 100644 index 00000000..6718cc00 --- /dev/null +++ b/lib/crystalball/predictor/regex_specs.rb @@ -0,0 +1,44 @@ +# frozen_string_literal: true + +require 'crystalball/predictor/strategy' + +module Crystalball + class Predictor + # This strategy is almost the same as associated_specs.rb, but the only difference is that the `to` parameter also accept regex. + # Used with `predictor.use Crystalball::Predictor::FilenamePatternSpecs.new(from: %r{models/(.*).rb}, to: "./spec/models/%s_spec.rb")`. + # When used will look for files matched to `from` regex and use captures to fill `to` regex to + # get paths of proper specs + class RegexSpecs + include Strategy + + # @param [file glob] scope - to find all the spec files scope to work with + # @param [Regexp] from - regular expression to match specific files and get proper captures + # @param [Regexp] to - regex in sprintf format to get proper files using captures of regexp + def initialize(scope:, from:, to:) + @scope = scope + @from = from + @to = to + end + + def call(diff, _map) + super do + regex_string = diff.map(&:relative_path).grep(from).map { |source_file_path| to % captures(source_file_path) } + regex_string.flat_map { |regex| Dir[scope].grep(Regexp.new regex)} + end + end + + private + + attr_reader :scope, :from, :to + + def captures(file_path) + match = file_path.match(from) + if match.names.any? + match.names.map(&:to_sym).zip(match.captures).to_h + else + match.captures + end + end + end + end +end diff --git a/lib/crystalball/rspec/runner.rb b/lib/crystalball/rspec/runner.rb index e124f735..d16e44e0 100644 --- a/lib/crystalball/rspec/runner.rb +++ b/lib/crystalball/rspec/runner.rb @@ -17,13 +17,22 @@ def run(args, err = $stderr, out = $stdout) Crystalball.log :info, "Crystalball starts to glow..." prediction = build_prediction - + dry_run?(prediction) + Crystalball.log :debug, "Prediction: #{prediction.first(5).join(' ')}#{'...' if prediction.size > 5}" Crystalball.log :info, "Starting RSpec." super(args + prediction, err, out) end + def dry_run?(prediction) + args = Hash[ ARGV.flat_map{|s| s.scan(/--?([^=\s]+)(?:=(\S+))?/) } ] + if args.key?('dry-run') + puts prediction.to_a + exit + end + end + def reset! self.prediction_builder = nil self.config = nil diff --git a/spec/data/file1/spec1_spec.rb b/spec/data/file1/spec1_spec.rb new file mode 100644 index 00000000..e69de29b diff --git a/spec/data/file1/spec2_spec.rb b/spec/data/file1/spec2_spec.rb new file mode 100644 index 00000000..e69de29b diff --git a/spec/data/file2/spec1_spec.rb b/spec/data/file2/spec1_spec.rb new file mode 100644 index 00000000..e69de29b diff --git a/spec/data/file2/spec2_spec.rb b/spec/data/file2/spec2_spec.rb new file mode 100644 index 00000000..e69de29b diff --git a/spec/map_generator/factory_bot_strategy/factory_runner_patch_spec.rb b/spec/map_generator/factory_bot_strategy/factory_runner_patch_spec.rb index 57f5e722..43b696d2 100644 --- a/spec/map_generator/factory_bot_strategy/factory_runner_patch_spec.rb +++ b/spec/map_generator/factory_bot_strategy/factory_runner_patch_spec.rb @@ -15,7 +15,7 @@ def run(*args, &block) end before do - class_double('FactoryBotConstant', factory_by_name: nil).as_stubbed_const + class_double('FactoryBotConstant').as_stubbed_const allow(Crystalball::MapGenerator::FactoryBotStrategy).to receive(:factory_bot_constant).and_return(FactoryBotConstant) end @@ -43,7 +43,7 @@ def run(*); end before do allow(Crystalball::MapGenerator::FactoryBotStrategy).to receive(:used_factories).and_return(used_factories) - allow(FactoryBotConstant).to receive(:factory_by_name).with(:bad_dummy) { double(name: :dummy) } + allow(FactoryBotConstant).to receive_message_chain(:factories, :find).with(:bad_dummy) { double(name: :dummy) } instance.instance_variable_set(:@name, :bad_dummy) end diff --git a/spec/predictor/regex_specs_spec.rb b/spec/predictor/regex_specs_spec.rb new file mode 100644 index 00000000..107da94b --- /dev/null +++ b/spec/predictor/regex_specs_spec.rb @@ -0,0 +1,45 @@ +# frozen_string_literal: true + +require 'spec_helper' + +describe Crystalball::Predictor::RegexSpecs do + subject(:predictor) { described_class.new scope: 'spec/data/**/*_spec.rb', from: %r{models/(?.*).rb}, to: 'spec/data/%s/(.*).rb' } + let(:path1) { 'models/file1.rb' } + let(:spec_file1_spec1) { 'spec/data/file1/spec1_spec.rb' } + let(:spec_file1_spec2) { 'spec/data/file1/spec2_spec.rb' } + let(:spec_file2_spec1) { 'spec/data/file2/spec1_spec.rb' } + let(:spec_file2_spec2) { 'spec/data/file2/spec1_spec.rb' } + let(:diff) { [double(relative_path: path1)] } + + describe '#call' do + subject { predictor.call(diff, {}) } + + it { is_expected.to eq(["./#{spec_file1_spec1}", "./#{spec_file1_spec2}"]) } + + context 'when path does not contain specs' do + let(:path1) { 'models/file3.rb' } + + it { is_expected.to eq([]) } + end + + context 'when path does not match "FROM" pattern' do + let(:path1) { 'lib/file3.rb' } + + it { is_expected.to eq([]) } + end + + context 'when path is out of scope' do + let(:predictor) { described_class.new scope: 'spec/data/file1/*_spec.rb', from: %r{models/(?.*).rb}, to: 'spec/data/%s/(.*).rb' } + let(:path1) { 'models/file2.rb' } + + it { is_expected.to eq([]) } + end + + context 'without named captures' do + let(:predictor) { described_class.new scope: 'spec', from: /Gemfile/, to: 'spec' } + let(:path1) { 'Gemfile' } + + it { is_expected.to eq ["./spec"] } + end + end +end