crystalball 0.5.0 → 0.6.0

Sign up to get free protection for your applications and to get access to all the features.
Files changed (78) hide show
  1. checksums.yaml +4 -4
  2. data/.gitignore +4 -1
  3. data/.rubocop.yml +4 -0
  4. data/.travis.yml +1 -1
  5. data/CHANGELOG.md +18 -0
  6. data/LICENSE +22 -674
  7. data/README.md +13 -158
  8. data/crystalball.gemspec +6 -2
  9. data/docs/img/favicon.ico +0 -0
  10. data/docs/img/logo.png +0 -0
  11. data/docs/index.md +44 -0
  12. data/docs/map_generators.md +149 -0
  13. data/docs/predictors.md +75 -0
  14. data/docs/runner.md +24 -0
  15. data/lib/crystalball.rb +8 -3
  16. data/lib/crystalball/active_record.rb +4 -0
  17. data/lib/crystalball/example_group_map.rb +19 -0
  18. data/lib/crystalball/execution_map.rb +17 -16
  19. data/lib/crystalball/extensions/git.rb +4 -0
  20. data/lib/crystalball/extensions/git/base.rb +14 -0
  21. data/lib/crystalball/extensions/git/lib.rb +18 -0
  22. data/lib/crystalball/factory_bot.rb +3 -0
  23. data/lib/crystalball/git_repo.rb +2 -7
  24. data/lib/crystalball/logging.rb +51 -0
  25. data/lib/crystalball/map_generator.rb +5 -7
  26. data/lib/crystalball/map_generator/allocated_objects_strategy.rb +6 -5
  27. data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +1 -0
  28. data/lib/crystalball/map_generator/base_strategy.rb +5 -4
  29. data/lib/crystalball/map_generator/configuration.rb +1 -1
  30. data/lib/crystalball/map_generator/coverage_strategy.rb +6 -5
  31. data/lib/crystalball/map_generator/described_class_strategy.rb +5 -5
  32. data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
  33. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -0
  34. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +30 -0
  35. data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +27 -0
  36. data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +25 -0
  37. data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
  38. data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
  39. data/lib/crystalball/map_generator/strategies_collection.rb +9 -9
  40. data/lib/crystalball/map_storage/yaml_storage.rb +7 -6
  41. data/lib/crystalball/prediction.rb +12 -11
  42. data/lib/crystalball/predictor.rb +7 -5
  43. data/lib/crystalball/predictor/associated_specs.rb +9 -4
  44. data/lib/crystalball/predictor/helpers/affected_example_groups_detector.rb +20 -0
  45. data/lib/crystalball/predictor/helpers/path_formatter.rb +18 -0
  46. data/lib/crystalball/predictor/modified_execution_paths.rb +11 -5
  47. data/lib/crystalball/predictor/modified_specs.rb +8 -2
  48. data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
  49. data/lib/crystalball/predictor/strategy.rb +16 -0
  50. data/lib/crystalball/predictor_evaluator.rb +1 -1
  51. data/lib/crystalball/rails.rb +1 -0
  52. data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
  53. data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -0
  54. data/lib/crystalball/rails/helpers/schema_definition_parser/active_record.rb +21 -0
  55. data/lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb +27 -0
  56. data/lib/crystalball/rails/map_generator/action_view_strategy.rb +5 -5
  57. data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +1 -1
  58. data/lib/crystalball/rails/map_generator/i18n_strategy.rb +5 -5
  59. data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +1 -0
  60. data/lib/crystalball/rails/predictor/modified_schema.rb +81 -0
  61. data/lib/crystalball/rails/tables_map.rb +53 -0
  62. data/lib/crystalball/rails/tables_map_generator.rb +84 -0
  63. data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
  64. data/lib/crystalball/rspec/filtering.rb +52 -0
  65. data/lib/crystalball/rspec/prediction_builder.rb +21 -11
  66. data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
  67. data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
  68. data/lib/crystalball/rspec/runner.rb +39 -27
  69. data/lib/crystalball/rspec/runner/configuration.rb +24 -14
  70. data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
  71. data/lib/crystalball/source_diff.rb +12 -2
  72. data/lib/crystalball/source_diff/file_diff.rb +1 -1
  73. data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
  74. data/lib/crystalball/version.rb +1 -1
  75. data/mkdocs.yml +23 -0
  76. metadata +102 -7
  77. data/lib/crystalball/case_map.rb +0 -19
  78. data/lib/crystalball/simple_predictor.rb +0 -18
@@ -1,11 +1,15 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'crystalball/predictor/strategy'
4
+
3
5
  module Crystalball
4
6
  class Predictor
5
7
  # Used with `predictor.use Crystalball::Predictor::AssociatedSpecs.new(from: %r{models/(.*).rb}, to: "./spec/models/%s_spec.rb")`.
6
8
  # When used will look for files matched to `from` regex and use captures to fill `to` string to
7
9
  # get paths of proper specs
8
10
  class AssociatedSpecs
11
+ include Strategy
12
+
9
13
  # @param [Regexp] from - regular expression to match specific files and get proper captures
10
14
  # @param [String] to - string in sprintf format to get proper files using captures of regexp
11
15
  def initialize(from:, to:)
@@ -13,14 +17,15 @@ module Crystalball
13
17
  @to = to
14
18
  end
15
19
 
16
- # This strategy does not depend on a previously generated case map.
20
+ # This strategy does not depend on a previously generated example group map.
17
21
  # It uses the defined regex rules to infer which specs to run.
18
22
  # @param [Crystalball::SourceDiff] diff - the diff from which to predict
19
23
  # which specs should run
20
24
  # @return [Array<String>] the spec paths associated with the changes
