crystalball 0.5.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +7 -0
 - data/.gitignore +19 -0
 - data/.rspec +3 -0
 - data/.rubocop.yml +18 -0
 - data/.travis.yml +20 -0
 - data/CODE_OF_CONDUCT.md +74 -0
 - data/Gemfile +6 -0
 - data/LICENSE +674 -0
 - data/README.md +196 -0
 - data/Rakefile +8 -0
 - data/bin/console +15 -0
 - data/bin/crystalball +5 -0
 - data/bin/setup +8 -0
 - data/crystalball.gemspec +48 -0
 - data/lib/crystalball.rb +38 -0
 - data/lib/crystalball/case_map.rb +19 -0
 - data/lib/crystalball/execution_map.rb +55 -0
 - data/lib/crystalball/git_repo.rb +58 -0
 - data/lib/crystalball/map_generator.rb +81 -0
 - data/lib/crystalball/map_generator/allocated_objects_strategy.rb +44 -0
 - data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +44 -0
 - data/lib/crystalball/map_generator/base_strategy.rb +21 -0
 - data/lib/crystalball/map_generator/configuration.rb +54 -0
 - data/lib/crystalball/map_generator/coverage_strategy.rb +34 -0
 - data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
 - data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
 - data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
 - data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
 - data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
 - data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
 - data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
 - data/lib/crystalball/map_storage/yaml_storage.rb +68 -0
 - data/lib/crystalball/prediction.rb +34 -0
 - data/lib/crystalball/predictor.rb +56 -0
 - data/lib/crystalball/predictor/associated_specs.rb +40 -0
 - data/lib/crystalball/predictor/modified_execution_paths.rb +21 -0
 - data/lib/crystalball/predictor/modified_specs.rb +27 -0
 - data/lib/crystalball/predictor_evaluator.rb +55 -0
 - data/lib/crystalball/rails.rb +10 -0
 - data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
 - data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
 - data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
 - data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +88 -0
 - data/lib/crystalball/rspec/prediction_builder.rb +43 -0
 - data/lib/crystalball/rspec/runner.rb +95 -0
 - data/lib/crystalball/rspec/runner/configuration.rb +70 -0
 - data/lib/crystalball/simple_predictor.rb +18 -0
 - data/lib/crystalball/source_diff.rb +37 -0
 - data/lib/crystalball/source_diff/file_diff.rb +53 -0
 - data/lib/crystalball/version.rb +5 -0
 - metadata +263 -0
 
| 
         @@ -0,0 +1,81 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 4 
     | 
    
         
            +
              # Class to generate execution map during RSpec build execution
         
     | 
| 
      
 5 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 6 
     | 
    
         
            +
                extend Forwardable
         
     | 
| 
      
 7 
     | 
    
         
            +
             
     | 
| 
      
 8 
     | 
    
         
            +
                attr_reader :configuration
         
     | 
| 
      
 9 
     | 
    
         
            +
                delegate %i[map_storage strategies dump_threshold map_class] => :configuration
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                class << self
         
     | 
| 
      
 12 
     | 
    
         
            +
                  # Registers Crystalball handlers to generate execution map during specs execution
         
     | 
| 
      
 13 
     | 
    
         
            +
                  #
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # @param [Proc] block to configure MapGenerator and Register strategies
         
     | 
| 
      
 15 
     | 
    
         
            +
                  def start!(&block)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    generator = new(&block)
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    ::RSpec.configure do |c|
         
     | 
| 
      
 19 
     | 
    
         
            +
                      c.before(:suite) { generator.start! }
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                      c.around(:each) { |e| generator.refresh_for_case(e) }
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                      c.after(:suite) { generator.finalize! }
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
                  end
         
     | 
| 
      
 26 
     | 
    
         
            +
                end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                def initialize
         
     | 
| 
      
 29 
     | 
    
         
            +
                  @configuration = Configuration.new
         
     | 
| 
      
 30 
     | 
    
         
            +
                  @configuration.commit = repo.object('HEAD').sha if repo
         
     | 
| 
      
 31 
     | 
    
         
            +
                  yield @configuration if block_given?
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                # Registers strategies and prepares metadata for execution map
         
     | 
| 
      
 35 
     | 
    
         
            +
                def start!
         
     | 
