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.



247
248
249
250
251
252
253
254
# 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

Instance Attribute Details

#experimentObject (readonly)

Returns the value of attribute experiment.



245
246
247
# File 'lib/fast/experiment.rb', line 245

def experiment
  @experiment
end

#fail_experimentsObject (readonly)

Returns the value of attribute fail_experiments.



245
246
247
# File 'lib/fast/experiment.rb', line 245

def fail_experiments
  @fail_experiments
end

#ok_experimentsObject (readonly)

Returns the value of attribute ok_experiments.



245
246
247
# File 'lib/fast/experiment.rb', line 245

def ok_experiments
  @ok_experiments
end

Instance Method Details

#build_combinationsObject

Increase the ‘@round` by 1 to Fast::ExperimentCombinations#generate_combinations.



338
339
340
341
342
343
344
345
346
# 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

#done!Object



326
327
328
329
330
331
332
333
334
335
# 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

#experimental_filename(combination) ⇒ String

Returns with a derived name with the combination number.

Returns:

  • (String)

    with a derived name with the combination number.



262
263
264
265
266
267
# 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

#failed_with(combination) ⇒ void

This method returns an undefined value.

Track failed experiments to avoid run them again.



284
285
286
# File 'lib/fast/experiment.rb', line 284

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



273
274
275
276
277
278
279
280
# 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

#partial_replace(*indices) ⇒ void

This method returns an undefined value.

rubocop:disable Metrics/MethodLength

Execute partial replacements generating new file with the content replaced.



298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
# 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

#runObject



348
349
350
351
352
353
354
355
356
357
358
359
360
# 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

#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.



365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
# 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

#searchString

Returns:



257
258
259
# File 'lib/fast/experiment.rb', line 257

def search
  experiment.expression
end

#search_casesArray<Astrolabe::Node>

Returns:

  • (Array<Astrolabe::Node>)


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

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



320
321
322
323
324
# 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