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.
Files changed (73) hide show
  1. checksums.yaml +7 -0
  2. data/LICENSE +22 -0
  3. data/README.md +56 -0
  4. data/bin/crystalball +5 -0
  5. data/lib/crystalball/active_record.rb +4 -0
  6. data/lib/crystalball/example_group_map.rb +19 -0
  7. data/lib/crystalball/execution_map.rb +56 -0
  8. data/lib/crystalball/extensions/git/base.rb +14 -0
  9. data/lib/crystalball/extensions/git/lib.rb +17 -0
  10. data/lib/crystalball/extensions/git.rb +4 -0
  11. data/lib/crystalball/factory_bot.rb +3 -0
  12. data/lib/crystalball/git_repo.rb +53 -0
  13. data/lib/crystalball/logging.rb +51 -0
  14. data/lib/crystalball/map_compactor/example_context.rb +29 -0
  15. data/lib/crystalball/map_compactor/example_groups_data_compactor.rb +66 -0
  16. data/lib/crystalball/map_compactor.rb +44 -0
  17. data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +45 -0
  18. data/lib/crystalball/map_generator/allocated_objects_strategy.rb +45 -0
  19. data/lib/crystalball/map_generator/base_strategy.rb +22 -0
  20. data/lib/crystalball/map_generator/configuration.rb +58 -0
  21. data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
  22. data/lib/crystalball/map_generator/coverage_strategy.rb +35 -0
  23. data/lib/crystalball/map_generator/described_class_strategy.rb +35 -0
  24. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch/factory_path_fetcher.rb +30 -0
  25. data/lib/crystalball/map_generator/factory_bot_strategy/dsl_patch.rb +40 -0
  26. data/lib/crystalball/map_generator/factory_bot_strategy/factory_gem_loader.rb +27 -0
  27. data/lib/crystalball/map_generator/factory_bot_strategy/factory_runner_patch.rb +25 -0
  28. data/lib/crystalball/map_generator/factory_bot_strategy.rb +59 -0
  29. data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
  30. data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
  31. data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
  32. data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
  33. data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
  34. data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
  35. data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
  36. data/lib/crystalball/map_generator.rb +84 -0
  37. data/lib/crystalball/map_storage/yaml_storage.rb +69 -0
  38. data/lib/crystalball/prediction.rb +35 -0
  39. data/lib/crystalball/predictor/associated_specs.rb +45 -0
  40. data/lib/crystalball/predictor/helpers/affected_example_groups_detector.rb +20 -0
  41. data/lib/crystalball/predictor/helpers/path_formatter.rb +18 -0
  42. data/lib/crystalball/predictor/modified_execution_paths.rb +27 -0
  43. data/lib/crystalball/predictor/modified_specs.rb +33 -0
  44. data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
  45. data/lib/crystalball/predictor/strategy.rb +16 -0
  46. data/lib/crystalball/predictor.rb +58 -0
  47. data/lib/crystalball/predictor_evaluator.rb +55 -0
  48. data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -0
  49. data/lib/crystalball/rails/helpers/schema_definition_parser/active_record.rb +21 -0
  50. data/lib/crystalball/rails/helpers/schema_definition_parser/table_content_parser.rb +27 -0
  51. data/lib/crystalball/rails/helpers/schema_definition_parser.rb +36 -0
  52. data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
  53. data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
  54. data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +89 -0
  55. data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -0
  56. data/lib/crystalball/rails/predictor/modified_schema.rb +81 -0
  57. data/lib/crystalball/rails/tables_map.rb +53 -0
  58. data/lib/crystalball/rails/tables_map_generator/configuration.rb +39 -0
  59. data/lib/crystalball/rails/tables_map_generator.rb +84 -0
  60. data/lib/crystalball/rails.rb +11 -0
  61. data/lib/crystalball/rspec/filtering.rb +52 -0
  62. data/lib/crystalball/rspec/prediction_builder.rb +53 -0
  63. data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
  64. data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
  65. data/lib/crystalball/rspec/runner/configuration.rb +80 -0
  66. data/lib/crystalball/rspec/runner.rb +107 -0
  67. data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
  68. data/lib/crystalball/source_diff/file_diff.rb +53 -0
  69. data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
  70. data/lib/crystalball/source_diff.rb +48 -0
  71. data/lib/crystalball/version.rb +5 -0
  72. data/lib/crystalball.rb +44 -0
  73. metadata +314 -0
