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,46 @@
|
|
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/action_view_strategy/patch"
|
6
|
+
|
7
|
+
module Crystalball
|
8
|
+
module Rails
|
9
|
+
class MapGenerator
|
10
|
+
# Map generator strategy to build map of views affected by an example.
|
11
|
+
# It patches `ActionView::Template#compile!` to get original name of compiled views.
|
12
|
+
class ActionViewStrategy
|
13
|
+
include ::Crystalball::MapGenerator::BaseStrategy
|
14
|
+
include ::Crystalball::MapGenerator::Helpers::PathFilter
|
15
|
+
|
16
|
+
class << self
|
17
|
+
# List of views affected by current example
|
18
|
+
#
|
19
|
+
# @return [Array<String>]
|
20
|
+
attr_reader :views
|
21
|
+
|
22
|
+
# Reset cached list of views
|
23
|
+
def reset_views
|
24
|
+
@views = []
|
25
|
+
end
|
26
|
+
end
|
27
|
+
|
28
|
+
def after_start
|
29
|
+
Patch.apply!
|
30
|
+
end
|
31
|
+
|
32
|
+
def before_finalize
|
33
|
+
Patch.revert!
|
34
|
+
end
|
35
|
+
|
36
|
+
# Adds views related to the spec to the example group map
|
37
|
+
# @param [Crystalball::ExampleGroupMap] example_group_map - object holding example metadata and used files
|
38
|
+
def call(example_group_map, _)
|
39
|
+
self.class.reset_views
|
40
|
+
yield example_group_map
|
41
|
+
example_group_map.push(*filter(self.class.views))
|
42
|
+
end
|
43
|
+
end
|
44
|
+
end
|
45
|
+
end
|
46
|
+
end
|
@@ -0,0 +1,89 @@
|
|
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
|
@@ -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 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
|
@@ -0,0 +1,81 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/rails/helpers/schema_definition_parser"
|
4
|
+
require "crystalball/predictor/helpers/affected_example_groups_detector"
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
module Rails
|
8
|
+
class Predictor
|
9
|
+
# Used with `predictor.use Crystalball::Rails::Predictor::ModifiedSchema.new(tables_map_path:)`.
|
10
|
+
# When used will check db/schema.rb for changes and add specs which depend on files affected
|
11
|
+
# by changed tables
|
12
|
+
class ModifiedSchema
|
13
|
+
include ::Crystalball::Predictor::Helpers::AffectedExampleGroupsDetector
|
14
|
+
SCHEMA_PATH = "db/schema.rb"
|
15
|
+
|
16
|
+
attr_reader :tables_map_path
|
17
|
+
|
18
|
+
# @param [String] tables_map_path - path to generated TablesMap
|
19
|
+
def initialize(tables_map_path:)
|
20
|
+
@tables_map_path = tables_map_path
|
21
|
+
end
|
22
|
+
|
23
|
+
# @param [Crystalball::SourceDiff] diff - the diff from which to predict
|
24
|
+
# which specs should run
|
25
|
+
# @param [Crystalball::ExecutionMap] map - the map with the relations of
|
26
|
+
# examples and used files
|
27
|
+
# @return [Array<String>] the spec paths associated with the changes
|
28
|
+
def call(diff, map)
|
29
|
+
return [] if schema_diff(diff).nil?
|
30
|
+
|
31
|
+
old_schema = old_schema(diff)
|
32
|
+
new_schema = new_schema(diff)
|
33
|
+
|
34
|
+
changed_tables = changed_tables(old_schema, new_schema)
|
35
|
+
|
36
|
+
files = changed_tables.flat_map do |table_name|
|
37
|
+
files = tables_map[table_name]
|
38
|
+
Crystalball.log :warn, "There are no model files for changed table `#{table_name}`. Check https://github.com/toptal/crystalball#warning for detailed description" unless files&.any?
|
39
|
+
files
|
40
|
+
end.compact
|
41
|
+
detect_examples(files, map)
|
42
|
+
end
|
43
|
+
|
44
|
+
# @return [Crystalball::Rails::TablesMap]
|
45
|
+
def tables_map
|
46
|
+
@tables_map ||= MapStorage::YAMLStorage.load(Pathname(tables_map_path))
|
47
|
+
end
|
48
|
+
|
49
|
+
private
|
50
|
+
|
51
|
+
def schema_diff(diff)
|
52
|
+
diff.find { |file_diff| [SCHEMA_PATH, "./#{SCHEMA_PATH}"].include? file_diff.relative_path }
|
53
|
+
end
|
54
|
+
|
55
|
+
def old_schema(diff)
|
56
|
+
old_schema_contents = schema_content(diff.repository, diff.from)
|
57
|
+
Crystalball::Rails::Helpers::SchemaDefinitionParser.parse(old_schema_contents)
|
58
|
+
end
|
59
|
+
|
60
|
+
def new_schema(diff)
|
61
|
+
new_schema_contents = schema_content(diff.repository, diff.to)
|
62
|
+
Crystalball::Rails::Helpers::SchemaDefinitionParser.parse(new_schema_contents)
|
63
|
+
end
|
64
|
+
|
65
|
+
def schema_content(repository, revision)
|
66
|
+
if revision
|
67
|
+
repository.lib.show(revision, SCHEMA_PATH)
|
68
|
+
else
|
69
|
+
File.read(File.join(repository.dir.path, SCHEMA_PATH))
|
70
|
+
end
|
71
|
+
end
|
72
|
+
|
73
|
+
def changed_tables(schema1, schema2)
|
74
|
+
schema1.map do |table_name, body|
|
75
|
+
table_name if schema2[table_name] != body
|
76
|
+
end.compact
|
77
|
+
end
|
78
|
+
end
|
79
|
+
end
|
80
|
+
end
|
81
|
+
end
|
@@ -0,0 +1,53 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
module Rails
|
5
|
+
# Storage for tables map
|
6
|
+
class TablesMap
|
7
|
+
extend Forwardable
|
8
|
+
|
9
|
+
# Simple data object for map metadata information
|
10
|
+
class Metadata
|
11
|
+
attr_reader :commit, :version
|
12
|
+
|
13
|
+
# @param [String] commit - SHA of commit
|
14
|
+
# @param [Numeric] version - map generator version number
|
15
|
+
def initialize(commit: nil, version: nil, **_)
|
16
|
+
@commit = commit
|
17
|
+
@version = version
|
18
|
+
end
|
19
|
+
|
20
|
+
def to_h
|
21
|
+
{type: TablesMap.name, commit: commit, version: version}
|
22
|
+
end
|
23
|
+
end
|
24
|
+
|
25
|
+
attr_reader :example_groups, :metadata
|
26
|
+
|
27
|
+
delegate %i[commit version] => :metadata
|
28
|
+
delegate %i[size [] []=] => :example_groups
|
29
|
+
|
30
|
+
# @param [Hash] metadata - add or override metadata of execution map
|
31
|
+
# @param [Hash] example_groups - initial list of tables
|
32
|
+
def initialize(metadata: {}, example_groups: {})
|
33
|
+
@metadata = Metadata.new(**metadata)
|
34
|
+
@example_groups = example_groups
|
35
|
+
end
|
36
|
+
|
37
|
+
# Remove all example_groups
|
38
|
+
def clear!
|
39
|
+
self.example_groups = {}
|
40
|
+
end
|
41
|
+
|
42
|
+
def add(files:, for_table:)
|
43
|
+
example_groups[for_table] ||= []
|
44
|
+
example_groups[for_table] += files
|
45
|
+
example_groups[for_table].uniq!
|
46
|
+
end
|
47
|
+
|
48
|
+
private
|
49
|
+
|
50
|
+
attr_writer :example_groups, :metadata
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,39 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/map_generator/object_sources_detector"
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
module Rails
|
7
|
+
class TablesMapGenerator
|
8
|
+
# Configuration of tables map generator. Is can be accessed as a first argument inside
|
9
|
+
# `Crystalball::Rails::TablesMapGenerator.start! { |config| config } block.
|
10
|
+
class Configuration
|
11
|
+
attr_writer :map_storage
|
12
|
+
attr_accessor :commit
|
13
|
+
attr_accessor :version
|
14
|
+
attr_writer :root_path
|
15
|
+
attr_writer :object_sources_detector
|
16
|
+
|
17
|
+
def map_storage_path
|
18
|
+
@map_storage_path ||= Pathname("tables_map.yml")
|
19
|
+
end
|
20
|
+
|
21
|
+
def map_storage_path=(value)
|
22
|
+
@map_storage_path = Pathname(value)
|
23
|
+
end
|
24
|
+
|
25
|
+
def map_storage
|
26
|
+
@map_storage ||= MapStorage::YAMLStorage.new(map_storage_path)
|
27
|
+
end
|
28
|
+
|
29
|
+
def root_path
|
30
|
+
@root_path ||= Dir.pwd
|
31
|
+
end
|
32
|
+
|
33
|
+
def object_sources_detector
|
34
|
+
@object_sources_detector ||= ::Crystalball::MapGenerator::ObjectSourcesDetector.new(root_path: root_path)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
@@ -0,0 +1,84 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/rails/tables_map"
|
4
|
+
require "crystalball/rails/tables_map_generator/configuration"
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
module Rails
|
8
|
+
# Class to generate tables to files map during RSpec build execution
|
9
|
+
class TablesMapGenerator
|
10
|
+
extend Forwardable
|
11
|
+
|
12
|
+
attr_reader :configuration
|
13
|
+
delegate %i[map_storage object_sources_detector] => :configuration
|
14
|
+
|
15
|
+
class << self
|
16
|
+
# Registers Crystalball handlers to generate execution map during specs execution
|
17
|
+
#
|
18
|
+
# @param [Proc] block to configure MapGenerator and Register strategies
|
19
|
+
def start!(&block)
|
20
|
+
generator = new(&block)
|
21
|
+
|
22
|
+
::RSpec.configure do |c|
|
23
|
+
c.before(:suite) { generator.start! }
|
24
|
+
c.after(:suite) { generator.finalize! }
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
|
29
|
+
def initialize
|
30
|
+
@configuration = Configuration.new
|
31
|
+
@configuration.commit = repo.object("HEAD").sha if repo
|
32
|
+
yield @configuration if block_given?
|
33
|
+
object_sources_detector.after_register
|
34
|
+
end
|
35
|
+
|
36
|
+
# Prepares metadata for execution map
|
37
|
+
def start!
|
38
|
+
self.map = nil
|
39
|
+
map_storage.clear!
|
40
|
+
|
41
|
+
map_storage.dump(map.metadata.to_h)
|
42
|
+
|
43
|
+
self.started = true
|
44
|
+
end
|
45
|
+
|
46
|
+
# Finalizes and saves map
|
47
|
+
def finalize!
|
48
|
+
return unless started
|
49
|
+
|
50
|
+
collect_tables_info
|
51
|
+
|
52
|
+
object_sources_detector.before_finalize
|
53
|
+
map_storage.dump(map.example_groups) if map.size.positive?
|
54
|
+
end
|
55
|
+
|
56
|
+
# @return [Crystalball::Rails::TablesMap]
|
57
|
+
def map
|
58
|
+
@map ||= TablesMap.new(metadata: {commit: configuration.commit, version: configuration.version})
|
59
|
+
end
|
60
|
+
|
61
|
+
private
|
62
|
+
|
63
|
+
def repo
|
64
|
+
@repo = GitRepo.open(".") unless defined?(@repo)
|
65
|
+
@repo
|
66
|
+
end
|
67
|
+
|
68
|
+
def collect_tables_info
|
69
|
+
ActiveRecord::Base.descendants.each do |descendant|
|
70
|
+
table_name = descendant.table_name
|
71
|
+
|
72
|
+
next if table_name.nil?
|
73
|
+
|
74
|
+
files = object_sources_detector.detect([descendant])
|
75
|
+
|
76
|
+
map.add(files: files, for_table: table_name)
|
77
|
+
end
|
78
|
+
end
|
79
|
+
|
80
|
+
attr_writer :map
|
81
|
+
attr_accessor :started
|
82
|
+
end
|
83
|
+
end
|
84
|
+
end
|
@@ -0,0 +1,11 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/rails/map_generator/action_view_strategy"
|
4
|
+
require "crystalball/rails/map_generator/i18n_strategy"
|
5
|
+
require "crystalball/active_record"
|
6
|
+
|
7
|
+
module Crystalball
|
8
|
+
# Module containting Rails-specific stuff for Crystalball
|
9
|
+
module Rails
|
10
|
+
end
|
11
|
+
end
|
@@ -0,0 +1,52 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
module RSpec
|
5
|
+
# This class is meant to remove the example filtering options
|
6
|
+
# for example_groups when a prediction contains a file path and the same file
|
7
|
+
# example id.
|
8
|
+
#
|
9
|
+
# For example, if a prediction contains `./spec/foo_spec.rb[1:1] ./spec/foo_spec.rb`,
|
10
|
+
# only `./spec/foo_spec.rb[1:1]` would run, because of the way RSpec
|
11
|
+
# filters are designed.
|
12
|
+
#
|
13
|
+
# Therefore, we need to manually remove the filters from such example_groups.
|
14
|
+
class Filtering
|
15
|
+
# @param [RSpec::Core::Configuration] config
|
16
|
+
# @param [Array<String>] paths
|
17
|
+
def self.remove_unnecessary_filters(config, paths)
|
18
|
+
new(config).remove_unnecessary_filters(paths)
|
19
|
+
end
|
20
|
+
|
21
|
+
def initialize(configuration)
|
22
|
+
@configuration = configuration
|
23
|
+
end
|
24
|
+
|
25
|
+
def remove_unnecessary_filters(files_or_directories)
|
26
|
+
directories, files = files_or_directories.partition { |f| File.directory?(f) }
|
27
|
+
remove_unecessary_filters_from_files(files)
|
28
|
+
remove_unecessary_filters_from_directories(directories)
|
29
|
+
end
|
30
|
+
|
31
|
+
def remove_unecessary_filters_from_directories(directories)
|
32
|
+
directories.each do |dir|
|
33
|
+
files = configuration.__send__(:gather_directories, dir)
|
34
|
+
remove_unecessary_filters_from_files(files)
|
35
|
+
end
|
36
|
+
end
|
37
|
+
|
38
|
+
def remove_unecessary_filters_from_files(files)
|
39
|
+
files.select { |f| ::RSpec::Core::Example.parse_id(f).last.nil? }.each do |file|
|
40
|
+
next remove_unecessary_filters(fd) if File.directory?(file)
|
41
|
+
|
42
|
+
path = ::RSpec::Core::Metadata.relative_path(File.expand_path(file))
|
43
|
+
configuration.filter_manager.inclusions[:ids]&.delete(path)
|
44
|
+
end
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :configuration
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
@@ -0,0 +1,53 @@
|
|
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
|
+
predictor.prediction
|
18
|
+
end
|
19
|
+
|
20
|
+
def expired_map?
|
21
|
+
expiration_period = config["map_expiration_period"].to_i
|
22
|
+
return false unless expiration_period.positive?
|
23
|
+
|
24
|
+
execution_map.timestamp.to_i <= Time.now.to_i - config["map_expiration_period"]
|
25
|
+
end
|
26
|
+
|
27
|
+
def execution_map
|
28
|
+
@execution_map ||= Crystalball::MapStorage::YAMLStorage.load(config["execution_map_path"])
|
29
|
+
end
|
30
|
+
|
31
|
+
def repo
|
32
|
+
@repo ||= Crystalball::GitRepo.open(config["repo_path"])
|
33
|
+
end
|
34
|
+
|
35
|
+
private
|
36
|
+
|
37
|
+
# This method should be overridden in ancestor. Example:
|
38
|
+
#
|
39
|
+
# def predictor
|
40
|
+
# super do |p|
|
41
|
+
# p.use Crystalball::Predictor::ModifiedExecutionPaths.new
|
42
|
+
# p.use Crystalball::Predictor::ModifiedSpecs.new
|
43
|
+
# end
|
44
|
+
# end
|
45
|
+
#
|
46
|
+
def predictor(&block)
|
47
|
+
raise NotImplementedError, "Configure `prediction_builder_class_name` in `crystalball.yml` and override `predictor` method" unless block_given?
|
48
|
+
|
49
|
+
@predictor ||= Crystalball::Predictor.new(execution_map, repo, from: config["diff_from"], to: config["diff_to"], &block)
|
50
|
+
end
|
51
|
+
end
|
52
|
+
end
|
53
|
+
end
|
@@ -0,0 +1,70 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
module RSpec
|
5
|
+
module PredictionPruning
|
6
|
+
# A class to prune given world example groups to fit the limit.
|
7
|
+
class ExamplesPruner
|
8
|
+
# Simple data object for holding context ids array with total examples size
|
9
|
+
class ContextIdsSet
|
10
|
+
attr_reader :ids, :size
|
11
|
+
alias to_a ids
|
12
|
+
|
13
|
+
def initialize
|
14
|
+
@size = 0
|
15
|
+
@ids = []
|
16
|
+
end
|
17
|
+
|
18
|
+
def add(id, size = 1)
|
19
|
+
@size += size
|
20
|
+
@ids << id
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
attr_reader :world, :limit
|
25
|
+
|
26
|
+
# @param [RSpec::Core::World] rspec_world RSpec world instance
|
27
|
+
# @param [Integer] to upper bound limit for prediction.
|
28
|
+
def initialize(rspec_world, to:)
|
29
|
+
@world = rspec_world
|
30
|
+
@limit = to
|
31
|
+
end
|
32
|
+
|
33
|
+
# @return [Array<String>] set of example and context ids to run
|
34
|
+
def pruned_set
|
35
|
+
resulting_set = ContextIdsSet.new
|
36
|
+
world.ordered_example_groups.each { |g| prune_to_limit(g, resulting_set) }
|
37
|
+
resulting_set.to_a
|
38
|
+
end
|
39
|
+
|
40
|
+
private
|
41
|
+
|
42
|
+
def prune_to_limit(group, resulting_set)
|
43
|
+
return if resulting_set.size >= limit
|
44
|
+
|
45
|
+
group_size = world.example_count([group])
|
46
|
+
|
47
|
+
if resulting_set.size + group_size > limit
|
48
|
+
(group.descendants - [group]).each do |g|
|
49
|
+
prune_to_limit(g, resulting_set)
|
50
|
+
end
|
51
|
+
|
52
|
+
add_examples(group, resulting_set)
|
53
|
+
else
|
54
|
+
resulting_set.add(group.id, group_size)
|
55
|
+
end
|
56
|
+
end
|
57
|
+
|
58
|
+
def add_examples(group, resulting_set)
|
59
|
+
limit_diff = limit - resulting_set.size
|
60
|
+
|
61
|
+
return unless limit_diff.positive?
|
62
|
+
|
63
|
+
group.filtered_examples.first(limit_diff).each do |example|
|
64
|
+
resulting_set.add(example.id)
|
65
|
+
end
|
66
|
+
end
|
67
|
+
end
|
68
|
+
end
|
69
|
+
end
|
70
|
+
end
|
@@ -0,0 +1,56 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require "crystalball/rspec/prediction_pruning/examples_pruner"
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
module RSpec
|
7
|
+
# Module contains logic related to examples_limit configuration option for our runner.
|
8
|
+
module PredictionPruning
|
9
|
+
def self.included(base)
|
10
|
+
base.extend ClassMethods
|
11
|
+
end
|
12
|
+
|
13
|
+
# Class methods for prediction pruning logic
|
14
|
+
module ClassMethods
|
15
|
+
def examples_limit
|
16
|
+
config["examples_limit"].to_i
|
17
|
+
end
|
18
|
+
|
19
|
+
private
|
20
|
+
|
21
|
+
def prune_prediction_to_limit(prediction)
|
22
|
+
return prediction if !examples_limit.positive? || prediction.size <= examples_limit
|
23
|
+
|
24
|
+
Crystalball.log :warn, "Prediction size #{prediction.size} is over the limit (#{examples_limit})"
|
25
|
+
Crystalball.log :warn, "Prediction is pruned to fit the limit!"
|
26
|
+
|
27
|
+
# Actual examples size is not less than prediction size.
|
28
|
+
prediction.first(examples_limit)
|
29
|
+
end
|
30
|
+
end
|
31
|
+
|
32
|
+
private
|
33
|
+
|
34
|
+
def examples_limit
|
35
|
+
self.class.examples_limit
|
36
|
+
end
|
37
|
+
|
38
|
+
def reconfiguration_needed?
|
39
|
+
examples_limit.positive? && @world.example_count > examples_limit
|
40
|
+
end
|
41
|
+
|
42
|
+
def reconfigure_to_limit
|
43
|
+
pruner = ExamplesPruner.new(@world, to: examples_limit)
|
44
|
+
|
45
|
+
@options = ::RSpec::Core::ConfigurationOptions.new(pruner.pruned_set)
|
46
|
+
@world.reset
|
47
|
+
@world.filtered_examples.clear
|
48
|
+
@world.instance_variable_get(:@example_group_counts_by_spec_file).clear
|
49
|
+
@configuration.reset
|
50
|
+
@configuration.reset_filters
|
51
|
+
|
52
|
+
@options.configure(@configuration)
|
53
|
+
end
|
54
|
+
end
|
55
|
+
end
|
56
|
+
end
|