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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/bin/crystalball +5 -0
- data/lib/crystalball/active_record.rb +4 -0
- data/lib/crystalball/example_group_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +56 -0
- data/lib/crystalball/extensions/git/base.rb +14 -0
- data/lib/crystalball/extensions/git/lib.rb +17 -0
- data/lib/crystalball/extensions/git.rb +4 -0
- data/lib/crystalball/factory_bot.rb +3 -0
- data/lib/crystalball/git_repo.rb +53 -0
- data/lib/crystalball/logging.rb +51 -0
- data/lib/crystalball/map_compactor/example_context.rb +29 -0
- data/lib/crystalball/map_compactor/example_groups_data_compactor.rb +66 -0
- data/lib/crystalball/map_compactor.rb +44 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +45 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +45 -0
- data/lib/crystalball/map_generator/base_strategy.rb +22 -0
- data/lib/crystalball/map_generator/configuration.rb +58 -0
- data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
- data/lib/crystalball/map_generator/coverage_strategy.rb +35 -0
- data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +30 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +27 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +25 -0
- data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
- data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
- data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
- data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
- data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
- data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
- data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
- data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
- data/lib/crystalball/map_generator.rb +84 -0
- data/lib/crystalball/map_storage/yaml_storage.rb +69 -0
- data/lib/crystalball/prediction.rb +35 -0
- data/lib/crystalball/predictor/associated_specs.rb +45 -0
- data/lib/crystalball/predictor/helpers/affected_example_groups_detector.rb +20 -0
- data/lib/crystalball/predictor/helpers/path_formatter.rb +18 -0
- data/lib/crystalball/predictor/modified_execution_paths.rb +27 -0
- data/lib/crystalball/predictor/modified_specs.rb +33 -0
- data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
- data/lib/crystalball/predictor/strategy.rb +16 -0
- data/lib/crystalball/predictor.rb +58 -0
- data/lib/crystalball/predictor_evaluator.rb +55 -0
- data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser/active_record.rb +21 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb +27 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +89 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
- data/lib/crystalball/rails/predictor/modified_schema.rb +81 -0
- data/lib/crystalball/rails/tables_map.rb +53 -0
- data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
- data/lib/crystalball/rails/tables_map_generator.rb +84 -0
- data/lib/crystalball/rails.rb +11 -0
- data/lib/crystalball/rspec/filtering.rb +52 -0
- data/lib/crystalball/rspec/prediction_builder.rb +53 -0
- data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
- data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
- data/lib/crystalball/rspec/runner/configuration.rb +80 -0
- data/lib/crystalball/rspec/runner.rb +107 -0
- data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
- data/lib/crystalball/source_diff/file_diff.rb +53 -0
- data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
- data/lib/crystalball/source_diff.rb +48 -0
- data/lib/crystalball/version.rb +5 -0
- data/lib/crystalball.rb +44 -0
- 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
|
data/lib/crystalball.rb
ADDED
@@ -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
|