| 
      
 36 
     | 
    
         
            +
                  raise 'Repository is not pristine! Please stash all your changes' if repo && !repo.pristine?
         
     | 
| 
      
 37 
     | 
    
         
            +
             
     | 
| 
      
 38 
     | 
    
         
            +
                  self.map = nil
         
     | 
| 
      
 39 
     | 
    
         
            +
                  map_storage.clear!
         
     | 
| 
      
 40 
     | 
    
         
            +
                  map_storage.dump(map.metadata.to_h)
         
     | 
| 
      
 41 
     | 
    
         
            +
             
     | 
| 
      
 42 
     | 
    
         
            +
                  strategies.reverse.each(&:after_start)
         
     | 
| 
      
 43 
     | 
    
         
            +
                  self.started = true
         
     | 
| 
      
 44 
     | 
    
         
            +
                end
         
     | 
| 
      
 45 
     | 
    
         
            +
             
     | 
| 
      
 46 
     | 
    
         
            +
                # Runs example and collects execution map for it
         
     | 
| 
      
 47 
     | 
    
         
            +
                def refresh_for_case(example)
         
     | 
| 
      
 48 
     | 
    
         
            +
                  map << strategies.run(CaseMap.new(example), example) { example.run }
         
     | 
| 
      
 49 
     | 
    
         
            +
                  check_dump_threshold
         
     | 
| 
      
 50 
     | 
    
         
            +
                end
         
     | 
| 
      
 51 
     | 
    
         
            +
             
     | 
| 
      
 52 
     | 
    
         
            +
                # Finalizes strategies and saves map
         
     | 
| 
      
 53 
     | 
    
         
            +
                def finalize!
         
     | 
| 
      
 54 
     | 
    
         
            +
                  return unless started
         
     | 
| 
      
 55 
     | 
    
         
            +
             
     | 
| 
      
 56 
     | 
    
         
            +
                  strategies.each(&:before_finalize)
         
     | 
| 
      
 57 
     | 
    
         
            +
                  map_storage.dump(map.cases) if map.size.positive?
         
     | 
| 
      
 58 
     | 
    
         
            +
                end
         
     | 
| 
      
 59 
     | 
    
         
            +
             
     | 
| 
      
 60 
     | 
    
         
            +
                def map
         
     | 
| 
      
 61 
     | 
    
         
            +
                  @map ||= map_class.new(metadata: {commit: configuration.commit, version: configuration.version})
         
     | 
| 
      
 62 
     | 
    
         
            +
                end
         
     | 
| 
      
 63 
     | 
    
         
            +
             
     | 
| 
      
 64 
     | 
    
         
            +
                private
         
     | 
| 
      
 65 
     | 
    
         
            +
             
     | 
| 
      
 66 
     | 
    
         
            +
                attr_writer :map
         
     | 
| 
      
 67 
     | 
    
         
            +
                attr_accessor :started
         
     | 
| 
      
 68 
     | 
    
         
            +
             
     | 
| 
      
 69 
     | 
    
         
            +
                def repo
         
     | 
| 
      
 70 
     | 
    
         
            +
                  @repo = GitRepo.open('.') unless defined?(@repo)
         
     | 
| 
      
 71 
     | 
    
         
            +
                  @repo
         
     | 
| 
      
 72 
     | 
    
         
            +
                end
         
     | 
| 
      
 73 
     | 
    
         
            +
             
     | 
| 
      
 74 
     | 
    
         
            +
                def check_dump_threshold
         
     | 
| 
      
 75 
     | 
    
         
            +
                  return unless dump_threshold.positive? && map.size >= dump_threshold
         
     | 
| 
      
 76 
     | 
    
         
            +
             
     | 
| 
      
 77 
     | 
    
         
            +
                  map_storage.dump(map.cases)
         
     | 
| 
      
 78 
     | 
    
         
            +
                  map.clear!
         
     | 
| 
      
 79 
     | 
    
         
            +
                end
         
     | 
| 
      
 80 
     | 
    
         
            +
              end
         
     | 
| 
      
 81 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'crystalball/map_generator/base_strategy'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'crystalball/map_generator/object_sources_detector'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'crystalball/map_generator/allocated_objects_strategy/object_tracker'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 8 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Map generator strategy to get paths to files contains definition for all objects and its
         
     | 
