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.
- checksums.yaml +4 -4
- data/README.md +1 -1
- data/lib/crystalball/example_group_map.rb +2 -1
- data/lib/crystalball/execution_map.rb +1 -1
- data/lib/crystalball/logging.rb +6 -4
- data/lib/crystalball/map_generator/base_strategy.rb +26 -5
- data/lib/crystalball/map_generator/configuration.rb +10 -4
- data/lib/crystalball/map_generator/coverage_strategy.rb +21 -10
- data/lib/crystalball/map_generator/described_class_strategy.rb +12 -4
- data/lib/crystalball/map_generator/oneshot_coverage_strategy.rb +11 -17
- data/lib/crystalball/map_generator/strategies_collection.rb +18 -12
- data/lib/crystalball/map_generator.rb +63 -15
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +1 -1
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +15 -4
- data/lib/crystalball/rails.rb +0 -1
- data/lib/crystalball/version.rb +1 -1
- data/lib/crystalball.rb +0 -1
- metadata +2 -14
- data/lib/crystalball/factory_bot.rb +0 -3
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +0 -45
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +0 -45
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +0 -30
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +0 -40
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +0 -27
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +0 -25
- data/lib/crystalball/map_generator/factory_bot_strategy.rb +0 -59
- data/lib/crystalball/map_generator/parser_strategy/processor.rb +0 -129
- data/lib/crystalball/map_generator/parser_strategy.rb +0 -60
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +0 -89
- 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:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: 0f042eadd4e47f777de23c64b43723187f1f1ae907cea2c28dbf037f78b0bf8a
|
4
|
+
data.tar.gz: 8a6ba30fcf62bfd0ab606ecf696c018577c5822f856f9d8bc1112842ac5d48bb
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: d3b53dc72d8378fb0e0a5de23f15090a9c2ad04ee30f329c29da6863efd94dc9e3556c102a23d43b052e0d8a6273fa5682b6adf8978e594a4e8821b9b3e86094
|
7
|
+
data.tar.gz: 78eb78621614e18e319558343ba8c20a2464b05b9ae382fc1eec49cea1708dfefbabfbb47628a7e7f9d0c4bc45272de765269df8072f87f4efca688590f5c081
|
data/README.md
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
# Crystalball
|
2
2
|
|
3
3
|

|
4
|
-

|
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
|
data/lib/crystalball/logging.rb
CHANGED
@@ -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,
|
10
|
-
|
11
|
-
|
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
|
-
#
|
14
|
-
#
|
15
|
-
# @param [
|
16
|
-
# @
|
17
|
-
def
|
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
|
-
|
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
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
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
|
27
|
-
|
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
|
-
|
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
|
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
|
-
|
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
|
-
#
|
14
|
-
#
|
15
|
-
# @
|
16
|
-
|
17
|
-
|
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 |
|
19
|
-
|
20
|
-
|
21
|
-
|
22
|
-
|
23
|
-
|
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
|
-
#
|
45
|
-
|
46
|
-
|
47
|
-
|
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
|
-
|
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
|
-
|
85
|
+
dump_map_storage(example_groups)
|
60
86
|
end
|
61
87
|
|
62
88
|
def map
|
63
|
-
@map ||= map_class.new(metadata: {
|
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
|
-
|
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!
|
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
|
-
|
39
|
-
|
40
|
-
|
41
|
-
|
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
|
data/lib/crystalball/rails.rb
CHANGED
data/lib/crystalball/version.rb
CHANGED
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.
|
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-
|
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,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
|