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,84 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Class to generate execution map during RSpec build execution
5
+ class MapGenerator
6
+ extend Forwardable
7
+
8
+ attr_reader :configuration
9
+ delegate %i[map_storage strategies dump_threshold map_class] => :configuration
10
+
11
+ class << self
12
+ # Registers Crystalball handlers to generate execution map during specs execution
13
+ #
14
+ # @param [Proc] block to configure MapGenerator and Register strategies
15
+ def start!(&block)
16
+ generator = new(&block)
17
+
18
+ ::RSpec.configure do |c|
19
+ c.before(:suite) { generator.start! }
20
+
21
+ c.around(:each) { |e| generator.refresh_for_case(e) }
22
+
23
+ c.after(:suite) { generator.finalize! }
24
+ end
25
+ end
26
+ end
27
+
28
+ def initialize
29
+ @configuration = Configuration.new
30
+ @configuration.commit = repo.gcommit("HEAD") if repo
31
+ yield @configuration if block_given?
32
+ end
33
+
34
+ # Registers strategies and prepares metadata for execution map
35
+ def start!
36
+ self.map = nil
37
+ map_storage.clear!
38
+ map_storage.dump(map.metadata.to_h)
39
+
40
+ strategies.reverse.each(&:after_start)
41
+ self.started = true
42
+ end
43
+
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
48
+ end
49
+
50
+ # Finalizes strategies and saves map
51
+ def finalize!
52
+ return unless started
53
+
54
+ strategies.each(&:before_finalize)
55
+
56
+ return unless map.size.positive?
57
+
58
+ example_groups = (configuration.compact_map? ? MapCompactor.compact_map!(map) : map).example_groups
59
+ map_storage.dump(example_groups)
60
+ end
61
+
62
+ def map
63
+ @map ||= map_class.new(metadata: {commit: configuration.commit&.sha, timestamp: configuration.commit&.date&.to_i, version: configuration.version})
64
+ end
65
+
66
+ private
67
+
68
+ attr_writer :map
69
+ attr_accessor :started
70
+
71
+ def repo
72
+ @repo = GitRepo.open(".") unless defined?(@repo)
73
+ @repo
74
+ end
75
+
76
+ def check_dump_threshold
77
+ return if configuration.compact_map
78
+ return unless dump_threshold.positive? && map.size >= dump_threshold
79
+
80
+ map_storage.dump(map.example_groups)
81
+ map.clear!
82
+ end
83
+ end
84
+ end
@@ -0,0 +1,69 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "yaml"
4
+
5
+ module Crystalball
6
+ class MapStorage
7
+ # Exception class for missing map files
8
+ class NoFilesFoundError < StandardError; end
9
+
10
+ # YAML persistence adapter for execution map storage
11
+ class YAMLStorage
12
+ attr_reader :path
13
+
14
+ class << self
15
+ # Loads map from given path
16
+ #
17
+ # @param [String] path to map
18
+ # @return [Crystalball::ExecutionMap]
19
+ def load(path)
20
+ meta, example_groups = *read_files(path).transpose
21
+
22
+ guard_metadata_consistency(meta)
23
+
24
+ Object.const_get(meta.first[:type]).new(metadata: meta.first, example_groups: example_groups.compact.inject(&:merge!))
25
+ end
26
+
27
+ private
28
+
29
+ def read_files(path)
30
+ paths = path.directory? ? path.each_child.select(&:file?) : [path]
31
+
32
+ raise NoFilesFoundError, "No files or folder exists #{path}" unless paths.any?(&:exist?)
33
+
34
+ paths.map do |file|
35
+ metadata, *example_groups = file.read.split("---\n").reject(&:empty?).map do |yaml|
36
+ YAML.safe_load(yaml, permitted_classes: [Symbol])
37
+ end
38
+ example_groups = example_groups.inject(&:merge!)
39
+
40
+ [metadata, example_groups]
41
+ end
42
+ end
43
+
44
+ def guard_metadata_consistency(metadata)
45
+ uniq = metadata.uniq
46
+ raise "Can't load execution maps with different metadata. Metadata: #{uniq}" if uniq.size > 1
47
+ end
48
+ end
49
+
50
+ # @param [String] path to store execution map
51
+ def initialize(path)
52
+ @path = path
53
+ end
54
+
55
+ # Removes storage file
56
+ def clear!
57
+ path.delete if path.exist?
58
+ end
59
+
60
+ # Writes data to storage file
61
+ #
62
+ # @param [Hash] data to write to storage file
63
+ def dump(data)
64
+ path.dirname.mkpath
65
+ path.open("a") { |f| f.write YAML.dump(data) }
66
+ end
67
+ end
68
+ end
69
+ end
@@ -0,0 +1,35 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Class for Crystalball prediction results
5
+ class Prediction
6
+ def initialize(records)
7
+ @records = records
8
+ end
9
+
10
+ # When the records are something like:
11
+ # ./spec/foo ./spec/foo/bar_spec.rb
12
+ # this returns just ./spec/foo
13
+ def compact
14
+ sort_by(&:length).each_with_object([]) do |c, result|
15
+ result << c unless result.any? { |r| c.start_with?(r) }
16
+ end.compact
17
+ end
18
+
19
+ def to_a
20
+ records
21
+ end
22
+
23
+ def method_missing(*args, &block)
24
+ records.respond_to?(*args) ? records.public_send(*args, &block) : super
25
+ end
26
+
27
+ def respond_to_missing?(*args)
28
+ records.respond_to?(*args)
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :records
34
+ end
35
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/predictor/strategy"
4
+
5
+ module Crystalball
6
+ class Predictor
7
+ # Used with `predictor.use Crystalball::Predictor::AssociatedSpecs.new(from: %r{models/(.*).rb}, to: "./spec/models/%s_spec.rb")`.
8
+ # When used will look for files matched to `from` regex and use captures to fill `to` string to
9
+ # get paths of proper specs
10
+ class AssociatedSpecs
11
+ include Strategy
12
+
13
+ # @param [Regexp] from - regular expression to match specific files and get proper captures
14
+ # @param [String] to - string in sprintf format to get proper files using captures of regexp
15
+ def initialize(from:, to:)
16
+ @from = from
17
+ @to = to
18
+ end
19
+
20
+ # This strategy does not depend on a previously generated example group map.
21
+ # It uses the defined regex rules to infer which specs to run.
22
+ # @param [Crystalball::SourceDiff] diff - the diff from which to predict
23
+ # which specs should run
24
+ # @return [Array<String>] the spec paths associated with the changes
25
+ def call(diff, _map)
26
+ super do
27
+ diff.map(&:relative_path).grep(from).map { |source_file_path| to % captures(source_file_path) }
28
+ end
29
+ end
30
+
31
+ private
32
+
33
+ attr_reader :from, :to
34
+
35
+ def captures(file_path)
36
+ match = file_path.match(from)
37
+ if match.names.any?
38
+ match.names.map(&:to_sym).zip(match.captures).to_h
39
+ else
40
+ match.captures
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,20 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ class Predictor
5
+ module Helpers
6
+ # Helper module to fetch example groups affected by given list of changed files
7
+ module AffectedExampleGroupsDetector
8
+ # Fetch examples affected by given list of files
9
+ # @param [Array<String>] files - list of files
10
+ # @param [Crystalball::ExecutionMap] map - execution map with examples
11
+ # @return [Array<String>] list of affected examples
12
+ def detect_examples(files, map)
13
+ map.example_groups.map do |uid, example_group_map|
14
+ uid if files.any? { |file| example_group_map.include?(file) }
15
+ end.compact
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
@@ -0,0 +1,18 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ class Predictor
5
+ module Helpers
6
+ # Helper module for converting relative path to RSpec format
7
+ module PathFormatter
8
+ def format_paths(paths)
9
+ paths.map { |path| format_path(path) }
10
+ end
11
+
12
+ def format_path(path)
13
+ path.start_with?("./") ? path : "./#{path}"
14
+ end
15
+ end
16
+ end
17
+ end
18
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/predictor/strategy"
4
+ require "crystalball/predictor/helpers/affected_example_groups_detector"
5
+
6
+ module Crystalball
7
+ class Predictor
8
+ # Used with `predictor.use Crystalball::Predictor::ModifiedExecutionPaths.new`. When used will check the map which
9
+ # specs depend on which files and will return only those specs which depend on files modified since last time map
10
+ # was generated.
11
+ class ModifiedExecutionPaths
12
+ include Helpers::AffectedExampleGroupsDetector
13
+ include Strategy
14
+
15
+ # @param [Crystalball::SourceDiff] diff - the diff from which to predict
16
+ # which specs should run
17
+ # @param [Crystalball::ExampleGroupMap] map - the map with the relations of
18
+ # examples and used files
19
+ # @return [Array<String>] the spec paths associated with the changes
20
+ def call(diff, map)
21
+ super do
22
+ detect_examples(diff.map(&:relative_path), map)
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,33 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/predictor/strategy"
4
+
5
+ module Crystalball
6
+ class Predictor
7
+ # Used with `predictor.use Crystalball::Predictor::ModifiedSpecs.new`. Will find files that match spec regexp and
8
+ # return all new or modified files. You can specify spec regexp using first parameter to `#initialize`.
9
+ class ModifiedSpecs
10
+ include Strategy
11
+
12
+ # @param [Regexp] spec_pattern - regexp to filter specs files
13
+ def initialize(spec_pattern = %r{spec/.*_spec\.rb\z})
14
+ @spec_pattern = spec_pattern
15
+ end
16
+
17
+ # This strategy does not depend on a previously generated example group map.
18
+ # It uses the spec pattern to determine which specs should run.
19
+ # @param [Crystalball::SourceDiff] diff - the diff from which to predict
20
+ # which specs should run
21
+ # @return [Array<String>] the spec paths associated with the changes
22
+ def call(diff, _)
23
+ super do
24
+ diff.reject(&:deleted?).map(&:new_relative_path).grep(spec_pattern)
25
+ end
26
+ end
27
+
28
+ private
29
+
30
+ attr_reader :spec_pattern
31
+ end
32
+ end
33
+ end
@@ -0,0 +1,39 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/predictor/strategy"
4
+ require "crystalball/predictor/helpers/affected_example_groups_detector"
5
+
6
+ module Crystalball
7
+ class Predictor
8
+ # Used with `predictor.use Crystalball::Predictor::ModifiedSupportSpecs.new`. Will find files that match passed regexp and
9
+ # return full spec files which uses matched support spec files. Perfectly works for shared_context and shared_examples.
10
+ class ModifiedSupportSpecs
11
+ include Strategy
12
+ include Helpers::AffectedExampleGroupsDetector
13
+
14
+ # @param [Regexp] support_spec_pattern - regexp to filter support specs files
15
+ def initialize(support_spec_pattern = %r{spec/support/.*\.rb\z})
16
+ @support_spec_pattern = support_spec_pattern
17
+ end
18
+
19
+ # @param [Crystalball::SourceDiff] diff - the diff from which to predict
20
+ # which specs should run
21
+ # @param [Crystalball::ExampleGroupMap] map - the map with the relations of
22
+ # examples and used files
23
+ # @return [Array<String>] the spec paths associated with the changes
24
+ def call(diff, map)
25
+ super do
26
+ changed_support_files = diff.map(&:relative_path).grep(support_spec_pattern)
27
+
28
+ examples = detect_examples(changed_support_files, map)
29
+
30
+ examples.map { |e| e.to_s.split("[").first }.uniq
31
+ end
32
+ end
33
+
34
+ private
35
+
36
+ attr_reader :support_spec_pattern
37
+ end
38
+ end
39
+ end
@@ -0,0 +1,16 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/predictor/helpers/path_formatter"
4
+
5
+ module Crystalball
6
+ class Predictor
7
+ # Base module to include in any strategy. Provides output formatting similar to RSpec
8
+ module Strategy
9
+ include Helpers::PathFormatter
10
+
11
+ def call(*)
12
+ format_paths(yield)
13
+ end
14
+ end
15
+ end
16
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Class to predict test failures with given execution map and sources diff
5
+ class Predictor
6
+ attr_reader :map, :from, :to, :prediction_strategies
7
+
8
+ # @param [Crystalball::ExecutionMap] map execution map
9
+ # @param [Crystalball::GitRepo] repo to build execution list on
10
+ # @param [String] from starting commit for diff. Default: HEAD
11
+ # @param [String] to ending commit for diff. Default: nil
12
+ def initialize(map, repo, from: "HEAD", to: nil)
13
+ @map = map
14
+ @repo = repo
15
+ @from = from
16
+ @to = to
17
+ @prediction_strategies = []
18
+ yield self if block_given?
19
+ end
20
+
21
+ # Adds additional predictor to use
22
+ #
23
+ # @param [#call] strategy - the strategy can be any object that responds to #call
24
+ def use(strategy)
25
+ prediction_strategies << strategy
26
+ end
27
+
28
+ # @return [Crystalball::Prediction] list of examples which may fail
29
+ def prediction
30
+ Prediction.new(filter(raw_prediction(diff)))
31
+ end
32
+
33
+ def diff
34
+ @diff ||= begin
35
+ ancestor = repo.merge_base(from, to || "HEAD").sha
36
+ repo.diff(ancestor, to)
37
+ end
38
+ end
39
+
40
+ private
41
+
42
+ # TODO: check if it would be better to call predictors with one case instead of passing the whole map.
43
+ def predict!(current_diff)
44
+ prediction_strategies.flat_map { |strategy| strategy.call(current_diff, map) }
45
+ end
46
+ alias raw_prediction predict!
47
+
48
+ attr_reader :repo
49
+
50
+ def filter(example_groups)
51
+ example_groups.compact.select { |example_group| extract_file_path(example_group).exist? }.uniq
52
+ end
53
+
54
+ def extract_file_path(example)
55
+ repo.repo_path.join(example.split("[").first).expand_path
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,55 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Class to collect statistics about prediction quality
5
+ class PredictorEvaluator
6
+ attr_reader :predictor, :actual_failures
7
+
8
+ # @param [Crystalball::Predictor] predictor - configured predictor to fetch list of examples which might fail
9
+ # @param [Array<String>] actual_failures - list of actual failed examples
10
+ def initialize(predictor, actual_failures:)
11
+ @predictor = predictor
12
+ @actual_failures = actual_failures
13
+ end
14
+
15
+ def predicted_failures
16
+ @predicted_failures ||= actual_failures.select do |failure|
17
+ prediction.any? { |p| failure.include?(p) }
18
+ end
19
+ end
20
+
21
+ def unpredicted_failures
22
+ actual_failures - predicted_failures
23
+ end
24
+
25
+ def diff_size
26
+ predictor.diff.lines
27
+ end
28
+
29
+ def prediction_to_diff_ratio
30
+ prediction_size.to_f / diff_size
31
+ end
32
+
33
+ def prediction_scale
34
+ prediction_size.to_f / map_size
35
+ end
36
+
37
+ def prediction_rate
38
+ actual_failures.empty? ? 1.0 : predicted_failures.size.to_f / actual_failures.size
39
+ end
40
+
41
+ def prediction_size
42
+ @prediction_size ||= predictor.map.example_groups.keys.select { |example| prediction.any? { |p| example.include?(p) } }.size
43
+ end
44
+
45
+ def map_size
46
+ predictor.map.size
47
+ end
48
+
49
+ private
50
+
51
+ def prediction
52
+ @prediction ||= predictor.prediction
53
+ end
54
+ end
55
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ module Rails
5
+ module Helpers
6
+ # Interface for schema parsers
7
+ module BaseSchemaParser
8
+ def self.parse(*_)
9
+ raise NotImplementedError
10
+ end
11
+
12
+ # @return [Hash] stored info about all method calls which ended in #method_missing
13
+ attr_accessor :hash
14
+
15
+ def initialize
16
+ @hash = {}
17
+ end
18
+
19
+ private
20
+
21
+ # Store info about call in hash. First argument of method call used as a key
22
+ def method_missing(method_name, *args, &block)
23
+ name = args.shift
24
+ add_to_hash(name, options: [method_name] + args)
25
+
26
+ new_parser = self.class.new
27
+ add_to_hash(name, content: new_parser.instance_exec(&block)) if block
28
+ add_to_hash(name, content: new_parser.hash)
29
+ new_parser
30
+ end
31
+
32
+ def respond_to_missing?(*_)
33
+ true
34
+ end
35
+
36
+ def add_to_hash(name, options: nil, content: nil)
37
+ hash[name] ||= {}
38
+ add_optional(name, :options, options)
39
+ add_optional(name, :content, content)
40
+ end
41
+
42
+ def add_optional(name, key, value)
43
+ return unless value
44
+
45
+ hash[name][key] ||= []
46
+ hash[name][key] << value
47
+ end
48
+ end
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,21 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ module Rails
5
+ module Helpers
6
+ class SchemaDefinitionParser
7
+ # This mock will be used during SchemaDefinitionParser.parse
8
+ module ActiveRecord
9
+ # A simple mock to read definition of schema
10
+ class Schema
11
+ def self.define(*_args, &block)
12
+ collector = SchemaDefinitionParser.new
13
+ collector.instance_exec(collector, &block)
14
+ collector.hash
15
+ end
16
+ end
17
+ end
18
+ end
19
+ end
20
+ end
21
+ end
@@ -0,0 +1,27 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/rails/helpers/base_schema_parser"
4
+
5
+ module Crystalball
6
+ module Rails
7
+ module Helpers
8
+ class SchemaDefinitionParser
9
+ # Class used to parse ActiveRecord::Schema create_table definition and provide hash representation
10
+ class TableContentParser
11
+ include BaseSchemaParser
12
+
13
+ # Parse create_table definition of schema
14
+ # @param [Proc] block - block for create_table definition
15
+ # @return [Hash] hash representation of table definition
16
+ def self.parse(&block)
17
+ return {} unless block
18
+
19
+ collector = new
20
+ collector.instance_exec(collector, &block)
21
+ collector.hash
22
+ end
23
+ end
24
+ end
25
+ end
26
+ end
27
+ end
@@ -0,0 +1,36 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/rails/helpers/base_schema_parser"
4
+ require "crystalball/rails/helpers/schema_definition_parser/active_record"
5
+ require "crystalball/rails/helpers/schema_definition_parser/table_content_parser"
6
+
7
+ module Crystalball
8
+ module Rails
9
+ module Helpers
10
+ # Class used to parse ActiveRecord::Schema definition and provide hash representation
11
+ class SchemaDefinitionParser
12
+ include BaseSchemaParser
13
+
14
+ # Parse schema content
15
+ # @param [String] schema - schema file content
16
+ # @return [Hash] hash representation of schema
17
+ def self.parse(schema)
18
+ return {} if schema&.empty?
19
+
20
+ new.instance_eval(schema)
21
+ end
22
+
23
+ private
24
+
25
+ def create_table(table_name, *options, &block)
26
+ add_to_hash(table_name, options: ["create_table"] + options, content: TableContentParser.parse(&block))
27
+ end
28
+
29
+ def add_foreign_key(table1, table2, *options)
30
+ add_to_hash(table1, options: ["add_foreign_key", table2] + options)
31
+ add_to_hash(table2, options: ["add_foreign_key", table1] + options) if table1 != table2
32
+ end
33
+ end
34
+ end
35
+ end
36
+ end
@@ -0,0 +1,42 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "action_view"
4
+
5
+ module Crystalball
6
+ module Rails
7
+ class MapGenerator
8
+ class ActionViewStrategy
9
+ # Module to add new patched `compile!` method to ActionView::Template
10
+ module Patch
11
+ class << self
12
+ # Patches `ActionView::Template#compile!`. Renames original `compile!` to `cb_original_compile!` and
13
+ # replaces it with custom one
14
+ def apply!
15
+ ::ActionView::Template.class_eval do
16
+ include Patch
17
+
18
+ alias_method :cb_original_compile!, :compile!
19
+ alias_method :compile!, :cb_patched_compile!
20
+ end
21
+ end
22
+
23
+ # Reverts original behavior of `ActionView::Template#compile!`
24
+ def revert!
25
+ ::ActionView::Template.class_eval do
26
+ alias_method :compile!, :cb_original_compile! # rubocop:disable Lint/DuplicateMethods
27
+ undef_method :cb_patched_compile!
28
+ end
29
+ end
30
+ end
31
+
32
+ # Will replace original `ActionView::Template#compile!`. Pushes path of a view to
33
+ # `ActionViewStrategy.views` and calls original `compile!`
34
+ def cb_patched_compile!(*args)
35
+ ActionViewStrategy.views.push identifier
36
+ cb_original_compile!(*args)
37
+ end
38
+ end
39
+ end
40
+ end
41
+ end
42
+ end