| 
      
 10 
     | 
    
         
            +
                # ancestors allocated during test example.
         
     | 
| 
      
 11 
     | 
    
         
            +
                class AllocatedObjectsStrategy
         
     | 
| 
      
 12 
     | 
    
         
            +
                  include BaseStrategy
         
     | 
| 
      
 13 
     | 
    
         
            +
                  extend Forwardable
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  attr_reader :execution_detector, :object_tracker
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  delegate %i[after_register before_finalize] => :execution_detector
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def self.build(only: [], root: Dir.pwd)
         
     | 
| 
      
 20 
     | 
    
         
            +
                    hierarchy_fetcher = ObjectSourcesDetector::HierarchyFetcher.new(only)
         
     | 
| 
      
 21 
     | 
    
         
            +
                    execution_detector = ObjectSourcesDetector.new(root_path: root, hierarchy_fetcher: hierarchy_fetcher)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                    new(execution_detector: execution_detector, object_tracker: ObjectTracker.new(only_of: only))
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  # @param [#detect] execution_detector
         
     | 
| 
      
 27 
     | 
    
         
            +
                  # @param [#created_during] object_tracker
         
     | 
| 
      
 28 
     | 
    
         
            +
                  def initialize(execution_detector:, object_tracker:)
         
     | 
| 
      
 29 
     | 
    
         
            +
                    @object_tracker = object_tracker
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @execution_detector = execution_detector
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  # Adds to the affected files every file which contain the definition of the
         
     | 
| 
      
 34 
     | 
    
         
            +
                  # classes of the objects allocated during the spec execution.
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
         
     | 
| 
      
 36 
     | 
    
         
            +
                  def call(case_map, example)
         
     | 
| 
      
 37 
     | 
    
         
            +
                    classes = object_tracker.used_classes_during do
         
     | 
| 
      
 38 
     | 
    
         
            +
                      yield case_map, example
         
     | 
| 
      
 39 
     | 
    
         
            +
                    end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    case_map.push(*execution_detector.detect(classes))
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,44 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'set'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 6 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 7 
     | 
    
         
            +
                class AllocatedObjectsStrategy
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Class to list object classes used during a block
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class ObjectTracker
         
     | 
| 
      
 10 
     | 
    
         
            +
                    attr_reader :only_of
         
     | 
| 
      
 11 
     | 
    
         
            +
             
     | 
| 
      
 12 
     | 
    
         
            +
                    # @param [Array<Module>] only_of - classes or modules to watch on
         
     | 
| 
      
 13 
     | 
    
         
            +
                    def initialize(only_of: ['Object'])
         
     | 
| 
      
 14 
     | 
    
         
            +
                      @only_of = only_of
         
     | 
| 
      
 15 
     | 
    
         
            +
                      @created_object_classes = Set.new
         
     | 
| 
      
 16 
     | 
    
         
            +
                    end
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                    # @yield a block to execute
         
     | 
| 
      
 19 
     | 
    
         
            +
                    # @return [Array<Object>] classes of objects allocated during the block execution
         
     | 
| 
      
 20 
     | 
    
         
            +
                    def used_classes_during(&block)
         
     | 
| 
      
 21 
     | 
    
         
            +
                      self.created_object_classes = Set.new
         
     | 
| 
      
 22 
     | 
    
         
            +
                      trace_point.enable(&block)
         
     | 
| 
      
 23 
     | 
    
         
            +
                      created_object_classes
         
     | 
| 
      
 24 
     | 
    
         
            +
                    end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                    private
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    attr_accessor :created_object_classes
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                    def whitelisted_constants
         
     | 
| 
      
 31 
     | 
    
         
            +
                      @whitelisted_constants ||= only_of.map { |str| Object.const_get(str) }
         
     | 
| 
      
 32 
     | 
    
         
            +
                    end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                    def trace_point
         
     | 
| 
      
 35 
     | 
    
         
            +
                      @trace_point ||= TracePoint.new(:c_call) do |tp|
         
     | 
| 
      
 36 
     | 
    
         
            +
                        next unless tp.method_id == :new || tp.method_id == :allocate
         
     | 
