crystalball 0.5.0 → 0.6.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 +4 -4
- data/.gitignore +4 -1
- data/.rubocop.yml +4 -0
- data/.travis.yml +1 -1
- data/CHANGELOG.md +18 -0
- data/LICENSE +22 -674
- data/README.md +13 -158
- data/crystalball.gemspec +6 -2
- data/docs/img/favicon.ico +0 -0
- data/docs/img/logo.png +0 -0
- data/docs/index.md +44 -0
- data/docs/map_generators.md +149 -0
- data/docs/predictors.md +75 -0
- data/docs/runner.md +24 -0
- data/lib/crystalball.rb +8 -3
- data/lib/crystalball/active_record.rb +4 -0
- data/lib/crystalball/example_group_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +17 -16
- data/lib/crystalball/extensions/git.rb +4 -0
- data/lib/crystalball/extensions/git/base.rb +14 -0
- data/lib/crystalball/extensions/git/lib.rb +18 -0
- data/lib/crystalball/factory_bot.rb +3 -0
- data/lib/crystalball/git_repo.rb +2 -7
- data/lib/crystalball/logging.rb +51 -0
- data/lib/crystalball/map_generator.rb +5 -7
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +6 -5
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +1 -0
- data/lib/crystalball/map_generator/base_strategy.rb +5 -4
- data/lib/crystalball/map_generator/configuration.rb +1 -1
- data/lib/crystalball/map_generator/coverage_strategy.rb +6 -5
- data/lib/crystalball/map_generator/described_class_strategy.rb +5 -5
- data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
- data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -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/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/parser_strategy.rb +60 -0
- data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
- data/lib/crystalball/map_generator/strategies_collection.rb +9 -9
- data/lib/crystalball/map_storage/yaml_storage.rb +7 -6
- data/lib/crystalball/prediction.rb +12 -11
- data/lib/crystalball/predictor.rb +7 -5
- data/lib/crystalball/predictor/associated_specs.rb +9 -4
- 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 +11 -5
- data/lib/crystalball/predictor/modified_specs.rb +8 -2
- data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
- data/lib/crystalball/predictor/strategy.rb +16 -0
- data/lib/crystalball/predictor_evaluator.rb +1 -1
- data/lib/crystalball/rails.rb +1 -0
- data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
- data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -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/map_generator/action_view_strategy.rb +5 -5
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +1 -1
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +5 -5
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +1 -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.rb +84 -0
- data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
- data/lib/crystalball/rspec/filtering.rb +52 -0
- data/lib/crystalball/rspec/prediction_builder.rb +21 -11
- data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
- data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
- data/lib/crystalball/rspec/runner.rb +39 -27
- data/lib/crystalball/rspec/runner/configuration.rb +24 -14
- data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
- data/lib/crystalball/source_diff.rb +12 -2
- data/lib/crystalball/source_diff/file_diff.rb +1 -1
- data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
- data/lib/crystalball/version.rb +1 -1
- data/mkdocs.yml +23 -0
- metadata +102 -7
- data/lib/crystalball/case_map.rb +0 -19
- data/lib/crystalball/simple_predictor.rb +0 -18
@@ -0,0 +1,40 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher'
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
class MapGenerator
|
7
|
+
class FactoryBotStrategy
|
8
|
+
# Module to add new `factory` method to FactoryBot::Syntax::Default::DSL and FactoryBot::Syntax::Default::ModifyDSL
|
9
|
+
module DSLPatch
|
10
|
+
class << self
|
11
|
+
# Patches `FactoryBot::Syntax::Default::DSL#factory` and `FactoryBot::Syntax::Default::ModifyDSL#factory`.
|
12
|
+
def apply!
|
13
|
+
classes_to_patch.each { |klass| klass.prepend DSLPatch }
|
14
|
+
end
|
15
|
+
|
16
|
+
private
|
17
|
+
|
18
|
+
def classes_to_patch
|
19
|
+
[
|
20
|
+
FactoryBotStrategy.factory_bot_constant::Syntax::Default::DSL,
|
21
|
+
FactoryBotStrategy.factory_bot_constant::Syntax::Default::ModifyDSL
|
22
|
+
]
|
23
|
+
end
|
24
|
+
end
|
25
|
+
|
26
|
+
# Overrides `FactoryBot::Syntax::Default::DSL#factory` and `FactoryBot::Syntax::Default::ModifyDSL#factory`.
|
27
|
+
# Pushes path of a factory to `FactoryBotStrategy.factory_definitions` and calls original `factory`
|
28
|
+
def factory(*args, &block)
|
29
|
+
factory_path = FactoryPathFetcher.fetch
|
30
|
+
name = args.first.to_s
|
31
|
+
|
32
|
+
FactoryBotStrategy.factory_definitions[name] ||= []
|
33
|
+
FactoryBotStrategy.factory_definitions[name] << factory_path
|
34
|
+
|
35
|
+
super
|
36
|
+
end
|
37
|
+
end
|
38
|
+
end
|
39
|
+
end
|
40
|
+
end
|
@@ -0,0 +1,30 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
module DSLPatch
|
7
|
+
# This module is used to fetch file path with factory definition from callstack
|
8
|
+
module FactoryPathFetcher
|
9
|
+
# Fetches file path with factory definition from callstack
|
10
|
+
#
|
11
|
+
# @return [String]
|
12
|
+
def self.fetch
|
13
|
+
factories_definition_paths = FactoryBotStrategy
|
14
|
+
.factory_bot_constant
|
15
|
+
.definition_file_paths
|
16
|
+
.map { |path| Pathname(path).expand_path.to_s }
|
17
|
+
|
18
|
+
factory_definition_call = caller.find do |method_call|
|
19
|
+
factories_definition_paths.any? do |path|
|
20
|
+
method_call.start_with?(path)
|
21
|
+
end
|
22
|
+
end
|
23
|
+
|
24
|
+
factory_definition_call.split(':').first
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
end
|
@@ -0,0 +1,27 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
# A helper module to load `factory_bot` or `factory_girl`
|
7
|
+
module FactoryGemLoader
|
8
|
+
class << self
|
9
|
+
NAMES = %w[factory_bot factory_girl].freeze
|
10
|
+
|
11
|
+
# Tries to require `factory_bot` first. Requires `factory_girl` if `factory_bot` is not available
|
12
|
+
# Raises `LoadError` if both of them are not available.
|
13
|
+
def require!
|
14
|
+
NAMES.any? do |factory_gem_name|
|
15
|
+
begin
|
16
|
+
require factory_gem_name
|
17
|
+
true
|
18
|
+
rescue LoadError
|
19
|
+
false
|
20
|
+
end
|
21
|
+
end || (raise LoadError, "Can't load `factory_bot` or `factory_girl`")
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
26
|
+
end
|
27
|
+
end
|
@@ -0,0 +1,25 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
module Crystalball
|
4
|
+
class MapGenerator
|
5
|
+
class FactoryBotStrategy
|
6
|
+
# Module to add new `run` method to FactoryBot::FactoryRunner
|
7
|
+
module FactoryRunnerPatch
|
8
|
+
class << self
|
9
|
+
# Patches `FactoryBot::FactoryRunner#run`.
|
10
|
+
def apply!
|
11
|
+
FactoryBotStrategy.factory_bot_constant::FactoryRunner.prepend FactoryRunnerPatch
|
12
|
+
end
|
13
|
+
end
|
14
|
+
|
15
|
+
# Overrides `FactoryBot::FactoryRunner#run`. Pushes factory name to
|
16
|
+
# `FactoryBotStrategy.used_factories` and calls original `run`
|
17
|
+
def run(*)
|
18
|
+
factory = FactoryBotStrategy.factory_bot_constant.factory_by_name(@name)
|
19
|
+
FactoryBotStrategy.used_factories << factory.name.to_s
|
20
|
+
super
|
21
|
+
end
|
22
|
+
end
|
23
|
+
end
|
24
|
+
end
|
25
|
+
end
|
@@ -0,0 +1,60 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'crystalball/map_generator/parser_strategy/processor'
|
4
|
+
require 'crystalball/map_generator/helpers/path_filter'
|
5
|
+
|
6
|
+
module Crystalball
|
7
|
+
class MapGenerator
|
8
|
+
# Map generator strategy based on parsing source files to detect constant definition
|
9
|
+
# and tracing method calls on those constants.
|
10
|
+
class ParserStrategy
|
11
|
+
include BaseStrategy
|
12
|
+
include Helpers::PathFilter
|
13
|
+
|
14
|
+
attr_reader :const_definition_paths
|
15
|
+
|
16
|
+
def initialize(root = Dir.pwd, pattern:)
|
17
|
+
@root_path = Pathname.new(root).realpath.to_s
|
18
|
+
@processor = Processor.new
|
19
|
+
@const_definition_paths = {}
|
20
|
+
@pattern = pattern
|
21
|
+
end
|
22
|
+
|
23
|
+
def after_register
|
24
|
+
files_to_inspect.each do |path|
|
25
|
+
processor.consts_defined_in(path).each do |const|
|
26
|
+
(const_definition_paths[const] ||= []) << path
|
27
|
+
end
|
28
|
+
end
|
29
|
+
end
|
30
|
+
|
31
|
+
# Parses the current example group map seeking calls to class methods and adds
|
32
|
+
# the classes to the map.
|
33
|
+
# @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
|
34
|
+
# @param [RSpec::Core::Example] example - a RSpec example
|
35
|
+
def call(example_map, example)
|
36
|
+
paths = []
|
37
|
+
yield example_map, example
|
38
|
+
example_map.each do |path|
|
39
|
+
next unless path.end_with?('.rb')
|
40
|
+
|
41
|
+
used_consts = processor.consts_interacted_with_in(path)
|
42
|
+
paths.push(*used_files(used_consts))
|
43
|
+
end
|
44
|
+
example_map.push(*filter(paths))
|
45
|
+
end
|
46
|
+
|
47
|
+
private
|
48
|
+
|
49
|
+
attr_reader :processor, :pattern, :root_path
|
50
|
+
|
51
|
+
def used_files(used_consts)
|
52
|
+
const_definition_paths.select { |k, _| Array(used_consts).include?(k) }.values.flatten
|
53
|
+
end
|
54
|
+
|
55
|
+
def files_to_inspect
|
56
|
+
Dir.glob(File.join(root_path, '**/*.rb')).grep(pattern)
|
57
|
+
end
|
58
|
+
end
|
59
|
+
end
|
60
|
+
end
|
@@ -0,0 +1,129 @@
|
|
1
|
+
# frozen_string_literal: true
|
2
|
+
|
3
|
+
require 'parser/current'
|
4
|
+
|
5
|
+
module Crystalball
|
6
|
+
class MapGenerator
|
7
|
+
class ParserStrategy
|
8
|
+
# Parses the given source files and adds the class and module definitions
|
9
|
+
# to the `consts_defined` array.
|
10
|
+
class Processor
|
11
|
+
def consts_defined_in(path)
|
12
|
+
self.current_scope = nil
|
13
|
+
self.consts_defined = []
|
14
|
+
parse_and_process(path)
|
15
|
+
consts_defined
|
16
|
+
end
|
17
|
+
|
18
|
+
def consts_interacted_with_in(path)
|
19
|
+
self.current_scope = nil
|
20
|
+
self.consts_interacted_with = []
|
21
|
+
parse_and_process(path)
|
22
|
+
consts_interacted_with
|
23
|
+
end
|
24
|
+
|
25
|
+
protected
|
26
|
+
|
27
|
+
attr_accessor :consts_defined, :consts_interacted_with, :current_scope
|
28
|
+
|
29
|
+
private
|
30
|
+
|
31
|
+
def on_send(node)
|
32
|
+
const = filtered_children(node).detect { |c| c.type == :const }
|
33
|
+
return unless const
|
34
|
+
|
35
|
+
add_constant_interacted(qualified_name_from_node(const), nil)
|
36
|
+
end
|
37
|
+
|
38
|
+
def on_casgn(node)
|
39
|
+
namespace, name, = node.children
|
40
|
+
scope_name = namespace ? qualified_name(qualified_name_from_node(namespace), current_scope) : current_scope
|
41
|
+
add_constant_defined(name, scope_name)
|
42
|
+
end
|
43
|
+
|
44
|
+
def on_class(node)
|
45
|
+
const, superclass, body = node.children
|
46
|
+
add_constant_interacted(qualified_name_from_node(superclass), current_scope) if superclass
|
47
|
+
process_class_or_module(const, body)
|
48
|
+
end
|
49
|
+
|
50
|
+
def on_module(node)
|
51
|
+
const, body = node.children
|
52
|
+
process_class_or_module(const, body)
|
53
|
+
end
|
54
|
+
|
55
|
+
def process_class_or_module(const, body)
|
56
|
+
const_name = qualified_name_from_node(const)
|
57
|
+
result = add_constant_defined(const_name, current_scope)
|
58
|
+
self.current_scope = result if body && nested_consts?(body)
|
59
|
+
end
|
60
|
+
|
61
|
+
def nested_consts?(node)
|
62
|
+
filtered_children(node).any? { |c| %i[const casgn].include?(c.type) || nested_consts?(c) }
|
63
|
+
end
|
64
|
+
|
65
|
+
def filtered_children(node)
|
66
|
+
return [] unless node.is_a?(Parser::AST::Node)
|
67
|
+
|
68
|
+
node.children.grep(Parser::AST::Node)
|
69
|
+
end
|
70
|
+
|
71
|
+
def parse_and_process(path)
|
72
|
+
node = Parser::CurrentRuby.parse(File.read(path))
|
73
|
+
process_node_and_children(node)
|
74
|
+
rescue Parser::SyntaxError
|
75
|
+
nil
|
76
|
+
end
|
77
|
+
|
78
|
+
# @param [AST::Node, nil] node
|
79
|
+
# @return [String, nil]
|
80
|
+
def process(node)
|
81
|
+
return if node.nil?
|
82
|
+
|
83
|
+
on_handler = :"on_#{node.type}"
|
84
|
+
__send__(on_handler, node) if respond_to?(on_handler, true)
|
85
|
+
end
|
86
|
+
|
87
|
+
def process_node_and_children(node)
|
88
|
+
process(node)
|
89
|
+
filtered_children(node).each do |child|
|
90
|
+
process_node_and_children(child)
|
91
|
+
end
|
92
|
+
end
|
93
|
+
|
94
|
+
def add_constant_defined(name, scope)
|
95
|
+
add_constant(name, scope, collection: consts_defined)
|
96
|
+
end
|
97
|
+
|
98
|
+
def add_constant_interacted(name, scope)
|
99
|
+
add_constant(name, scope, collection: consts_interacted_with)
|
100
|
+
end
|
101
|
+
|
102
|
+
def add_constant(name, scope, collection:)
|
103
|
+
collection ||= []
|
104
|
+
qualified_const_name = qualified_name(name, scope)
|
105
|
+
collection << qualified_const_name
|
106
|
+
qualified_const_name
|
107
|
+
end
|
108
|
+
|
109
|
+
# @param [Parser::AST::Node] node - :const node in format s(:const, scope, :ConstName)
|
110
|
+
# where scope can be `nil` or another :const node.
|
111
|
+
# For example, `Foo::Bar` is represented as `s(:const, s(:const, nil, :Foo), :Bar)`
|
112
|
+
def qualified_name_from_node(node)
|
113
|
+
return unless node.is_a?(Parser::AST::Node)
|
114
|
+
|
115
|
+
scope, name = node.to_a
|
116
|
+
return name.to_s unless scope
|
117
|
+
|
118
|
+
qualified_name(name, qualified_name_from_node(scope))
|
119
|
+
end
|
120
|
+
|
121
|
+
def qualified_name(name, scope = nil)
|
122
|
+
return "#{scope.sub(/\A::/, '')}::#{name}" if scope
|
123
|
+
|
124
|
+
name.to_s
|
125
|
+
end
|
126
|
+
end
|
127
|
+
end
|
128
|
+
end
|
129
|
+
end
|
@@ -10,12 +10,12 @@ module Crystalball
|
|
10
10
|
@strategies = strategies
|
11
11
|
end
|
12
12
|
|
13
|
-
# Calls every strategy on the given
|
14
|
-
# @param [Crystalball::
|
15
|
-
# @return [Crystalball::
|
16
|
-
def run(
|
17
|
-
run_for_strategies(
|
18
|
-
|
13
|
+
# Calls every strategy on the given example group map and returns the modified example group map
|
14
|
+
# @param [Crystalball::ExampleGroupMap] example_group_map - initial example group map
|
15
|
+
# @return [Crystalball::ExampleGroupMap] example group map augmented by each strategy
|
16
|
+
def run(example_group_map, example, &block)
|
17
|
+
run_for_strategies(example_group_map, example, *_strategies.reverse, &block)
|
18
|
+
example_group_map
|
19
19
|
end
|
20
20
|
|
21
21
|
def method_missing(method_name, *args, &block)
|
@@ -32,11 +32,11 @@ module Crystalball
|
|
32
32
|
@strategies
|
33
33
|
end
|
34
34
|
|
35
|
-
def run_for_strategies(
|
36
|
-
return yield(
|
35
|
+
def run_for_strategies(example_group_map, example, *strats, &block)
|
36
|
+
return yield(example_group_map) if strats.empty?
|
37
37
|
|
38
38
|
strat = strats.shift
|
39
|
-
strat.call(
|
39
|
+
strat.call(example_group_map, example) { |c| run_for_strategies(c, example, *strats, &block) }
|
40
40
|
end
|
41
41
|
end
|
42
42
|
end
|
@@ -17,11 +17,11 @@ module Crystalball
|
|
17
17
|
# @param [String] path to map
|
18
18
|
# @return [Crystalball::ExecutionMap]
|
19
19
|
def load(path)
|
20
|
-
meta,
|
20
|
+
meta, example_groups = *read_files(path).transpose
|
21
21
|
|
22
22
|
guard_metadata_consistency(meta)
|
23
23
|
|
24
|
-
Object.const_get(meta.first[:type]).new(metadata: meta.first,
|
24
|
+
Object.const_get(meta.first[:type]).new(metadata: meta.first, example_groups: example_groups.inject(&:merge!))
|
25
25
|
end
|
26
26
|
|
27
27
|
private
|
@@ -29,15 +29,15 @@ module Crystalball
|
|
29
29
|
def read_files(path)
|
30
30
|
paths = path.directory? ? path.each_child.select(&:file?) : [path]
|
31
31
|
|
32
|
-
raise NoFilesFoundError unless paths.any?(&:exist?)
|
32
|
+
raise NoFilesFoundError, "No files or folder exists #{path}" unless paths.any?(&:exist?)
|
33
33
|
|
34
34
|
paths.map do |file|
|
35
|
-
metadata, *
|
35
|
+
metadata, *example_groups = file.read.split("---\n").reject(&:empty?).map do |yaml|
|
36
36
|
YAML.safe_load(yaml, [Symbol])
|
37
37
|
end
|
38
|
-
|
38
|
+
example_groups = example_groups.inject(&:merge!)
|
39
39
|
|
40
|
-
[metadata,
|
40
|
+
[metadata, example_groups]
|
41
41
|
end
|
42
42
|
end
|
43
43
|
|
@@ -61,6 +61,7 @@ module Crystalball
|
|
61
61
|
#
|
62
62
|
# @param [Hash] data to write to storage file
|
63
63
|
def dump(data)
|
64
|
+
path.dirname.mkpath
|
64
65
|
path.open('a') { |f| f.write YAML.dump(data) }
|
65
66
|
end
|
66
67
|
end
|
@@ -3,32 +3,33 @@
|
|
3
3
|
module Crystalball
|
4
4
|
# Class for Crystalball prediction results
|
5
5
|
class Prediction
|
6
|
-
def initialize(
|
7
|
-
@
|
6
|
+
def initialize(records)
|
7
|
+
@records = records
|
8
8
|
end
|
9
9
|
|
10
|
+
# When the records are something like:
|
11
|
+
# ./spec/foo ./spec/foo/bar_spec.rb
|
12
|
+
# this returns just ./spec/foo
|
10
13
|
def compact
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
end
|
15
|
-
result
|
14
|
+
sort_by(&:length).each_with_object([]) do |c, result|
|
15
|
+
result << c unless result.any? { |r| c.start_with?(r) }
|
16
|
+
end.compact
|
16
17
|
end
|
17
18
|
|
18
19
|
def to_a
|
19
|
-
|
20
|
+
records
|
20
21
|
end
|
21
22
|
|
22
23
|
def method_missing(*args, &block)
|
23
|
-
|
24
|
+
records.respond_to?(*args) ? records.public_send(*args, &block) : super
|
24
25
|
end
|
25
26
|
|
26
27
|
def respond_to_missing?(*args)
|
27
|
-
|
28
|
+
records.respond_to?(*args)
|
28
29
|
end
|
29
30
|
|
30
31
|
private
|
31
32
|
|
32
|
-
attr_reader :
|
33
|
+
attr_reader :records
|
33
34
|
end
|
34
35
|
end
|
@@ -29,10 +29,12 @@ module Crystalball
|
|
29
29
|
def prediction
|
30
30
|
Prediction.new(filter(raw_prediction(diff)))
|
31
31
|
end
|
32
|
-
alias cases prediction
|
33
32
|
|
34
33
|
def diff
|
35
|
-
|
34
|
+
@diff ||= begin
|
35
|
+
ancestor = repo.merge_base(from, to || 'HEAD').sha
|
36
|
+
repo.diff(ancestor, to)
|
37
|
+
end
|
36
38
|
end
|
37
39
|
|
38
40
|
private
|
@@ -45,11 +47,11 @@ module Crystalball
|
|
45
47
|
|
46
48
|
attr_reader :repo
|
47
49
|
|
48
|
-
def filter(
|
49
|
-
|
50
|
+
def filter(example_groups)
|
51
|
+
example_groups.compact.select { |example_group| extract_file_path(example_group).exist? }.uniq
|
50
52
|
end
|
51
53
|
|
52
|
-
def
|
54
|
+
def extract_file_path(example)
|
53
55
|
repo.repo_path.join(example.split('[').first).expand_path
|
54
56
|
end
|
55
57
|
end
|