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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +56 -0
  4. data/bin/crystalball +5 -0
  5. data/lib/crystalball/active_record.rb +4 -0
  6. data/lib/crystalball/example_group_map.rb +19 -0
  7. data/lib/crystalball/execution_map.rb +56 -0
  8. data/lib/crystalball/extensions/git/base.rb +14 -0
  9. data/lib/crystalball/extensions/git/lib.rb +17 -0
  10. data/lib/crystalball/extensions/git.rb +4 -0
  11. data/lib/crystalball/factory_bot.rb +3 -0
  12. data/lib/crystalball/git_repo.rb +53 -0
  13. data/lib/crystalball/logging.rb +51 -0
  14. data/lib/crystalball/map_compactor/example_context.rb +29 -0
  15. data/lib/crystalball/map_compactor/example_groups_data_compactor.rb +66 -0
  16. data/lib/crystalball/map_compactor.rb +44 -0
  17. data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +45 -0
  18. data/lib/crystalball/map_generator/allocated_objects_strategy.rb +45 -0
  19. data/lib/crystalball/map_generator/base_strategy.rb +22 -0
  20. data/lib/crystalball/map_generator/configuration.rb +58 -0
  21. data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
  22. data/lib/crystalball/map_generator/coverage_strategy.rb +35 -0
  23. data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
  24. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +30 -0
  25. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -0
  26. data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +27 -0
  27. data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +25 -0
  28. data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
  29. data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
  30. data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
  31. data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
  32. data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
  33. data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
  34. data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
  35. data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
  36. data/lib/crystalball/map_generator.rb +84 -0
  37. data/lib/crystalball/map_storage/yaml_storage.rb +69 -0
  38. data/lib/crystalball/prediction.rb +35 -0
  39. data/lib/crystalball/predictor/associated_specs.rb +45 -0
  40. data/lib/crystalball/predictor/helpers/affected_example_groups_detector.rb +20 -0
  41. data/lib/crystalball/predictor/helpers/path_formatter.rb +18 -0
  42. data/lib/crystalball/predictor/modified_execution_paths.rb +27 -0
  43. data/lib/crystalball/predictor/modified_specs.rb +33 -0
  44. data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
  45. data/lib/crystalball/predictor/strategy.rb +16 -0
  46. data/lib/crystalball/predictor.rb +58 -0
  47. data/lib/crystalball/predictor_evaluator.rb +55 -0
  48. data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
  49. data/lib/crystalball/rails/helpers/schema_definition_parser/active_record.rb +21 -0
  50. data/lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb +27 -0
  51. data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -0
  52. data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
  53. data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
  54. data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +89 -0
  55. data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
  56. data/lib/crystalball/rails/predictor/modified_schema.rb +81 -0
  57. data/lib/crystalball/rails/tables_map.rb +53 -0
  58. data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
  59. data/lib/crystalball/rails/tables_map_generator.rb +84 -0
  60. data/lib/crystalball/rails.rb +11 -0
  61. data/lib/crystalball/rspec/filtering.rb +52 -0
  62. data/lib/crystalball/rspec/prediction_builder.rb +53 -0
  63. data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
  64. data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
  65. data/lib/crystalball/rspec/runner/configuration.rb +80 -0
  66. data/lib/crystalball/rspec/runner.rb +107 -0
  67. data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
  68. data/lib/crystalball/source_diff/file_diff.rb +53 -0
  69. data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
  70. data/lib/crystalball/source_diff.rb +48 -0
  71. data/lib/crystalball/version.rb +5 -0
  72. data/lib/crystalball.rb +44 -0
  73. 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