| 
      
 37 
     | 
    
         
            +
                        next unless whitelisted_constants.any? { |c| tp.self <= c }
         
     | 
| 
      
 38 
     | 
    
         
            +
                        created_object_classes << tp.self
         
     | 
| 
      
 39 
     | 
    
         
            +
                      end
         
     | 
| 
      
 40 
     | 
    
         
            +
                    end
         
     | 
| 
      
 41 
     | 
    
         
            +
                  end
         
     | 
| 
      
 42 
     | 
    
         
            +
                end
         
     | 
| 
      
 43 
     | 
    
         
            +
              end
         
     | 
| 
      
 44 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,21 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 4 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 5 
     | 
    
         
            +
                # Map generator strategy interface
         
     | 
| 
      
 6 
     | 
    
         
            +
                module BaseStrategy
         
     | 
| 
      
 7 
     | 
    
         
            +
                  def after_register; end
         
     | 
| 
      
 8 
     | 
    
         
            +
             
     | 
| 
      
 9 
     | 
    
         
            +
                  def after_start; end
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                  def before_finalize; end
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  # Each strategy must implement #call augmenting the affected_files list and
         
     | 
| 
      
 14 
     | 
    
         
            +
                  # yielding back the CaseMap.
         
     | 
| 
      
 15 
     | 
    
         
            +
                  # @param [Crystalball::CaseMap] _case_map - object holding example metadata and affected files
         
     | 
| 
      
 16 
     | 
    
         
            +
                  def call(_case_map, _example)
         
     | 
| 
      
 17 
     | 
    
         
            +
                    raise NotImplementedError
         
     | 
| 
      
 18 
     | 
    
         
            +
                  end
         
     | 
| 
      
 19 
     | 
    
         
            +
                end
         
     | 
| 
      
 20 
     | 
    
         
            +
              end
         
     | 
| 
      
 21 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,54 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'crystalball/map_generator/strategies_collection'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 6 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 7 
     | 
    
         
            +
                # Configuration of map generator. Is can be accessed as a first argument inside
         
     | 
| 
      
 8 
     | 
    
         
            +
                # `Crystalball::MapGenerator.start! { |config| config } block.
         
     | 
| 
      
 9 
     | 
    
         
            +
                class Configuration
         
     | 
| 
      
 10 
     | 
    
         
            +
                  attr_writer :map_storage
         
     | 
| 
      
 11 
     | 
    
         
            +
                  attr_writer :map_class
         
     | 
| 
      
 12 
     | 
    
         
            +
                  attr_accessor :commit
         
     | 
| 
      
 13 
     | 
    
         
            +
                  attr_accessor :version
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  attr_reader :strategies
         
     | 
| 
      
 16 
     | 
    
         
            +
             
     | 
| 
      
 17 
     | 
    
         
            +
                  def initialize
         
     | 
| 
      
 18 
     | 
    
         
            +
                    @strategies = StrategiesCollection.new
         
     | 
| 
      
 19 
     | 
    
         
            +
                  end
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                  def map_class
         
     | 
| 
      
 22 
     | 
    
         
            +
                    @map_class ||= ExecutionMap
         
     | 
| 
      
 23 
     | 
    
         
            +
                  end
         
     | 
| 
      
 24 
     | 
    
         
            +
             
     | 
| 
      
 25 
     | 
    
         
            +
                  def map_storage_path
         
     | 
| 
      
 26 
     | 
    
         
            +
                    @map_storage_path ||= Pathname('execution_map.yml')
         
     | 
| 
      
 27 
     | 
    
         
            +
                  end
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                  def map_storage_path=(value)
         
     | 
| 
      
 30 
     | 
    
         
            +
                    @map_storage_path = Pathname(value)
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                  def map_storage
         
     | 
| 
      
 34 
     | 
    
         
            +
                    @map_storage ||= MapStorage::YAMLStorage.new(map_storage_path)
         
     | 
| 
      
 35 
     | 
    
         
            +
                  end
         
     | 
| 
      
 36 
     | 
    
         
            +
             
     | 
| 
      
 37 
     | 
    
         
            +
                  def dump_threshold
         
     | 
| 
      
 38 
     | 
    
         
            +
                    @dump_threshold ||= 100
         
     | 
