rspec-cover_it 0.0.7 → 0.1.0
This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
- checksums.yaml +4 -4
- data/.github/workflows/linters.yml +34 -0
- data/.github/workflows/rspec.yml +33 -0
- data/.mdl_rules.rb +2 -0
- data/.mdlrc +2 -0
- data/.quiet_quality.yml +8 -0
- data/.rspec +1 -0
- data/.rubocop.yml +21 -0
- data/README.md +110 -132
- data/lib/rspec/cover_it/context.rb +2 -5
- data/lib/rspec/cover_it/context_coverage.rb +46 -18
- data/lib/rspec/cover_it/coverage_state.rb +11 -22
- data/lib/rspec/cover_it/version.rb +1 -1
- data/lib/rspec/cover_it.rb +11 -1
- data/rspec-cover_it.gemspec +2 -0
- metadata +37 -2
checksums.yaml
CHANGED
@@ -1,7 +1,7 @@
|
|
1
1
|
---
|
2
2
|
SHA256:
|
3
|
-
metadata.gz:
|
4
|
-
data.tar.gz:
|
3
|
+
metadata.gz: e0d27633b32f88f6dcc94af45b0ecd536b785c6229e3808ef0a1b95058f20b62
|
4
|
+
data.tar.gz: 0ade00b1ddfa4db191e9d927f6361bfcf8de243b543f2592ecbefd4a0b7f84de
|
5
5
|
SHA512:
|
6
|
-
metadata.gz:
|
7
|
-
data.tar.gz:
|
6
|
+
metadata.gz: e29cce1280f10140900b51f384b0e2e73a98e8b4fc1e79caf170e71e9c3cf64fac01fdcd5560472580c9544cde5169722db18508bfe119164e2f1dfe11dda9ba
|
7
|
+
data.tar.gz: 8895302c10ac12dbab2e71292e47f055067337d415650f8cd28671180d5690c1ea8ed59c4665cdc99ee9776af29e1372533b862cf73d2058fea3663c722c3525
|
@@ -0,0 +1,34 @@
|
|
1
|
+
name: Linters
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
StandardRB:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
steps:
|
9
|
+
- uses: actions/checkout@v3
|
10
|
+
|
11
|
+
- name: Set up ruby
|
12
|
+
uses: ruby/setup-ruby@v1
|
13
|
+
with:
|
14
|
+
ruby-version: 3.1
|
15
|
+
|
16
|
+
- name: Cache gems
|
17
|
+
uses: actions/cache@v3
|
18
|
+
with:
|
19
|
+
path: vendor/bundle
|
20
|
+
key: ${{ runner.os }}-linters-${{ hashFiles('Gemfile.lock') }}
|
21
|
+
restore-keys:
|
22
|
+
${{ runner.os }}-linters-
|
23
|
+
|
24
|
+
- name: Install gems
|
25
|
+
run: bundle install --jobs 4 --retry 3
|
26
|
+
|
27
|
+
- name: Run standard
|
28
|
+
run: bundle exec standardrb
|
29
|
+
|
30
|
+
- name: Run rubocop (complexity checks)
|
31
|
+
run: bundle exec rubocop --parallel
|
32
|
+
|
33
|
+
- name: Run markdownlint
|
34
|
+
run: bundle exec mdl .
|
@@ -0,0 +1,33 @@
|
|
1
|
+
name: RSpec
|
2
|
+
|
3
|
+
on: [push]
|
4
|
+
|
5
|
+
jobs:
|
6
|
+
RSpec:
|
7
|
+
runs-on: ubuntu-latest
|
8
|
+
strategy:
|
9
|
+
fail-fast: false
|
10
|
+
matrix:
|
11
|
+
ruby-version: ['2.7', '3.0', '3.1', '3.2', 'head']
|
12
|
+
|
13
|
+
steps:
|
14
|
+
- uses: actions/checkout@v3
|
15
|
+
|
16
|
+
- name: Set up ruby
|
17
|
+
uses: ruby/setup-ruby@v1
|
18
|
+
with:
|
19
|
+
ruby-version: ${{ matrix.ruby-version }}
|
20
|
+
|
21
|
+
- name: Cache gems
|
22
|
+
uses: actions/cache@v3
|
23
|
+
with:
|
24
|
+
path: vendor/bundle
|
25
|
+
key: ${{ runner.os }}-rspec-${{ matrix.ruby-version }}-${{ hashFiles('Gemfile.lock') }}
|
26
|
+
restore-keys:
|
27
|
+
${{ runner.os }}-rspec-${{ matrix.ruby-version }}-
|
28
|
+
|
29
|
+
- name: Install gems
|
30
|
+
run: bundle install --jobs 4 --retry 3
|
31
|
+
|
32
|
+
- name: Run RSpec
|
33
|
+
run: SIMPLECOV=true SIMPLECOV_TEXT=true bundle exec rspec
|
data/.mdl_rules.rb
ADDED
data/.mdlrc
ADDED
data/.quiet_quality.yml
ADDED
data/.rspec
ADDED
@@ -0,0 +1 @@
|
|
1
|
+
--require spec_helper
|
data/.rubocop.yml
ADDED
@@ -0,0 +1,21 @@
|
|
1
|
+
---
|
2
|
+
AllCops:
|
3
|
+
SuggestExtensions: false
|
4
|
+
DisabledByDefault: true
|
5
|
+
|
6
|
+
Metrics/AbcSize:
|
7
|
+
Max: 15
|
8
|
+
Metrics/CyclomaticComplexity:
|
9
|
+
Max: 8
|
10
|
+
Metrics/PerceivedComplexity:
|
11
|
+
Max: 7
|
12
|
+
|
13
|
+
Metrics/ClassLength:
|
14
|
+
CountComments: false
|
15
|
+
Max: 150
|
16
|
+
Metrics/MethodLength:
|
17
|
+
CountComments: false
|
18
|
+
Max: 15
|
19
|
+
Metrics/ParameterLists:
|
20
|
+
Max: 5
|
21
|
+
CountKeywordArgs: true
|
data/README.md
CHANGED
@@ -1,138 +1,116 @@
|
|
1
1
|
# RSpec::CoverIt
|
2
2
|
|
3
|
-
|
4
|
-
|
5
|
-
|
6
|
-
|
7
|
-
|
8
|
-
|
9
|
-
|
10
|
-
|
11
|
-
|
12
|
-
|
13
|
-
|
14
|
-
|
15
|
-
|
16
|
-
|
17
|
-
|
18
|
-
|
19
|
-
|
20
|
-
|
21
|
-
coverage
|
22
|
-
|
23
|
-
|
24
|
-
|
25
|
-
|
26
|
-
|
27
|
-
|
28
|
-
|
29
|
-
|
30
|
-
|
31
|
-
|
32
|
-
|
33
|
-
and then `RSpec::CoverIt.setup(**config_options)` (before loading your code, as
|
34
|
-
you would with any other coverage library). It's.. _semi_ compatible with other
|
35
|
-
coverage systems - it only starts Coverage if it's not already running, and it
|
36
|
-
only uses `peek_result`, so it doesn't affect other systems outcomes.
|
37
|
-
Rough configuration options:
|
38
|
-
|
39
|
-
* `filter`: Only paths starting with this (matching this regex?) can matter
|
40
|
-
* `autoenforce`: off by default - with this disabled, you turn on coverage
|
41
|
-
enforcement for a given top-level describe ExampleGroup by adding metadata
|
42
|
-
like `cover_it: true` or `cover_it: 95`. If it's enabled though, you instead
|
43
|
-
can turn enforcement _off_ for an example group by setting `cover_it: false`.
|
44
|
-
* `global_threshold`: 100% by default. This whole "90% coverage is the right
|
45
|
-
target" thing is mostly a side-effect of the way we check the entire project
|
46
|
-
under one number.. but it's an easy setting to support, and I'm sure people
|
47
|
-
will disagree with me.
|
48
|
-
|
49
|
-
## Example Group Metadata
|
50
|
-
|
51
|
-
In example groups, you can use metadata to control the behavior of
|
52
|
-
`rspec-cover_it`. These keys have meaning:
|
53
|
-
|
54
|
-
* `cover_it`: if boolean, it enables or disables the coverage enforcement for
|
55
|
-
this ExampleGroup. If numeric, it's enabling, and specifying the coverage
|
56
|
-
threshold at the same time (as a percentage - `cover_it: 95` requires 95%
|
57
|
-
coverage of the target class by this example group).
|
58
|
-
* `covers_path`: The path (relative to the spec file!) of the code the spec is
|
59
|
-
intending to cover. Later, this can be an array of paths, for the multi-spec
|
60
|
-
case `covers` is intended for as well. This is an annoying work-around for
|
61
|
-
the fact that we can't perfectly infer the location of the source code in
|
62
|
-
some cases - in particular, `lib/foo/version.rb` tends to cause a problem
|
63
|
-
for specs on `foo.rb`, since the version file is invariably loaded first.
|
64
|
-
Note - in gems, this _frequently_ also happens when you glob-load a directory
|
65
|
-
_before_ defining the namespace they are all loading objects into. Then the
|
66
|
-
first file in that directory that loads ends up being the one that actually
|
67
|
-
creates the namespace.
|
68
|
-
* `covers`: An array of classes and modules that this example groups _thinks
|
69
|
-
it is completely testing_. Ideally, you'd have a separate test file for each,
|
70
|
-
but sometimes that's hard to do - you can let one spec claim responsibility
|
71
|
-
for multiple classes and modules (such as Concerns) using this. Be default
|
72
|
-
it is just `[described_class]`. Additionally, if your top-level example
|
73
|
-
group _does not describe a Class or Module_, you may use `covers` to let it
|
74
|
-
invoke `rspec-cover_it` anyway - some people `describe "a descriptive string"`
|
75
|
-
instead of `describe AClass`, and .. fine.
|
76
|
-
|
77
|
-
## Implementation
|
78
|
-
|
79
|
-
We record the coverage information in a `before(:suite)` rspec hook using
|
80
|
-
`Coverage.peek_result`, and hold onto that information. Then before and after
|
81
|
-
each 'context' (which really amounts to 'each spec file'), we grab the coverage
|
82
|
-
information again.
|
83
|
-
|
84
|
-
We use `Object.const_source_location` to find the file that defines the
|
85
|
-
`described_class`, and _that_ is what is being assessed for coverage. This
|
86
|
-
means that, if you are reopening the class somewhere else, that coverage won't
|
87
|
-
be checked; if you are including 15 Concerns, and don't intend to write separate
|
88
|
-
specs for them, be sure to list them as `covers:` metadata on the test. Also,
|
89
|
-
shame!
|
90
|
-
|
91
|
-
## Output
|
92
|
-
|
93
|
-
When there's missing coverage on the class under test, you'll currently see
|
94
|
-
output like this:
|
3
|
+
The standard approach to monitoring "test coverage" in ruby is a tool called
|
4
|
+
[SimpleCov](https://github.com/simplecov-ruby/simplecov), and a variety of
|
5
|
+
systems made to hook into its output, to display a single number that represents
|
6
|
+
how much of your _total code_ is executed during your test suite. You set up
|
7
|
+
SimpleCov, and then you commit your team to maintaining whatever test coverage
|
8
|
+
number you already have achieved globally, or to improving that coverage number.
|
9
|
+
If you have one of the better tools, you can commit to things like "all merged
|
10
|
+
PRs should have 100% (or 95%, etc) coverage on all of their touched code."
|
11
|
+
|
12
|
+
That's.. better than nothing. A lot better - it can especially tell you when you
|
13
|
+
forgot to write tests for some component, or when you're adjusting a class that
|
14
|
+
doesn't have any tests written for it. But it's _not great_, not compared to a
|
15
|
+
system like RSpec that lets you specify with _granularity_ how things should
|
16
|
+
behave. The problem with SimpleCov is that it's _holistic_ - you can't adopt
|
17
|
+
simplecov one class at a time, or enforce full coverage on new code _in your
|
18
|
+
CI system_, you need external tooling with state persistence for that.
|
19
|
+
|
20
|
+
`RSpec::CoverIt` takes a different approach, somewhat inspired by the
|
21
|
+
[rspec-coverage](https://github.com/jamesdabbs/rspec-coverage) gem (which was
|
22
|
+
itself apparently inspired by a rubyconf-2016 talk by Ryan Davis). With CoverIt,
|
23
|
+
coverage-enforcement happens as part of your test-suite, enforced by rspec
|
24
|
+
either globally or as specified.
|
25
|
+
|
26
|
+
## Installation and Setup
|
27
|
+
|
28
|
+
Add the gem to your Gemfile or gemspec just like `simplecov` or `rspec`. Then
|
29
|
+
in your `spec_helper.rb`, _before_ you require your application or gem code,
|
30
|
+
require `rspec/cover_it`, and then invoke `RSpec::CoverIt.setup`, with the
|
31
|
+
appropriate options to configure it (described further down). A reasonable
|
32
|
+
initial setup might look like this:
|
95
33
|
|
96
34
|
```
|
97
|
-
|
98
|
-
|
99
|
-
|
100
|
-
|
101
|
-
|
102
|
-
Failure/Error: fail(MissingCoverage, message)
|
103
|
-
Missing coverage in /Users/emueller/src/quiet_quality/lib/quiet_quality/message.rb on line 7
|
104
|
-
# /Users/emueller/src/rspec-cover_it/lib/rspec/cover_it/context_coverage.rb:40:in `enforce!'
|
105
|
-
# /Users/emueller/src/rspec-cover_it/lib/rspec/cover_it/coverage_state.rb:37:in `block in finish_tracking_for'
|
106
|
-
# /Users/emueller/src/rspec-cover_it/lib/rspec/cover_it/coverage_state.rb:35:in `finish_tracking_for'
|
107
|
-
# /Users/emueller/src/rspec-cover_it/lib/rspec/cover_it.rb:26:in `block (2 levels) in setup'
|
108
|
-
.....................................................................................................................................................................................................................................................................................................
|
109
|
-
|
110
|
-
Finished in 1.06 seconds (files took 0.28925 seconds to load)
|
111
|
-
852 examples, 0 failures, 1 error occurred outside of examples
|
35
|
+
require "rspec/cover_it"
|
36
|
+
project_root = File.expand_path("../..", __FILE__)
|
37
|
+
RSpec::CoverIt.setup(filter: project_root, autoenforce: true)
|
38
|
+
|
39
|
+
require File.expand_path("../../lib/my_gem", __FILE__)
|
112
40
|
```
|
113
41
|
|
114
|
-
##
|
115
|
-
|
116
|
-
|
117
|
-
|
118
|
-
|
119
|
-
|
120
|
-
|
121
|
-
|
122
|
-
|
123
|
-
|
124
|
-
|
125
|
-
|
126
|
-
|
127
|
-
|
128
|
-
|
129
|
-
|
130
|
-
|
131
|
-
|
132
|
-
|
133
|
-
|
134
|
-
|
135
|
-
|
136
|
-
|
137
|
-
|
138
|
-
|
42
|
+
## Configuration and Usage
|
43
|
+
|
44
|
+
When invoking `RSpec::CoverIt.setup`, you may supply these options:
|
45
|
+
|
46
|
+
* `filter`: Don't track coverage information about files that don't start with
|
47
|
+
this prefix - this is largely a performance optimization, allowing the pretest
|
48
|
+
coverage tracking to track information only about the current project, and not
|
49
|
+
the various gems it might depend on.
|
50
|
+
* `autoenforce`: This is off by default, which means that coverage-enforcement
|
51
|
+
will only be applied to top-level example groups that _request_ it, by
|
52
|
+
supplying the `cover_it: true` spec metaddata. If you configure `autoenforce`
|
53
|
+
to be `true`, then all specs will attempt to enforce coverage, as long as they
|
54
|
+
can figure out their targets (unless they are told not to).
|
55
|
+
|
56
|
+
When setting up a test file, you can configure some additional details for that
|
57
|
+
example group - `CoverIt` is aware of the following bits of spec metadata:
|
58
|
+
|
59
|
+
* `cover_it`: If supplied with a truthy value, it activates coverage enforcement
|
60
|
+
for this example-group - if supplied with a falsey value, it _deactivates_
|
61
|
+
enforcement. If supplied with a _numeric_ value, it is treated as a percentage,
|
62
|
+
and used to configure the target coverage threshold for the class' definition,
|
63
|
+
a feature which I intend not to use, but I expect some people to care deeply
|
64
|
+
about.
|
65
|
+
* `covers_path`: in certain cases, a class or module may be "defined" in several
|
66
|
+
locations. While actually enforcing coverage for multiple code files from one
|
67
|
+
test file isn't yet implemented, if `rspec-cover_it` infers the location of
|
68
|
+
the code under test _incorrectly_, you can tell it where to actually enforce
|
69
|
+
coverage against by supplying a path here (relative to the directory
|
70
|
+
containing the test file).
|
71
|
+
|
72
|
+
## Implementation Approach
|
73
|
+
|
74
|
+
When setting up the tool, we activate the built-in ruby Coverage system (we
|
75
|
+
start it in legacy-supporting mode, but we are compatible with it having already
|
76
|
+
been started in the more modern mode as well). Then we register three rspec
|
77
|
+
hooks:
|
78
|
+
|
79
|
+
* Before the suite _starts_ (which should be after everything is loaded), we
|
80
|
+
record the coverage _so far_ - this is the 'initialization' coverage, which
|
81
|
+
mostly includes the lines that run during class evaluation.
|
82
|
+
* Before each 'context' (spec file), we record the coverage information
|
83
|
+
* After each 'context' we record the coverage information _again_. Then we
|
84
|
+
subtract the two sets of coverage, which tells us how many times each line
|
85
|
+
was run _during this example group_, and add it to the 'initialization'
|
86
|
+
coverage to see the effective coverage for just this test file.
|
87
|
+
|
88
|
+
Then, for each test file, we use ruby's source-introspection system to tell
|
89
|
+
us where the "described class" was defined, and check what fraction of that
|
90
|
+
source file is effectively covered by this example group. If there is some
|
91
|
+
missing coverage, we raise an exception explaining the missing coverage.
|
92
|
+
|
93
|
+
But of course, if you only run part of the tests in a spec file (perhaps by
|
94
|
+
specifying a line number, or a description filter with `-e`), it probably won't
|
95
|
+
be fully covered. We don't want to fail the test suite every time you're working
|
96
|
+
on the specs for something, so we reach into RSpec a little to check if the
|
97
|
+
set of _filtered_ examples (the ones being run) match the _full_ set of
|
98
|
+
examples. If they don't, don't bother checking coverage for this test file.
|
99
|
+
|
100
|
+
## Caveats and Shortcomings
|
101
|
+
|
102
|
+
For the moment, there is no support for _autoloading_, which makes this library
|
103
|
+
awkward to use for the majority of large rails applications. I intend to resolve
|
104
|
+
this issue soon, but it's not a trivial one, as coverage-tracking on a per-spec
|
105
|
+
basis relies on being able to separate coverage that happens during code-loading
|
106
|
+
from coverage that happens during "tests other than this spec which have already
|
107
|
+
occurred" - we'll need to add some kind of autoloading hooks to support it
|
108
|
+
properly.
|
109
|
+
|
110
|
+
Until then, the gem will only be useful to a rails application in an eager-
|
111
|
+
loaded context - that's not a major issue in CI, but it's awkward when running
|
112
|
+
tests locally. Happily, `rspec-cover_it` is fully compatible with `simplecov`
|
113
|
+
(though you do need to start `SimpleCov` _before_ `Rspec::CoverIt`), so you can
|
114
|
+
simply use simplecov locally when resolving coverage issues (or set up eager-
|
115
|
+
loading locally based on an environment variable, the solution I prefer), and
|
116
|
+
still use `RSpec::CoverIt` to enforce coverage in your CI system.
|
@@ -24,10 +24,6 @@ module RSpec
|
|
24
24
|
metadata.fetch(:described_class, nil)
|
25
25
|
end
|
26
26
|
|
27
|
-
def target_class_name
|
28
|
-
target_class.name
|
29
|
-
end
|
30
|
-
|
31
27
|
def scope_name
|
32
28
|
scope.file_path
|
33
29
|
end
|
@@ -55,7 +51,8 @@ module RSpec
|
|
55
51
|
end
|
56
52
|
|
57
53
|
def inferred_path
|
58
|
-
|
54
|
+
return nil if target_class.nil?
|
55
|
+
Object.const_source_location(target_class.name).first
|
59
56
|
end
|
60
57
|
end
|
61
58
|
end
|
@@ -8,23 +8,22 @@ module RSpec
|
|
8
8
|
|
9
9
|
attr_accessor :precontext_coverage, :postcontext_coverage
|
10
10
|
|
11
|
-
def pretest_coverage
|
12
|
-
@_pretest_coverage ||= pretest_results[target_path]
|
13
|
-
end
|
14
|
-
|
15
11
|
def local_coverage
|
16
|
-
|
12
|
+
assert_ready!
|
13
|
+
return nil unless precontext_coverage && postcontext_coverage && coverable_lines?
|
17
14
|
@_local_coverage ||= pretest_coverage
|
18
15
|
.zip(precontext_coverage, postcontext_coverage)
|
19
16
|
.map { |a, b, c| line_calculation(a, b, c) }
|
20
17
|
end
|
21
18
|
|
22
19
|
def local_coverage_rate
|
20
|
+
assert_ready!
|
23
21
|
return nil unless covered_line_count
|
24
22
|
covered_line_count.to_f / coverable_line_count.to_f
|
25
23
|
end
|
26
24
|
|
27
25
|
def enforce!(default_threshold:)
|
26
|
+
assert_ready!
|
28
27
|
if precovered?
|
29
28
|
fail_with_missing_code!
|
30
29
|
elsif local_coverage_rate < (context.specific_threshold || default_threshold)
|
@@ -34,6 +33,19 @@ module RSpec
|
|
34
33
|
|
35
34
|
private
|
36
35
|
|
36
|
+
def pretest_coverage
|
37
|
+
@_pretest_coverage ||= pretest_results[target_path]
|
38
|
+
end
|
39
|
+
|
40
|
+
def coverable_lines?
|
41
|
+
pretest_coverage.compact.any?
|
42
|
+
end
|
43
|
+
|
44
|
+
def assert_ready!
|
45
|
+
return if precontext_coverage && postcontext_coverage
|
46
|
+
fail(NotReady, "ContextCoverage was not ready yet, something has gone wrong")
|
47
|
+
end
|
48
|
+
|
37
49
|
def fail_with_missing_code!
|
38
50
|
fail(MissingCode, <<~MESSAGE.tr("\n", " "))
|
39
51
|
Example group `#{context.scope_name}` is attempting to cover the code for class
|
@@ -46,21 +58,37 @@ module RSpec
|
|
46
58
|
MESSAGE
|
47
59
|
end
|
48
60
|
|
61
|
+
def uncovered_lines
|
62
|
+
@_uncovered_lines ||= local_coverage.each_with_index.select { |v, _i| v&.zero? }.map(&:last)
|
63
|
+
end
|
64
|
+
|
65
|
+
def single_uncovered_line_summary
|
66
|
+
"on line #{uncovered_lines.first}"
|
67
|
+
end
|
68
|
+
|
69
|
+
def few_uncovered_lines_summary
|
70
|
+
"on lines #{uncovered_lines.map(&:to_s).join(", ")}"
|
71
|
+
end
|
72
|
+
|
73
|
+
def many_uncovered_lines_summary
|
74
|
+
shortened_list = uncovered_lines.first(10).map(&:to_s).join(", ")
|
75
|
+
"on #{uncovered_lines.length} lines, including #{shortened_list}"
|
76
|
+
end
|
77
|
+
|
78
|
+
def uncovered_lines_summary
|
79
|
+
if uncovered_lines.length == 1
|
80
|
+
single_uncovered_line_summary
|
81
|
+
elsif uncovered_lines.length <= 10
|
82
|
+
few_uncovered_lines_summary
|
83
|
+
else
|
84
|
+
many_uncovered_lines_summary
|
85
|
+
end
|
86
|
+
end
|
87
|
+
|
49
88
|
def fail_with_missing_coverage!
|
50
|
-
|
51
|
-
|
52
|
-
summary =
|
53
|
-
if lines.length == 1
|
54
|
-
"on line #{lines.first}"
|
55
|
-
elsif lines.length <= 10
|
56
|
-
"on lines #{lines.map(&:to_s).join(", ")}"
|
57
|
-
else
|
58
|
-
"on #{lines.length} lines, including #{lines.first(10).map(&:to_s).join(", ")}"
|
59
|
-
end
|
60
|
-
|
61
|
-
fail(MissingCoverage, <<~MESSAGE.tr("\n", " "))
|
89
|
+
fail(MissingCoverage, <<~MESSAGE.tr("\n", " ").strip)
|
62
90
|
Example group `#{context.scope_name}` is missing coverage on
|
63
|
-
`#{context.target_class}` in `#{context.target_path}` #{
|
91
|
+
`#{context.target_class}` in `#{context.target_path}` #{uncovered_lines_summary}
|
64
92
|
MESSAGE
|
65
93
|
end
|
66
94
|
|
@@ -1,7 +1,8 @@
|
|
1
1
|
module RSpec
|
2
2
|
module CoverIt
|
3
3
|
class CoverageState
|
4
|
-
|
4
|
+
attr_accessor :pretest_results
|
5
|
+
attr_reader :filter, :context_coverages
|
5
6
|
|
6
7
|
def initialize(filter: nil, autoenforce: false, default_threshold: 100.0)
|
7
8
|
@filter, @autoenforce, @default_threshold = filter, autoenforce, default_threshold
|
@@ -23,25 +24,22 @@ module RSpec
|
|
23
24
|
context = context_for(scope, rspec_context)
|
24
25
|
return unless context.cover_it?
|
25
26
|
|
26
|
-
|
27
|
-
|
28
|
-
|
27
|
+
context_coverage = ContextCoverage.new(context: context, pretest_results: pretest_results)
|
28
|
+
context_coverage.precontext_coverage = get_current_coverage(context.target_path)
|
29
|
+
@context_coverages[context.target_class] = context_coverage
|
29
30
|
end
|
30
31
|
|
31
32
|
def finish_tracking_for(scope, rspec_context)
|
32
33
|
context = context_for(scope, rspec_context)
|
33
34
|
return unless context.cover_it?
|
34
35
|
|
35
|
-
|
36
|
-
|
37
|
-
|
38
|
-
end
|
36
|
+
context_coverage = @context_coverages.fetch(context.target_class)
|
37
|
+
context_coverage.postcontext_coverage = get_current_coverage(context.target_path)
|
38
|
+
context_coverage.enforce!(default_threshold: default_threshold_rate)
|
39
39
|
end
|
40
40
|
|
41
41
|
private
|
42
42
|
|
43
|
-
attr_reader :pretest_results
|
44
|
-
|
45
43
|
def autoenforce?
|
46
44
|
@autoenforce
|
47
45
|
end
|
@@ -54,25 +52,16 @@ module RSpec
|
|
54
52
|
Context.new(scope: scope, rspec_context: rspec_context, autoenforce: autoenforce?)
|
55
53
|
end
|
56
54
|
|
57
|
-
def context_coverage_for(context)
|
58
|
-
@context_coverages[context.target_class] ||= ContextCoverage.new(
|
59
|
-
context: context,
|
60
|
-
pretest_results: pretest_results
|
61
|
-
)
|
62
|
-
end
|
63
|
-
|
64
55
|
def get_current_coverage(path = nil)
|
65
56
|
result = Coverage.peek_result
|
66
57
|
|
67
58
|
if path
|
68
59
|
value = result[path]
|
69
60
|
value.is_a?(Hash) ? value.fetch(:lines) : value
|
61
|
+
elsif result.any? { |_k, v| v.is_a?(Hash) }
|
62
|
+
result.transform_values { |v| v.fetch(:lines) }
|
70
63
|
else
|
71
|
-
|
72
|
-
result.transform_values { |v| v.fetch(:lines) }
|
73
|
-
else
|
74
|
-
result
|
75
|
-
end
|
64
|
+
result
|
76
65
|
end
|
77
66
|
end
|
78
67
|
end
|
data/lib/rspec/cover_it.rb
CHANGED
@@ -3,6 +3,7 @@ require "coverage"
|
|
3
3
|
module RSpec
|
4
4
|
module CoverIt
|
5
5
|
Error = Class.new(StandardError)
|
6
|
+
NotReady = Class.new(Error)
|
6
7
|
MissingCode = Class.new(Error)
|
7
8
|
MissingCoverage = Class.new(Error)
|
8
9
|
end
|
@@ -19,10 +20,19 @@ module RSpec
|
|
19
20
|
|
20
21
|
def self.setup(filter: nil, autoenforce: false)
|
21
22
|
RSpec::CoverIt.state = CoverageState.new(filter: filter, autoenforce: autoenforce)
|
22
|
-
|
23
|
+
setup_pretest_tracking
|
24
|
+
setup_per_context_tracking
|
25
|
+
end
|
23
26
|
|
27
|
+
private_class_method def self.setup_pretest_tracking
|
28
|
+
RSpec::CoverIt.state.start_tracking
|
24
29
|
RSpec.configure do |config|
|
25
30
|
config.prepend_before(:suite) { RSpec::CoverIt.state.finish_load_tracking }
|
31
|
+
end
|
32
|
+
end
|
33
|
+
|
34
|
+
private_class_method def self.setup_per_context_tracking
|
35
|
+
RSpec.configure do |config|
|
26
36
|
config.prepend_before(:context) { |context| RSpec::CoverIt.state.start_tracking_for(self.class, context) }
|
27
37
|
config.append_after(:context) { |context| RSpec::CoverIt.state.finish_tracking_for(self.class, context) }
|
28
38
|
end
|
data/rspec-cover_it.gemspec
CHANGED
@@ -31,6 +31,8 @@ Gem::Specification.new do |spec|
|
|
31
31
|
|
32
32
|
spec.add_development_dependency "pry", "~> 0.14"
|
33
33
|
spec.add_development_dependency "standard", "~> 1.28"
|
34
|
+
spec.add_development_dependency "rubocop", "~> 1.28"
|
34
35
|
spec.add_development_dependency "mdl", "~> 0.12"
|
35
36
|
spec.add_development_dependency "quiet_quality", "~> 1.2"
|
37
|
+
spec.add_development_dependency "simplecov", "~> 0.22.0"
|
36
38
|
end
|
metadata
CHANGED
@@ -1,14 +1,14 @@
|
|
1
1
|
--- !ruby/object:Gem::Specification
|
2
2
|
name: rspec-cover_it
|
3
3
|
version: !ruby/object:Gem::Version
|
4
|
-
version: 0.0
|
4
|
+
version: 0.1.0
|
5
5
|
platform: ruby
|
6
6
|
authors:
|
7
7
|
- Eric Mueller
|
8
8
|
autorequire:
|
9
9
|
bindir: bin
|
10
10
|
cert_chain: []
|
11
|
-
date: 2023-06-
|
11
|
+
date: 2023-06-10 00:00:00.000000000 Z
|
12
12
|
dependencies:
|
13
13
|
- !ruby/object:Gem::Dependency
|
14
14
|
name: rspec
|
@@ -52,6 +52,20 @@ dependencies:
|
|
52
52
|
- - "~>"
|
53
53
|
- !ruby/object:Gem::Version
|
54
54
|
version: '1.28'
|
55
|
+
- !ruby/object:Gem::Dependency
|
56
|
+
name: rubocop
|
57
|
+
requirement: !ruby/object:Gem::Requirement
|
58
|
+
requirements:
|
59
|
+
- - "~>"
|
60
|
+
- !ruby/object:Gem::Version
|
61
|
+
version: '1.28'
|
62
|
+
type: :development
|
63
|
+
prerelease: false
|
64
|
+
version_requirements: !ruby/object:Gem::Requirement
|
65
|
+
requirements:
|
66
|
+
- - "~>"
|
67
|
+
- !ruby/object:Gem::Version
|
68
|
+
version: '1.28'
|
55
69
|
- !ruby/object:Gem::Dependency
|
56
70
|
name: mdl
|
57
71
|
requirement: !ruby/object:Gem::Requirement
|
@@ -80,6 +94,20 @@ dependencies:
|
|
80
94
|
- - "~>"
|
81
95
|
- !ruby/object:Gem::Version
|
82
96
|
version: '1.2'
|
97
|
+
- !ruby/object:Gem::Dependency
|
98
|
+
name: simplecov
|
99
|
+
requirement: !ruby/object:Gem::Requirement
|
100
|
+
requirements:
|
101
|
+
- - "~>"
|
102
|
+
- !ruby/object:Gem::Version
|
103
|
+
version: 0.22.0
|
104
|
+
type: :development
|
105
|
+
prerelease: false
|
106
|
+
version_requirements: !ruby/object:Gem::Requirement
|
107
|
+
requirements:
|
108
|
+
- - "~>"
|
109
|
+
- !ruby/object:Gem::Version
|
110
|
+
version: 0.22.0
|
83
111
|
description: |
|
84
112
|
We're all used to tools that enforce _total_ coverage numbers, but this gem
|
85
113
|
tries for something different. Instead of keeping your whole project above
|
@@ -91,7 +119,14 @@ executables: []
|
|
91
119
|
extensions: []
|
92
120
|
extra_rdoc_files: []
|
93
121
|
files:
|
122
|
+
- ".github/workflows/linters.yml"
|
123
|
+
- ".github/workflows/rspec.yml"
|
94
124
|
- ".gitignore"
|
125
|
+
- ".mdl_rules.rb"
|
126
|
+
- ".mdlrc"
|
127
|
+
- ".quiet_quality.yml"
|
128
|
+
- ".rspec"
|
129
|
+
- ".rubocop.yml"
|
95
130
|
- Gemfile
|
96
131
|
- LICENSE
|
97
132
|
- README.md
|