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.
Files changed (34) hide show
  1. checksums.yaml +7 -0
  2. data/.idea/.gitignore +8 -0
  3. data/.idea/minitest-subjective.iml +50 -0
  4. data/.idea/misc.xml +4 -0
  5. data/.idea/modules.xml +8 -0
  6. data/.idea/vcs.xml +6 -0
  7. data/.rubocop.yml +7 -0
  8. data/.ruby-version +1 -0
  9. data/LICENSE.txt +21 -0
  10. data/README.md +71 -0
  11. data/Rakefile +12 -0
  12. data/lib/minitest/subjective/case_inquirer.rb +58 -0
  13. data/lib/minitest/subjective/file_result/branch_statistics/branch_hits.rb +42 -0
  14. data/lib/minitest/subjective/file_result/branch_statistics/conditional_hits.rb +42 -0
  15. data/lib/minitest/subjective/file_result/branch_statistics/formatting.rb +84 -0
  16. data/lib/minitest/subjective/file_result/branch_statistics.rb +55 -0
  17. data/lib/minitest/subjective/file_result/line_statistics/formatting.rb +35 -0
  18. data/lib/minitest/subjective/file_result/line_statistics/line_hits.rb +31 -0
  19. data/lib/minitest/subjective/file_result/line_statistics.rb +57 -0
  20. data/lib/minitest/subjective/file_result/location.rb +17 -0
  21. data/lib/minitest/subjective/file_result/method_statistics/formatting.rb +29 -0
  22. data/lib/minitest/subjective/file_result/method_statistics/method_hits.rb +37 -0
  23. data/lib/minitest/subjective/file_result/method_statistics.rb +66 -0
  24. data/lib/minitest/subjective/file_result/range.rb +68 -0
  25. data/lib/minitest/subjective/file_result.rb +57 -0
  26. data/lib/minitest/subjective/formatter/colors.rb +62 -0
  27. data/lib/minitest/subjective/formatter.rb +34 -0
  28. data/lib/minitest/subjective/reporter.rb +59 -0
  29. data/lib/minitest/subjective/result_extensions.rb +26 -0
  30. data/lib/minitest/subjective/test_extensions.rb +17 -0
  31. data/lib/minitest/subjective/version.rb +7 -0
  32. data/lib/minitest/subjective.rb +70 -0
  33. data/lib/minitest/subjective_plugin.rb +37 -0
  34. 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,8 @@
1
+ # Default ignored files
2
+ /shelf/
3
+ /workspace.xml
4
+ # Editor-based HTTP Client requests
5
+ /httpRequests/
6
+ # Datasource local storage ignored files
7
+ /dataSources/
8
+ /dataSources.local.xml
@@ -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
@@ -0,0 +1,4 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="ProjectRootManager" version="2" project-jdk-name="rbenv: 3.3.1" project-jdk-type="RUBY_SDK" />
4
+ </project>
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
@@ -0,0 +1,6 @@
1
+ <?xml version="1.0" encoding="UTF-8"?>
2
+ <project version="4">
3
+ <component name="VcsDirectoryMappings">
4
+ <mapping directory="$PROJECT_DIR$" vcs="Git" />
5
+ </component>
6
+ </project>
data/.rubocop.yml ADDED
@@ -0,0 +1,7 @@
1
+ plugins:
2
+ - rubocop-performance
3
+ - rubocop-minitest
4
+ - rubocop-rake
5
+
6
+ AllCops:
7
+ NewCops: enable
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,12 @@
1
+ # frozen_string_literal: true
2
+
3
+ require 'bundler/gem_tasks'
4
+ require 'minitest/test_task'
5
+
6
+ Minitest::TestTask.create
7
+
8
+ require 'rubocop/rake_task'
9
+
10
+ RuboCop::RakeTask.new
11
+
12
+ task default: %i[test rubocop]
@@ -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,7 @@
1
+ # frozen_string_literal: true
2
+
3
+ module Minitest
4
+ module Subjective
5
+ VERSION = '0.1.0-alpha'
6
+ end
7
+ 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: []