| 
      
 39 
     | 
    
         
            +
                  end
         
     | 
| 
      
 40 
     | 
    
         
            +
             
     | 
| 
      
 41 
     | 
    
         
            +
                  def dump_threshold=(value)
         
     | 
| 
      
 42 
     | 
    
         
            +
                    @dump_threshold = value.to_i
         
     | 
| 
      
 43 
     | 
    
         
            +
                  end
         
     | 
| 
      
 44 
     | 
    
         
            +
             
     | 
| 
      
 45 
     | 
    
         
            +
                  # Register new strategy for map generation
         
     | 
| 
      
 46 
     | 
    
         
            +
                  #
         
     | 
| 
      
 47 
     | 
    
         
            +
                  # @param [Crystalball::MapGenerator::BaseStrategy] strategy
         
     | 
| 
      
 48 
     | 
    
         
            +
                  def register(strategy)
         
     | 
| 
      
 49 
     | 
    
         
            +
                    @strategies.push strategy
         
     | 
| 
      
 50 
     | 
    
         
            +
                    strategy.after_register
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,34 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'coverage'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'crystalball/map_generator/base_strategy'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'crystalball/map_generator/coverage_strategy/execution_detector'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 8 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Map generator strategy based on harvesting Coverage information during example execution
         
     | 
| 
      
 10 
     | 
    
         
            +
                class CoverageStrategy
         
     | 
| 
      
 11 
     | 
    
         
            +
                  include BaseStrategy
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  attr_reader :execution_detector
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def initialize(execution_detector = ExecutionDetector.new)
         
     | 
| 
      
 16 
     | 
    
         
            +
                    @execution_detector = execution_detector
         
     | 
| 
      
 17 
     | 
    
         
            +
                  end
         
     | 
| 
      
 18 
     | 
    
         
            +
             
     | 
| 
      
 19 
     | 
    
         
            +
                  def after_register
         
     | 
| 
      
 20 
     | 
    
         
            +
                    Coverage.start
         
     | 
| 
      
 21 
     | 
    
         
            +
                  end
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                  # Adds to the case_map's affected files the ones the ones in which
         
     | 
| 
      
 24 
     | 
    
         
            +
                  # the coverage has changed after the tests runs.
         
     | 
| 
      
 25 
     | 
    
         
            +
                  # @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
         
     | 
| 
      
 26 
     | 
    
         
            +
                  def call(case_map, _)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    before = Coverage.peek_result
         
     | 
| 
      
 28 
     | 
    
         
            +
                    yield case_map
         
     | 
| 
      
 29 
     | 
    
         
            +
                    after = Coverage.peek_result
         
     | 
| 
      
 30 
     | 
    
         
            +
                    case_map.push(*execution_detector.detect(before, after))
         
     | 
| 
      
 31 
     | 
    
         
            +
                  end
         
     | 
| 
      
 32 
     | 
    
         
            +
                end
         
     | 
| 
      
 33 
     | 
    
         
            +
              end
         
     | 
| 
      
 34 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,23 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require_relative '../helpers/path_filter'
         
     | 
| 
      
 4 
     | 
    
         
            +
             
     | 
| 
      
 5 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 6 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 7 
     | 
    
         
            +
                class CoverageStrategy
         
     | 
| 
      
 8 
     | 
    
         
            +
                  # Class for detecting code execution path based on coverage information diff
         
     | 
| 
      
 9 
     | 
    
         
            +
                  class ExecutionDetector
         
     | 
| 
      
 10 
     | 
    
         
            +
                    include ::Crystalball::MapGenerator::Helpers::PathFilter
         
     | 
| 
      
 11 
     | 
    
         
            +
                    # Detects files affected during example execution. Transforms absolute paths to relative.
         
     | 
| 
      
 12 
     | 
    
         
            +
                    # Exclude paths outside of repository
         
     | 
| 
      
 13 
     | 
    
         
            +
                    #
         
     | 
| 
      
 14 
     | 
    
         
            +
                    # @param[Array<String>] list of files affected before example execution
         
     | 
| 
      
 15 
     | 
    
         
            +
                    # @param[Array<String>] list of files affected after example execution
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # @return [Array<String>]
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def detect(before, after)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      filter after.reject { |file_name, after_coverage| before[file_name] == after_coverage }.keys
         
     | 
