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,80 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/rspec/standard_prediction_builder"
4
+
5
+ module Crystalball
6
+ module RSpec
7
+ class Runner
8
+ # Class for storing local runner configuration
9
+ class Configuration
10
+ def initialize(config = {}) # rubocop:disable Metrics/MethodLength
11
+ @values = {
12
+ "execution_map_path" => "tmp/crystalball_data.yml",
13
+ "map_expiration_period" => 86_400,
14
+ "repo_path" => Dir.pwd,
15
+ "requires" => [],
16
+ "diff_from" => "HEAD",
17
+ "diff_to" => nil,
18
+ "runner_class_name" => "Crystalball::RSpec::Runner",
19
+ "prediction_builder_class_name" => "Crystalball::RSpec::StandardPredictionBuilder",
20
+ "log_level" => :info,
21
+ "log_file" => "log/crystalball.log"
22
+ }.merge(config)
23
+ end
24
+
25
+ def to_h
26
+ dynamic_values = {}
27
+ (private_methods - Object.private_instance_methods - %i[run_requires values raw_value]).each do |method|
28
+ dynamic_values[method.to_s] = send(method)
29
+ end
30
+
31
+ values.merge(dynamic_values)
32
+ end
33
+
34
+ def [](key)
35
+ respond_to?(key, true) ? send(key) : raw_value(key)
36
+ end
37
+
38
+ private
39
+
40
+ def raw_value(key)
41
+ ENV.fetch("CRYSTALBALL_#{key.to_s.upcase}", values[key])
42
+ end
43
+
44
+ def prediction_builder_class
45
+ @prediction_builder_class ||= begin
46
+ run_requires
47
+
48
+ Object.const_get(self["prediction_builder_class_name"])
49
+ end
50
+ end
51
+
52
+ def runner_class
53
+ @runner_class ||= begin
54
+ run_requires
55
+
56
+ Object.const_get(self["runner_class_name"])
57
+ end
58
+ end
59
+
60
+ def execution_map_path
61
+ @execution_map_path ||= Pathname.new(raw_value("execution_map_path"))
62
+ end
63
+
64
+ def repo_path
65
+ @repo_path ||= Pathname.new(raw_value("repo_path"))
66
+ end
67
+
68
+ def log_file
69
+ @log_file ||= Pathname.new(raw_value("log_file"))
70
+ end
71
+
72
+ attr_reader :values
73
+
74
+ def run_requires
75
+ Array(self["requires"]).each { |f| require f }
76
+ end
77
+ end
78
+ end
79
+ end
80
+ end
@@ -0,0 +1,107 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "rspec/core"
4
+ require "crystalball/rspec/prediction_builder"
5
+ require "crystalball/rspec/filtering"
6
+ require "crystalball/rspec/prediction_pruning"
7
+
8
+ module Crystalball
9
+ module RSpec
10
+ # Our custom RSpec runner to run predictions
11
+ class Runner < ::RSpec::Core::Runner
12
+ include PredictionPruning
13
+
14
+ class << self
15
+ def run(args, err = $stderr, out = $stdout)
16
+ return config["runner_class"].run(args, err, out) unless config["runner_class"] == self
17
+
18
+ Crystalball.log :info, "Crystalball starts to glow..."
19
+ prediction = build_prediction
20
+
21
+ Crystalball.log :debug, "Prediction: #{prediction.first(5).join(' ')}#{'...' if prediction.size > 5}"
22
+ Crystalball.log :info, "Starting RSpec."
23
+
24
+ super(args + prediction, err, out)
25
+ end
26
+
27
+ def reset!
28
+ self.prediction_builder = nil
29
+ self.config = nil
30
+ end
31
+
32
+ def prepare
33
+ config["runner_class"].load_execution_map
34
+ end
35
+
36
+ def prediction_builder
37
+ @prediction_builder ||= config["prediction_builder_class"].new(config)
38
+ end
39
+
40
+ def config
41
+ @config ||= begin
42
+ config_src = if config_file
43
+ require "yaml"
44
+ YAML.safe_load(config_file.read)
45
+ else
46
+ {}
47
+ end
48
+
49
+ Configuration.new(config_src)
50
+ end
51
+ end
52
+
53
+ protected
54
+
55
+ def load_execution_map
56
+ check_map
57
+ prediction_builder.execution_map
58
+ end
59
+
60
+ private
61
+
62
+ attr_writer :config, :prediction_builder
63
+
64
+ def config_file
65
+ file = Pathname.new(ENV.fetch("CRYSTALBALL_CONFIG", "crystalball.yml"))
66
+ file = Pathname.new("config/crystalball.yml") unless file.exist?
67
+ file.exist? ? file : nil
68
+ end
69
+
70
+ def build_prediction
71
+ check_map
72
+ prune_prediction_to_limit(prediction_builder.prediction.sort_by(&:length))
73
+ end
74
+
75
+ def check_map
76
+ Crystalball.log :warn, "Maps are outdated!" if prediction_builder.expired_map?
77
+ end
78
+ end
79
+
80
+ def setup(err, out)
81
+ configure(err, out)
82
+ @configuration.load_spec_files
83
+
84
+ Filtering.remove_unnecessary_filters(@configuration, @options.options[:files_or_directories_to_run])
85
+
86
+ if reconfiguration_needed?
87
+ Crystalball.log :warn, "Prediction examples size #{@world.example_count} is over the limit (#{examples_limit})"
88
+ Crystalball.log :warn, "Prediction is pruned to fit the limit!"
89
+
90
+ reconfigure_to_limit
91
+ @configuration.load_spec_files
92
+ end
93
+
94
+ @world.announce_filters
95
+ end
96
+
97
+ # Backward compatibility for RSpec < 3.7
98
+ def configure(err, out)
99
+ @configuration.error_stream = err
100
+ @configuration.output_stream = out if @configuration.output_stream == $stdout
101
+ @options.configure(@configuration)
102
+ end
103
+ end
104
+ end
105
+ end
106
+
107
+ require "crystalball/rspec/runner/configuration"
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ module RSpec
5
+ # Simple version of predictor
6
+ class StandardPredictionBuilder < PredictionBuilder
7
+ private
8
+
9
+ def predictor
10
+ super do |p|
11
+ p.use Crystalball::Predictor::ModifiedExecutionPaths.new
12
+ p.use Crystalball::Predictor::ModifiedSpecs.new
13
+ end
14
+ end
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ class SourceDiff
5
+ # Data object for single file in Git repo diff
6
+ class FileDiff
7
+ # @param [Git::DiffFile] git_diff - raw diff for a single file made by ruby-git gem
8
+ def initialize(git_diff)
9
+ @git_diff = git_diff
10
+ end
11
+
12
+ def moved?
13
+ !(git_diff.patch =~ /rename from.*\nrename to/).nil?
14
+ end
15
+
16
+ def modified?
17
+ !moved? && git_diff.type == "modified"
18
+ end
19
+
20
+ def deleted?
21
+ git_diff.type == "deleted"
22
+ end
23
+
24
+ def new?
25
+ git_diff.type == "new"
26
+ end
27
+
28
+ # @return relative path to file
29
+ def relative_path
30
+ git_diff.path
31
+ end
32
+
33
+ # @return new relative path to file if file was moved
34
+ def new_relative_path
35
+ return relative_path unless moved?
36
+
37
+ git_diff.patch.match(/rename from.*\nrename to (.*)/)[1]
38
+ end
39
+
40
+ def method_missing(method, *args, &block)
41
+ git_diff.public_send(method, *args, &block) || super
42
+ end
43
+
44
+ def respond_to_missing?(method, *)
45
+ git_diff.respond_to?(method, false) || super
46
+ end
47
+
48
+ private
49
+
50
+ attr_reader :git_diff
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,50 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ class SourceDiff
5
+ # Determinates if file_diff's patch contains changes for whitespaces or comments only
6
+ module FormattingChecker
7
+ class << self
8
+ # Returns `true` if file_diff's patch contains changes for whitespaces or comments only
9
+ #
10
+ # @param [Crystalball::SourceDiff::FileDiff] file_diff
11
+ # @return [Boolean]
12
+ def pure_formatting?(file_diff)
13
+ return false unless stripable_file?(file_diff.path) && file_diff.modified?
14
+
15
+ patch = file_diff.patch.to_s.lines
16
+
17
+ return true if patch.empty?
18
+
19
+ added = collect_patch(patch, "+")
20
+ removed = collect_patch(patch, "-")
21
+
22
+ trim_patch(added) == trim_patch(removed)
23
+ end
24
+
25
+ private
26
+
27
+ STRIPABLE_FILES = %w[.rb .erb].freeze # TODO: move to config
28
+
29
+ def stripable_file?(file_name)
30
+ STRIPABLE_FILES.include?(Pathname(file_name).extname)
31
+ end
32
+
33
+ def collect_patch(patch, sign)
34
+ patch.each.with_object([]) do |line, result|
35
+ next if line.start_with?("+++", "---", "@@") # Skip meta of a patch
36
+
37
+ result << line[1..-1] if line.start_with?(sign, " ")
38
+ end
39
+ end
40
+
41
+ def trim_patch(patch)
42
+ patch.map do |line|
43
+ line = line.gsub(/\s/, "")
44
+ line.start_with?("#") || line.empty? ? nil : line
45
+ end.compact
46
+ end
47
+ end
48
+ end
49
+ end
50
+ end
@@ -0,0 +1,48 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/source_diff/file_diff"
4
+ require "crystalball/source_diff/formatting_checker"
5
+ require "forwardable"
6
+
7
+ module Crystalball
8
+ # Wrapper class representing Git source diff for given repo
9
+ class SourceDiff
10
+ include Enumerable
11
+ extend Forwardable
12
+
13
+ delegate %i[stats lines from to] => :git_diff
14
+ alias size count
15
+
16
+ # @param [Git::Diff] git_diff raw diff made by ruby-git gem
17
+ def initialize(git_diff)
18
+ @git_diff = git_diff
19
+ end
20
+
21
+ # Iterates over each changed file of diff
22
+ #
23
+ def each
24
+ changeset.each { |file| yield file }
25
+ end
26
+
27
+ def empty?
28
+ changeset.none?
29
+ end
30
+
31
+ # @return [Git::Base]
32
+ def repository
33
+ @repository ||= git_diff.instance_variable_get(:@base)
34
+ end
35
+
36
+ private
37
+
38
+ attr_reader :git_diff
39
+
40
+ # TODO: Include untracked to changeset
41
+ def changeset
42
+ @changeset ||= git_diff.map do |diff_file|
43
+ file_diff = FileDiff.new(diff_file)
44
+ file_diff unless FormattingChecker.pure_formatting?(file_diff)
45
+ end.compact
46
+ end
47
+ end
48
+ end
@@ -0,0 +1,5 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ VERSION = "0.7.1"
5
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/logging"
4
+ require "crystalball/git_repo"
5
+ require "crystalball/extensions/git"
6
+ require "crystalball/rspec/prediction_builder"
7
+ require "crystalball/rspec/runner"
8
+ require "crystalball/prediction"
9
+ require "crystalball/predictor"
10
+ require "crystalball/predictor/modified_execution_paths"
11
+ require "crystalball/predictor/modified_specs"
12
+ require "crystalball/predictor/modified_support_specs"
13
+ require "crystalball/predictor/associated_specs"
14
+ require "crystalball/example_group_map"
15
+ require "crystalball/execution_map"
16
+ require "crystalball/map_generator"
17
+ require "crystalball/map_generator/configuration"
18
+ require "crystalball/map_generator/coverage_strategy"
19
+ require "crystalball/map_generator/allocated_objects_strategy"
20
+ require "crystalball/map_generator/described_class_strategy"
21
+ require "crystalball/map_storage/yaml_storage"
22
+ require "crystalball/map_compactor"
23
+ require "crystalball/version"
24
+
25
+ # Main module for the library
26
+ module Crystalball
27
+ # Prints the list of specs which might fail
28
+ #
29
+ # @param [String] workdir - path to the root directory of repository (usually contains .git folder inside). Default: current directory
30
+ # @param [String] map_path - path to the execution map. Default: crystalball_data.yml
31
+ # @param [Proc] block - used to configure predictors
32
+ #
33
+ # @example
34
+ # Crystalball.foresee do |predictor|
35
+ # predictor.use Crystalball::Predictor::ModifiedExecutionPaths.new
36
+ # predictor.use Crystalball::Predictor::ModifiedSpecs.new
37
+ # end
38
+ def self.foresee(workdir: ".", map_path: "crystalball_data.yml", &block)
39
+ map = MapStorage::YAMLStorage.load(Pathname(map_path))
40
+ Predictor.new(map, GitRepo.open(Pathname(workdir)), from: map.commit, &block).prediction.compact
41
+ end
42
+
43
+ extend Logging
44
+ end