crystalball 0.5.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 +7 -0
- data/.gitignore +19 -0
- data/.rspec +3 -0
- data/.rubocop.yml +18 -0
- data/.travis.yml +20 -0
- data/CODE_OF_CONDUCT.md +74 -0
- data/Gemfile +6 -0
- data/LICENSE +674 -0
- data/README.md +196 -0
- data/Rakefile +8 -0
- data/bin/console +15 -0
- data/bin/crystalball +5 -0
- data/bin/setup +8 -0
- data/crystalball.gemspec +48 -0
- data/lib/crystalball.rb +38 -0
- data/lib/crystalball/case_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +55 -0
- data/lib/crystalball/git_repo.rb +58 -0
- data/lib/crystalball/map_generator.rb +81 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +44 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +44 -0
- data/lib/crystalball/map_generator/base_strategy.rb +21 -0
- data/lib/crystalball/map_generator/configuration.rb +54 -0
- data/lib/crystalball/map_generator/coverage_strategy.rb +34 -0
- data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
- data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
- data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
- data/lib/crystalball/map_generator/object_sources_detector.rb +54 -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/strategies_collection.rb +43 -0
- data/lib/crystalball/map_storage/yaml_storage.rb +68 -0
- data/lib/crystalball/prediction.rb +34 -0
- data/lib/crystalball/predictor.rb +56 -0
- data/lib/crystalball/predictor/associated_specs.rb +40 -0
- data/lib/crystalball/predictor/modified_execution_paths.rb +21 -0
- data/lib/crystalball/predictor/modified_specs.rb +27 -0
- data/lib/crystalball/predictor_evaluator.rb +55 -0
- data/lib/crystalball/rails.rb +10 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +88 -0
- data/lib/crystalball/rspec/prediction_builder.rb +43 -0
- data/lib/crystalball/rspec/runner.rb +95 -0
- data/lib/crystalball/rspec/runner/configuration.rb +70 -0
- data/lib/crystalball/simple_predictor.rb +18 -0
- data/lib/crystalball/source_diff.rb +37 -0
- data/lib/crystalball/source_diff/file_diff.rb +53 -0
- data/lib/crystalball/version.rb +5 -0
- metadata +263 -0
@@ -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 `old_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
|
@@ -0,0 +1,47 @@
|
|
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 case map the locale files used by the example
|
38
|
+
# @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
|
39
|
+
def call(case_map, _)
|
40
|
+
self.class.reset_locale_files
|
41
|
+
yield case_map
|
42
|
+
case_map.push(*filter(self.class.locale_files.compact))
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
47
|
+
end
|
@@ -0,0 +1,88 @@
|
|
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
|
+
cb_add_filename_to_values(value, filename)
|
66
|
+
else
|
67
|
+
data[key] = {cb_filename: filename, cb_value: value}.freeze
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
71
|
+
|
72
|
+
def cb_remove_and_track_filename_from_values(data)
|
73
|
+
return data unless data.is_a?(Hash)
|
74
|
+
|
75
|
+
if data.key?(:cb_filename)
|
76
|
+
::Crystalball::Rails::MapGenerator::I18nStrategy.locale_files << data[:cb_filename]
|
77
|
+
return data[:cb_value]
|
78
|
+
end
|
79
|
+
|
80
|
+
data.each.with_object({}) do |(key, value), collector|
|
81
|
+
collector[key] = cb_remove_and_track_filename_from_values(value)
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
85
|
+
end
|
86
|
+
end
|
87
|
+
end
|
88
|
+
end
|
@@ -0,0 +1,43 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'pathname'
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
module RSpec
|
7
|
+
# Class for building a prediction for RSpec runner.
|
8
|
+
# Accepts configuration hash and builds a prediction according to configuration.
|
9
|
+
class PredictionBuilder
|
10
|
+
attr_reader :config
|
11
|
+
|
12
|
+
def initialize(config = {})
|
13
|
+
@config = config
|
14
|
+
end
|
15
|
+
|
16
|
+
def prediction
|
17
|
+
base_predictor.prediction
|
18
|
+
end
|
19
|
+
|
20
|
+
def expired_map?
|
21
|
+
return false if config['map_expiration_period'] <= 0
|
22
|
+
|
23
|
+
map_commit = repo.gcommit(map.commit) || raise("Cant find map commit info #{map.commit}")
|
24
|
+
|
25
|
+
map_commit.date < Time.now - config['map_expiration_period']
|
26
|
+
end
|
27
|
+
|
28
|
+
def map
|
29
|
+
@map ||= Crystalball::MapStorage::YAMLStorage.load(config['map_path'])
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def repo
|
35
|
+
@repo ||= Crystalball::GitRepo.open(config['repo_path'])
|
36
|
+
end
|
37
|
+
|
38
|
+
def base_predictor
|
39
|
+
@base_predictor ||= config['predictor_class'].new(map, repo, from: config['diff_from'], to: config['diff_to'])
|
40
|
+
end
|
41
|
+
end
|
42
|
+
end
|
43
|
+
end
|
@@ -0,0 +1,95 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'rspec/core'
|
4
|
+
require 'crystalball/rspec/prediction_builder'
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
module RSpec
|
8
|
+
# Our custom RSpec runner to run predictions
|
9
|
+
class Runner < ::RSpec::Core::Runner
|
10
|
+
class << self
|
11
|
+
def run(args, err = $stderr, out = $stdout)
|
12
|
+
return config['runner_class'].run(args, err, out) unless config['runner_class'] == self
|
13
|
+
|
14
|
+
out.puts "Crystalball starts to glow..."
|
15
|
+
super(args + build_prediction(out), err, out)
|
16
|
+
end
|
17
|
+
|
18
|
+
def reset!
|
19
|
+
self.prediction_builder = nil
|
20
|
+
self.config = nil
|
21
|
+
end
|
22
|
+
|
23
|
+
def prepare
|
24
|
+
config['runner_class'].load_map
|
25
|
+
end
|
26
|
+
|
27
|
+
def prediction_builder
|
28
|
+
@prediction_builder ||= PredictionBuilder.new(config)
|
29
|
+
end
|
30
|
+
|
31
|
+
def config
|
32
|
+
@config ||= begin
|
33
|
+
config_src = if config_file
|
34
|
+
require 'yaml'
|
35
|
+
YAML.safe_load(config_file.read)
|
36
|
+
else
|
37
|
+
{}
|
38
|
+
end
|
39
|
+
|
40
|
+
Configuration.new(config_src)
|
41
|
+
end
|
42
|
+
end
|
43
|
+
|
44
|
+
protected
|
45
|
+
|
46
|
+
def load_map
|
47
|
+
check_map($stdout) unless ENV['CRYSTALBALL_SKIP_MAP_CHECK']
|
48
|
+
prediction_builder.map
|
49
|
+
end
|
50
|
+
|
51
|
+
private
|
52
|
+
|
53
|
+
attr_writer :config, :prediction_builder
|
54
|
+
|
55
|
+
def config_file
|
56
|
+
file = Pathname.new(ENV.fetch('CRYSTALBALL_CONFIG', 'crystalball.yml'))
|
57
|
+
file = Pathname.new('config/crystalball.yml') unless file.exist?
|
58
|
+
file.exist? ? file : nil
|
59
|
+
end
|
60
|
+
|
61
|
+
def build_prediction(out)
|
62
|
+
check_map(out) unless ENV['CRYSTALBALL_SKIP_MAP_CHECK']
|
63
|
+
prediction = prediction_builder.prediction.compact
|
64
|
+
out.puts "Prediction: #{prediction.first(5).join(' ')}#{'...' if prediction.size > 5}"
|
65
|
+
out.puts "Starting RSpec."
|
66
|
+
prediction
|
67
|
+
end
|
68
|
+
|
69
|
+
def check_map(out)
|
70
|
+
out.puts 'Maps are outdated!' if prediction_builder.expired_map?
|
71
|
+
end
|
72
|
+
end
|
73
|
+
|
74
|
+
def run_specs(example_groups)
|
75
|
+
check_examples_limit(example_groups)
|
76
|
+
super
|
77
|
+
end
|
78
|
+
|
79
|
+
def check_examples_limit(example_groups)
|
80
|
+
limit = self.class.config['examples_limit'].to_i
|
81
|
+
return if ENV['CRYSTALBALL_SKIP_EXAMPLES_LIMIT'] || !limit.positive?
|
82
|
+
|
83
|
+
examples_count = @world.example_count(example_groups)
|
84
|
+
|
85
|
+
return if examples_count <= limit
|
86
|
+
|
87
|
+
@configuration.output_stream.puts "Example group size (#{examples_count}) is over the limit (#{limit})"
|
88
|
+
@configuration.output_stream.puts "Aborting spec run"
|
89
|
+
exit
|
90
|
+
end
|
91
|
+
end
|
92
|
+
end
|
93
|
+
end
|
94
|
+
|
95
|
+
require 'crystalball/rspec/runner/configuration'
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/simple_predictor'
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
module RSpec
|
7
|
+
class Runner
|
8
|
+
# Class for storing local runner configuration
|
9
|
+
class Configuration
|
10
|
+
def initialize(config = {})
|
11
|
+
@values = {
|
12
|
+
'map_path' => 'tmp/execution_maps',
|
13
|
+
'map_expiration_period' => 86_400,
|
14
|
+
'repo_path' => Dir.pwd,
|
15
|
+
'predictor_class_name' => 'Crystalball::SimplePredictor',
|
16
|
+
'requires' => [],
|
17
|
+
'diff_from' => 'HEAD',
|
18
|
+
'diff_to' => nil,
|
19
|
+
'runner_class_name' => 'Crystalball::RSpec::Runner'
|
20
|
+
}.merge(config)
|
21
|
+
end
|
22
|
+
|
23
|
+
def to_h
|
24
|
+
dynamic_values = {}
|
25
|
+
(private_methods - Object.private_instance_methods - %i[run_requires values]).each do |method|
|
26
|
+
dynamic_values[method.to_s] = send(method)
|
27
|
+
end
|
28
|
+
|
29
|
+
values.merge(dynamic_values)
|
30
|
+
end
|
31
|
+
|
32
|
+
def [](key)
|
33
|
+
respond_to?(key, true) ? send(key) : values[key]
|
34
|
+
end
|
35
|
+
|
36
|
+
private
|
37
|
+
|
38
|
+
def predictor_class
|
39
|
+
@predictor_class ||= begin
|
40
|
+
run_requires
|
41
|
+
|
42
|
+
Object.const_get(self['predictor_class_name'])
|
43
|
+
end
|
44
|
+
end
|
45
|
+
|
46
|
+
def runner_class
|
47
|
+
@runner_class ||= begin
|
48
|
+
run_requires
|
49
|
+
|
50
|
+
Object.const_get(self['runner_class_name'])
|
51
|
+
end
|
52
|
+
end
|
53
|
+
|
54
|
+
def map_path
|
55
|
+
@map_path ||= Pathname.new(values['map_path'])
|
56
|
+
end
|
57
|
+
|
58
|
+
def repo_path
|
59
|
+
@repo_path ||= Pathname.new(values['repo_path'])
|
60
|
+
end
|
61
|
+
|
62
|
+
attr_reader :values
|
63
|
+
|
64
|
+
def run_requires
|
65
|
+
self['requires'].each { |f| require f }
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,18 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/predictor'
|
4
|
+
require 'crystalball/predictor/modified_execution_paths'
|
5
|
+
require 'crystalball/predictor/modified_specs'
|
6
|
+
|
7
|
+
module Crystalball
|
8
|
+
# Class to predict test failures with given execution map and sources diff
|
9
|
+
class SimplePredictor < Predictor
|
10
|
+
def initialize(*args, &block)
|
11
|
+
super(*args) do |p|
|
12
|
+
p.use Crystalball::Predictor::ModifiedExecutionPaths.new
|
13
|
+
p.use Crystalball::Predictor::ModifiedSpecs.new
|
14
|
+
block&.call(p)
|
15
|
+
end
|
16
|
+
end
|
17
|
+
end
|
18
|
+
end
|
@@ -0,0 +1,37 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/source_diff/file_diff'
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
# Wrapper class representing Git source diff for given repo
|
7
|
+
class SourceDiff
|
8
|
+
include Enumerable
|
9
|
+
extend Forwardable
|
10
|
+
|
11
|
+
delegate %i[stats size lines from to] => :git_diff
|
12
|
+
|
13
|
+
# @param [Git::Diff] git_diff raw diff made by ruby-git gem
|
14
|
+
def initialize(git_diff)
|
15
|
+
@git_diff = git_diff
|
16
|
+
end
|
17
|
+
|
18
|
+
# Iterates over each changed file of diff
|
19
|
+
#
|
20
|
+
def each
|
21
|
+
changeset.each { |file| yield file }
|
22
|
+
end
|
23
|
+
|
24
|
+
def empty?
|
25
|
+
changeset.none?
|
26
|
+
end
|
27
|
+
|
28
|
+
private
|
29
|
+
|
30
|
+
attr_reader :git_diff
|
31
|
+
|
32
|
+
# TODO: Include untracked to changeset
|
33
|
+
def changeset
|
34
|
+
@changeset ||= git_diff.map { |file_diff| FileDiff.new(file_diff) }
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|