| 
      
 19 
     | 
    
         
            +
                    end
         
     | 
| 
      
 20 
     | 
    
         
            +
                  end
         
     | 
| 
      
 21 
     | 
    
         
            +
                end
         
     | 
| 
      
 22 
     | 
    
         
            +
              end
         
     | 
| 
      
 23 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,35 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'crystalball/map_generator/base_strategy'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'crystalball/map_generator/object_sources_detector'
         
     | 
| 
      
 5 
     | 
    
         
            +
             
     | 
| 
      
 6 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 7 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 8 
     | 
    
         
            +
                # Map generator strategy to get paths to files contains definition of described_class and its
         
     | 
| 
      
 9 
     | 
    
         
            +
                # ancestors.
         
     | 
| 
      
 10 
     | 
    
         
            +
                class DescribedClassStrategy
         
     | 
| 
      
 11 
     | 
    
         
            +
                  include BaseStrategy
         
     | 
| 
      
 12 
     | 
    
         
            +
                  extend Forwardable
         
     | 
| 
      
 13 
     | 
    
         
            +
             
     | 
| 
      
 14 
     | 
    
         
            +
                  attr_reader :execution_detector
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                  delegate %i[after_register before_finalize] => :execution_detector
         
     | 
| 
      
 17 
     | 
    
         
            +
             
     | 
| 
      
 18 
     | 
    
         
            +
                  # @param [#detect] execution_detector - object that, given a list of objects,
         
     | 
| 
      
 19 
     | 
    
         
            +
                  #   returns the paths where the classes or modules of the list are defined
         
     | 
| 
      
 20 
     | 
    
         
            +
                  def initialize(execution_detector: ObjectSourcesDetector.new(root_path: Dir.pwd))
         
     | 
| 
      
 21 
     | 
    
         
            +
                    @execution_detector = execution_detector
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
             
     | 
| 
      
 24 
     | 
    
         
            +
                  # @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
         
     | 
| 
      
 25 
     | 
    
         
            +
                  # @param [RSpec::Core::Example] example
         
     | 
| 
      
 26 
     | 
    
         
            +
                  def call(case_map, example)
         
     | 
| 
      
 27 
     | 
    
         
            +
                    yield case_map
         
     | 
| 
      
 28 
     | 
    
         
            +
             
     | 
| 
      
 29 
     | 
    
         
            +
                    described_class = example.metadata[:described_class]
         
     | 
| 
      
 30 
     | 
    
         
            +
             
     | 
| 
      
 31 
     | 
    
         
            +
                    case_map.push(*execution_detector.detect([described_class])) if described_class
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
                end
         
     | 
| 
      
 34 
     | 
    
         
            +
              end
         
     | 
| 
      
 35 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,25 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 4 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 5 
     | 
    
         
            +
                module Helpers
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Helper module to filter file paths
         
     | 
| 
      
 7 
     | 
    
         
            +
                  module PathFilter
         
     | 
| 
      
 8 
     | 
    
         
            +
                    attr_reader :root_path
         
     | 
| 
      
 9 
     | 
    
         
            +
             
     | 
| 
      
 10 
     | 
    
         
            +
                    # @param [String] root_path - absolute path to root folder of repository
         
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(root_path = Dir.pwd)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @root_path = root_path
         
     | 
| 
      
 13 
     | 
    
         
            +
                    end
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                    # @param [Array<String>] paths
         
     | 
| 
      
 16 
     | 
    
         
            +
                    # @return relatve paths inside root_path only
         
     | 
| 
      
 17 
     | 
    
         
            +
                    def filter(paths)
         
     | 
| 
      
 18 
     | 
    
         
            +
                      paths
         
     | 
| 
      
 19 
     | 
    
         
            +
                        .select { |file_name| file_name.start_with?(root_path) }
         
     | 
| 
      
 20 
     | 
    
         
            +
                        .map { |file_name| file_name.sub("#{root_path}/", '') }
         
     | 
| 
      
 21 
     | 
    
         
            +
                    end
         
     | 
| 
      
 22 
     | 
    
         
            +
                  end
         
     | 
| 
      
 23 
     | 
    
         
            +
                end
         
     | 