checksums.yaml ADDED
@@ -0,0 +1,7 @@
1
+ ---
2
+ SHA256:
3
+ metadata.gz: 2edc0f271a5823ee73e40d74c51f6e357d578424aa059757b4ff6690316adc74
4
+ data.tar.gz: b9803cc94cd0d6b8437d317e7b47d25ebec6de6bf4b4a53735b9e12c97b6dd13
5
+ SHA512:
6
+ metadata.gz: 24937f95403f4ba4637a9f7f5e3558b8a7f2fc4d7759577a237de8b849e0e6a20a4a87e764d0f73f58ac5b5cf18650f7a8ef15d94aaa479b726afe2ccf8fb599
7
+ data.tar.gz: 85ce3e28abde0853b087e685c7bc81dec2a0a11c136ff79b81dab558abbd55ce1c020d260d316c5c11ca865819a40f2483b33f65390a71f4ffe178e5cb931b4b
data/LICENSE ADDED
@@ -0,0 +1,22 @@
1
+ Copyright (c) 2025 GitLab Inc
2
+
3
+ MIT License
4
+
5
+ Permission is hereby granted, free of charge, to any person obtaining
6
+ a copy of this software and associated documentation files (the
7
+ "Software"), to deal in the Software without restriction, including
8
+ without limitation the rights to use, copy, modify, merge, publish,
9
+ distribute, sublicense, and/or sell copies of the Software, and to
10
+ permit persons to whom the Software is furnished to do so, subject to
11
+ the following conditions:
12
+
13
+ The above copyright notice and this permission notice shall be
14
+ included in all copies or substantial portions of the Software.
15
+
16
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND,
17
+ EXPRESS OR IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF
18
+ MERCHANTABILITY, FITNESS FOR A PARTICULAR PURPOSE AND
19
+ NONINFRINGEMENT. IN NO EVENT SHALL THE AUTHORS OR COPYRIGHT HOLDERS BE
20
+ LIABLE FOR ANY CLAIM, DAMAGES OR OTHER LIABILITY, WHETHER IN AN ACTION
21
+ OF CONTRACT, TORT OR OTHERWISE, ARISING FROM, OUT OF OR IN CONNECTION
22
+ WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE SOFTWARE.
data/README.md ADDED
@@ -0,0 +1,56 @@
1
+ # Crystalball
2
+
3
+ ![Build Status](https://gitlab.com/acunskis/crystalball/badges/main/pipeline.svg)
4
+ ![Test Coverage](https://gitlab.com/acunskis/crystalball/badges/main/coverage.svg?job=rspec)
5
+
6
+ Crystalball is a Ruby library which implements [Regression Test Selection mechanism](https://tenderlovemaking.com/2015/02/13/predicting-test-failues.html) originally published by Aaron Patterson.
7
+ Its main purpose is to select a minimal subset of your test suite which should be run to ensure your changes didn't break anything.
8
+
9
+ ## Installation
10
+
11
+ Add this line to your application's Gemfile:
12
+
13
+ ```ruby
14
+ group :test do
15
+ gem 'crystalball'
16
+ end
17
+ ```
18
+
19
+ And then execute:
20
+
21
+ ```console
22
+ bundle install
23
+ ```
24
+
25
+ Or install it yourself as:
26
+
27
+ ```console
28
+ gem install crystalball
29
+ ```
30
+
31
+ ## Usage
32
+
33
+ Please see our [official documentation](https://gitlab.com/acunskis/crystalball/-/blob/main/docs/index.md).
34
+
35
+ ### Versioning
36
+
37
+ We use [semantic versioning](https://semver.org/) for our [releases](https://gitlab.com/acunskis/crystalball/-/releases).
38
+
39
+ ## Development
40
+
41
+ After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake spec` to run the tests. You can also run `bin/console` for an interactive prompt that will allow you to experiment.
42
+
43
+ To install this gem onto your local machine, run `bundle exec rake install`.
44
+
45
+ ### Release
46
+
47
+ In order to release new version, manual pipeline should be triggered via Gitlab UI and version component input should be set according to [semver versioning](#versioning) strategy. Pipeline will then automatically bump version, push updated files and release tag and build and push gem to <rubygems.org>.
48
+
49
+ ## Contributing
50
+
51
+ Bug reports and pull requests are welcome on GitHub at <https://github.com/toptal/crystalball>.
52
+ This project is intended to be a safe, welcoming space for collaboration, and contributors are expected to adhere to the [Contributor Covenant](http://contributor-covenant.org) code of conduct.
53
+
54
+ ## License
55
+
56
+ Crystalball is released under the [MIT License](https://opensource.org/licenses/MIT).
data/bin/crystalball ADDED
@@ -0,0 +1,5 @@
1
+ #!/usr/bin/env ruby
2
+ # frozen_string_literal: true
3
+
4
+ require 'crystalball'
5
+ Crystalball::RSpec::Runner.invoke
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/rails/tables_map_generator"
4
+ require "crystalball/rails/predictor/modified_schema"
@@ -0,0 +1,19 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Data object to store execution map for specific example
5
+ class ExampleGroupMap
6
+ attr_reader :uid, :file_path, :used_files
7
+ extend Forwardable
8
+
9
+ delegate %i[push each] => :used_files
10
+
11
+ # @param [Example|ExampleGroup] example - RSpec example or example group
12
+ # @param [Array<String>] used_files - list of files affected by example
13
+ def initialize(example, used_files = [])
14
+ @uid = example.id
15
+ @file_path = example.file_path
16
+ @used_files = used_files
17
+ end
18
+ end
19
+ end
@@ -0,0 +1,56 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ # Storage for execution map
5
+ class ExecutionMap
6
+ extend Forwardable
7
+
8
+ # Simple data object for map metadata information
9
+ class Metadata
10
+ attr_reader :commit, :type, :version, :timestamp
11
+
12
+ # @param [String] commit - SHA of commit
13
+ # @param [String] type - type of execution map
14
+ # @param [Numeric] version - map generator version number
15
+ def initialize(commit: nil, type: nil, version: nil, timestamp: nil)
16
+ @commit = commit
17
+ @type = type
18
+ @timestamp = timestamp
19
+ @version = version
20
+ end
21
+
22
+ def to_h
23
+ {type: type, commit: commit, timestamp: timestamp, version: version}
24
+ end
25
+ end
26
+
27
+ attr_reader :example_groups, :metadata
28
+
29
+ delegate %i[commit version timestamp] => :metadata
30
+ delegate %i[size] => :example_groups
31
+
32
+ # @param [Hash] metadata - add or override metadata of execution map
33
+ # @param [Hash] example_groups - initial list of example groups data
34
+ def initialize(metadata: {}, example_groups: {})
35
+ @example_groups = example_groups
36
+
37
+ @metadata = Metadata.new(type: self.class.name, **metadata)
38
+ end
39
+
40
+ # Adds example group map to the list
41
+ #
42
+ # @param [Crystalball::ExampleGroupMap] example_group_map
43
+ def <<(example_group_map)
44
+ example_groups[example_group_map.uid] = example_group_map.used_files.uniq
45
+ end
46
+
47
+ # Remove all example_groups
48
+ def clear!
49
+ self.example_groups = {}
50
+ end
51
+
52
+ private
53
+
54
+ attr_writer :example_groups, :metadata
55
+ end
56
+ end
@@ -0,0 +1,14 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Represents git repo object itself.
5
+ class Base
6
+ # `git merge-base ...`. Returns common ancestor for all passed commits
7
+ #
8
+ # @param [Array<Object>] args - list of commits to process. Last argument can be options for merge-base command
9
+ # @return [Git::Object::Commit]
10
+ def merge_base(*args)
11
+ gcommit(lib.merge_base(*args))
12
+ end
13
+ end
14
+ end
@@ -0,0 +1,17 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Git
4
+ # Class which holds whole collection of raw methods to work with git
5
+ class Lib
6
+ # `git merge-base ...`. Returns common ancestor for all passed commits
7
+ #
8
+ # @param [Array<Object>] args - list of commits to process. Last argument can be options for merge-base command
9
+ # @return [String]
10
+ def merge_base(*args)
11
+ opts = args.last.is_a?(Hash) ? args.pop : {}
12
+ arg_opts = opts.filter_map { |k, v| "--#{k}" if v } + args
13
+
14
+ command("merge-base", *arg_opts.flatten)
15
+ end
16
+ end
17
+ end
@@ -0,0 +1,4 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/extensions/git/base"
4
+ require "crystalball/extensions/git/lib"
@@ -0,0 +1,3 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/map_generator/factory_bot_strategy"
@@ -0,0 +1,53 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "git"
4
+ require "crystalball/source_diff"
5
+
6
+ module Crystalball
7
+ # Wrapper class representing Git repository
8
+ class GitRepo
9
+ attr_reader :repo_path
10
+
11
+ class << self
12
+ # @return [Crystalball::GitRepo] instance for given path
13
+ def open(repo_path)
14
+ path = Pathname(repo_path)
15
+ new(path) if exists?(path)
16
+ end
17
+
18
+ # Check if given path is under git control (contains .git folder)
19
+ def exists?(path)
20
+ path.join(".git").directory?
21
+ end
22
+ end
23
+
24
+ # @param [Pathname] repo_path path to repository root folder
25
+ def initialize(repo_path)
26
+ @repo_path = repo_path
27
+ end
28
+
29
+ # Proxy all unknown calls to `Git` object
30
+ def method_missing(method, *args, &block)
31
+ repo.public_send(method, *args, &block)
32
+ end
33
+
34
+ def respond_to_missing?(method, *)
35
+ repo.respond_to?(method, false)
36
+ end
37
+
38
+ # Creates diff
39
+ #
40
+ # @param [String] from starting commit to build a diff. Default: HEAD
41
+ # @param [String] to ending commit to build a diff. Default: nil, will build diff of uncommitted changes
42
+ # @return [SourceDiff]
43
+ def diff(from = "HEAD", to = nil)
44
+ SourceDiff.new(repo.diff(from, to))
45
+ end
46
+
47
+ private
48
+
49
+ def repo
50
+ @repo ||= Git.open(repo_path)
51
+ end
52
+ end
53
+ end
@@ -0,0 +1,51 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "logger"
4
+
5
+ module Crystalball
6
+ # This module logs information to the standard output based on the configured log level,
7
+ # and also logs unfiltered information to the configured log file.
8
+ module Logging
9
+ def log(severity_sym, *args, &block)
10
+ output_stream.log(severity(severity_sym), *args, &block)
11
+ log_file_output_stream.log(severity(severity_sym), *args, &block)
12
+ end
13
+
14
+ def self.extended(base)
15
+ base.private_class_method :severity, :output_stream, :log_file_output_stream, :configured_level, :config
16
+ end
17
+
18
+ # @api private
19
+ def reset_logger
20
+ @output_stream = nil
21
+ @log_file_output_stream = nil
22
+ end
23
+
24
+ def severity(severity_sym)
25
+ ::Logger.const_get(severity_sym.to_s.upcase)
26
+ end
27
+
28
+ def output_stream
29
+ @output_stream ||= ::Logger.new(STDOUT).tap do |logger|
30
+ logger.level = severity(configured_level)
31
+ end
32
+ end
33
+
34
+ def log_file_output_stream
35
+ @log_file_output_stream ||= begin
36
+ config["log_file"].dirname.mkpath
37
+ ::Logger.new(config["log_file"]).tap do |logger|
38
+ logger.level = ::Logger::DEBUG
39
+ end
40
+ end
41
+ end
42
+
43
+ def configured_level
44
+ config["log_level"].to_sym
45
+ end
46
+
47
+ def config
48
+ @config ||= Crystalball::RSpec::Runner.config
49
+ end
50
+ end
51
+ end
@@ -0,0 +1,29 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ module MapCompactor
5
+ # Class representing RSpec context data
6
+ class ExampleContext
7
+ attr_reader :address
8
+
9
+ def initialize(address)
10
+ @address = address
11
+ end
12
+
13
+ def parent
14
+ @parent ||= begin
15
+ parent_uid = address.split(":")[0..-2].join(":")
16
+ parent_uid.empty? ? nil : self.class.new(parent_uid)
17
+ end
18
+ end
19
+
20
+ def include?(example_id)
21
+ example_id =~ /\[#{address}[\:\]]/
22
+ end
23
+
24
+ def depth
25
+ @depth ||= address.split(":").size
26
+ end
27
+ end
28
+ end
29
+ end
@@ -0,0 +1,66 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/map_compactor/example_context"
4
+
5
+ module Crystalball
6
+ module MapCompactor
7
+ # Class representing example groups data compacting logic for a single file
8
+ class ExampleGroupsDataCompactor
9
+ # @param [Hash] plain_data a hash of examples and used files
10
+ def self.compact!(plain_data)
11
+ new(plain_data).compact!
12
+ end
13
+
14
+ def compact!
15
+ contexts = extract_contexts(plain_data.keys).sort_by(&:depth)
16
+
17
+ contexts.each do |context|
18
+ compact_data[context.address] = compact_context!(context)
19
+ end
20
+ compact_data
21
+ end
22
+
23
+ private
24
+
25
+ attr_reader :compact_data, :plain_data
26
+
27
+ def initialize(plain_data)
28
+ @plain_data = plain_data
29
+ @compact_data = {}
30
+ end
31
+
32
+ def compact_context!(context) # rubocop:disable Metrics/MethodLength
33
+ result = nil
34
+ plain_data.each do |example_uid, used_files|
35
+ next unless context.include?(example_uid)
36
+
37
+ if result.nil?
38
+ result = used_files
39
+ result -= deep_used_files(context.parent) if context.parent
40
+ else
41
+ result &= used_files
42
+ end
43
+ end
44
+ result
45
+ end
46
+
47
+ def deep_used_files(context)
48
+ result = compact_data[context.address]
49
+ result += deep_used_files(context.parent) if context.parent
50
+ result
51
+ end
52
+
53
+ def extract_contexts(example_uids)
54
+ result = []
55
+ example_uids.each do |example_uid|
56
+ context_numbers = /\[(.*)\]/.match(example_uid)[1].split(":")
57
+ until context_numbers.empty?
58
+ result << ExampleContext.new(context_numbers.join(":"))
59
+ context_numbers.pop
60
+ end
61
+ end
62
+ result.compact.uniq(&:address)
63
+ end
64
+ end
65
+ end
66
+ end
@@ -0,0 +1,44 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "ostruct"
4
+
5
+ require "crystalball/example_group_map"
6
+ require "crystalball/execution_map"
7
+ require "crystalball/map_compactor/example_groups_data_compactor"
8
+
9
+ module Crystalball
10
+ # a module for compacting execution map by moving out repeated used files to upper contexts records.
11
+ module MapCompactor
12
+ class << self
13
+ # @param [Crystalball::ExecutionMap] map execution map to be compacted
14
+ # @return [Crystalball::ExecutionMap] compact map
15
+ def compact_map!(map)
16
+ new_map = Crystalball::ExecutionMap.new(metadata: map.metadata.to_h)
17
+
18
+ compact_examples!(map.example_groups).each do |context, used_files|
19
+ new_map << ExampleGroupMap.new(OpenStruct.new(id: context, file_path: example_filename(context)), used_files)
20
+ end
21
+
22
+ new_map
23
+ end
24
+
25
+ def compact_examples!(example_groups)
26
+ result = {}
27
+ example_groups.group_by { |k, _v| example_filename(k) }.each do |filename, examples|
28
+ compact_data = ExampleGroupsDataCompactor.compact!(examples.to_h)
29
+
30
+ compact_data.each do |context_address, used_files|
31
+ result["#{filename}[#{context_address}]"] = used_files unless used_files.empty?
32
+ end
33
+ end
34
+ result
35
+ end
36
+
37
+ private
38
+
39
+ def example_filename(example_id)
40
+ example_id.split("[").first
41
+ end
42
+ end
43
+ end
44
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "set"
4
+
5
+ module Crystalball
6
+ class MapGenerator
7
+ class AllocatedObjectsStrategy
8
+ # Class to list object classes used during a block
9
+ class ObjectTracker
10
+ attr_reader :only_of
11
+
12
+ # @param [Array<Module>] only_of - classes or modules to watch on
13
+ def initialize(only_of: ["Object"])
14
+ @only_of = only_of
15
+ @created_object_classes = Set.new
16
+ end
17
+
18
+ # @yield a block to execute
19
+ # @return [Array<Object>] classes of objects allocated during the block execution
20
+ def used_classes_during(&block)
21
+ self.created_object_classes = Set.new
22
+ trace_point.enable(&block)
23
+ created_object_classes
24
+ end
25
+
26
+ private
27
+
28
+ attr_accessor :created_object_classes
29
+
30
+ def whitelisted_constants
31
+ @whitelisted_constants ||= only_of.map { |str| Object.const_get(str) }
32
+ end
33
+
34
+ def trace_point
35
+ @trace_point ||= TracePoint.new(:c_call) do |tp|
36
+ next unless tp.method_id == :new || tp.method_id == :allocate
37
+ next unless whitelisted_constants.any? { |c| tp.self <= c }
38
+
39
+ created_object_classes << tp.self
40
+ end
41
+ end
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,45 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/map_generator/base_strategy"
4
+ require "crystalball/map_generator/object_sources_detector"
5
+ require "crystalball/map_generator/allocated_objects_strategy/object_tracker"
6
+
7
+ module Crystalball
8
+ class MapGenerator
9
+ # Map generator strategy to get paths to files contains definition for all objects and its
10
+ # ancestors allocated during test example.
11
+ class AllocatedObjectsStrategy
12
+ include BaseStrategy
13
+ extend Forwardable
14
+
15
+ attr_reader :execution_detector, :object_tracker
16
+
17
+ delegate %i[after_register before_finalize] => :execution_detector
18
+
19
+ def self.build(only: [], root: Dir.pwd)
20
+ hierarchy_fetcher = ObjectSourcesDetector::HierarchyFetcher.new(only)
21
+ execution_detector = ObjectSourcesDetector.new(root_path: root, hierarchy_fetcher: hierarchy_fetcher)
22
+
23
+ new(execution_detector: execution_detector, object_tracker: ObjectTracker.new(only_of: only))
24
+ end
25
+
26
+ # @param [#detect] execution_detector
27
+ # @param [#created_during] object_tracker
28
+ def initialize(execution_detector:, object_tracker:)
29
+ @object_tracker = object_tracker
30
+ @execution_detector = execution_detector
31
+ end
32
+
33
+ # Adds to the used files every file which contain the definition of the
34
+ # classes of the objects allocated during the spec execution.
35
+ # @param [Crystalball::ExampleGroupMap] example_map - object holding example metadata and used files
36
+ # @param [RSpec::Core::Example] example - a RSpec example
37
+ def call(example_map, example)
38
+ classes = object_tracker.used_classes_during do
39
+ yield example_map, example
40
+ end
41
+ example_map.push(*execution_detector.detect(classes))
42
+ end
43
+ end
44
+ end
45
+ end
@@ -0,0 +1,22 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Crystalball
4
+ class MapGenerator
5
+ # Map generator strategy interface
6
+ module BaseStrategy
7
+ def after_register; end
8
+
9
+ def after_start; end
10
+
11
+ def before_finalize; end
12
+
13
+ # Each strategy must implement #call augmenting the used_files list and
14
+ # yielding back the ExampleGroupMap.
15
+ # @param [Crystalball::ExampleGroupMap] _example_map - object holding example metadata and used files
16
+ # @param [RSpec::Core::Example] _example - a RSpec example
17
+ def call(_example_map, _example)
18
+ raise NotImplementedError
19
+ end
20
+ end
21
+ end
22
+ end
@@ -0,0 +1,58 @@
1
+ # frozen_string_literal: true
2
+
3
+ require "crystalball/map_generator/strategies_collection"
4
+
5
+ module Crystalball
6
+ class MapGenerator
7
+ # Configuration of map generator. Is can be accessed as a first argument inside
8
+ # `Crystalball::MapGenerator.start! { |config| config } block.
9
+ class Configuration
10
+ attr_writer :map_storage
11
+ attr_writer :map_class
12
+ attr_accessor :commit, :version, :compact_map
13
+
14
+ attr_reader :strategies
15
+
16
+ def initialize
17
+ @strategies = StrategiesCollection.new
18
+ @compact_map = true
19
+ end
20
+
21
+ def compact_map?
22
+ !!@compact_map
23
+ end
24
+
25
+ def map_class
26
+ @map_class ||= ExecutionMap
27
+ end
28
+
29
+ def map_storage_path
30
+ @map_storage_path ||= Pathname("tmp/crystalball_data.yml")
31
+ end
32
+
33
+ def map_storage_path=(value)
34
+ @map_storage_path = Pathname(value)
35
+ end
36
+
37
+ def map_storage
38
+ @map_storage ||= MapStorage::YAMLStorage.new(map_storage_path)
39
+ end
40
+
41
+ def dump_threshold
42
+ @dump_threshold ||= 100
43
+ end
44
+
45
+ def dump_threshold=(value)
46
+ @dump_threshold = value.to_i
47
+ end
48
+
49
+ # Register new strategy for map generation
50
+ #
51
+ # @param [Crystalball::MapGenerator::BaseStrategy] strategy
52
+ def register(strategy)
53
+ @strategies.push strategy
54
+ strategy.after_register
55
+ end
56
+ end
57
+ end
58
+ end
@@ -0,0 +1,23 @@
1
+ # frozen_string_literal: true
2
+
3
+ require_relative "../helpers/path_filter"
4
+
5
+ module Crystalball
6
+ class MapGenerator
7
+ class CoverageStrategy
8
+ # Class for detecting code execution path based on coverage information diff
9
+ class ExecutionDetector
10
+ include ::Crystalball::MapGenerator::Helpers::PathFilter
11
+ # Detects files affected during example execution. Transforms absolute paths to relative.
12
+ # Exclude paths outside of repository
13
+ #
14
+ # @param[Array<String>] list of files affected before example execution
15
+ # @param[Array<String>] list of files affected after example execution
16
+ # @return [Array<String>]
17
+ def detect(before, after)
18
+ filter after.reject { |file_name, after_coverage| before[file_name] == after_coverage }.keys
19
+ end
20
+ end
21
+ end
22
+ end
23
+ end