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,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
|