| 
      
 24 
     | 
    
         
            +
              end
         
     | 
| 
      
 25 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,54 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            require 'crystalball/map_generator/helpers/path_filter'
         
     | 
| 
      
 4 
     | 
    
         
            +
            require 'crystalball/map_generator/object_sources_detector/hierarchy_fetcher'
         
     | 
| 
      
 5 
     | 
    
         
            +
            require 'crystalball/map_generator/object_sources_detector/definition_tracer'
         
     | 
| 
      
 6 
     | 
    
         
            +
             
     | 
| 
      
 7 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 8 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 9 
     | 
    
         
            +
                # Class for files paths affected object definition
         
     | 
| 
      
 10 
     | 
    
         
            +
                class ObjectSourcesDetector
         
     | 
| 
      
 11 
     | 
    
         
            +
                  include ::Crystalball::MapGenerator::Helpers::PathFilter
         
     | 
| 
      
 12 
     | 
    
         
            +
             
     | 
| 
      
 13 
     | 
    
         
            +
                  attr_reader :definition_tracer, :hierarchy_fetcher
         
     | 
| 
      
 14 
     | 
    
         
            +
             
     | 
| 
      
 15 
     | 
    
         
            +
                  def initialize(
         
     | 
| 
      
 16 
     | 
    
         
            +
                    root_path:,
         
     | 
| 
      
 17 
     | 
    
         
            +
                    definition_tracer: DefinitionTracer.new(root_path),
         
     | 
| 
      
 18 
     | 
    
         
            +
                    hierarchy_fetcher: HierarchyFetcher.new
         
     | 
| 
      
 19 
     | 
    
         
            +
                  )
         
     | 
| 
      
 20 
     | 
    
         
            +
                    super(root_path)
         
     | 
| 
      
 21 
     | 
    
         
            +
             
     | 
| 
      
 22 
     | 
    
         
            +
                    @definition_tracer = definition_tracer
         
     | 
| 
      
 23 
     | 
    
         
            +
                    @hierarchy_fetcher = hierarchy_fetcher
         
     | 
| 
      
 24 
     | 
    
         
            +
                  end
         
     | 
| 
      
 25 
     | 
    
         
            +
             
     | 
| 
      
 26 
     | 
    
         
            +
                  def after_register
         
     | 
| 
      
 27 
     | 
    
         
            +
                    definition_tracer.start
         
     | 
| 
      
 28 
     | 
    
         
            +
                  end
         
     | 
| 
      
 29 
     | 
    
         
            +
             
     | 
| 
      
 30 
     | 
    
         
            +
                  def before_finalize
         
     | 
| 
      
 31 
     | 
    
         
            +
                    definition_tracer.stop
         
     | 
| 
      
 32 
     | 
    
         
            +
                  end
         
     | 
| 
      
 33 
     | 
    
         
            +
             
     | 
| 
      
 34 
     | 
    
         
            +
                  # Detects files affected during example execution. Transforms absolute paths to relative.
         
     | 
| 
      
 35 
     | 
    
         
            +
                  # Exclude paths outside of repository
         
     | 
| 
      
 36 
     | 
    
         
            +
                  #
         
     | 
| 
      
 37 
     | 
    
         
            +
                  # @param[Array<String>] list of files affected before example execution
         
     | 
| 
      
 38 
     | 
    
         
            +
                  # @return [Array<String>]
         
     | 
| 
      
 39 
     | 
    
         
            +
                  def detect(objects)
         
     | 
| 
      
 40 
     | 
    
         
            +
                    modules = objects.map do |object|
         
     | 
| 
      
 41 
     | 
    
         
            +
                      object.is_a?(Module) ? object : object.class
         
     | 
| 
      
 42 
     | 
    
         
            +
                    end.uniq
         
     | 
| 
      
 43 
     | 
    
         
            +
             
     | 
| 
      
 44 
     | 
    
         
            +
                    paths = modules.flat_map do |mod|
         
     | 
| 
      
 45 
     | 
    
         
            +
                      hierarchy_fetcher.ancestors_for(mod).flat_map do |ancestor|
         
     | 
| 
      
 46 
     | 
    
         
            +
                        definition_tracer.constants_definition_paths[ancestor]
         
     | 
