crystalball 0.5.0 → 0.6.0
Sign up to get free protection for your applications and to get access to all the features.
- 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
|