Class: Fast::ExperimentFile

Inherits:
Object
  • Object
show all
Defined in:
lib/fast/experiment.rb

Overview

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.

Combines an 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.

Examples:

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

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

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 # => []

See Also:

Instance Attribute Summary collapse

Instance Method Summary collapse

Constructor Details

#initialize(file, experiment) ⇒ ExperimentFile

Returns a new instance of ExperimentFile.



252
253
254
255
256
257
258
259
# File 'lib/fast/experiment.rb', line 252

def initialize(file, experiment)
  @file = file
  @ast = Fast.ast_from_file(file) if file
  @experiment = experiment
  @ok_experiments = []
  @fail_experiments = []
  @round = 0
end

Instance Attribute Details

#experimentObject (readonly)

Returns the value of attribute experiment.



250
251
252
# File 'lib/fast/experiment.rb', line 250

def experiment
  @experiment
end

#fail_experimentsObject (readonly)

Returns the value of attribute fail_experiments.



250
251
252
# File 'lib/fast/experiment.rb', line 250

def fail_experiments
  @fail_experiments
end

#ok_experimentsObject (readonly)

Returns the value of attribute ok_experiments.



250
251
252
# File 'lib/fast/experiment.rb', line 250

def ok_experiments
  @ok_experiments
end

Instance Method Details

#build_combinationsObject



353
354
355
356
357
358
359
360
361
# File 'lib/fast/experiment.rb', line 353

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

#cleanup_generated_files!Object



331
332
333
334
335
# File 'lib/fast/experiment.rb', line 331

def cleanup_generated_files!
  Dir.glob(File.join(File.dirname(@file), "experiment_*_#{File.basename(@file)}")).each do |generated_file|
    File.delete(generated_file) if File.exist?(generated_file)
  end
end

#done!Object



337
338
339
340
341
342
343
344
345
346
347
348
349
350
# File 'lib/fast/experiment.rb', line 337

def done!
  count_executed_combinations = @fail_experiments.size + @ok_experiments.size
  puts "Done with #{@file} after #{count_executed_combinations} combinations"
  unless perfect_combination = @ok_experiments.last # rubocop:disable Lint/AssignmentInCondition
    cleanup_generated_files! if experiment.autoclean?
    return
  end

  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}`
  cleanup_generated_files! if experiment.autoclean?
end

#experimental_filename(combination) ⇒ String

Returns with a derived name with the combination number.

Returns:

  • (String)

    with a derived name with the combination number.



267
268
269
270
271
272
# File 'lib/fast/experiment.rb', line 267

def experimental_filename(combination)
  parts = @file.split('/')
  dir = parts[0..-2]
  filename = "experiment_#{[*combination].join('_')}_#{parts[-1]}"
  File.join(*dir, filename)
end

#failed_with(combination) ⇒ void

This method returns an undefined value.

Track failed experiments to avoid run them again.



289
290
291
# File 'lib/fast/experiment.rb', line 289

def failed_with(combination)
  @fail_experiments << combination
end

#ok_with(combination) ⇒ Object

Keep track of ok experiments depending on the current combination. It keep the combinations unique removing single replacements after the first round.

Returns:

  • void



278
279
280
281
282
283
284
285
# File 'lib/fast/experiment.rb', line 278

def ok_with(combination)
  @ok_experiments << combination
  return unless combination.is_a?(Array)

  combination.each do |element|
    @ok_experiments.delete(element)
  end
end

#partial_replace(*indices) ⇒ void

This method returns an undefined value.

rubocop:disable Metrics/MethodLength

Execute partial replacements generating new file with the content replaced.



303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
# File 'lib/fast/experiment.rb', line 303

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

#runObject



363
364
365
366
367
368
369
370
371
372
373
374
375
376
# File 'lib/fast/experiment.rb', line 363

def run
  cleanup_generated_files! if experiment.autoclean?
  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

#run_partial_replacement_with(combination) ⇒ Object

Writes a new file with partial replacements based on the current combination. Raise error if no changes was made with the given combination indices.

Parameters:

  • combination (Array<Integer>)

    to be replaced.



381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
# File 'lib/fast/experiment.rb', line 381

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}"
    File.delete(experimental_file) if experiment.autoclean? && File.exist?(experimental_file)
  end
end

#searchString

Returns:



262
263
264
# File 'lib/fast/experiment.rb', line 262

def search
  experiment.expression
end

#search_casesArray<Fast::Node>

Returns:



294
295
296
# File 'lib/fast/experiment.rb', line 294

def search_cases
  Fast.search(experiment.expression, @ast) || []
end

#write_experiment_file(combination, new_content) ⇒ Object

Write new file name depending on the combination

Parameters:

  • combination (Array<Integer>)
  • new_content (String)

    to be persisted



325
326
327
328
329
# File 'lib/fast/experiment.rb', line 325

def write_experiment_file(combination, new_content)
  filename = experimental_filename(combination)
  File.open(filename, 'w+') { |f| f.puts new_content }
  filename
end