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