gitlab-crystalball 0.8.2 → 1.0.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.
Files changed (30) hide show
  1. checksums.yaml +4 -4
  2. data/README.md +1 -1
  3. data/lib/crystalball/example_group_map.rb +2 -1
  4. data/lib/crystalball/execution_map.rb +1 -1
  5. data/lib/crystalball/logging.rb +6 -4
  6. data/lib/crystalball/map_generator/base_strategy.rb +26 -5
  7. data/lib/crystalball/map_generator/configuration.rb +10 -4
  8. data/lib/crystalball/map_generator/coverage_strategy.rb +21 -10
  9. data/lib/crystalball/map_generator/described_class_strategy.rb +12 -4
  10. data/lib/crystalball/map_generator/oneshot_coverage_strategy.rb +11 -17
  11. data/lib/crystalball/map_generator/strategies_collection.rb +18 -12
  12. data/lib/crystalball/map_generator.rb +63 -15
  13. data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +1 -1
  14. data/lib/crystalball/rails/map_generator/action_view_strategy.rb +15 -4
  15. data/lib/crystalball/rails.rb +0 -1
  16. data/lib/crystalball/version.rb +1 -1
  17. data/lib/crystalball.rb +0 -1
  18. metadata +2 -14
  19. data/lib/crystalball/factory_bot.rb +0 -3
  20. data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +0 -45
  21. data/lib/crystalball/map_generator/allocated_objects_strategy.rb +0 -45
  22. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +0 -30
  23. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +0 -40
  24. data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +0 -27
  25. data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +0 -25
  26. data/lib/crystalball/map_generator/factory_bot_strategy.rb +0 -59
  27. data/lib/crystalball/map_generator/parser_strategy/processor.rb +0 -129
  28. data/lib/crystalball/map_generator/parser_strategy.rb +0 -60
  29. data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +0 -89
  30. data/lib/crystalball/rails/map_generator/i18n_strategy.rb +0 -47
checksums.yaml CHANGED
@@ -1,7 +1,7 @@
1
1
  ---
2
2
  SHA256:
3
- metadata.gz: bfe4631fc79255c0c4091aca48807cb31f7a596730233b3afaa5a7cf1e913be7
4
- data.tar.gz: af6a47b8418995cd7845041d8a0c773ecfb40c64d1cd3c3e0292a574979b5a1a
3
+ metadata.gz: 0f042eadd4e47f777de23c64b43723187f1f1ae907cea2c28dbf037f78b0bf8a
4
+ data.tar.gz: 8a6ba30fcf62bfd0ab606ecf696c018577c5822f856f9d8bc1112842ac5d48bb
5
5
  SHA512:
6
- metadata.gz: b8c59e192fe8be3eee41156660274e82338122e9057b2323c5a72adc66a570ea99b3dc5bbf6aa99f4c4742fbdfebb81c07fc01bf0f7565fcfdf2380fccbe2ce2
7
- data.tar.gz: f2352dc108e8ffa284b4a4505f5df7312f970771f573cfdf46bfa9d93090f2a88136c0e9bb39ab393ddcd8411dc8bacbd2e40d064ef5b5aa89f9acc6266acfc9
6
+ metadata.gz: d3b53dc72d8378fb0e0a5de23f15090a9c2ad04ee30f329c29da6863efd94dc9e3556c102a23d43b052e0d8a6273fa5682b6adf8978e594a4e8821b9b3e86094
7
+ data.tar.gz: 78eb78621614e18e319558343ba8c20a2464b05b9ae382fc1eec49cea1708dfefbabfbb47628a7e7f9d0c4bc45272de765269df8072f87f4efca688590f5c081
data/README.md CHANGED
@@ -1,7 +1,7 @@
1
1
  # Crystalball
2
2
 