| 
      
 47 
     | 
    
         
            +
                      end
         
     | 
| 
      
 48 
     | 
    
         
            +
                    end.compact.uniq
         
     | 
| 
      
 49 
     | 
    
         
            +
             
     | 
| 
      
 50 
     | 
    
         
            +
                    filter paths
         
     | 
| 
      
 51 
     | 
    
         
            +
                  end
         
     | 
| 
      
 52 
     | 
    
         
            +
                end
         
     | 
| 
      
 53 
     | 
    
         
            +
              end
         
     | 
| 
      
 54 
     | 
    
         
            +
            end
         
     | 
| 
         @@ -0,0 +1,39 @@ 
     | 
|
| 
      
 1 
     | 
    
         
            +
            # frozen_string_literal: true
         
     | 
| 
      
 2 
     | 
    
         
            +
             
     | 
| 
      
 3 
     | 
    
         
            +
            module Crystalball
         
     | 
| 
      
 4 
     | 
    
         
            +
              class MapGenerator
         
     | 
| 
      
 5 
     | 
    
         
            +
                class ObjectSourcesDetector
         
     | 
| 
      
 6 
     | 
    
         
            +
                  # Class to save paths to classes and modules definitions during code loading. Should be
         
     | 
| 
      
 7 
     | 
    
         
            +
                  # started as soon as possible. Use #constants_definition_paths to fetch traced info
         
     | 
| 
      
 8 
     | 
    
         
            +
                  class DefinitionTracer
         
     | 
| 
      
 9 
     | 
    
         
            +
                    attr_reader :trace_point, :constants_definition_paths, :root_path
         
     | 
| 
      
 10 
     | 
    
         
            +
             
     | 
| 
      
 11 
     | 
    
         
            +
                    def initialize(root_path)
         
     | 
| 
      
 12 
     | 
    
         
            +
                      @root_path = root_path
         
     | 
| 
      
 13 
     | 
    
         
            +
                      @constants_definition_paths = {}
         
     | 
| 
      
 14 
     | 
    
         
            +
                    end
         
     | 
| 
      
 15 
     | 
    
         
            +
             
     | 
| 
      
 16 
     | 
    
         
            +
                    def start
         
     | 
| 
      
 17 
     | 
    
         
            +
                      self.trace_point ||= TracePoint.new(:class) do |tp|
         
     | 
| 
      
 18 
     | 
    
         
            +
                        mod = tp.self
         
     | 
| 
      
 19 
     | 
    
         
            +
                        path = tp.path
         
     | 
| 
      
 20 
     | 
    
         
            +
             
     | 
| 
      
 21 
     | 
    
         
            +
                        next unless path&.start_with?(root_path)
         
     | 
| 
      
 22 
     | 
    
         
            +
             
     | 
| 
      
 23 
     | 
    
         
            +
                        constants_definition_paths[mod] ||= []
         
     | 
| 
      
 24 
     | 
    
         
            +
                        constants_definition_paths[mod] << path
         
     | 
| 
      
 25 
     | 
    
         
            +
                      end.tap(&:enable)
         
     | 
| 
      
 26 
     | 
    
         
            +
                    end
         
     | 
| 
      
 27 
     | 
    
         
            +
             
     | 
| 
      
 28 
     | 
    
         
            +
                    def stop
         
     | 
| 
      
 29 
     | 
    
         
            +
                      trace_point&.disable
         
     | 
| 
      
 30 
     | 
    
         
            +
                      self.trace_point = nil
         
     | 
| 
      
 31 
     | 
    
         
            +
                    end
         
     | 
| 
      
 32 
     | 
    
         
            +
             
     | 
| 
      
 33 
     | 
    
         
            +
                    private
         
     | 
| 
      
 34 
     | 
    
         
            +
             
     | 
| 
      
 35 
     | 
    
         
            +
                    attr_writer :trace_point, :constants_definition_paths
         
     | 
| 
      
 36 
     | 
    
         
            +
                  end
         
     | 
| 
      
 37 
     | 
    
         
            +
                end
         
     | 
| 
      
 38 
     | 
    
         
            +
              end
         
     | 
| 
      
 39 
     | 
    
         
            +
            end
         
     |