gitlab-crystalball 0.7.1
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/LICENSE +22 -0
- data/README.md +56 -0
- data/bin/crystalball +5 -0
- data/lib/crystalball/active_record.rb +4 -0
- data/lib/crystalball/example_group_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +56 -0
- data/lib/crystalball/extensions/git/base.rb +14 -0
- data/lib/crystalball/extensions/git/lib.rb +17 -0
- data/lib/crystalball/extensions/git.rb +4 -0
- data/lib/crystalball/factory_bot.rb +3 -0
- data/lib/crystalball/git_repo.rb +53 -0
- data/lib/crystalball/logging.rb +51 -0
- data/lib/crystalball/map_compactor/example_context.rb +29 -0
- data/lib/crystalball/map_compactor/example_groups_data_compactor.rb +66 -0
- data/lib/crystalball/map_compactor.rb +44 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +45 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +45 -0
- data/lib/crystalball/map_generator/base_strategy.rb +22 -0
- data/lib/crystalball/map_generator/configuration.rb +58 -0
- data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
- data/lib/crystalball/map_generator/coverage_strategy.rb +35 -0
- data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +30 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +27 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +25 -0
- data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
- data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -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/object_sources_detector.rb +54 -0
- data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
- data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
- data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
- data/lib/crystalball/map_generator.rb +84 -0
- data/lib/crystalball/map_storage/yaml_storage.rb +69 -0
- data/lib/crystalball/prediction.rb +35 -0
- data/lib/crystalball/predictor/associated_specs.rb +45 -0
- data/lib/crystalball/predictor/helpers/affected_example_groups_detector.rb +20 -0
- data/lib/crystalball/predictor/helpers/path_formatter.rb +18 -0
- data/lib/crystalball/predictor/modified_execution_paths.rb +27 -0
- data/lib/crystalball/predictor/modified_specs.rb +33 -0
- data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
- data/lib/crystalball/predictor/strategy.rb +16 -0
- data/lib/crystalball/predictor.rb +58 -0
- data/lib/crystalball/predictor_evaluator.rb +55 -0
- data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser/active_record.rb +21 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb +27 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +89 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
- data/lib/crystalball/rails/predictor/modified_schema.rb +81 -0
- data/lib/crystalball/rails/tables_map.rb +53 -0
- data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
- data/lib/crystalball/rails/tables_map_generator.rb +84 -0
- data/lib/crystalball/rails.rb +11 -0
- data/lib/crystalball/rspec/filtering.rb +52 -0
- data/lib/crystalball/rspec/prediction_builder.rb +53 -0
- data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
- data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
- data/lib/crystalball/rspec/runner/configuration.rb +80 -0
- data/lib/crystalball/rspec/runner.rb +107 -0
- data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
- data/lib/crystalball/source_diff/file_diff.rb +53 -0
- data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
- data/lib/crystalball/source_diff.rb +48 -0
- data/lib/crystalball/version.rb +5 -0
- data/lib/crystalball.rb +44 -0
- metadata +314 -0
@@ -0,0 +1,35 @@
|
|
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 unless Coverage.running?
|
21
|
+
end
|
22
|
+
|
23
|
+
# Adds to the example_map's used files the ones the ones in which
|
24
|
+
# the coverage has changed after the tests runs.
|
25
|
+
# @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
|
26
|
+
# @param [RSpec::Core::Example] example - a RSpec example
|
27
|
+
def call(example_map, example)
|
28
|
+
before = Coverage.peek_result
|
29
|
+
yield example_map, example
|
30
|
+
after = Coverage.peek_result
|
31
|
+
example_map.push(*execution_detector.detect(before, after))
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
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::ExampleGroupMap] example_map - object holding example metadata and used files
|
25
|
+
# @param [RSpec::Core::Example] example - a RSpec example
|
26
|
+
def call(example_map, example)
|
27
|
+
yield example_map, example
|
28
|
+
|
29
|
+
described_class = example.metadata[:described_class]
|
30
|
+
|
31
|
+
example_map.push(*execution_detector.detect([described_class])) if described_class
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
module DSLPatch
|
7
|
+
# This module is used to fetch file path with factory definition from callstack
|
8
|
+
module FactoryPathFetcher
|
9
|
+
# Fetches file path with factory definition from callstack
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
def self.fetch
|
13
|
+
factories_definition_paths = FactoryBotStrategy
|
14
|
+
.factory_bot_constant
|
15
|
+
.definition_file_paths
|
16
|
+
.map { |path| Pathname(path).expand_path.to_s }
|
17
|
+
|
18
|
+
factory_definition_call = caller.find do |method_call|
|
19
|
+
factories_definition_paths.any? do |path|
|
20
|
+
method_call.start_with?(path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
factory_definition_call.split(":").first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher"
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
class MapGenerator
|
7
|
+
class FactoryBotStrategy
|
8
|
+
# Module to add new `factory` method to FactoryBot::Syntax::Default::DSL and FactoryBot::Syntax::Default::ModifyDSL
|
9
|
+
module DSLPatch
|
10
|
+
class << self
|
11
|
+
# Patches `FactoryBot::Syntax::Default::DSL#factory` and `FactoryBot::Syntax::Default::ModifyDSL#factory`.
|
12
|
+
def apply!
|
13
|
+
classes_to_patch.each { |klass| klass.prepend DSLPatch }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def classes_to_patch
|
19
|
+
[
|
20
|
+
FactoryBotStrategy.factory_bot_constant::Syntax::Default::DSL,
|
21
|
+
FactoryBotStrategy.factory_bot_constant::Syntax::Default::ModifyDSL
|
22
|
+
]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Overrides `FactoryBot::Syntax::Default::DSL#factory` and `FactoryBot::Syntax::Default::ModifyDSL#factory`.
|
27
|
+
# Pushes path of a factory to `FactoryBotStrategy.factory_definitions` and calls original `factory`
|
28
|
+
def factory(*args, &block)
|
29
|
+
factory_path = FactoryPathFetcher.fetch
|
30
|
+
name = args.first.to_s
|
31
|
+
|
32
|
+
FactoryBotStrategy.factory_definitions[name] ||= []
|
33
|
+
FactoryBotStrategy.factory_definitions[name] << factory_path
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
# A helper module to load `factory_bot` or `factory_girl`
|
7
|
+
module FactoryGemLoader
|
8
|
+
class << self
|
9
|
+
NAMES = %w[factory_bot factory_girl].freeze
|
10
|
+
|
11
|
+
# Tries to require `factory_bot` first. Requires `factory_girl` if `factory_bot` is not available
|
12
|
+
# Raises `LoadError` if both of them are not available.
|
13
|
+
def require!
|
14
|
+
NAMES.any? do |factory_gem_name|
|
15
|
+
begin
|
16
|
+
require factory_gem_name
|
17
|
+
true
|
18
|
+
rescue LoadError
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end || (raise LoadError, "Can't load `factory_bot` or `factory_girl`")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
# Module to add new `run` method to FactoryBot::FactoryRunner
|
7
|
+
module FactoryRunnerPatch
|
8
|
+
class << self
|
9
|
+
# Patches `FactoryBot::FactoryRunner#run`.
|
10
|
+
def apply!
|
11
|
+
FactoryBotStrategy.factory_bot_constant::FactoryRunner.prepend FactoryRunnerPatch
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Overrides `FactoryBot::FactoryRunner#run`. Pushes factory name to
|
16
|
+
# `FactoryBotStrategy.used_factories` and calls original `run`
|
17
|
+
def run(*)
|
18
|
+
factory = FactoryBotStrategy.factory_bot_constant.factory_by_name(@name)
|
19
|
+
FactoryBotStrategy.used_factories << factory.name.to_s
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,59 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/map_generator/factory_bot_strategy/factory_gem_loader"
|
4
|
+
|
5
|
+
Crystalball::MapGenerator::FactoryBotStrategy::FactoryGemLoader.require!
|
6
|
+
|
7
|
+
require "crystalball/map_generator/base_strategy"
|
8
|
+
require "crystalball/map_generator/helpers/path_filter"
|
9
|
+
require "crystalball/map_generator/factory_bot_strategy/dsl_patch"
|
10
|
+
require "crystalball/map_generator/factory_bot_strategy/factory_runner_patch"
|
11
|
+
|
12
|
+
module Crystalball
|
13
|
+
class MapGenerator
|
14
|
+
# Map generator strategy to include list of strategies which was used in an example.
|
15
|
+
class FactoryBotStrategy
|
16
|
+
include ::Crystalball::MapGenerator::BaseStrategy
|
17
|
+
include ::Crystalball::MapGenerator::Helpers::PathFilter
|
18
|
+
|
19
|
+
class << self
|
20
|
+
def factory_bot_constant
|
21
|
+
defined?(::FactoryBot) ? ::FactoryBot : ::FactoryGirl
|
22
|
+
end
|
23
|
+
|
24
|
+
# List of factories used by current example
|
25
|
+
#
|
26
|
+
# @return [Array<String>]
|
27
|
+
def used_factories
|
28
|
+
@used_factories ||= []
|
29
|
+
end
|
30
|
+
|
31
|
+
# Map of factories to files
|
32
|
+
#
|
33
|
+
# @return [Hash<String, String>]
|
34
|
+
def factory_definitions
|
35
|
+
@factory_definitions ||= {}
|
36
|
+
end
|
37
|
+
|
38
|
+
# Reset cached list of factories
|
39
|
+
def reset_used_factories
|
40
|
+
@used_factories = []
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
def after_register
|
45
|
+
DSLPatch.apply!
|
46
|
+
FactoryRunnerPatch.apply!
|
47
|
+
end
|
48
|
+
|
49
|
+
# Adds factories related to the spec to the map
|
50
|
+
# @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
|
51
|
+
# @param [RSpec::Core::Example] example - a RSpec example
|
52
|
+
def call(example_map, example)
|
53
|
+
self.class.reset_used_factories
|
54
|
+
yield example_map, example
|
55
|
+
example_map.push(*filter(self.class.used_factories.flat_map { |f| self.class.factory_definitions[f] }))
|
56
|
+
end
|
57
|
+
end
|
58
|
+
end
|
59
|
+
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,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
|
@@ -0,0 +1,36 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class ObjectSourcesDetector
|
6
|
+
# Class to get full hierarchy of a module(including singleton_class)
|
7
|
+
class HierarchyFetcher
|
8
|
+
attr_reader :stop_modules
|
9
|
+
|
10
|
+
# @param [Array<String>] stop_modules - list of classes or modules which
|
11
|
+
# will be used to stop hierarchy lookup
|
12
|
+
def initialize(stop_modules = [])
|
13
|
+
@stop_modules = stop_modules
|
14
|
+
end
|
15
|
+
|
16
|
+
# @param [Module] mod - the module for which to fetch the ancestors
|
17
|
+
# @return [Array<Module>] list of ancestors of a module
|
18
|
+
def ancestors_for(mod)
|
19
|
+
(pick_ancestors(mod) + pick_ancestors(mod.singleton_class)).uniq
|
20
|
+
end
|
21
|
+
|
22
|
+
private
|
23
|
+
|
24
|
+
def stop_consts
|
25
|
+
@stop_consts ||= stop_modules.map { |str| Object.const_get(str) }
|
26
|
+
end
|
27
|
+
|
28
|
+
def pick_ancestors(mod)
|
29
|
+
ancestors = mod.ancestors
|
30
|
+
index = ancestors.index { |k| stop_consts.include?(k) } || ancestors.size
|
31
|
+
ancestors[0...index]
|
32
|
+
end
|
33
|
+
end
|
34
|
+
end
|
35
|
+
end
|
36
|
+
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,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "parser/current"
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
class MapGenerator
|
7
|
+
class ParserStrategy
|
8
|
+
# Parses the given source files and adds the class and module definitions
|
9
|
+
# to the `consts_defined` array.
|
10
|
+
class Processor
|
11
|
+
def consts_defined_in(path)
|
12
|
+
self.current_scope = nil
|
13
|
+
self.consts_defined = []
|
14
|
+
parse_and_process(path)
|
15
|
+
consts_defined
|
16
|
+
end
|
17
|
+
|
18
|
+
def consts_interacted_with_in(path)
|
19
|
+
self.current_scope = nil
|
20
|
+
self.consts_interacted_with = []
|
21
|
+
parse_and_process(path)
|
22
|
+
consts_interacted_with
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
attr_accessor :consts_defined, :consts_interacted_with, :current_scope
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
const = filtered_children(node).detect { |c| c.type == :const }
|
33
|
+
return unless const
|
34
|
+
|
35
|
+
add_constant_interacted(qualified_name_from_node(const), nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_casgn(node)
|
39
|
+
namespace, name, = node.children
|
40
|
+
scope_name = namespace ? qualified_name(qualified_name_from_node(namespace), current_scope) : current_scope
|
41
|
+
add_constant_defined(name, scope_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_class(node)
|
45
|
+
const, superclass, body = node.children
|
46
|
+
add_constant_interacted(qualified_name_from_node(superclass), current_scope) if superclass
|
47
|
+
process_class_or_module(const, body)
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_module(node)
|
51
|
+
const, body = node.children
|
52
|
+
process_class_or_module(const, body)
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_class_or_module(const, body)
|
56
|
+
const_name = qualified_name_from_node(const)
|
57
|
+
result = add_constant_defined(const_name, current_scope)
|
58
|
+
self.current_scope = result if body && nested_consts?(body)
|
59
|
+
end
|
60
|
+
|
61
|
+
def nested_consts?(node)
|
62
|
+
filtered_children(node).any? { |c| %i[const casgn].include?(c.type) || nested_consts?(c) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def filtered_children(node)
|
66
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
67
|
+
|
68
|
+
node.children.grep(Parser::AST::Node)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_and_process(path)
|
72
|
+
node = Parser::CurrentRuby.parse(File.read(path))
|
73
|
+
process_node_and_children(node)
|
74
|
+
rescue Parser::SyntaxError
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [AST::Node, nil] node
|
79
|
+
# @return [String, nil]
|
80
|
+
def process(node)
|
81
|
+
return if node.nil?
|
82
|
+
|
83
|
+
on_handler = :"on_#{node.type}"
|
84
|
+
__send__(on_handler, node) if respond_to?(on_handler, true)
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_node_and_children(node)
|
88
|
+
process(node)
|
89
|
+
filtered_children(node).each do |child|
|
90
|
+
process_node_and_children(child)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_constant_defined(name, scope)
|
95
|
+
add_constant(name, scope, collection: consts_defined)
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_constant_interacted(name, scope)
|
99
|
+
add_constant(name, scope, collection: consts_interacted_with)
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_constant(name, scope, collection:)
|
103
|
+
collection ||= []
|
104
|
+
qualified_const_name = qualified_name(name, scope)
|
105
|
+
collection << qualified_const_name
|
106
|
+
qualified_const_name
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Parser::AST::Node] node - :const node in format s(:const, scope, :ConstName)
|
110
|
+
# where scope can be `nil` or another :const node.
|
111
|
+
# For example, `Foo::Bar` is represented as `s(:const, s(:const, nil, :Foo), :Bar)`
|
112
|
+
def qualified_name_from_node(node)
|
113
|
+
return unless node.is_a?(Parser::AST::Node)
|
114
|
+
|
115
|
+
scope, name = node.to_a
|
116
|
+
return name.to_s unless scope
|
117
|
+
|
118
|
+
qualified_name(name, qualified_name_from_node(scope))
|
119
|
+
end
|
120
|
+
|
121
|
+
def qualified_name(name, scope = nil)
|
122
|
+
return "#{scope.sub(/\A::/, '')}::#{name}" if scope
|
123
|
+
|
124
|
+
name.to_s
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/map_generator/parser_strategy/processor"
|
4
|
+
require "crystalball/map_generator/helpers/path_filter"
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
class MapGenerator
|
8
|
+
# Map generator strategy based on parsing source files to detect constant definition
|
9
|
+
# and tracing method calls on those constants.
|
10
|
+
class ParserStrategy
|
11
|
+
include BaseStrategy
|
12
|
+
include Helpers::PathFilter
|
13
|
+
|
14
|
+
attr_reader :const_definition_paths
|
15
|
+
|
16
|
+
def initialize(root = Dir.pwd, pattern:)
|
17
|
+
@root_path = Pathname.new(root).realpath.to_s
|
18
|
+
@processor = Processor.new
|
19
|
+
@const_definition_paths = {}
|
20
|
+
@pattern = pattern
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_register
|
24
|
+
files_to_inspect.each do |path|
|
25
|
+
processor.consts_defined_in(path).each do |const|
|
26
|
+
(const_definition_paths[const] ||= []) << path
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Parses the current example group map seeking calls to class methods and adds
|
32
|
+
# the classes to the map.
|
33
|
+
# @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
|
34
|
+
# @param [RSpec::Core::Example] example - a RSpec example
|
35
|
+
def call(example_map, example)
|
36
|
+
paths = []
|
37
|
+
yield example_map, example
|
38
|
+
example_map.each do |path|
|
39
|
+
next unless path.end_with?(".rb")
|
40
|
+
|
41
|
+
used_consts = processor.consts_interacted_with_in(path)
|
42
|
+
paths.push(*used_files(used_consts))
|
43
|
+
end
|
44
|
+
example_map.push(*filter(paths))
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :processor, :pattern, :root_path
|
50
|
+
|
51
|
+
def used_files(used_consts)
|
52
|
+
const_definition_paths.select { |k, _| Array(used_consts).include?(k) }.values.flatten
|
53
|
+
end
|
54
|
+
|
55
|
+
def files_to_inspect
|
56
|
+
Dir.glob(File.join(root_path, "**/*.rb")).grep(pattern)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
# Manages map generation strategies
|
6
|
+
class StrategiesCollection
|
7
|
+
include Enumerable
|
8
|
+
|
9
|
+
def initialize(strategies = [])
|
10
|
+
@strategies = strategies
|
11
|
+
end
|
12
|
+
|
13
|
+
# Calls every strategy on the given example group map and returns the modified example group map
|
14
|
+
# @param [Crystalball::ExampleGroupMap] example_group_map - initial example group map
|
15
|
+
# @return [Crystalball::ExampleGroupMap] example group map augmented by each strategy
|
16
|
+
def run(example_group_map, example, &block)
|
17
|
+
run_for_strategies(example_group_map, example, *_strategies.reverse, &block)
|
18
|
+
example_group_map
|
19
|
+
end
|
20
|
+
|
21
|
+
def method_missing(method_name, *args, &block)
|
22
|
+
_strategies.public_send(method_name, *args, &block) || super
|
23
|
+
end
|
24
|
+
|
25
|
+
def respond_to_missing?(method_name, *_args)
|
26
|
+
_strategies.respond_to?(method_name, false) || super
|
27
|
+
end
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def _strategies
|
32
|
+
@strategies
|
33
|
+
end
|
34
|
+
|
35
|
+
def run_for_strategies(example_group_map, example, *strats, &block)
|
36
|
+
return yield(example_group_map) if strats.empty?
|
37
|
+
|
38
|
+
strat = strats.shift
|
39
|
+
strat.call(example_group_map, example) { |c| run_for_strategies(c, example, *strats, &block) }
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|