3
3
  ![Build Status](https://gitlab.com/gitlab-org/ruby/gems/crystalball/badges/main/pipeline.svg)
4
- ![Test Coverage](https://gitlab.com/gitlab-org/ruby/gems/crystalball/badges/main/coverage.svg?job=rspec%20spec)
4
+ ![Test Coverage](https://gitlab.com/gitlab-org/ruby/gems/crystalball/badges/main/coverage.svg?job=rspec)
5
5
 
6
6
  Crystalball is a Ruby library which implements [Regression Test Selection mechanism](https://tenderlovemaking.com/2015/02/13/predicting-test-failues.html) originally published by Aaron Patterson.
7
7
  Its main purpose is to select a minimal subset of your test suite which should be run to ensure your changes didn't break anything.
@@ -3,9 +3,10 @@
3
3
  module Crystalball
4
4
  # Data object to store execution map for specific example
5
5
  class ExampleGroupMap
6
- attr_reader :uid, :file_path, :used_files
7
6
  extend Forwardable
8
7
 
8
+ attr_reader :uid, :file_path, :used_files
9
+
9
10
  delegate %i[push each] => :used_files
10
11
 
11
12
  # @param [Example|ExampleGroup] example - RSpec example or example group
@@ -20,7 +20,7 @@ module Crystalball
20
20
  end
21
21
 
22
22
  def to_h
23
- {type: type, commit: commit, timestamp: timestamp, version: version}
23
+ { type: type, commit: commit, timestamp: timestamp, version: version }
24
24
  end
25
25
  end
26
26
 
@@ -6,9 +6,11 @@ module Crystalball
6
6
  # This module logs information to the standard output based on the configured log level,
7
7
  # and also logs unfiltered information to the configured log file.
8
8
  module Logging
9
- def log(severity_sym, *args, &block)
10
- output_stream.log(severity(severity_sym), *args, &block)
11
- log_file_output_stream.log(severity(severity_sym), *args, &block)
9
+ def log(severity_sym, message, prefix_class_name: false)
10
+ msg = prefix_class_name ? "[#{self.class.name.split('::').last}] #{message}" : message
11
+
12
+ output_stream.log(severity(severity_sym), msg)
13
+ log_file_output_stream.log(severity(severity_sym), msg)
12
14
  end
13
15
 
14
16
  def self.extended(base)
@@ -26,7 +28,7 @@ module Crystalball
26
28
  end
27
29
 
28
30
  def output_stream
29
- @output_stream ||= ::Logger.new(STDOUT).tap do |logger|
31
+ @output_stream ||= ::Logger.new(STDOUT, progname: "crystalball").tap do |logger|
30
32
  logger.level = severity(configured_level)
31
33
  end
32
34
  end
@@ -4,19 +4,40 @@ module Crystalball
4
4
  class MapGenerator
5
5
  # Map generator strategy interface
6
6
  module BaseStrategy
7
+ include Logging
8
+
7
9
  def after_register; end
8
10
 
9
11
  def after_start; end
10
12
 
11
13
  def before_finalize; end
12
14
 
13
- # Each strategy must implement #call augmenting the used_files list and
14
- # yielding back the ExampleGroupMap.
15
- # @param [Crystalball::ExampleGroupMap] _example_map - object holding example metadata and used files
16
- # @param [RSpec::Core::Example] _example - a RSpec example
17
- def call(_example_map, _example)
15
+ # Run before the execution of the example or example group
16
+ #
17
+ # @param _example [RSpec::Core::ExampleGroup, RSpec::Core::Example]
18
+ # @return [void]
19
+ def run_before(_example)
20
+ raise NotImplementedError
21
+ end
22
+
23
+ # Run after the execution of the example or example group
24
+ #
25
+ # @param _example_map [Crystalball::ExampleGroupMap]
26
+ # @param _example [RSpec::Core::ExampleGroup, RSpec::Core::Example]
27
+ # @return [Crystalball::ExampleGroupMap]
28
+ def run_after(_example_map, _example)
18
29
  raise NotImplementedError
19
30
  end
31
+
32
+ private
33
+
34
+ # Print debug log messages with included strategy class name prefix
35
+ #
36
+ # @param msg [String]
37
+ # @return [void]
38
+ def log_debug(msg)
39
+ log(:debug, msg, prefix_class_name: true)
40
+ end
20
41
  end
21
42
  end
22
43
  end
@@ -7,15 +7,14 @@ module Crystalball
7
7
  # Configuration of map generator. Is can be accessed as a first argument inside
8
8
  # `Crystalball::MapGenerator.start! { |config| config } block.
9
9
  class Configuration
10
- attr_writer :map_storage
11
- attr_writer :map_class
10
+ attr_writer :map_storage, :map_class
12
11
  attr_accessor :commit, :version, :compact_map
13
-
14
- attr_reader :strategies
12
+ attr_reader :strategies, :hook_type
15
13
 
16
14
  def initialize
17
15
  @strategies = StrategiesCollection.new
18
16
  @compact_map = true
17
+ @hook_type = :example
19
18
  end
20
19
 
21
20
  def compact_map?
@@ -46,6 +45,13 @@ module Crystalball
46
45
  @dump_threshold = value.to_i
47
46
  end
48
47
 
48
+ def hook_type=(type)
49
+ valid_types = %i[example context]
50
+ raise ArgumentError, "hook type should be one of #{valid_types}" unless valid_types.include?(type)
51
+
52
+ @hook_type = type
53
+ end
54
+
49
55
  # Register new strategy for map generation
50
56
  #
51
57
  # @param [Crystalball::MapGenerator::BaseStrategy] strategy
@@ -14,22 +14,33 @@ module Crystalball
14
14
 
15
15
  def initialize(execution_detector: ExecutionDetector.new)
16
16
  @execution_detector = execution_detector
17
+ @before_coverage = nil
17
18
  end
18
19
 
19
20
  def after_register
20
- Coverage.start(lines: true) unless Coverage.running?
21
+ return if Coverage.running?
22
+
23
+ log_debug("Starting coverage capture")
24
+ Coverage.start(lines: true)
25
+ end
26
+
27
+ def run_before(example)
28
+ log_debug("Fetching current coverage state before execution of example id: #{example.id}")
29
+ @before_coverage = Coverage.peek_result
21
30
  end
22
31
 
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
+ def run_after(example_map, example)
33
+ log_debug("Recording mappings for example id: #{example.id}")
34
+ mappings = execution_detector.detect(before_coverage, Coverage.peek_result)
35
+ log_debug("#{example.id} recorded #{mappings.size} files")
36
+ example_map.push(*mappings)
37
+ ensure
38
+ @before_coverage = nil
32
39
  end
40
+
41
+ private
42
+
43
+ attr_reader :before_coverage
33
44
  end
34
45
  end
35
46
  end
@@ -9,6 +9,7 @@ module Crystalball
9
9
  # ancestors.
10
10
  class DescribedClassStrategy
11
11
  include BaseStrategy
12
+
12
13
  extend Forwardable
13
14
 
14
15
  attr_reader :execution_detector
@@ -21,14 +22,21 @@ module Crystalball
21
22
  @execution_detector = execution_detector
22
23
  end
23
24
 
25
+ def run_before(_example); end
26
+
24
27
  # @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
25
28
  # @param [RSpec::Core::Example] example - a RSpec example
26
- def call(example_map, example)
27
- yield example_map, example
28
-
29
+ def run_after(example_map, example)
30
+ log_debug("Recording mappings for example id: #{example.id}")
29
31
  described_class = example.metadata[:described_class]
30
32
 
31
- example_map.push(*execution_detector.detect([described_class])) if described_class
33
+ if described_class
34
+ mappings = execution_detector.detect([described_class])
35
+ log_debug("#{example.id} recorded #{mappings.size} files")
36
+ example_map.push(*mappings)
37
+ else
38
+ log_debug("#{example.id} did not record any mappings because it has no described_class")
39
+ end
32
40
  end
33
41
  end
34
42
  end
@@ -10,36 +10,30 @@ module Crystalball
10
10
  # Map generator strategy based on harvesting Coverage information during example execution
11
11
  class OneshotCoverageStrategy
12
12
  include BaseStrategy
13
- include Logging
14
13
  include Helpers::PathFilter
15
14
 
16
15
  def after_register
17
16
  raise "Coverage must not be started for oneshot_line strategy" if Coverage.running?
18
17
  end
19
18
 
19
+ def run_before(_example)
20
+ log_debug("Starting oneshot_line coverage capture")
21
+ return Coverage.start(oneshot_lines: true) unless Coverage.running?
22
+
23
+ log(:warn, "Coverage has been already started, restarting coverage for oneshot_lines!", prefix_class_name: true)
24
+ Coverage.result(stop: true, clear: true)
25
+ Coverage.start(oneshot_lines: true)
26
+ end
27
+
20
28
  # Adds to the example_map's used files the ones the ones in which
21
29
  # the coverage has changed after the tests runs.
22
30
  # @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
23
31
  # @param [RSpec::Core::Example] example - a RSpec example
24
- def call(example_map, example)
25
- start_coverage
26
- yield example_map, example
32
+ def run_after(example_map, example)
27
33
  paths = filter(Coverage.result(stop: true, clear: true).keys)
28
- log(:debug, "[Crystalball] #{example.id} recorded #{paths.size} files")
34
+ log_debug("#{example.id} recorded #{paths.size} files")
29
35
  example_map.push(*paths)
30
36
  end
31
-
32
- # Start coverage or restart it if it was already started
33
- #
34
- # @return [void] <description>
35
- def start_coverage
36
- log(:debug, "[Crystalball] Starting oneshot_line coverage capture")
37
- return Coverage.start(oneshot_lines: true) unless Coverage.running?
38
-
39
- log(:warn, "[Crystalball] Coverage has been already started, restarting coverage for oneshot_lines!")
40
- Coverage.result(stop: true, clear: true)
41
- Coverage.start(oneshot_lines: true)
42
- end
43
37
  end
44
38
  end
45
39
  end
@@ -10,11 +10,24 @@ module Crystalball
10
10
  @strategies = strategies
11
11
  end
12
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)
13
+ # Run before hook action for example or example group
14
+ #
15
+ # @param example [RSpec::Core::Example]
16
+ # @return [void]
17
+ def run_before(example)
18
+ _strategies.reverse_each { |strategy| strategy.run_before(example) }
19
+ end
20
+
21
+ # Run after hook action for example or example group and update example group map
22
+ #
23
+ # @param example_group_map [ExampleGroupMap]
24
+ # @param example [RSpec::Core::Example]
25
+ # @return [ExampleGroupMap]
26
+ def run_after(example_group_map, example)
27
+ _strategies.reverse_each do |strategy|
28
+ strategy.run_after(example_group_map, example)
29
+ end
30
+
18
31
  example_group_map
19
32
  end
20
33
 
@@ -31,13 +44,6 @@ module Crystalball
31
44
  def _strategies
32
45
  @strategies
33
46
  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
47
  end
42
48
  end
43
49
  end
@@ -3,9 +3,11 @@
3
3
  module Crystalball
4
4
  # Class to generate execution map during RSpec build execution
5
5
  class MapGenerator
6
+ include Logging
6
7
  extend Forwardable
7
8
 
8
9
  attr_reader :configuration
10
+
9
11
  delegate %i[map_storage strategies dump_threshold map_class] => :configuration
10
12
 
11
13
  class << self
@@ -15,12 +17,17 @@ module Crystalball
15
17
  def start!(&block)
16
18
  generator = new(&block)
17
19
 
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! }
20
+ ::RSpec.configure do |config|
21
+ config.before(:suite) { generator.start! }
22
+ config.after(:suite) { generator.finalize! }
23
+
24
+ if generator.configuration.hook_type == :example
25
+ config.prepend_before(:example) { |e| generator.execute_before(e) }
26
+ config.append_after(:example) { |e| generator.execute_after(e) }
27
+ else
28
+ config.prepend_before(:context) { |e| generator.execute_before(e.class) }
29
+ config.append_after(:context) { |e| generator.execute_after(e.class) }
30
+ end
24
31
  end
25
32
  end
26
33
  end
@@ -33,6 +40,8 @@ module Crystalball
33
40
 
34
41
  # Registers strategies and prepares metadata for execution map
35
42
  def start!
43
+ log_with_prefix("Starting Crystalball execution map builder")
44
+
36
45
  self.map = nil
37
46
  map_storage.clear!
38
47
  map_storage.dump(map.metadata.to_h)
@@ -41,26 +50,47 @@ module Crystalball
41
50
  self.started = true
42
51
  end
43
52
 
44
- # Runs example and collects execution map for it
45
- def refresh_for_case(example)
46
- map << strategies.run(ExampleGroupMap.new(example), example) { example.run }
47
- check_dump_threshold
53
+ # Run before step of strategy for given example or example group
54
+ #
55
+ # @param example [RSpec::Core::ExampleGroup, RSpec::Core::Example]
56
+ # @return [void]
57
+ def execute_before(example)
58
+ strategies.run_before(example)
59
+ end
60
+
61
+ # Run after step and collect execution map
62
+ #
63
+ # @param example [RSpec::Core::ExampleGroup, RSpec::Core::Example]
64
+ # @return [void]
65
+ def execute_after(example)
66
+ result = strategies.run_after(ExampleGroupMap.new(example), example)
67
+ # do not populate map with nil values
68
+ map << result unless result.used_files.empty?
69
+ check_dump_threshold!
48
70
  end
49
71
 
50
72
  # Finalizes strategies and saves map
51
73
  def finalize!
52
74
  return unless started
53
75
 
76
+ log_with_prefix("Finalizing mappings strategies and saving final map")
54
77
  strategies.each(&:before_finalize)
55
78
 
56
- return unless map.size.positive?
79
+ unless map.size.positive?
80
+ log_with_prefix("No files recorded in the execution map")
81
+ return
82
+ end
57
83
 
58
84
  example_groups = (configuration.compact_map? ? MapCompactor.compact_map!(map) : map).example_groups
59
- map_storage.dump(example_groups)
85
+ dump_map_storage(example_groups)
60
86
  end
61
87
 
62
88
  def map
63
- @map ||= map_class.new(metadata: {commit: configuration.commit&.sha, timestamp: configuration.commit&.date&.to_i, version: configuration.version})
89
+ @map ||= map_class.new(metadata: {
90
+ commit: configuration.commit&.sha,
91
+ timestamp: configuration.commit&.date&.to_i,
92
+ version: configuration.version
93
+ })
64
94
  end
65
95
 
66
96
  private
@@ -68,17 +98,35 @@ module Crystalball
68
98
  attr_writer :map
69
99
  attr_accessor :started
70
100
 
101
+ def strategy_classes
102
+ @strategy_classes ||= strategies.map { |strategy| strategy.class.name.split("::").last }.join("|")
103
+ end
104
+
71
105
  def repo
72
106
  @repo = GitRepo.open(".") unless defined?(@repo)
73
107
  @repo
74
108
  end
75
109
 
76
- def check_dump_threshold
110
+ def check_dump_threshold!
77
111
  return if configuration.compact_map
78
112
  return unless dump_threshold.positive? && map.size >= dump_threshold
79
113
 
80
- map_storage.dump(map.example_groups)
114
+ log_with_prefix("Intermediate dump threshold (#{dump_threshold}) reached!")
115
+ dump_map_storage(map.example_groups)
116
+ end
117
+
118
+ def dump_map_storage(example_groups)
119
+ log_with_prefix("Dumping #{example_groups.size} examples to #{configuration.map_storage_path} file")
120
+ map_storage.dump(example_groups)
81
121
  map.clear!
82
122
  end
123
+
124
+ # Log message with prefix containing strategy names
125
+ #
126
+ # @param message [String]
127
+ # @return [void]
128
+ def log_with_prefix(message)
129
+ log(:info, "[#{strategy_classes}]: #{message}")
130
+ end
83
131
  end
84
132
  end
@@ -23,7 +23,7 @@ module Crystalball
23
23
  # Reverts original behavior of `ActionView::Template#compile!`
24
24
  def revert!
25
25
  ::ActionView::Template.class_eval do
26
- alias_method :compile!, :cb_original_compile! # rubocop:disable Lint/DuplicateMethods
26
+ alias_method :compile!, :cb_original_compile!
27
27
  undef_method :cb_patched_compile!
28
28
  end
29
29
  end
@@ -33,12 +33,23 @@ module Crystalball
33
33
  Patch.revert!
34
34
  end
35
35
 
36
+ # Reset affected views before test execution starts
37
+ #
38
+ # @param _example [RSpec::Core::Example, RSpec::Core::ExampleGroup]
39
+ # @return [void]
40
+ def run_before(_example)
41
+ self.class.reset_views
42
+ end
43
+
36
44
  # Adds views related to the spec to the example group map
45
+ #
37
46
  # @param [Crystalball::ExampleGroupMap] example_group_map - object holding example metadata and used files
38
- def call(example_group_map, _)
39
- self.class.reset_views
40
- yield example_group_map
41
- example_group_map.push(*filter(self.class.views))
47
+ # @return [void]
48
+ def run_after(example_group_map, example)
49
+ log_debug("Recording mappings for example id: #{example.id}")
50
+ mappings = filter(self.class.views)
51
+ log_debug("#{example.id} recorded #{mappings.size} files")
52
+ example_group_map.push(*mappings)
42
53
  end
43
54
  end
44
55
  end
@@ -1,7 +1,6 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  require "crystalball/rails/map_generator/action_view_strategy"
4
- require "crystalball/rails/map_generator/i18n_strategy"
5
4
  require "crystalball/active_record"
6
5
 
7
6
  module Crystalball
@@ -1,5 +1,5 @@
1
1
  # frozen_string_literal: true
2
2
 
3
3
  module Crystalball
4
- VERSION = "0.8.2"
4
+ VERSION = "1.0.0"
5
5
  end
data/lib/crystalball.rb CHANGED
@@ -17,7 +17,6 @@ require "crystalball/map_generator"
17
17
  require "crystalball/map_generator/configuration"
18
18
  require "crystalball/map_generator/coverage_strategy"
19
19
  require "crystalball/map_generator/oneshot_coverage_strategy"
20
- require "crystalball/map_generator/allocated_objects_strategy"
21
20
  require "crystalball/map_generator/described_class_strategy"
22
21
  require "crystalball/map_storage/yaml_storage"
23
22
  require "crystalball/map_compactor"
metadata CHANGED
@@ -1,13 +1,13 @@
1
1
  --- !ruby/object:Gem::Specification
2
2
  name: gitlab-crystalball
3
3
  version: !ruby/object:Gem::Version
4
- version: 0.8.2
4
+ version: 1.0.0
5
5
  platform: ruby
6
6
  authors:
7
7
  - Developer Experience Team
8
8
  bindir: bin
9
9
  cert_chain: []
10
- date: 2025-05-20 00:00:00.000000000 Z
10
+ date: 2025-06-02 00:00:00.000000000 Z
11
11
  dependencies:
12
12
  - !ruby/object:Gem::Dependency
13
13
  name: git
@@ -238,32 +238,22 @@ files:
238
238
  - lib/crystalball/extensions/git.rb
239
239
  - lib/crystalball/extensions/git/base.rb
240
240
  - lib/crystalball/extensions/git/lib.rb
241
- - lib/crystalball/factory_bot.rb
242
241
  - lib/crystalball/git_repo.rb
243
242
  - lib/crystalball/logging.rb
244
243
  - lib/crystalball/map_compactor.rb
245
244
  - lib/crystalball/map_compactor/example_context.rb
246
245
  - lib/crystalball/map_compactor/example_groups_data_compactor.rb
247
246
  - lib/crystalball/map_generator.rb
248
- - lib/crystalball/map_generator/allocated_objects_strategy.rb
249
- - lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb
250
247
  - lib/crystalball/map_generator/base_strategy.rb
251
248
  - lib/crystalball/map_generator/configuration.rb
252
249
  - lib/crystalball/map_generator/coverage_strategy.rb
253
250
  - lib/crystalball/map_generator/coverage_strategy/execution_detector.rb
254
251
  - lib/crystalball/map_generator/described_class_strategy.rb
255
- - lib/crystalball/map_generator/factory_bot_strategy.rb
256
- - lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb
257
- - lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb
258
- - lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb
259
- - lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb
260
252
  - lib/crystalball/map_generator/helpers/path_filter.rb
261
253
  - lib/crystalball/map_generator/object_sources_detector.rb
262
254
  - lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb
263
255
  - lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb
264
256
  - lib/crystalball/map_generator/oneshot_coverage_strategy.rb
265
- - lib/crystalball/map_generator/parser_strategy.rb
266
- - lib/crystalball/map_generator/parser_strategy/processor.rb
267
257
  - lib/crystalball/map_generator/strategies_collection.rb
268
258
  - lib/crystalball/map_storage/yaml_storage.rb
269
259
  - lib/crystalball/prediction.rb
@@ -283,8 +273,6 @@ files:
283
273
  - lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb
284
274
  - lib/crystalball/rails/map_generator/action_view_strategy.rb
285
275
  - lib/crystalball/rails/map_generator/action_view_strategy/patch.rb
286
- - lib/crystalball/rails/map_generator/i18n_strategy.rb
287
- - lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb
288
276
  - lib/crystalball/rails/predictor/modified_schema.rb
289
277
  - lib/crystalball/rails/tables_map.rb
290
278
  - lib/crystalball/rails/tables_map_generator.rb
@@ -1,3 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "crystalball/map_generator/factory_bot_strategy"
@@ -1,45 +0,0 @@
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
-
39
- created_object_classes << tp.self
40
- end
41
- end
42
- end
43
- end
44
- end
45
- end
@@ -1,45 +0,0 @@
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 used files every file which contain the definition of the
34
- # classes of the objects allocated during the spec execution.
35
- # @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
36
- # @param [RSpec::Core::Example] example - a RSpec example
37
- def call(example_map, example)
38
- classes = object_tracker.used_classes_during do
39
- yield example_map, example
40
- end
41
- example_map.push(*execution_detector.detect(classes))
42
- end
43
- end
44
- end
45
- end
@@ -1,30 +0,0 @@
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
@@ -1,40 +0,0 @@
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
@@ -1,27 +0,0 @@
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
@@ -1,25 +0,0 @@
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
@@ -1,59 +0,0 @@
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
@@ -1,129 +0,0 @@
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
@@ -1,60 +0,0 @@
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
@@ -1,89 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "i18n"
4
-
5
- module Crystalball
6
- module Rails
7
- class MapGenerator
8
- class I18nStrategy
9
- # Module to add new patched `load_file`, `store_translations` and `lookup`
10
- # methods to `I18n::Backend::Simple`.
11
- module SimplePatch
12
- class << self
13
- # Patches `I18n::Backend::Simple`.
14
- def apply!
15
- ::I18n::Backend::Simple.class_eval do
16
- include SimplePatch
17
-
18
- %i[load_file store_translations lookup].each do |method|
19
- alias_method :"cb_original_#{method}", method
20
- alias_method method, :"cb_patched_#{method}"
21
- end
22
- end
23
- end
24
-
25
- # Reverts original behavior of `I18n::Backend::Simple`
26
- def revert!
27
- ::I18n::Backend::Simple.class_eval do
28
- %i[load_file store_translations lookup].each do |method|
29
- alias_method method, :"cb_original_#{method}"
30
- undef_method :"cb_patched_#{method}"
31
- end
32
- end
33
- ::I18n.reload!
34
- end
35
- end
36
-
37
- # Will replace original `I18n::Backend::Simple#load_file`.
38
- # Stores filename in current thread
39
- def cb_patched_load_file(filename, *args)
40
- Thread.current[:cb_locale_file_name] = filename
41
- cb_original_load_file(filename, *args)
42
- end
43
-
44
- # Will replace original `I18n::Backend::Simple#store_translations`.
45
- # Adds filename for each value
46
- def cb_patched_store_translations(locale, data, *args)
47
- cb_add_filename_to_values(data, Thread.current[:cb_locale_file_name])
48
- cb_original_store_translations(locale, data, *args)
49
- end
50
-
51
- # Will replace original `I18n::Backend::Simple#lookup`.
52
- # Records origin filename of each value used.
53
- def cb_patched_lookup(*args)
54
- value = cb_original_lookup(*args)
55
- cb_remove_and_track_filename_from_values(value)
56
- end
57
-
58
- private
59
-
60
- def cb_add_filename_to_values(data, filename)
61
- data.each do |key, value|
62
- case value
63
- when Hash
64
- next if value.frozen?
65
-
66
- cb_add_filename_to_values(value, filename)
67
- else
68
- data[key] = {cb_filename: filename, cb_value: value}.freeze
69
- end
70
- end
71
- end
72
-
73
- def cb_remove_and_track_filename_from_values(data)
74
- return data unless data.is_a?(Hash)
75
-
76
- if data.key?(:cb_filename)
77
- ::Crystalball::Rails::MapGenerator::I18nStrategy.locale_files << data[:cb_filename]
78
- return data[:cb_value]
79
- end
80
-
81
- data.each.with_object({}) do |(key, value), collector|
82
- collector[key] = cb_remove_and_track_filename_from_values(value)
83
- end
84
- end
85
- end
86
- end
87
- end
88
- end
89
- end
@@ -1,47 +0,0 @@
1
- # frozen_string_literal: true
2
-
3
- require "crystalball/map_generator/base_strategy"
4
- require "crystalball/map_generator/helpers/path_filter"
5
- require "crystalball/rails/map_generator/i18n_strategy/simple_patch"
6
-
7
- module Crystalball
8
- module Rails
9
- class MapGenerator
10
- # Map generator strategy to build map of locale files used by an example.
11
- class I18nStrategy
12
- include ::Crystalball::MapGenerator::BaseStrategy
13
- include ::Crystalball::MapGenerator::Helpers::PathFilter
14
-
15
- class << self
16
- # List of locale files affected by current example
17
- #
18
- # @return [Array<String>]
19
- def locale_files
20
- @locale_files ||= []
21
- end
22
-
23
- # Reset cached list of locale files
24
- def reset_locale_files
25
- @locale_files = []
26
- end
27
- end
28
-
29
- def after_register
30
- SimplePatch.apply!
31
- end
32
-
33
- def before_finalize
34
- SimplePatch.revert!
35
- end
36
-
37
- # Adds to the example group map the locale files used by the example
38
- # @param [Crystalball::ExampleGroupMap] example_group_map - object holding example metadata and used files
39
- def call(example_group_map, _)
40
- self.class.reset_locale_files
41
- yield example_group_map
42
- example_group_map.push(*filter(self.class.locale_files.compact))
43
- end
44
- end
45
- end
46
- end
47
- end