class Fast::ExperimentFile
Combines an {Fast::Experiment} with a specific file. It coordinates and regulate multiple replacements in the same file. Everytime it {#run} a file, it uses {#partial_replace} and generate a new file with the new content. It executes the {Fast::Experiment#policy} block yielding the new file. Depending on the policy result, it adds the occurrence to {#fail_experiments} or {#ok_experiments}. When all possible occurrences are replaced in isolated experiments, it #{build_combinations} with the winner experiments going to a next round of experiments with multiple partial replacements until find all possible combinations. @note it can easily spend days handling multiple one to one combinations,
because of that, after the first round of replacements the algorithm goes replacing all winner solutions in the same shot. If it fails, it goes combining one to one.
@see Fast::Experiment
@example Temporary spec to analyze
tempfile = Tempfile.new('some_spec.rb') tempfile.write <<~RUBY let(:user) { create(:user) } let(:address) { create(:address) } let(:phone_number) { create(:phone_number) } let(:country) { create(:country) } let(:language) { create(:language) } RUBY tempfile.close
@example Temporary experiment to replace create with build stubbed
experiment = Fast.experiment('RSpec/ReplaceCreateWithBuildStubbed') do lookup 'some_spec.rb' search '(send nil create)' edit { |node| replace(node.loc.selector, 'build_stubbed') } policy { |new_file| system("rspec --fail-fast #{new_file}") } end
@example ExperimentFile
exploring combinations and failures
experiment_file = Fast::ExperimentFile.new(tempfile.path, experiment) experiment_file.build_combinations # => [1, 2, 3, 4, 5] experiment_file.ok_with(1) experiment_file.failed_with(2) experiment_file.ok_with(3) experiment_file.ok_with(4) experiment_file.ok_with(5) # Try a combination of all OK individual replacements. experiment_file.build_combinations # => [[1, 3, 4, 5]] experiment_file.failed_with([1, 3, 4, 5]) # If the above failed, divide and conquer. experiment_file.build_combinations # => [[1, 3], [1, 4], [1, 5], [3, 4], [3, 5], [4, 5]] experiment_file.ok_with([1, 3]) experiment_file.failed_with([1, 4]) experiment_file.build_combinations # => [[4, 5], [1, 3, 4], [1, 3, 5]] experiment_file.failed_with([1, 3, 4]) experiment_file.build_combinations # => [[4, 5], [1, 3, 5]] experiment_file.failed_with([4, 5]) experiment_file.build_combinations # => [[1, 3, 5]] experiment_file.ok_with([1, 3, 5]) experiment_file.build_combinations # => []
Attributes
Public Class Methods
# File lib/fast/experiment.rb, line 247 def initialize(file, experiment) @file = file @ast = Fast.ast_from_file(file) if file @experiment = experiment @ok_experiments = [] @fail_experiments = [] @round = 0 end
Public Instance Methods
Increase the `@round` by 1 to {ExperimentCombinations#generate_combinations}.
# File lib/fast/experiment.rb, line 338 def build_combinations @round += 1 ExperimentCombinations.new( round: @round, occurrences_count: search_cases.size, ok_experiments: @ok_experiments, fail_experiments: @fail_experiments ).generate_combinations end
# File lib/fast/experiment.rb, line 326 def done! count_executed_combinations = @fail_experiments.size + @ok_experiments.size puts "Done with #{@file} after #{count_executed_combinations} combinations" return unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition puts 'The following changes were applied to the file:' `diff #{experimental_filename(perfect_combination)} #{@file}` puts "mv #{experimental_filename(perfect_combination)} #{@file}" `mv #{experimental_filename(perfect_combination)} #{@file}` end
@return [String] with a derived name with the combination number.
# File lib/fast/experiment.rb, line 262 def experimental_filename(combination) parts = @file.split('/') dir = parts[0..-2] filename = "experiment_#{[*combination].join('_')}_#{parts[-1]}" File.join(*dir, filename) end
Track failed experiments to avoid run them again. @return [void]
# File lib/fast/experiment.rb, line 284 def failed_with(combination) @fail_experiments << combination end
Keep track of ok experiments depending on the current combination. It keep the combinations unique removing single replacements after the first round. @return void
# File lib/fast/experiment.rb, line 273 def ok_with(combination) @ok_experiments << combination return unless combination.is_a?(Array) combination.each do |element| @ok_experiments.delete(element) end end
rubocop:disable Metrics/MethodLength
Execute partial replacements generating new file with the content replaced. @return [void]
# File lib/fast/experiment.rb, line 298 def partial_replace(*indices) replacement = experiment.replacement new_content = Fast.replace_file experiment.expression, @file do |node, *captures| if indices.nil? || indices.empty? || indices.include?(match_index) if replacement.parameters.length == 1 instance_exec node, &replacement else instance_exec node, *captures, &replacement end end end return unless new_content write_experiment_file(indices, new_content) new_content end
# File lib/fast/experiment.rb, line 348 def run while (combinations = build_combinations).any? if combinations.size > 1000 puts "Ignoring #{@file} because it has #{combinations.size} possible combinations" break end puts "#{@file} - Round #{@round} - Possible combinations: #{combinations.inspect}" while combination = combinations.shift # rubocop:disable Lint/AssignmentInCondition run_partial_replacement_with(combination) end end done! end
Writes a new file with partial replacements based on the current combination. Raise error if no changes was made with the given combination indices. @param [Array<Integer>] combination to be replaced.
# File lib/fast/experiment.rb, line 365 def run_partial_replacement_with(combination) content = partial_replace(*combination) experimental_file = experimental_filename(combination) File.open(experimental_file, 'w+') { |f| f.puts content } raise 'No changes were made to the file.' if FileUtils.compare_file(@file, experimental_file) result = experiment.ok_if.call(experimental_file) if result ok_with(combination) puts "✅ #{experimental_file} - Combination: #{combination}" else failed_with(combination) puts "🔴 #{experimental_file} - Combination: #{combination}" end end
@return [String] from {Fast::Experiment#expression}.
# File lib/fast/experiment.rb, line 257 def search experiment.expression end
@return [Array<Astrolabe::Node>]
# File lib/fast/experiment.rb, line 289 def search_cases Fast.search(experiment.expression, @ast) || [] end
Write new file name depending on the combination @param [Array<Integer>] combination @param [String] new_content to be persisted
# File lib/fast/experiment.rb, line 320 def write_experiment_file(combination, new_content) filename = experimental_filename(combination) File.open(filename, 'w+') { |f| f.puts new_content } filename end