minitest-subjective 0.1.0.pre.alpha
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/.idea/.gitignore +8 -0
- data/.idea/minitest-subjective.iml +50 -0
- data/.idea/misc.xml +4 -0
- data/.idea/modules.xml +8 -0
- data/.idea/vcs.xml +6 -0
- data/.rubocop.yml +7 -0
- data/.ruby-version +1 -0
- data/LICENSE.txt +21 -0
- data/README.md +71 -0
- data/Rakefile +12 -0
- data/lib/minitest/subjective/case_inquirer.rb +58 -0
- data/lib/minitest/subjective/file_result/branch_statistics/branch_hits.rb +42 -0
- data/lib/minitest/subjective/file_result/branch_statistics/conditional_hits.rb +42 -0
- data/lib/minitest/subjective/file_result/branch_statistics/formatting.rb +84 -0
- data/lib/minitest/subjective/file_result/branch_statistics.rb +55 -0
- data/lib/minitest/subjective/file_result/line_statistics/formatting.rb +35 -0
- data/lib/minitest/subjective/file_result/line_statistics/line_hits.rb +31 -0
- data/lib/minitest/subjective/file_result/line_statistics.rb +57 -0
- data/lib/minitest/subjective/file_result/location.rb +17 -0
- data/lib/minitest/subjective/file_result/method_statistics/formatting.rb +29 -0
- data/lib/minitest/subjective/file_result/method_statistics/method_hits.rb +37 -0
- data/lib/minitest/subjective/file_result/method_statistics.rb +66 -0
- data/lib/minitest/subjective/file_result/range.rb +68 -0
- data/lib/minitest/subjective/file_result.rb +57 -0
- data/lib/minitest/subjective/formatter/colors.rb +62 -0
- data/lib/minitest/subjective/formatter.rb +34 -0
- data/lib/minitest/subjective/reporter.rb +59 -0
- data/lib/minitest/subjective/result_extensions.rb +26 -0
- data/lib/minitest/subjective/test_extensions.rb +17 -0
- data/lib/minitest/subjective/version.rb +7 -0
- data/lib/minitest/subjective.rb +70 -0
- data/lib/minitest/subjective_plugin.rb +37 -0
- metadata +90 -0
checksums.yaml
ADDED
|
@@ -0,0 +1,7 @@
|
|
|
1
|
+
---
|
|
2
|
+
SHA256:
|
|
3
|
+
metadata.gz: f5760ba45aa4159efb193a53b8c0a2da6d983881793d60d64a9c0dd25b55a40e
|
|
4
|
+
data.tar.gz: 3b0fa96c440e22577be2321a9ee6c4ec46d69de0d3bb989e3ca0e104bcfda6c8
|
|
5
|
+
SHA512:
|
|
6
|
+
metadata.gz: d826c7e3dd1687ff376c8fa29c9ac00e2ede3c11aa8e364219fe79ca9bbedce0e636a53c986e502e76a6756bba4eb3c78375692fd839d0d64976c4173a325b58
|
|
7
|
+
data.tar.gz: 71a008c282cac260f3f70bf981e970040b8d6860c26a8a0f38df07be5aa90e76f52f643d3a63d1a18b2bada19da76f585fc69782fbccd04557eda02e231b5676
|
data/.idea/.gitignore
ADDED
|
@@ -0,0 +1,50 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<module type="RUBY_MODULE" version="4">
|
|
3
|
+
<component name="ModuleRunConfigurationManager">
|
|
4
|
+
<shared />
|
|
5
|
+
</component>
|
|
6
|
+
<component name="NewModuleRootManager">
|
|
7
|
+
<content url="file://$MODULE_DIR$">
|
|
8
|
+
<sourceFolder url="file://$MODULE_DIR$/features" isTestSource="true" />
|
|
9
|
+
<sourceFolder url="file://$MODULE_DIR$/spec" isTestSource="true" />
|
|
10
|
+
<sourceFolder url="file://$MODULE_DIR$/test" isTestSource="true" />
|
|
11
|
+
</content>
|
|
12
|
+
<orderEntry type="jdk" jdkName="rbenv: 4.0.2" jdkType="RUBY_SDK" />
|
|
13
|
+
<orderEntry type="sourceFolder" forTests="false" />
|
|
14
|
+
<orderEntry type="library" scope="PROVIDED" name="ast (v2.4.3, rbenv: 4.0.2) [gem]" level="application" />
|
|
15
|
+
<orderEntry type="library" scope="PROVIDED" name="bundler (v4.0.9, rbenv: 4.0.2) [gem]" level="application" />
|
|
16
|
+
<orderEntry type="library" scope="PROVIDED" name="date (v3.5.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
17
|
+
<orderEntry type="library" scope="PROVIDED" name="drb (v2.2.3, rbenv: 4.0.2) [gem]" level="application" />
|
|
18
|
+
<orderEntry type="library" scope="PROVIDED" name="erb (v6.0.4, rbenv: 4.0.2) [gem]" level="application" />
|
|
19
|
+
<orderEntry type="library" scope="PROVIDED" name="io-console (v0.8.2, rbenv: 4.0.2) [gem]" level="application" />
|
|
20
|
+
<orderEntry type="library" scope="PROVIDED" name="irb (v1.18.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
21
|
+
<orderEntry type="library" scope="PROVIDED" name="json (v2.19.4, rbenv: 4.0.2) [gem]" level="application" />
|
|
22
|
+
<orderEntry type="library" scope="PROVIDED" name="language_server-protocol (v3.17.0.5, rbenv: 4.0.2) [gem]" level="application" />
|
|
23
|
+
<orderEntry type="library" scope="PROVIDED" name="lint_roller (v1.1.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
24
|
+
<orderEntry type="library" scope="PROVIDED" name="minitest (v6.0.5, rbenv: 4.0.2) [gem]" level="application" />
|
|
25
|
+
<orderEntry type="library" scope="PROVIDED" name="minitest-mock (v5.27.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
26
|
+
<orderEntry type="library" scope="PROVIDED" name="parallel (v2.1.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
27
|
+
<orderEntry type="library" scope="PROVIDED" name="parser (v3.3.11.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
28
|
+
<orderEntry type="library" scope="PROVIDED" name="pp (v0.6.3, rbenv: 4.0.2) [gem]" level="application" />
|
|
29
|
+
<orderEntry type="library" scope="PROVIDED" name="prettyprint (v0.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
30
|
+
<orderEntry type="library" scope="PROVIDED" name="prism (v1.9.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
31
|
+
<orderEntry type="library" scope="PROVIDED" name="psych (v5.3.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
32
|
+
<orderEntry type="library" scope="PROVIDED" name="racc (v1.8.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
33
|
+
<orderEntry type="library" scope="PROVIDED" name="rainbow (v3.1.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
34
|
+
<orderEntry type="library" scope="PROVIDED" name="rake (v13.4.2, rbenv: 4.0.2) [gem]" level="application" />
|
|
35
|
+
<orderEntry type="library" scope="PROVIDED" name="rdoc (v7.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
36
|
+
<orderEntry type="library" scope="PROVIDED" name="regexp_parser (v2.12.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
37
|
+
<orderEntry type="library" scope="PROVIDED" name="reline (v0.6.3, rbenv: 4.0.2) [gem]" level="application" />
|
|
38
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop (v1.86.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
39
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-ast (v1.49.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
40
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-minitest (v0.39.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
41
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-performance (v1.26.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
42
|
+
<orderEntry type="library" scope="PROVIDED" name="rubocop-rake (v0.7.1, rbenv: 4.0.2) [gem]" level="application" />
|
|
43
|
+
<orderEntry type="library" scope="PROVIDED" name="ruby-progressbar (v1.13.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
44
|
+
<orderEntry type="library" scope="PROVIDED" name="stringio (v3.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
45
|
+
<orderEntry type="library" scope="PROVIDED" name="tsort (v0.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
46
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-display_width (v3.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
47
|
+
<orderEntry type="library" scope="PROVIDED" name="unicode-emoji (v4.2.0, rbenv: 4.0.2) [gem]" level="application" />
|
|
48
|
+
<orderEntry type="library" scope="PROVIDED" name="zeitwerk (v2.7.5, rbenv: 4.0.2) [gem]" level="application" />
|
|
49
|
+
</component>
|
|
50
|
+
</module>
|
data/.idea/misc.xml
ADDED
data/.idea/modules.xml
ADDED
|
@@ -0,0 +1,8 @@
|
|
|
1
|
+
<?xml version="1.0" encoding="UTF-8"?>
|
|
2
|
+
<project version="4">
|
|
3
|
+
<component name="ProjectModuleManager">
|
|
4
|
+
<modules>
|
|
5
|
+
<module fileurl="file://$PROJECT_DIR$/.idea/minitest-subjective.iml" filepath="$PROJECT_DIR$/.idea/minitest-subjective.iml" />
|
|
6
|
+
</modules>
|
|
7
|
+
</component>
|
|
8
|
+
</project>
|
data/.idea/vcs.xml
ADDED
data/.rubocop.yml
ADDED
data/.ruby-version
ADDED
|
@@ -0,0 +1 @@
|
|
|
1
|
+
4.0.2
|
data/LICENSE.txt
ADDED
|
@@ -0,0 +1,21 @@
|
|
|
1
|
+
The MIT License (MIT)
|
|
2
|
+
|
|
3
|
+
Copyright (c) 2025 Justin Malčić
|
|
4
|
+
|
|
5
|
+
Permission is hereby granted, free of charge, to any person obtaining a copy
|
|
6
|
+
of this software and associated documentation files (the "Software"), to deal
|
|
7
|
+
in the Software without restriction, including without limitation the rights
|
|
8
|
+
to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
|
|
9
|
+
copies of the Software, and to permit persons to whom the Software is
|
|
10
|
+
furnished to do so, subject to the following conditions:
|
|
11
|
+
|
|
12
|
+
The above copyright notice and this permission notice shall be included in
|
|
13
|
+
all copies or substantial portions of the Software.
|
|
14
|
+
|
|
15
|
+
THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
|
|
16
|
+
IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
|
|
17
|
+
FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
|
|
18
|
+
AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
|
|
19
|
+
LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
|
|
20
|
+
OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN
|
|
21
|
+
THE SOFTWARE.
|
data/README.md
ADDED
|
@@ -0,0 +1,71 @@
|
|
|
1
|
+
# Minitest Subjective
|
|
2
|
+
|
|
3
|
+
Is your testing [sociable](https://martinfowler.com/bliki/UnitTest.html#SolitaryOrSociable)?
|
|
4
|
+
If so, test coverage you collect when running all your tests together will be artifically inflated.
|
|
5
|
+
That's because coverage only reflects the fact that _something, somewhere_ touched the covered code,
|
|
6
|
+
not necessarily your well-designed test for that specific method, alas.
|
|
7
|
+
What would be great is a kind of coverage sensitive to the current test _subject_ as it changes while running tests.
|
|
8
|
+
|
|
9
|
+
This has been [discussed before](https://www.rubyevents.org/talks/improving-coverage-analysis) by Ryan Davis,
|
|
10
|
+
author of Minitest, and you should totally watch his talk to understand why this matters, and why he created
|
|
11
|
+
[`minitest-coverage`](https://github.com/minitest/minitest-coverage). That was a while ago, and Ruby now has
|
|
12
|
+
more coverage modes (e.g. branch coverage, very useful). This gem takes a different approach to the problem, which also
|
|
13
|
+
avoids needing any changes to the coverage API.
|
|
14
|
+
|
|
15
|
+
The premise is straightforward: where $c_0$ is the coverage after first loading a file (before running any tests),
|
|
16
|
+
$c_1$ is the coverage just before running tests _for that file in particular_,
|
|
17
|
+
and $c_2$ is the coverage after running the last test for that file,
|
|
18
|
+
coverage for that file can be expressed as:
|
|
19
|
+
$$c_0 + (c_2 - c_1)$$
|
|
20
|
+
This gem just implements addition and subtraction for the different kinds of coverage in coverage results,
|
|
21
|
+
plus a basic formatter so you can see the results.
|
|
22
|
+
It works with parallel testing, and isn't thread-safe because coverage can't be run per-thread anyway.
|
|
23
|
+
|
|
24
|
+
## Installation
|
|
25
|
+
|
|
26
|
+
Install the gem and add to the application's Gemfile by executing:
|
|
27
|
+
|
|
28
|
+
```bash
|
|
29
|
+
bundle add minitest-subjective
|
|
30
|
+
```
|
|
31
|
+
|
|
32
|
+
## Usage
|
|
33
|
+
|
|
34
|
+
Simply run your tests with the `--subjective` flag.
|
|
35
|
+
|
|
36
|
+
```bash
|
|
37
|
+
minitest --subjective
|
|
38
|
+
```
|
|
39
|
+
|
|
40
|
+
Works with Rails too.
|
|
41
|
+
|
|
42
|
+
```bash
|
|
43
|
+
rails t --subjective
|
|
44
|
+
```
|
|
45
|
+
|
|
46
|
+
If you can't easily pass the flag, you can set `MINITEST_SUBJECTIVE=1` instead.
|
|
47
|
+
|
|
48
|
+
## Development
|
|
49
|
+
|
|
50
|
+
After checking out the repo, run `bin/setup` to install dependencies. Then, run `rake test` to run the tests.
|
|
51
|
+
You can also run `bin/console` for an interactive prompt that will allow you to experiment.
|
|
52
|
+
|
|
53
|
+
To install this gem onto your local machine, run `bundle exec rake install`.
|
|
54
|
+
To release a new version, update the version number in `version.rb`, and then run `bundle exec rake release`,
|
|
55
|
+
which will create a git tag for the version, push git commits and the created tag,
|
|
56
|
+
and push the `.gem` file to [rubygems.org](https://rubygems.org).
|
|
57
|
+
|
|
58
|
+
## Contributing
|
|
59
|
+
|
|
60
|
+
Bug reports and pull requests are welcome on GitHub at https://github.com/jmalcic/minitest-subjective.
|
|
61
|
+
This project is intended to be a safe, welcoming space for collaboration,
|
|
62
|
+
and contributors are expected to adhere to the [Ruby code of conduct](https://www.ruby-lang.org/en/conduct/).
|
|
63
|
+
|
|
64
|
+
## License
|
|
65
|
+
|
|
66
|
+
The gem is available as open source under the terms of the [MIT License](https://opensource.org/licenses/MIT).
|
|
67
|
+
|
|
68
|
+
## Code of Conduct
|
|
69
|
+
|
|
70
|
+
Everyone interacting in the Minitest::Subjective project's codebases, issue trackers, chat rooms and mailing lists
|
|
71
|
+
is expected to follow the [code of conduct](https://www.ruby-lang.org/en/conduct/).
|
data/Rakefile
ADDED
|
@@ -0,0 +1,58 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class CaseInquirer # :nodoc:
|
|
6
|
+
attr_reader :class_name, :subject_file
|
|
7
|
+
|
|
8
|
+
def initialize(klass)
|
|
9
|
+
@class_name = klass.is_a?(String) ? klass : klass.name
|
|
10
|
+
@klass = klass.is_a?(String) ? safe_constantize(klass) : klass
|
|
11
|
+
load_subject
|
|
12
|
+
@subject_file = Object.const_source_location(subject_name)&.first
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def subject_name
|
|
16
|
+
return class_name unless test?
|
|
17
|
+
return [class_name_nesting, demodulized_class_name.delete_suffix('Test')].join('::') if rails_test?
|
|
18
|
+
|
|
19
|
+
[class_name_nesting, demodulized_class_name.delete_prefix('Test')].join('::')
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def test? = rails_test? || minitest_test?
|
|
23
|
+
|
|
24
|
+
def minitest_test?
|
|
25
|
+
demodulized_class_name.start_with?('Test') && klass < Minitest::Test
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def rails_test?
|
|
29
|
+
demodulized_class_name.end_with?('Test') && defined?(::ActiveSupport) && klass < ActiveSupport::TestCase
|
|
30
|
+
end
|
|
31
|
+
|
|
32
|
+
def integration_test? = rails_test? && defined?(::ActionDispatch) && klass < ActionDispatch::IntegrationTest
|
|
33
|
+
def ==(other) = class_name == other.class_name && klass == other.klass && subject_file == other.subject_file
|
|
34
|
+
|
|
35
|
+
protected
|
|
36
|
+
|
|
37
|
+
attr_reader :klass
|
|
38
|
+
|
|
39
|
+
private
|
|
40
|
+
|
|
41
|
+
def load_subject = safe_constantize(subject_name)
|
|
42
|
+
|
|
43
|
+
def class_name_nesting
|
|
44
|
+
class_name.split('::')[0..-2].join('::')
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def demodulized_class_name
|
|
48
|
+
class_name.split('::').last
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
def safe_constantize(name)
|
|
52
|
+
Object.const_get(name)
|
|
53
|
+
rescue NameError
|
|
54
|
+
nil
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class BranchStatistics
|
|
7
|
+
BranchHits = Struct.new(:label, :id, :range, :hits) do
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators :range, :starts_at?, :ends_at?, :cover?
|
|
11
|
+
|
|
12
|
+
def self.from_pair(key, hits)
|
|
13
|
+
label, id, *range = key
|
|
14
|
+
new(label:, id:, range: Range.from_array(range), hits:)
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def +(other)
|
|
18
|
+
return self unless matches?(other)
|
|
19
|
+
|
|
20
|
+
self.class.new(label, id, range, hits + other.hits)
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def -(other)
|
|
24
|
+
return self unless matches?(other)
|
|
25
|
+
|
|
26
|
+
self.class.new(label, id, range, hits - other.hits)
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def covered?
|
|
30
|
+
hits.positive?
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def matches?(other)
|
|
36
|
+
other && other.label == label && other.id == id && other.range == range
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,42 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class BranchStatistics
|
|
7
|
+
ConditionalHits = Struct.new(:label, :id, :range, :branches) do
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators :range, :starts_at?, :ends_at?, :cover?
|
|
11
|
+
|
|
12
|
+
def self.from_pair(key, branches)
|
|
13
|
+
label, id, *range = key
|
|
14
|
+
new(label:, id:, range: Range.from_array(range), branches: branches.collect { BranchHits.from_pair(*_1) })
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def +(other)
|
|
18
|
+
return self unless matches?(other)
|
|
19
|
+
|
|
20
|
+
self.class.new(label, id, range, branches.zip(other.branches).collect { |current, new| current + new })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def -(other)
|
|
24
|
+
return self unless matches?(other)
|
|
25
|
+
|
|
26
|
+
self.class.new(label, id, range, branches.zip(other.branches).collect { |current, new| current - new })
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
def covered?
|
|
30
|
+
branches.all?(&:covered?)
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
private
|
|
34
|
+
|
|
35
|
+
def matches?(other)
|
|
36
|
+
other && other.label == label && other.id == id && other.range == range
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
end
|
|
40
|
+
end
|
|
41
|
+
end
|
|
42
|
+
end
|
|
@@ -0,0 +1,84 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class BranchStatistics
|
|
7
|
+
module Formatting # :nodoc:
|
|
8
|
+
def format(formatter, line, line_number)
|
|
9
|
+
return unless conditionals_for_line(line_number).any?
|
|
10
|
+
|
|
11
|
+
line.each_char
|
|
12
|
+
.flat_map
|
|
13
|
+
.with_index { |character, column| format_character(formatter, character, line_number, column) }
|
|
14
|
+
.join
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
private
|
|
18
|
+
|
|
19
|
+
def format_character(formatter, character, line_number, column)
|
|
20
|
+
[*format_conditionals_starting_at_position(formatter, line_number, column),
|
|
21
|
+
*format_branches_starting_at_position(formatter, line_number, column),
|
|
22
|
+
character,
|
|
23
|
+
*format_branches_ending_at_position(formatter, line_number, column),
|
|
24
|
+
*format_conditionals_ending_at_position(formatter, line_number, column)]
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def format_conditionals_starting_at_position(formatter, line_number, column)
|
|
28
|
+
conditionals_starting_at_position(line_number, column).collect do |conditional|
|
|
29
|
+
[formatter.colors.format(:gray) { '[' },
|
|
30
|
+
formatter.colors.format(:gray) { conditional.label },
|
|
31
|
+
' '].join
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def format_branches_starting_at_position(formatter, line_number, column)
|
|
36
|
+
branches_starting_at_position(line_number, column).collect { formatter.colors.format(:gray) { '[' } }
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def format_conditionals_ending_at_position(formatter, line_number, column)
|
|
40
|
+
conditionals_ending_at_position(line_number, column + 1).collect { formatter.colors.format(:gray) { ']' } }
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def format_branches_ending_at_position(formatter, line_number, column)
|
|
44
|
+
branches_ending_at_position(line_number, column + 1).collect do |branch|
|
|
45
|
+
[' ', framed_hits(formatter, branch), formatter.colors.format(:gray) { ']' }].join
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def framed_hits(formatter, branch)
|
|
50
|
+
formatter.colors.format(hits: branch.hits) do
|
|
51
|
+
formatter.colors.format(:framed) do
|
|
52
|
+
"#{branch.label} #{hit_count(branch)}"
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
|
|
57
|
+
def conditionals_for_line(line_number)
|
|
58
|
+
@branches.filter { _1.cover?(line_number) }
|
|
59
|
+
end
|
|
60
|
+
|
|
61
|
+
def conditionals_starting_at_position(line_number, column)
|
|
62
|
+
@branches.filter { _1.starts_at?(line_number, column) }
|
|
63
|
+
end
|
|
64
|
+
|
|
65
|
+
def conditionals_ending_at_position(line_number, column)
|
|
66
|
+
@branches.filter { _1.ends_at?(line_number, column) }
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
def branches_starting_at_position(line_number, column)
|
|
70
|
+
@branches.flat_map(&:branches).filter { _1.starts_at?(line_number, column) }
|
|
71
|
+
end
|
|
72
|
+
|
|
73
|
+
def branches_ending_at_position(line_number, column)
|
|
74
|
+
@branches.flat_map(&:branches).filter { _1.ends_at?(line_number, column) }
|
|
75
|
+
end
|
|
76
|
+
|
|
77
|
+
def hit_count(branch)
|
|
78
|
+
"(#{branch.hits} hit#{'s' unless branch.hits == 1})"
|
|
79
|
+
end
|
|
80
|
+
end
|
|
81
|
+
end
|
|
82
|
+
end
|
|
83
|
+
end
|
|
84
|
+
end
|
|
@@ -0,0 +1,55 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require 'minitest/subjective/file_result/range'
|
|
5
|
+
require 'minitest/subjective/file_result/branch_statistics/formatting'
|
|
6
|
+
require 'minitest/subjective/file_result/branch_statistics/branch_hits'
|
|
7
|
+
require 'minitest/subjective/file_result/branch_statistics/conditional_hits'
|
|
8
|
+
|
|
9
|
+
module Minitest
|
|
10
|
+
module Subjective
|
|
11
|
+
class FileResult
|
|
12
|
+
class BranchStatistics # :nodoc:
|
|
13
|
+
include Formatting
|
|
14
|
+
|
|
15
|
+
attr_accessor :branches
|
|
16
|
+
|
|
17
|
+
def self.from_hash(branches)
|
|
18
|
+
new(branches.collect { |key, value| ConditionalHits.from_pair(key, value) })
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def initialize(branches)
|
|
22
|
+
@branches = branches
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def +(other)
|
|
26
|
+
return self unless other
|
|
27
|
+
|
|
28
|
+
self.class.new(branches.zip(other.branches).collect { |current, new| current + new })
|
|
29
|
+
end
|
|
30
|
+
|
|
31
|
+
def -(other)
|
|
32
|
+
return self unless other
|
|
33
|
+
|
|
34
|
+
self.class.new(branches.zip(other.branches).collect { |current, new| current - new })
|
|
35
|
+
end
|
|
36
|
+
|
|
37
|
+
def [](index)
|
|
38
|
+
branches.filter { _1.cover?(index) }
|
|
39
|
+
end
|
|
40
|
+
|
|
41
|
+
def filter(&)
|
|
42
|
+
self.class.new(branches.filter(&))
|
|
43
|
+
end
|
|
44
|
+
|
|
45
|
+
def covered?
|
|
46
|
+
branches.all?(&:covered?)
|
|
47
|
+
end
|
|
48
|
+
|
|
49
|
+
def ==(other)
|
|
50
|
+
other && branches == other.branches
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
end
|
|
55
|
+
end
|
|
@@ -0,0 +1,35 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class LineStatistics
|
|
7
|
+
module Formatting # :nodoc:
|
|
8
|
+
def format(formatter, lines)
|
|
9
|
+
lines.collect.with_index do |line, line_number|
|
|
10
|
+
[line_number(formatter, line_number),
|
|
11
|
+
hit_count(formatter, line_number + 1),
|
|
12
|
+
yield(line, line_number + 1)].join(' ')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
|
|
16
|
+
private
|
|
17
|
+
|
|
18
|
+
def line_number(formatter, line_number)
|
|
19
|
+
formatter.colors.format :gray do
|
|
20
|
+
line_number.next.to_s.rjust(count.to_s.length)
|
|
21
|
+
end
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def hit_count(formatter, line_number)
|
|
25
|
+
formatter.colors.format hits: self[line_number]&.hits do
|
|
26
|
+
self[line_number]&.then { "(#{_1.hits} hit#{'s' unless _1.hits == 1})" }
|
|
27
|
+
.to_s
|
|
28
|
+
.rjust(max_hits.to_s.length + 7)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
@@ -0,0 +1,31 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class LineStatistics
|
|
7
|
+
LineHits = Struct.new(:line, :hits, :branches) do
|
|
8
|
+
def self.from_pair(line, hits, branches: nil)
|
|
9
|
+
new(line:, hits:, branches:)
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
def +(other)
|
|
13
|
+
return self unless other && other.line == line
|
|
14
|
+
|
|
15
|
+
self.class.new(line, hits + other.hits, branches && (branches + other.branches))
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def -(other)
|
|
19
|
+
return self unless other && other.line == line
|
|
20
|
+
|
|
21
|
+
self.class.new(line, hits - other.hits, branches && (branches - other.branches))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def covered?
|
|
25
|
+
hits.positive? && (branches.nil? || branches.covered?)
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require 'minitest/subjective/file_result/line_statistics/line_hits'
|
|
5
|
+
require 'minitest/subjective/file_result/line_statistics/formatting'
|
|
6
|
+
|
|
7
|
+
module Minitest
|
|
8
|
+
module Subjective
|
|
9
|
+
class FileResult
|
|
10
|
+
class LineStatistics # :nodoc:
|
|
11
|
+
include Formatting
|
|
12
|
+
|
|
13
|
+
extend Forwardable
|
|
14
|
+
|
|
15
|
+
attr_accessor :lines
|
|
16
|
+
|
|
17
|
+
def_delegators :lines, :[], :each, :count, :compact
|
|
18
|
+
|
|
19
|
+
def self.from_hash(lines, branches:)
|
|
20
|
+
new(lines.filter_map.with_index { |value, key| value && LineHits.from_pair(key + 1, value, branches:) })
|
|
21
|
+
end
|
|
22
|
+
|
|
23
|
+
def initialize(lines = [])
|
|
24
|
+
@lines = lines.to_h { [_1.line, _1] }
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
def +(other)
|
|
28
|
+
return self unless other
|
|
29
|
+
|
|
30
|
+
self.class.new(lines.values.zip(other.lines.values).collect { |current, new| current + new })
|
|
31
|
+
end
|
|
32
|
+
|
|
33
|
+
def -(other)
|
|
34
|
+
return self unless other
|
|
35
|
+
|
|
36
|
+
self.class.new(lines.values.zip(other.lines.values).collect { |current, new| current - new })
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def max_hits
|
|
40
|
+
lines.values.collect(&:hits).max
|
|
41
|
+
end
|
|
42
|
+
|
|
43
|
+
def covered?
|
|
44
|
+
lines.values.all?(&:covered?)
|
|
45
|
+
end
|
|
46
|
+
|
|
47
|
+
def ==(other)
|
|
48
|
+
other && lines == other.lines && branches == other.branches
|
|
49
|
+
end
|
|
50
|
+
|
|
51
|
+
protected
|
|
52
|
+
|
|
53
|
+
attr_reader :branches
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
Location = Struct.new(:line, :column) do
|
|
7
|
+
def self.from_array(args)
|
|
8
|
+
new(line: args[0], column: args[1])
|
|
9
|
+
end
|
|
10
|
+
|
|
11
|
+
def to_s
|
|
12
|
+
[line, column].join(':')
|
|
13
|
+
end
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,29 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class MethodStatistics
|
|
7
|
+
module Formatting # :nodoc:
|
|
8
|
+
def format(formatter, line, line_number, &)
|
|
9
|
+
[hit_count(formatter, line_number), branches_or_line(line, line_number, &)].join(' ')
|
|
10
|
+
end
|
|
11
|
+
|
|
12
|
+
private
|
|
13
|
+
|
|
14
|
+
def branches_or_line(line, line_number, &)
|
|
15
|
+
self[line_number]&.then { _1.branches.branches.any? ? yield(_1.branches) : line } || line
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def hit_count(formatter, line_number)
|
|
19
|
+
formatter.colors.format hits: find_by_index(line_number)&.hits do
|
|
20
|
+
find_by_index(line_number)&.then { "(#{_1.hits} hit#{'s' unless _1.hits == 1})" }
|
|
21
|
+
.to_s
|
|
22
|
+
.rjust(max_hits.to_s.length + 7)
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
end
|
|
28
|
+
end
|
|
29
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class FileResult
|
|
6
|
+
class MethodStatistics
|
|
7
|
+
MethodHits = Struct.new(:klass, :name, :range, :hits, :branches) do
|
|
8
|
+
extend Forwardable
|
|
9
|
+
|
|
10
|
+
def_delegators :range, :cover?, :starts_at?, :ends_at?
|
|
11
|
+
|
|
12
|
+
def self.from_pair(key, hits, branches: [])
|
|
13
|
+
klass, name, *range = key
|
|
14
|
+
range = Range.from_array(range)
|
|
15
|
+
new(klass: klass.name, name:, range:, hits:, branches: branches.filter { range.cover?(_1.range) })
|
|
16
|
+
end
|
|
17
|
+
|
|
18
|
+
def +(other)
|
|
19
|
+
return self unless other
|
|
20
|
+
|
|
21
|
+
self.class.new(klass, name, range, hits + other.hits, branches && (branches + other.branches))
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def -(other)
|
|
25
|
+
return self unless other
|
|
26
|
+
|
|
27
|
+
self.class.new(klass, name, range, hits - other.hits, branches && (branches - other.branches))
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def covered?
|
|
31
|
+
hits.positive? && (branches.nil? || branches.covered?)
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
35
|
+
end
|
|
36
|
+
end
|
|
37
|
+
end
|
|
@@ -0,0 +1,66 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'forwardable'
|
|
4
|
+
require 'minitest/subjective/file_result/method_statistics/method_hits'
|
|
5
|
+
require 'minitest/subjective/file_result/range'
|
|
6
|
+
require 'minitest/subjective/file_result/method_statistics/formatting'
|
|
7
|
+
|
|
8
|
+
module Minitest
|
|
9
|
+
module Subjective
|
|
10
|
+
class FileResult
|
|
11
|
+
class MethodStatistics # :nodoc:
|
|
12
|
+
include Formatting
|
|
13
|
+
|
|
14
|
+
attr_reader :method_hits
|
|
15
|
+
|
|
16
|
+
def self.from_hash(methods, branches:)
|
|
17
|
+
new(methods.collect { |key, value| MethodHits.from_pair(key, value, branches:) })
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(methods)
|
|
21
|
+
@method_hits = methods
|
|
22
|
+
end
|
|
23
|
+
|
|
24
|
+
def +(other)
|
|
25
|
+
return self unless other
|
|
26
|
+
|
|
27
|
+
self.class.new(method_hits.zip(other.method_hits).collect { |current, new| current + new })
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def -(other)
|
|
31
|
+
return self unless other
|
|
32
|
+
|
|
33
|
+
self.class.new(method_hits.zip(other.method_hits).collect { |current, new| current - new })
|
|
34
|
+
end
|
|
35
|
+
|
|
36
|
+
def find_by(**options)
|
|
37
|
+
method_hits.find { |method| options.all? { |key, value| method.send(key) == value } }
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def max_hits
|
|
41
|
+
method_hits.collect(&:hits).max
|
|
42
|
+
end
|
|
43
|
+
|
|
44
|
+
def [](index)
|
|
45
|
+
method_hits.find { _1.cover?(index) } if index
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def find_by_index(index)
|
|
49
|
+
method_hits.find { _1.starts_at?(index) } if index
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def covered?
|
|
53
|
+
method_hits.all?(&:covered?)
|
|
54
|
+
end
|
|
55
|
+
|
|
56
|
+
def ==(other)
|
|
57
|
+
other && method_hits == other.method_hits && branches == other.branches
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
protected
|
|
61
|
+
|
|
62
|
+
attr_reader :branches
|
|
63
|
+
end
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
@@ -0,0 +1,68 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/subjective/file_result/location'
|
|
4
|
+
|
|
5
|
+
module Minitest
|
|
6
|
+
module Subjective
|
|
7
|
+
class FileResult
|
|
8
|
+
Range = Struct.new(:start, :end) do
|
|
9
|
+
def self.from_array(args)
|
|
10
|
+
new(start: Location.from_array(args[0..1]), end: Location.from_array(args[2..3]))
|
|
11
|
+
end
|
|
12
|
+
|
|
13
|
+
def starts_at?(line, column = nil)
|
|
14
|
+
column ? start == Location.new(line, column) : start.line == line
|
|
15
|
+
end
|
|
16
|
+
|
|
17
|
+
def ends_at?(line, column = nil)
|
|
18
|
+
column ? self.end == Location.new(line, column) : self.end.line == line
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def cover?(line_or_range, column_or_start_column = nil, end_column = nil)
|
|
22
|
+
(covers_line?(line_or_range) && !column_or_start_column) || covers_column?(line_or_range,
|
|
23
|
+
column_or_start_column, end_column)
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def to_s
|
|
27
|
+
if single_line?
|
|
28
|
+
[start.line, [start.column, self.end.column].join('-')].join(':')
|
|
29
|
+
else
|
|
30
|
+
[start.to_s, self.end.to_s].join('-')
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
private
|
|
35
|
+
|
|
36
|
+
def covers_column?(line_or_range, column_or_start_column, _end_column = nil)
|
|
37
|
+
return single_line_covers_column?(column_or_start_column) if single_line?
|
|
38
|
+
|
|
39
|
+
multiple_lines_covers_column?(line_or_range, column_or_start_column, nil)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def single_line_covers_column?(column)
|
|
43
|
+
(start.column..self.end.column).cover?(column)
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def multiple_lines_covers_column?(line_or_range, column_or_start_column, end_column = nil)
|
|
47
|
+
case line_or_range
|
|
48
|
+
when start.line then (start.column..).cover?(column_or_start_column)
|
|
49
|
+
when start.line.next...self.end.line then true
|
|
50
|
+
when self.end.line then (1..self.end.column).cover?(end_column)
|
|
51
|
+
else false
|
|
52
|
+
end
|
|
53
|
+
end
|
|
54
|
+
|
|
55
|
+
def covers_line?(line_or_range)
|
|
56
|
+
case line_or_range
|
|
57
|
+
when Range then (start.line..self.end.line).cover?(line_or_range.start.line..line_or_range.end.line)
|
|
58
|
+
else (start.line..self.end.line).cover?(line_or_range)
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
|
|
62
|
+
def single_line?
|
|
63
|
+
start.line == self.end.line
|
|
64
|
+
end
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
end
|
|
@@ -0,0 +1,57 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/subjective/file_result/branch_statistics'
|
|
4
|
+
require 'minitest/subjective/file_result/line_statistics'
|
|
5
|
+
require 'minitest/subjective/file_result/method_statistics'
|
|
6
|
+
require 'minitest/subjective/formatter'
|
|
7
|
+
|
|
8
|
+
module Minitest
|
|
9
|
+
module Subjective
|
|
10
|
+
class FileResult # :nodoc:
|
|
11
|
+
attr_reader :path, :line_statistics, :method_statistics
|
|
12
|
+
|
|
13
|
+
def self.from_result(path, modes)
|
|
14
|
+
BranchStatistics.from_hash(modes[:branches].to_h).then do |branches|
|
|
15
|
+
new(path, line_statistics: LineStatistics.from_hash(modes[:lines].to_a, branches:),
|
|
16
|
+
method_statistics: MethodStatistics.from_hash(modes[:methods].to_h, branches:))
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def initialize(path, line_statistics:, method_statistics: nil)
|
|
21
|
+
@path = path
|
|
22
|
+
@line_statistics = line_statistics
|
|
23
|
+
@method_statistics = method_statistics
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def +(other)
|
|
27
|
+
return self unless other
|
|
28
|
+
|
|
29
|
+
self.class.new(@path, line_statistics: other ? line_statistics + other.line_statistics : line_statistics,
|
|
30
|
+
method_statistics:
|
|
31
|
+
other ? method_statistics + other.method_statistics : method_statistics)
|
|
32
|
+
end
|
|
33
|
+
|
|
34
|
+
def -(other)
|
|
35
|
+
return self unless other
|
|
36
|
+
|
|
37
|
+
self.class.new(@path, line_statistics: other ? line_statistics - other.line_statistics : line_statistics,
|
|
38
|
+
method_statistics:
|
|
39
|
+
other ? method_statistics - other.method_statistics : method_statistics)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def blank?
|
|
43
|
+
line_statistics.nil?
|
|
44
|
+
end
|
|
45
|
+
|
|
46
|
+
def to_s = Formatter.new(self).render
|
|
47
|
+
|
|
48
|
+
def covered?
|
|
49
|
+
line_statistics.covered? && method_statistics.covered?
|
|
50
|
+
end
|
|
51
|
+
|
|
52
|
+
def ==(other)
|
|
53
|
+
other && line_statistics == other.line_statistics && method_statistics == other.method_statistics
|
|
54
|
+
end
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
@@ -0,0 +1,62 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
class Formatter
|
|
6
|
+
class Colors # :nodoc:
|
|
7
|
+
CSI = "\033["
|
|
8
|
+
FINAL_BYTE = 'm'
|
|
9
|
+
CODES = {
|
|
10
|
+
clear: 0,
|
|
11
|
+
gray: 90,
|
|
12
|
+
white: 37,
|
|
13
|
+
green: 32,
|
|
14
|
+
red: 31,
|
|
15
|
+
underline: 4,
|
|
16
|
+
strikethrough: 9,
|
|
17
|
+
framed: 51,
|
|
18
|
+
encircled: 52
|
|
19
|
+
}.freeze
|
|
20
|
+
SEPARATOR = ';'
|
|
21
|
+
|
|
22
|
+
def initialize
|
|
23
|
+
@stack = []
|
|
24
|
+
end
|
|
25
|
+
|
|
26
|
+
def format(color = nil, hits: false)
|
|
27
|
+
[public_send(color_for_hits(hits) || color || :transparent), yield, clear].join
|
|
28
|
+
end
|
|
29
|
+
|
|
30
|
+
def color_for_hits(count)
|
|
31
|
+
unstash
|
|
32
|
+
|
|
33
|
+
case count
|
|
34
|
+
when nil then :gray
|
|
35
|
+
when 0 then :red
|
|
36
|
+
when (1..) then :green
|
|
37
|
+
end
|
|
38
|
+
end
|
|
39
|
+
|
|
40
|
+
def gray = push :gray
|
|
41
|
+
def green = push :green
|
|
42
|
+
def red = push :red
|
|
43
|
+
def framed = push :framed
|
|
44
|
+
def encircled = push :encircled
|
|
45
|
+
def white = push :white
|
|
46
|
+
def transparent = push :clear
|
|
47
|
+
|
|
48
|
+
def clear = pop && (current || sequence(:clear))
|
|
49
|
+
def clear_all = stash && sequence(:clear)
|
|
50
|
+
|
|
51
|
+
private
|
|
52
|
+
|
|
53
|
+
def current = @stack.last
|
|
54
|
+
def push(key) = @stack.push(sequence(key)) && current
|
|
55
|
+
def pop = @stack.pop
|
|
56
|
+
def stash = @stash = @stack.dup.tap { @stack.clear }
|
|
57
|
+
def sequence(*keys) = [CSI, CODES.values_at(*keys).join(SEPARATOR), FINAL_BYTE].join
|
|
58
|
+
def unstash = @stash&.any? && @stack = @stash.dup.tap { @stash.clear }
|
|
59
|
+
end
|
|
60
|
+
end
|
|
61
|
+
end
|
|
62
|
+
end
|
|
@@ -0,0 +1,34 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/subjective/formatter/colors'
|
|
4
|
+
|
|
5
|
+
module Minitest
|
|
6
|
+
module Subjective
|
|
7
|
+
class Formatter # :nodoc:
|
|
8
|
+
attr_reader :colors
|
|
9
|
+
|
|
10
|
+
def initialize(result)
|
|
11
|
+
@result = result
|
|
12
|
+
@colors = Colors.new
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def render
|
|
16
|
+
formatted_lines.join("\n")
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
private
|
|
20
|
+
|
|
21
|
+
attr_reader :result
|
|
22
|
+
|
|
23
|
+
def lines = File.readlines(result.path, chomp: true)
|
|
24
|
+
|
|
25
|
+
def formatted_lines
|
|
26
|
+
result.line_statistics.format(self, lines) do |line, number|
|
|
27
|
+
result.method_statistics.format(self, line, number) do |branch_statistics|
|
|
28
|
+
branch_statistics.format(self, line, number)
|
|
29
|
+
end
|
|
30
|
+
end
|
|
31
|
+
end
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
end
|
|
@@ -0,0 +1,59 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/subjective/case_inquirer'
|
|
4
|
+
|
|
5
|
+
module Minitest
|
|
6
|
+
module Subjective
|
|
7
|
+
class Reporter < Minitest::Reporter # :nodoc:
|
|
8
|
+
attr_accessor :results
|
|
9
|
+
|
|
10
|
+
def initialize(io = $stdout, options = {})
|
|
11
|
+
super
|
|
12
|
+
self.results = {}
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
def record(result)
|
|
16
|
+
merge_result(result)
|
|
17
|
+
end
|
|
18
|
+
|
|
19
|
+
def report
|
|
20
|
+
results.each do |subject_name, result|
|
|
21
|
+
io.puts "Coverage for #{subject_name}:"
|
|
22
|
+
io.puts coverage_headline_for(result)
|
|
23
|
+
io.puts result.to_s unless result.covered?
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
|
|
27
|
+
private
|
|
28
|
+
|
|
29
|
+
def coverage_headline_for(result)
|
|
30
|
+
colors.format result.covered? ? :green : :red do
|
|
31
|
+
result.covered? ? 'All covered!' : 'Coverage missing!'
|
|
32
|
+
end
|
|
33
|
+
end
|
|
34
|
+
|
|
35
|
+
def colors
|
|
36
|
+
@colors ||= Formatter::Colors.new
|
|
37
|
+
end
|
|
38
|
+
|
|
39
|
+
def merge_result(result)
|
|
40
|
+
with_subject_name_for(result) do |subject_name|
|
|
41
|
+
results[subject_name] ||= Subjective.load_results[subject_name] || result.load_result
|
|
42
|
+
results[subject_name] = if results[subject_name]
|
|
43
|
+
results[subject_name] + result.coverage_result
|
|
44
|
+
else
|
|
45
|
+
result.coverage_result
|
|
46
|
+
end
|
|
47
|
+
end
|
|
48
|
+
end
|
|
49
|
+
|
|
50
|
+
def with_subject_name_for(result)
|
|
51
|
+
CaseInquirer.new(result.klass).tap do |inquirer|
|
|
52
|
+
next unless inquirer.subject_file
|
|
53
|
+
|
|
54
|
+
yield inquirer.subject_name
|
|
55
|
+
end
|
|
56
|
+
end
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
end
|
|
@@ -0,0 +1,26 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
module ResultExtensions # :nodoc: all
|
|
6
|
+
module ClassMethods
|
|
7
|
+
def from(runnable)
|
|
8
|
+
super.tap do |output|
|
|
9
|
+
output.load_result = Subjective.load_result_for(runnable.class)
|
|
10
|
+
output.coverage_result = Subjective.coverage_for(runnable.class)
|
|
11
|
+
end
|
|
12
|
+
end
|
|
13
|
+
end
|
|
14
|
+
|
|
15
|
+
attr_accessor :load_result, :coverage_result
|
|
16
|
+
|
|
17
|
+
def self.prepended(other)
|
|
18
|
+
other.singleton_class.prepend ClassMethods
|
|
19
|
+
end
|
|
20
|
+
|
|
21
|
+
def self.prepend_target
|
|
22
|
+
Result.prepend self
|
|
23
|
+
end
|
|
24
|
+
end
|
|
25
|
+
end
|
|
26
|
+
end
|
|
@@ -0,0 +1,17 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
module Minitest
|
|
4
|
+
module Subjective
|
|
5
|
+
module TestExtensions # :nodoc:
|
|
6
|
+
def self.prepend_target
|
|
7
|
+
Test.prepend self
|
|
8
|
+
end
|
|
9
|
+
|
|
10
|
+
def run(*)
|
|
11
|
+
Subjective.record_load_for(self.class)
|
|
12
|
+
Subjective.record_baseline_for(self.class)
|
|
13
|
+
super
|
|
14
|
+
end
|
|
15
|
+
end
|
|
16
|
+
end
|
|
17
|
+
end
|
|
@@ -0,0 +1,70 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require_relative 'subjective/version'
|
|
4
|
+
require 'coverage'
|
|
5
|
+
require 'forwardable'
|
|
6
|
+
require 'minitest'
|
|
7
|
+
require 'minitest/subjective/file_result'
|
|
8
|
+
require 'minitest/subjective_plugin'
|
|
9
|
+
|
|
10
|
+
module Minitest # :nodoc:
|
|
11
|
+
# = \Subjective
|
|
12
|
+
module Subjective
|
|
13
|
+
class << self
|
|
14
|
+
private
|
|
15
|
+
|
|
16
|
+
def coverage
|
|
17
|
+
Coverage
|
|
18
|
+
end
|
|
19
|
+
|
|
20
|
+
def file_result_for(path)
|
|
21
|
+
FileResult.from_result(path, coverage.peek_result[path].to_h)
|
|
22
|
+
end
|
|
23
|
+
end
|
|
24
|
+
|
|
25
|
+
def self.cattr_accessor(name) # :nodoc:
|
|
26
|
+
(class << self; self; end).attr_accessor name
|
|
27
|
+
end
|
|
28
|
+
|
|
29
|
+
cattr_accessor :load_results
|
|
30
|
+
cattr_accessor :baselines
|
|
31
|
+
@load_results = {}
|
|
32
|
+
@baselines = {}
|
|
33
|
+
|
|
34
|
+
def self.start_coverage
|
|
35
|
+
coverage.start(:all) unless coverage.running?
|
|
36
|
+
end
|
|
37
|
+
|
|
38
|
+
def self.record_autoload_for(klass, path = nil)
|
|
39
|
+
load_results[klass] ||= file_result_for(path)
|
|
40
|
+
end
|
|
41
|
+
|
|
42
|
+
def self.record_load_for(klass, _path = nil)
|
|
43
|
+
CaseInquirer.new(klass).tap do |inquirer|
|
|
44
|
+
load_results[inquirer.subject_name] ||= file_result_for(inquirer.subject_file)
|
|
45
|
+
end
|
|
46
|
+
end
|
|
47
|
+
|
|
48
|
+
def self.record_baseline_for(klass)
|
|
49
|
+
CaseInquirer.new(klass).tap do |inquirer|
|
|
50
|
+
baselines[inquirer.subject_name] = file_result_for(inquirer.subject_file)
|
|
51
|
+
end
|
|
52
|
+
end
|
|
53
|
+
|
|
54
|
+
def self.load_result_for(klass)
|
|
55
|
+
CaseInquirer.new(klass).then do |inquirer|
|
|
56
|
+
load_results[inquirer.subject_name]
|
|
57
|
+
end
|
|
58
|
+
end
|
|
59
|
+
|
|
60
|
+
def self.coverage_for(klass)
|
|
61
|
+
CaseInquirer.new(klass).then do |inquirer|
|
|
62
|
+
next unless coverage.running?
|
|
63
|
+
|
|
64
|
+
file_result_for(inquirer.subject_file) - baselines[inquirer.subject_name]
|
|
65
|
+
end
|
|
66
|
+
end
|
|
67
|
+
end
|
|
68
|
+
|
|
69
|
+
load :subjective if respond_to?(:load)
|
|
70
|
+
end
|
|
@@ -0,0 +1,37 @@
|
|
|
1
|
+
# frozen_string_literal: true
|
|
2
|
+
|
|
3
|
+
require 'minitest/subjective/reporter'
|
|
4
|
+
require 'minitest/subjective/result_extensions'
|
|
5
|
+
require 'minitest/subjective/test_extensions'
|
|
6
|
+
|
|
7
|
+
module Minitest # :nodoc:
|
|
8
|
+
class << self
|
|
9
|
+
private
|
|
10
|
+
|
|
11
|
+
def add_zeitwerk_hooks
|
|
12
|
+
return unless defined? ::Zeitwerk
|
|
13
|
+
|
|
14
|
+
::Zeitwerk::Registry.loaders.each do |loader|
|
|
15
|
+
loader.on_load do |cpath, _value, abspath|
|
|
16
|
+
Subjective.record_autoload_for(cpath, abspath)
|
|
17
|
+
end
|
|
18
|
+
end
|
|
19
|
+
end
|
|
20
|
+
end
|
|
21
|
+
|
|
22
|
+
def self.plugin_subjective_options(opts, options)
|
|
23
|
+
opts.on '--subjective', 'Collect focused coverage for the test subjects.' do
|
|
24
|
+
options[:subjective] ||= {}
|
|
25
|
+
end
|
|
26
|
+
end
|
|
27
|
+
|
|
28
|
+
def self.plugin_subjective_init(options)
|
|
29
|
+
return unless options[:subjective] || ENV['MINITEST_SUBJECTIVE']
|
|
30
|
+
|
|
31
|
+
add_zeitwerk_hooks
|
|
32
|
+
Subjective.start_coverage
|
|
33
|
+
Subjective::ResultExtensions.prepend_target
|
|
34
|
+
Subjective::TestExtensions.prepend_target
|
|
35
|
+
reporter << Subjective::Reporter.new
|
|
36
|
+
end
|
|
37
|
+
end
|
metadata
ADDED
|
@@ -0,0 +1,90 @@
|
|
|
1
|
+
--- !ruby/object:Gem::Specification
|
|
2
|
+
name: minitest-subjective
|
|
3
|
+
version: !ruby/object:Gem::Version
|
|
4
|
+
version: 0.1.0.pre.alpha
|
|
5
|
+
platform: ruby
|
|
6
|
+
authors:
|
|
7
|
+
- Justin Malčić
|
|
8
|
+
bindir: exe
|
|
9
|
+
cert_chain: []
|
|
10
|
+
date: 1980-01-02 00:00:00.000000000 Z
|
|
11
|
+
dependencies:
|
|
12
|
+
- !ruby/object:Gem::Dependency
|
|
13
|
+
name: minitest
|
|
14
|
+
requirement: !ruby/object:Gem::Requirement
|
|
15
|
+
requirements:
|
|
16
|
+
- - ">="
|
|
17
|
+
- !ruby/object:Gem::Version
|
|
18
|
+
version: '5.0'
|
|
19
|
+
type: :runtime
|
|
20
|
+
prerelease: false
|
|
21
|
+
version_requirements: !ruby/object:Gem::Requirement
|
|
22
|
+
requirements:
|
|
23
|
+
- - ">="
|
|
24
|
+
- !ruby/object:Gem::Version
|
|
25
|
+
version: '5.0'
|
|
26
|
+
description: Test coverage with live awareness of what you're actually trying to test.
|
|
27
|
+
email:
|
|
28
|
+
- j.malcic@me.com
|
|
29
|
+
executables: []
|
|
30
|
+
extensions: []
|
|
31
|
+
extra_rdoc_files: []
|
|
32
|
+
files:
|
|
33
|
+
- ".idea/.gitignore"
|
|
34
|
+
- ".idea/minitest-subjective.iml"
|
|
35
|
+
- ".idea/misc.xml"
|
|
36
|
+
- ".idea/modules.xml"
|
|
37
|
+
- ".idea/vcs.xml"
|
|
38
|
+
- ".rubocop.yml"
|
|
39
|
+
- ".ruby-version"
|
|
40
|
+
- LICENSE.txt
|
|
41
|
+
- README.md
|
|
42
|
+
- Rakefile
|
|
43
|
+
- lib/minitest/subjective.rb
|
|
44
|
+
- lib/minitest/subjective/case_inquirer.rb
|
|
45
|
+
- lib/minitest/subjective/file_result.rb
|
|
46
|
+
- lib/minitest/subjective/file_result/branch_statistics.rb
|
|
47
|
+
- lib/minitest/subjective/file_result/branch_statistics/branch_hits.rb
|
|
48
|
+
- lib/minitest/subjective/file_result/branch_statistics/conditional_hits.rb
|
|
49
|
+
- lib/minitest/subjective/file_result/branch_statistics/formatting.rb
|
|
50
|
+
- lib/minitest/subjective/file_result/line_statistics.rb
|
|
51
|
+
- lib/minitest/subjective/file_result/line_statistics/formatting.rb
|
|
52
|
+
- lib/minitest/subjective/file_result/line_statistics/line_hits.rb
|
|
53
|
+
- lib/minitest/subjective/file_result/location.rb
|
|
54
|
+
- lib/minitest/subjective/file_result/method_statistics.rb
|
|
55
|
+
- lib/minitest/subjective/file_result/method_statistics/formatting.rb
|
|
56
|
+
- lib/minitest/subjective/file_result/method_statistics/method_hits.rb
|
|
57
|
+
- lib/minitest/subjective/file_result/range.rb
|
|
58
|
+
- lib/minitest/subjective/formatter.rb
|
|
59
|
+
- lib/minitest/subjective/formatter/colors.rb
|
|
60
|
+
- lib/minitest/subjective/reporter.rb
|
|
61
|
+
- lib/minitest/subjective/result_extensions.rb
|
|
62
|
+
- lib/minitest/subjective/test_extensions.rb
|
|
63
|
+
- lib/minitest/subjective/version.rb
|
|
64
|
+
- lib/minitest/subjective_plugin.rb
|
|
65
|
+
homepage: https://buildingatlas.github.io/minitest-subjective
|
|
66
|
+
licenses:
|
|
67
|
+
- MIT
|
|
68
|
+
metadata:
|
|
69
|
+
homepage_uri: https://buildingatlas.github.io/minitest-subjective
|
|
70
|
+
source_code_uri: https://github.com/BuildingAtlas/minitest-subjective
|
|
71
|
+
changelog_uri: https://github.com/BuildingAtlas/minitest-subjective/tree/main/CHANGELOG.md
|
|
72
|
+
rubygems_mfa_required: 'true'
|
|
73
|
+
rdoc_options: []
|
|
74
|
+
require_paths:
|
|
75
|
+
- lib
|
|
76
|
+
required_ruby_version: !ruby/object:Gem::Requirement
|
|
77
|
+
requirements:
|
|
78
|
+
- - ">="
|
|
79
|
+
- !ruby/object:Gem::Version
|
|
80
|
+
version: 3.1.0
|
|
81
|
+
required_rubygems_version: !ruby/object:Gem::Requirement
|
|
82
|
+
requirements:
|
|
83
|
+
- - ">="
|
|
84
|
+
- !ruby/object:Gem::Version
|
|
85
|
+
version: '0'
|
|
86
|
+
requirements: []
|
|
87
|
+
rubygems_version: 4.0.10
|
|
88
|
+
specification_version: 4
|
|
89
|
+
summary: Test-subject focused coverage for Minitest
|
|
90
|
+
test_files: []
|