21
- def call(diff, _)
22
- diff.map(&:relative_path).grep(from)
23
- .map { |source_file_path| to % captures(source_file_path) }
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
24
29
  end
25
30
 
26
31
  private
@@ -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
@@ -1,20 +1,26 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'crystalball/predictor/strategy'
4
+ require 'crystalball/predictor/helpers/affected_example_groups_detector'
5
+
3
6
  module Crystalball
4
7
  class Predictor
5
8
  # Used with `predictor.use Crystalball::Predictor::ModifiedExecutionPaths.new`. When used will check the map which
6
9
  # specs depend on which files and will return only those specs which depend on files modified since last time map
7
10
  # was generated.
8
11
  class ModifiedExecutionPaths
12
+ include Helpers::AffectedExampleGroupsDetector
13
+ include Strategy
14
+
9
15
  # @param [Crystalball::SourceDiff] diff - the diff from which to predict
10
16
  # which specs should run
11
- # @param [Crystalball::CaseMap] map - the map with the relations of
12
- # examples and affected files
17
+ # @param [Crystalball::ExampleGroupMap] map - the map with the relations of
18
+ # examples and used files
13
19
  # @return [Array<String>] the spec paths associated with the changes
14
20
  def call(diff, map)
15
- map.cases.map do |uid, case_map|
16
- uid if diff.any? { |file| case_map.include?(file.relative_path) }
17
- end.compact
21
+ super do
22
+ detect_examples(diff.map(&:relative_path), map)
23
+ end
18
24
  end
19
25
  end
20
26
  end
@@ -1,22 +1,28 @@
1
1
  # frozen_string_literal: true
2
2
 
3
+ require 'crystalball/predictor/strategy'
4
+
3
5
  module Crystalball
4
6
  class Predictor
5
7
  # Used with `predictor.use Crystalball::Predictor::ModifiedSpecs.new`. Will find files that match spec regexp and
6
8
  # return all new or modified files. You can specify spec regexp using first parameter to `#initialize`.
7
9
  class ModifiedSpecs
10
+ include Strategy
11
+
8
12
  # @param [Regexp] spec_pattern - regexp to filter specs files
9
13
  def initialize(spec_pattern = %r{spec/.*_spec\.rb\z})
10
14
  @spec_pattern = spec_pattern
11
15
  end
12
16
 
13
- # This strategy does not depend on a previously generated case map.
17
+ # This strategy does not depend on a previously generated example group map.
14
18
  # It uses the spec pattern to determine which specs should run.
15
19
  # @param [Crystalball::SourceDiff] diff - the diff from which to predict
16
20
  # which specs should run
17
21
  # @return [Array<String>] the spec paths associated with the changes
18
22
  def call(diff, _)
19
- diff.reject(&:deleted?).map(&:new_relative_path).grep(spec_pattern)
23
+ super do
24
+ diff.reject(&:deleted?).map(&:new_relative_path).grep(spec_pattern)
25
+ end
20
26
  end
21
27
 
22
28
  private
@@ -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
@@ -39,7 +39,7 @@ module Crystalball
39
39
  end
40
40
 
41
41
  def prediction_size
42
- @prediction_size ||= predictor.map.cases.keys.select { |example| prediction.any? { |p| example.include?(p) } }.size
42
+ @prediction_size ||= predictor.map.example_groups.keys.select { |example| prediction.any? { |p| example.include?(p) } }.size
43
43
  end
44
44
 
45
45
  def map_size
@@ -2,6 +2,7 @@
2
2
 
3
3
  require 'crystalball/rails/map_generator/action_view_strategy'
4
4
  require 'crystalball/rails/map_generator/i18n_strategy'
5
+ require 'crystalball/active_record'
5
6
 
6
7
  module Crystalball
7
8
  # Module containting Rails-specific stuff for Crystalball
@@ -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,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,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
@@ -33,12 +33,12 @@ module Crystalball
33
33
  Patch.revert!
34
34
  end
35
35
 
36
- # Adds views related to the spec to the case map
37
- # @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
38
- def call(case_map, _)
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
39
  self.class.reset_views
40
- yield case_map
41
- case_map.push(*filter(self.class.views))
40
+ yield example_group_map
41
+ example_group_map.push(*filter(self.class.views))
42
42
  end
43
43
  end
44
44
  end
@@ -9,7 +9,7 @@ module Crystalball
9
9
  # Module to add new patched `compile!` method to ActionView::Template
10
10
  module Patch
11
11
  class << self
12
- # Patches `ActionView::Template#compile!`. Renames original `compile!` to `old_compile!` and
12
+ # Patches `ActionView::Template#compile!`. Renames original `compile!` to `cb_original_compile!` and
13
13
  # replaces it with custom one
14
14
  def apply!
15
15
  ::ActionView::Template.class_eval do
@@ -34,12 +34,12 @@ module Crystalball
34
34
  SimplePatch.revert!
35
35
  end
36
36
 
37
- # Adds to the case map the locale files used by the example
38
- # @param [Crystalball::CaseMap] case_map - object holding example metadata and affected files
39
- def call(case_map, _)
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
40
  self.class.reset_locale_files
41
- yield case_map
42
- case_map.push(*filter(self.class.locale_files.compact))
41
+ yield example_group_map
42
+ example_group_map.push(*filter(self.class.locale_files.compact))
43
43
  end
44
44
  end
45
45
  end
@@ -62,6 +62,7 @@ module Crystalball
62
62
  case value
63
63
  when Hash
64
64
  next if value.frozen?
65
+
65
66
  cb_add_filename_to_values(value, filename)
66
67
  else
67
68
  data[key] = {cb_filename: filename, cb_value: value}.freeze
@@ -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