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.
- checksums.yaml +7 -0
- data/LICENSE +22 -0
- data/README.md +56 -0
- data/bin/crystalball +5 -0
- data/lib/crystalball/active_record.rb +4 -0
- data/lib/crystalball/example_group_map.rb +19 -0
- data/lib/crystalball/execution_map.rb +56 -0
- data/lib/crystalball/extensions/git/base.rb +14 -0
- data/lib/crystalball/extensions/git/lib.rb +17 -0
- data/lib/crystalball/extensions/git.rb +4 -0
- data/lib/crystalball/factory_bot.rb +3 -0
- data/lib/crystalball/git_repo.rb +53 -0
- data/lib/crystalball/logging.rb +51 -0
- data/lib/crystalball/map_compactor/example_context.rb +29 -0
- data/lib/crystalball/map_compactor/example_groups_data_compactor.rb +66 -0
- data/lib/crystalball/map_compactor.rb +44 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy/object_tracker.rb +45 -0
- data/lib/crystalball/map_generator/allocated_objects_strategy.rb +45 -0
- data/lib/crystalball/map_generator/base_strategy.rb +22 -0
- data/lib/crystalball/map_generator/configuration.rb +58 -0
- data/lib/crystalball/map_generator/coverage_strategy/execution_detector.rb +23 -0
- data/lib/crystalball/map_generator/coverage_strategy.rb +35 -0
- data/lib/crystalball/map_generator/described_class_strategy.rb +35 -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/dsl_patch.rb +40 -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/factory_bot_strategy.rb +59 -0
- data/lib/crystalball/map_generator/helpers/path_filter.rb +25 -0
- data/lib/crystalball/map_generator/object_sources_detector/definition_tracer.rb +39 -0
- data/lib/crystalball/map_generator/object_sources_detector/hierarchy_fetcher.rb +36 -0
- data/lib/crystalball/map_generator/object_sources_detector.rb +54 -0
- data/lib/crystalball/map_generator/parser_strategy/processor.rb +129 -0
- data/lib/crystalball/map_generator/parser_strategy.rb +60 -0
- data/lib/crystalball/map_generator/strategies_collection.rb +43 -0
- data/lib/crystalball/map_generator.rb +84 -0
- data/lib/crystalball/map_storage/yaml_storage.rb +69 -0
- data/lib/crystalball/prediction.rb +35 -0
- data/lib/crystalball/predictor/associated_specs.rb +45 -0
- 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 +27 -0
- data/lib/crystalball/predictor/modified_specs.rb +33 -0
- data/lib/crystalball/predictor/modified_support_specs.rb +39 -0
- data/lib/crystalball/predictor/strategy.rb +16 -0
- data/lib/crystalball/predictor.rb +58 -0
- data/lib/crystalball/predictor_evaluator.rb +55 -0
- data/lib/crystalball/rails/helpers/base_schema_parser.rb +51 -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/helpers/schema_definition_parser.rb +36 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy/patch.rb +42 -0
- data/lib/crystalball/rails/map_generator/action_view_strategy.rb +46 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy/simple_patch.rb +89 -0
- data/lib/crystalball/rails/map_generator/i18n_strategy.rb +47 -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/configuration.rb +39 -0
- data/lib/crystalball/rails/tables_map_generator.rb +84 -0
- data/lib/crystalball/rails.rb +11 -0
- data/lib/crystalball/rspec/filtering.rb +52 -0
- data/lib/crystalball/rspec/prediction_builder.rb +53 -0
- data/lib/crystalball/rspec/prediction_pruning/examples_pruner.rb +70 -0
- data/lib/crystalball/rspec/prediction_pruning.rb +56 -0
- data/lib/crystalball/rspec/runner/configuration.rb +80 -0
- data/lib/crystalball/rspec/runner.rb +107 -0
- data/lib/crystalball/rspec/standard_prediction_builder.rb +17 -0
- data/lib/crystalball/source_diff/file_diff.rb +53 -0
- data/lib/crystalball/source_diff/formatting_checker.rb +50 -0
- data/lib/crystalball/source_diff.rb +48 -0
- data/lib/crystalball/version.rb +5 -0
- data/lib/crystalball.rb +44 -0
- 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
|
+

|
4
|
+

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